Python 使用关键字 class 来定义一个新类,class 关键字之后是一个空格,接下来是类名,然后以冒号:结尾,类体部分要具有相同的缩进,标识归属于这个类。
类的格式如下:
class ClassName :
class_suite #类体
这里,class_suite 由成员方法和成员属性构成。需要说明的是,一般而言,在面向对象编程中,函数和方法可看作同义词。但在 Python 中,函数和方法还是有所不同的。方法是指与特定实例绑定的函数,因此,我们常把类中的函数称为方法(这一点类似于 Java),而把不与实例绑定的普通功能块称为函数(如全局的内置函数 print( )、len( ) 等)。
当通过对象调用方法时,对象本身(即 self )将作为第一个参数被传递过去,而普通函数则不具备这个特性。例 1 演示了一个具体的类的设计和使用。
【例 1】Python 类的设计和使用(class-people.py)
class Person:
height = 140 #定义类的数据成员
#定义构造方法
def __init__(self, name, age, weight):
self.name = name #定义对象的数据成员属性
self.age = age
#定义私有属性,私有属性在类外部无法直接进行访问
self._weight = weight
def speak(self):
print ("%s 说:我 %d 岁,我体重为 %d kg,身高为 %d cm" %(self.name, self.age, self._weight, Person.height))
#实例化类
p1 = Person('Alice',10,30) #实例化类
p1.speak() #引用对象中的公有方法
p1.age = 11
p1.name = 'Bob'
p1.speak()
程序执行结果为:
Alice 说:我 10 岁,我体重为 30 kg,身高为 140 cm
Bob 说:我 11 岁,我体重为 30 kg,身高为 140 cm
下面简单解释一下范例中的代码。在 Python 中,类中的数据成员可大致分为两类:属于对象的数据成员和属于类的数据成员。
属于对象的数据成员主要是指,在构造方法 __init__() 中定义的数据成员(当然也可以在其他成员方法中定义),这类数据成员的定义和使用都必须以 self 作为前缀。同一个类定义下的不同对象之间互不影响。
而属于类的数据成员为所有对象共享,它不独属于任何一个对象。这一点类似于 C++、Java 中定义的静态数据成员。例 1 的第 02 行就定义了一个属于类的数据成员 height,它作为共享变量,属于所有后续创建的对象。
在本例的构造方法 __init__() 中(第 04~08 行),定义了三个数据成员(name、age和__weight),它们都以“self.”作为访问修饰,这表明它们是属于对象的数据成员。第 08 行定义的数据成员与第 05~06 行定义的数据成员不同,该数据成员的名称是以两个下画线“__”开始的,这是 Python 的一个约定,表明它是一个私有数据成员。
私有数据成员在类外通常是不能被直接访问的。如果想访问这类数据成员,需要借助公有成员函数(相当于类的外部接口),例如通过第 09~10 行定义的方法 speak( ) 就可以访问私有数据成员 __weight。
类似地,如果类中的某个方法是由两个下画线“__”开始的,则表明它是一个私有方法。私有方法只能在类的内部被调用。
在 Python 中,以下画线开头或结尾的成员通常都有特殊的含义。比如,有如下三种情况值得注意(下面的“xxx”表示任意合法的字符串)。
_xxx:以一个下画线开始的成员,表示保护成员,凡是被这样标识命名的都不能通过“from module import *”的方式导入。也就是说,这类保护成员只对自己和其子类开放访问权限。
__xxx__:前后都有两个下画线的成员,表示 Python 系统自定义的特殊成员。比如,__init__() 表示构造方法,__del__() 表示析构方法等。
__xxx:仅前面由两个下画线开始的成员,表示私有成员(如前所述)。这类成员只能供类内部使用,不能被继承,但可以通过“对象名._类名__xxx”这样特殊的方式来访问。因此,严格意义上,Python中不存在私有成员。
范例中的 speak( ) 方法,由于方法名开始处没有“__”,说明它是公有方法。如果要访问对象里的某个公有数据成员或方法,可通过下面的方式来实现。
对象名称.属性名 #访问属性
对象名称.方法名 #访问方法
例如,若想给 Person 类的对象 p1 中的属性 name 赋值“Bob”,并将年龄赋值为 11,可用如下方法实现(见例 1 的第15行和第16行)。
p1.age = 11 #修改Person类中的age属性
p1.name = 'Bob' #修改Person类中的name属性
如果想调用 Person 中的 speak( ) 方法,可采用下面的写法(参见例 1 的第 14 行和第 17 行)。
p1.speak() #调用Person类中的speak ()方法
对于取对象属性和方法的点操作符.,笔者建议读者直接读成中文“的”。例如,p1.name = "Bob",可以读成“p1 的 name 被赋值为 Bob”。再例如,“p1.talk()”可以读成“p1 的 talk() 方法”。
这样读是有原因的,点操作符.对应的英文为“dot”,通常“t”的发音弱化,因而读成“[d ɔ]”,而“[dɔ]”的发音很接近汉中语“的”的发音,如图 1 所示。此外,“的”在含义上也有“所属”的意思。因此将点操作符读成“的”,音和意皆有内涵。
图 1:点操作符“.”的发音
在 Python 类中定义一个方法同样需要使用 def 关键字,但与类外的一般函数定义不同,类中方法的参数必须包括 self 参数(表示对象本身),且为第一个参数。比如,构造方法(第 04 行)的第一个参数就是 self,后面的三个参数 name、age 和 weight 才是真正意义上的形参。再比如,speak( ) 方法(第 09 行)本不需要额外的参数,但按照 Python 的要求,它的第一个参数必须是 self。
Python __init__()构造方法
事实上,很多类在定义对象时,都倾向于将对象创建为有初始状态的。因此,在 Python 中,类中可能会定义一个名为 __init__() 的特殊方法,它的功能就如 C++、Java 中的构造方法,主要用于对象的初始化,如例 1 中第 04~08 行代码所示。第 13 行代码创建了一个新的实例 p1,它会自动调用构造方法。
p1 = Person('Alice',10,30) #实例化 Person 类,自动调用构造方法__int__()
与 C++、Java 等语言不同的是,Python 并没有使用“new”操作来生产一个新对象,而是自动为用户创建对象,然后隐式调用 __init__( ) 方法初始化该对象。对于上一行代码,Python 编译器会将其解释为如下代码:
Person().__init__ (p1, 'Alice', 10, 30) #此代码无法正确执行
其中 Person 是类名(即工厂函数),它会调用 __init__(),可以看到,p1 作为该函数的第一个实参被传递给了 __init__() 的第一个形参 self。现在,你该明白 self 的用途了吧,它就代表对象本身,有点类似于 C++ 中的 this 指针或 Java 中的 this 对象。事实上,作为函数形参的 self,并非 Python 的关键字,我们完全可以使用其他名称(如 this )来代替 self,但最好还是约定俗成地使用“self”。
下面回到关于 Person 类的数据成员的讨论上,假设我们在运行例 1 的环境下(相应的变量已经加载到内存之中),再追加如下语句,则会有不同的运行结果。
p2 = Person('Luna', 11, 31) #创建另外一个对象p2
Person.height = 150 #为属于类的数据成员height重新赋值
p1.speak() #输出pl对象的信息
p2.speak() #输出p2对象的信息
程序执行结果为:
Alice 说:我 10 岁,我体重为 30 kg,身高为 140 cm
Bob 说:我 11 岁,我体重为 30 kg,身高为 140 cm
Bob 说:我 11 岁,我体重为 30 kg,身高为 150 cm
Luna 说:我 11 岁,我体重为 31 kg,身高为 150 cm
从运行结果可以看出,我们在第 19 行更改了属于类的公有数据成员 height 的值,而该数据为所有对象共享,因此对象 p1 和 p2 中的 height 值都改变了。
在 Java、C++ 这类面向对象编程语言中,一旦类的设计“尘埃落定”,由这个类构造的对象内的属性和方法就会完全确定下来。而 Python 则留下一个“后门”,它能为对象添加新的临时属性。比如说,在上述代码中,p1 对象没有属性 nickname,但我们可以给它添加一个,代码如下所示:
In [1]: p1.nickname = 'zhang3 ' #为对象 p1 添力口一个临时属性 nickname
In [2]: p1.nickname
Out[2] : 'zhang3'
这个临时属性仅仅属于对象 p1,并不影响 Person 的其他对象(如 p2),如果我们试图访问 p2.nickname,将会产生错误信息:'Person' object has no attribute 'nickname'(Person 对象没有 'nickname' 这个属性)。