9.3. 初识类

引入了一些新语法:三种新的对象类型和一些新的语义。

9.3.1. 类定义语法

定义最简单的形式如下:

class ClassName:
    <statement-1>
    .
    .
    .
    <statement-N>

类的定义就像函数定义( def 语句),要先执行才能生效。(你当然可以把它放进 if 语句的某一分支,或者一个函数的内部。)

习惯上,定义语句的内容通常是函数定义,不过其它语句也可以,有时会很有用,后面我们再回过头来讨论。类中的函数定义通常包括了一个特殊形式的参数列表,用于方法调用约定——同样我们在后面讨论这些。

进入定义部分后,会创建出一个新的命名空间,作为局部作用域。因此,所有的赋值成为这个新命名空间的局部变量。特别是函数定义在此绑定了新的命名。

类定义完成时(正常退出),就创建了一个 类对象 。基本上它是对类定义创建的命名空间进行了一个包装;我们在下一节进一步学习类对象的知识。原始的局部作用域(类定义引入之前生效的那个)得到恢复,类对象在这里绑定到类定义头部的类名(例子中是 ClassName )。

9.3.2. 类对象

对象支持两种操作:属性引用和实例化。

属性引用 使用和 Python 中所有的属性引用一样的标准语法:obj.name。类对象创建后,类命名空间中所有的命名都是有效属性名。所以如果类定义是这样:

class MyClass:
    """A simple example class"""
    i = 12345
    def f(self):
        return 'hello world'

那么 MyClass.iMyClass.f 是有效的属性引用,分别返回一个整数和一个方法对象。也可以对类属性赋值,你可以通过给 MyClass.i 赋值来修改它。 __doc__ 也是一个有效的属性,返回类的文档字符串:"A simple example class"

实例化 使用函数符号。只要将类对象看作是一个返回新的类实例的无参数函数即可。例如(假设沿用前面的类):

x = MyClass()

以上创建了一个新的类 实例 并将该对象赋给局部变量 x

这个实例化操作(“调用”一个类对象)来创建一个空的对象。很多类都倾向于将对象创建为有初始状态的。因此类可能会定义一个名为 __init__() 的特殊方法,像下面这样:

def __init__(self):
    self.data = []

类定义了 __init__() 方法的话,类的实例化操作会自动为新创建的类实例调用 __init__() 方法。所以在下例中,可以这样创建一个新的实例:

x = MyClass()

当然,出于弹性的需要,__init__() 方法可以有参数。事实上,参数通过 __init__() 传递到类的实例化操作上。例如,

>>> class Complex:
...     def __init__(self, realpart, imagpart):
...         self.r = realpart
...         self.i = imagpart
...
>>> x = Complex(3.0, -4.5)
>>> x.r, x.i
(3.0, -4.5)

9.3.3. 实例对象

现在我们可以用实例对象作什么?实例对象唯一可用的操作就是属性引用。有两种有效的属性名。

数据属性 相当于 Smalltalk 中的“实例变量”或 C++ 中的“数据成员”。和局部变量一样,数据属性不需要声明,第一次使用时它们就会生成。例如,如果 x 是前面创建的 MyClass 实例,下面这段代码会打印出 16 而在堆栈中留下多余的东西:

x.counter = 1
while x.counter < 10:
    x.counter = x.counter * 2
print(x.counter)
del x.counter

另一种为实例对象所接受的引用属性是 方法 。方法是“属于”一个对象的函数。(在 Python 中,方法不止是实例所独有:其它类型的对象也可有方法。例如,链表对象有 append,insert,remove,sort 等等方法。然而,在后面的介绍中,除非特别说明,我们提到的方法特指类方法)

实例对象的有效名称依赖于它的类。按照定义,类中所有(用户定义)的函数对象对应它的实例中的方法。所以在我们的例子中,x.f 是一个有效的方法引用,因为 MyClass.f 是一个函数。但 x.i 不是,因为 MyClass.i 不是函数。不过 x.fMyClass.f 不同,它是一个 方法对象 ,不是一个函数对象。

9.3.4. 方法对象

通常,方法通过右绑定方式调用:

x.f()

在 MyClass 示例中,这会返回字符串 'hello world'。然而,也不是一定要直接调用方法。 x.f 是一个方法对象,它可以存储起来以后调用。例如:

xf = x.f
while True:
    print(xf())

会不断的打印 hello world

调用方法时发生了什么?你可能注意到调用 x.f() 时没有引用前面标出的变量,尽管在 f() 的函数定义中指明了一个参数。这个参数怎么了?事实上如果函数调用中缺少参数,Python 会抛出异常--甚至这个参数实际上没什么用……

实际上,你可能已经猜到了答案:方法的特别之处在于实例对象作为函数的第一个参数传给了函数。在我们的例子中,调用 x.f() 相当于 MyClass.f(x) 。通常,以 n 个参数的列表去调用一个方法就相当于将方法的对象插入到参数列表的最前面后,以这个列表去调用相应的函数。

如果你还是不理解方法的工作原理,了解一下它的实现也许有帮助。引用非数据属性的实例属性时,会搜索它的。如果这个命名确认为一个有效的函数对象类属性,就会将实例对象和函数对象封装进一个抽象对象:这就是方法对象。以一个参数列表调用方法对象时,它被重新拆封,用实例对象和原始的参数列表构造一个新的参数列表,然后函数对象调用这个新的参数列表。

9.3.5. 类和实例变量

一般来说,实例变量用于对每一个实例都是唯一的数据,变量用于类的所有实例共享的属性和方法:

class Dog:
    kind = 'canine'         # class variable shared by all instances
    def __init__(self, name):
        self.name = name    # instance variable unique to each instance
>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.kind                  # shared by all dogs
'canine'
>>> e.kind                  # shared by all dogs
'canine'
>>> d.name                  # unique to d
'Fido'
>>> e.name                  # unique to e
'Buddy'

正如在 9.1. 术语相关 讨论的, 可变 对象,例如列表和字典,的共享数据可能带来意外的效果。例如,下面代码中的 tricks 列表不应该用作类变量,因为所有的 Dog 实例将共享同一个列表:

class Dog:
    tricks = []             # mistaken use of a class variable
    def __init__(self, name):
        self.name = name
    def add_trick(self, trick):
        self.tricks.append(trick)
>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks                # unexpectedly shared by all dogs
['roll over', 'play dead']

这个的正确设计应该使用一个实例变量:

class Dog:
    def __init__(self, name):
        self.name = name
        self.tricks = []    # creates a new empty list for each dog
    def add_trick(self, trick):
        self.tricks.append(trick)
>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks
['roll over']
>>> e.tricks
['play dead']