第 6 章 对象和类

“对象并不神秘,神秘的是你的眼睛。”1

—— Elizabeth Bowen

“选取一个对象,对它进行修改,然后再进行一些其他的修改。”2

——Jasper Johns

1“No object is mysterious. The mystery is your eye.”原话应译为:没有什么是神秘的,神秘的是你的眼睛。本书作者使用 object 一语双关。——译者注

2“Take an object. Do something to it. Do somthing else to it.”原话应译为:选取一件事物,对它进行创作,再对它进行其他创作。本书作者使用 object 一语双关。——译者注

到目前为止,你已经学习了字符串、字典之类的数据结构,还有函数、模块之类的代码结构。本章将学习如何使用自定义的数据结构:对象

6.1 什么是对象

就像在第 2 章提到的一样,Python 里的所有数据都是以对象形式存在的,无论是简单的数字类型还是复杂的代码模块。然而,Python 特殊的语法形式巧妙地将实现对象机制的大量细节隐藏了起来。输入 num = 7 就可以创建一个值为 7 的整数对象,并且将这个对象赋值给变量 num。事实上,在 Python 中,只有当你想要创建属于自己的对象或者需要修改已有对象的行为时,才需要关注对象的内部实现细节。在本章,这两种情况都会涉及。

对象既包含数据(变量,更习惯称之为特性,attribute),也包含代码(函数,也称为方法)。它是某一类具体事物的特殊实例。例如,整数 7 就是一个包含了加法、乘法之类方法的对象,这些方法在 2.2 节曾经介绍过。整数 8 则是另一个对象。这意味着在 Python 里,78 都属于一个公共的类,我们称之为整数类。字符串 'cat''duck' 也是 Python 对象,它们都包含着 capitalize()replace() 之类的字符串方法,这些方法之前也都见到过。

当你想要创建一个别人从来没有创建过的新对象时,首先必须定义一个类,用以指明该类型的对象所包含的内容(特性和方法)。

可以把对象想象成名词,那么方法就是动词。对象代表着一个独立的事物,它的方法则定义了它是如何与其他事物相互作用的。

与模块不同,你可以同时创建许多同类的对象,它们的特性值可能各不相同。对象就像是包含了代码的超级数据结构。

6.2 使用class定义类

在第 1 章,我把对象比作塑料盒子。(class)则像是制作盒子用的模具。例如,Python 的内置类 String 可以创建像 'cat''duck' 这样的字符串对象。Python 中还有许多用来创建其他标准数据类型的类,包括列表、字典等。如果想要在 Python 中创建属于自己的对象,首先你必须用关键词 class 来定义一个类。先来看一个简单的例子。

假设你想要定义一些对象用于记录联系人,每个对象对应一个人。首先需要定义 Person 类作为生产对象的模具。在接下来的几个例子中,我们会不停更新这个类的内容,从最简单的开始,直到它成为一个可以实际使用的类。

首先创建的是最简单的类,即一个没有任何内容的空类:

  1. >>> class Person():
  2. ... pass

同函数一样,用 pass 表示这个类是一个空类。上面这种定义类的方法已经是最简形式,无法再省略。你可以通过类名来创建对象,同调用函数一样:

  1. >>> someone = Person()

在这个例子中,Person() 创建了一个 Person 类的对象,并给它赋值 someone 这个名字。但是,由于我们的 Person 类是空的,所以由它创建的对象 someone 实际上什么也做不了。实际编程中,你永远也不会创建这样一个没用的类,我在这里只是为了从零开始引出后面每一步的内容。

我们来试着重新定义一下 Person 类。这一次,将 Python 中特殊的对象初始化方法 __init__ 放入其中:

  1. >>> class Person():
  2. ... def __init__(self):
  3. ... pass

我承认 __init__()self 看起来很奇怪,但这就是实际的 Python 类的定义形式。__init__() 是 Python 中一个特殊的函数名,用于根据类的定义创建实例对象。3self 参数指向了这个正在被创建的对象本身。

3你会在 Python 中见到许多双下划线(double underscore)的名字。一些懒人喜欢将其简称为 dunder,这样比较节省音节。

当你在类声明里定义 __init__() 方法时,第一个参数必须为 self。尽管 self 并不是一个 Python 保留字,但它很常用。没有人(包括你自己)在阅读你的代码时需要猜测使用 self 的意图。

尽管我们添加了初始化方法,但用这个 Person 类创建的对象仍然什么也做不了。即将进行的第三次尝试要更吸引人了,你将学习如何创建一个简单可用的 Python 对象。这一次,会在初始化方法中添加 name 参数:

  1. >>> class Person():
  2. ... def __init__(self, name):
  3. ... self.name = name
  4. ...
  5. >>>

现在,用 Person 类创建一个对象,为 name 特性传递一个字符串参数:

  1. >>> hunter = Person('Elmer Fudd')

上面这短短的一行代码实际做了以下工作:

  • 查看 Person 类的定义;

  • 在内存中实例化(创建)一个新的对象;

  • 调用对象的 __init__ 方法,将这个新创建的对象作为 self 传入,并将另一个参数('Elmer-Fudd')作为 name 传入;

  • name 的值存入对象;

  • 返回这个新的对象;

  • 将名字 hunter 与这个对象关联。

这个新对象与任何其他的 Python 对象一样。你可以把它当作列表、元组、字典或集合中的元素,也可以把它当作参数传递给函数,或者把它做为函数的返回结果。

我们刚刚传入的 name 参数此时又在哪儿呢?它作为对象的特性存储在了对象里。可以直接对它进行读写操作:

  1. >>> print('The mighty hunter: ', hunter.name)
  2. The mighty hunter: Elmer Fudd

记住,在 Person 类定义的内部,你可以直接通过 self.name 访问 name 特性。而当创建了一个实际的对象后,例如这里的 hunter,需要通过 hunter.name 来访问它。

在类的定义中,__init__ 并不是必需的。只有当需要区分由该类创建的不同对象时,才需要指定 __init__ 方法。

6.3 继承

在你编写代码解决实际问题时,经常能找到一些已有的类,它们能够实现你所需的大部分功能,但不是全部。这时该怎么办?当然,你可以对这个已有的类进行修改,但这么做很容易让代码变得更加复杂,一不留神就可能会破坏原来可以正常工作的功能。

当然,也可以另起炉灶重新编写一个类:复制粘贴原来的代码再融入自己的新代码。但这意味着你需要维护更多的代码。同时,新类和旧类中实现同样功能的代码被分隔在了不同的地方(日后修改时需要改动多处)。

更好的解决方法是利用类的继承:从已有类中衍生出新的类,添加或修改部分功能。这是代码复用的一个绝佳的例子。使用继承得到的新类会自动获得旧类中的所有方法,而不需要进行任何复制。

你只需要在新类里面定义自己额外需要的方法,或者按照需求对继承的方法进行修改即可。修改得到的新方法会覆盖原有的方法。我们习惯将原始的类称为父类超类基类,将新的类称作孩子类子类衍生类。这些术语在面向对象的编程中不加以区分。

现在,我们来试试继承。首先,定义一个空类 Car。然后,定义一个 Car 的子类 Yugo。定义子类使用的也是 class 关键词,不过需要把父类的名字放在子类名字后面的括号里(class Yugo(Car)):

  1. >>> class Car():
  2. ... pass
  3. ...
  4. >>> class Yugo(Car):
  5. ... pass
  6. ...

接着,为每个类创建一个实例对象:

  1. >>> give_me_a_car = Car()
  2. >>> give_me_a_yugo = Yugo()

子类是父类的一种特殊情况,它属于父类。在面向对象的术语里,我们经常称 Yugo 是一个(is-a)Car。对象 give_me_a_yugoYugo 类的一个实例,但它同时继承了 Car 能做到的所有事情。当然,上面的例子中 CarYugo 就像潜艇上的甲板水手一样起不到任何实际作用。我们来更新一下类的定义,让它们发挥点儿作用:

  1. >>> class Car():
  2. ... def exclaim(self):
  3. ... print("I'm a Car!")
  4. ...
  5. >>> class Yugo(Car):
  6. ... pass
  7. ...

最后,为每一个类各创建一个对象,并调用刚刚声明的 exclaim 方法:

  1. >>> give_me_a_car = Car()
  2. >>> give_me_a_yugo = Yugo()
  3. >>> give_me_a_car.exclaim()
  4. I'm a Car!
  5. >>> give_me_a_yugo.exclaim()
  6. I'm a Car!

我们不需要进行任何特殊的操作,Yugo 就自动从 Car 那里继承了 exclaim() 方法。但事实上,我们并不希望 Yugoexlaim() 方法里宣称它是一个 Car,这可能会造成身份危机(无法区分 CarYugo)。让我们来看看怎么解决这个问题。

6.4 覆盖方法

就像上面的例子展示的一样,新创建的子类会自动继承父类的所有信息。接下来将看到子类如何替代——更习惯说覆盖(override)——父类的方法。YugoCar 一定存在着某些区别,不然的话,创建它又有什么意义?试着改写一下 Yugoexclaim() 方法的功能:

  1. >>> class Car():
  2. ... def exclaim(self):
  3. ... print("I'm a Car!")
  4. ...
  5. >>> class Yugo(Car):
  6. ... def exclaim(self):
  7. ... print("I'm a Yugo! Much like a Car, but more Yugo-ish.")
  8. ...

现在,为每个类创建一个对象:

  1. >>> give_me_a_car = Car()
  2. >>> give_me_a_yugo = Yugo()

看看它们各自会宣称什么?

  1. >>> give_me_a_car.exclaim()
  2. I'm a Car!
  3. >>> give_me_a_yugo.exclaim()
  4. I'm a Yugo! Much like a Car, but more Yugo-ish.

在上面的例子中,我们覆盖了父类的 exclaim() 方法。在子类中,可以覆盖任何父类的方法,包括 __init__()。下面的例子使用了之前创建过的 Person 类。我们来创建两个子类,分别代表医生(MDPerson)和律师(JDPerson):

  1. >>> class Person():
  2. ... def __init__(self, name):
  3. ... self.name = name
  4. ...
  5. >>> class MDPerson(Person):
  6. ... def __init__(self, name):
  7. ... self.name = "Doctor " + name
  8. ...
  9. >>> class JDPerson(Person):
  10. ... def __init__(self, name):
  11. ... self.name = name + ", Esquire"
  12. ...

在上面的例子中,子类的初始化方法 __init__() 接收的参数和父类 Person 一样,但存储到对象内部 name 特性的值却不尽相同:

  1. >>> person = Person('Fudd')
  2. >>> doctor = MDPerson('Fudd')
  3. >>> lawyer = JDPerson('Fudd')
  4. >>> print(person.name)
  5. Fudd
  6. >>> print(doctor.name)
  7. Doctor Fudd
  8. >>> print(lawyer.name)
  9. Fudd, Esquire

6.5 添加新方法

子类还可以添加父类中没有的方法。回到 Car 类和 Yugo 类,我们给 Yugo 类添加一个新的方法 need_a_push()

  1. >>> class Car():
  2. ... def exclaim(self):
  3. ... print("I'm a Car!")
  4. ...
  5. >>> class Yugo(Car):
  6. ... def exclaim(self):
  7. ... print("I'm a Yugo! Much like a Car, but more Yugo-ish.")
  8. ... def need_a_push(self):
  9. ... print("A little help here?")
  10. ...

接着,创建一个 Car 和一个 Yugo 对象:

  1. >>> give_me_a_car = Car()
  2. >>> give_me_a_yugo = Yugo()

Yugo 类的对象可以响应 need_a_push() 方法:

  1. >>> give_me_a_yugo.need_a_push()
  2. A little help here?

但比它广义的 Car 无法响应该方法:

  1. >>> give_me_a_car.need_a_push()
  2. Traceback (most recent call last):
  3. File "<stdin>", line 1, in <module>
  4. AttributeError: 'Car' object has no attribute 'need_a_push'

至此,Yugo 终于可以做一些 Car 做不到的事情了。它的与众不同的特征开始体现了出来。

6.6 使用super从父类得到帮助

我们已经知道如何在子类中覆盖父类的方法,但如果想要调用父类的方法怎么办?“哈哈!终于等到你问这个了。”super() 站出来说道。下面的例子将定义一个新的类 EmailPerson,用于表示有电子邮箱的 Person。首先,来定义熟悉的 Person 类:

  1. >>> class Person():
  2. ... def __init__(self, name):
  3. ... self.name = name
  4. ...

下面是子类的定义。注意,子类的初始化方法 __init__() 中添加了一个额外的 email 参数:

  1. >>> class EmailPerson(Person):
  2. ... def __init__(self, name, email):
  3. ... super().__init__(name)
  4. ... self.email = email

在子类中定义 __init__() 方法时,父类的 __init__() 方法会被覆盖。因此,在子类中,父类的初始化方法并不会被自动调用,我们必须显式调用它。以上代码实际上做了这样几件事情。

  • 通过 super() 方法获取了父类 Person 的定义。

  • 子类的 __init__() 调用了 Person.__init__() 方法。它会自动将 self 参数传递给父类。因此,你只需传入其余参数即可。在上面的例子中,Person() 能接受的其余参数指的是 name

  • self.email = email 这行新的代码才真正起到了将 EmailPersonPerson 区分开的作用。

接下来,创建一个 EmailPerson 类的对象:

  1. >>> bob = EmailPerson('Bob Frapples', 'bob@frapples.com')

我们既可以访问 name 特性,也可以访问 email 特性:

  1. >>> bob.name
  2. 'Bob Frapples'
  3. >>> bob.email
  4. 'bob@frapples.com'

为什么不像下面这样定义 EmailPerson 类呢?

  1. >>> class EmailPerson(Person):
  2. ... def __init__(self, name, email):
  3. ... self.name = name
  4. ... self.email = email

确实可以这么做,但这有悖我们使用继承的初衷。我们应该使用 super() 来让 Person 完成它应该做的事情,就像任何一个单纯的 Person 对象一样。除此之外,不这么写还有另一个好处:如果 Person 类的定义在未来发生改变,使用 super() 可以保证这些改变会自动反映到 EmailPerson 类上,而不需要手动修改。

子类可以按照自己的方式处理问题,但如果仍需要借助父类的帮助,使用 super() 是最佳的选择(就像现实生活中孩子与父母的关系一样)。

6.7 self的自辩

Python 中经常被争议的一点(除了使用空格 4 外)就是必须把 self 设置为实例方法(前面例子中你见到的所有方法都是实例方法)的第一个参数。Python 使用 self 参数来找到正确的对象所包含的特性和方法。通过下面的例子,我会告诉你调用对象方法背后 Python 实际做的工作。

4Python 使用空格而不使用 tab 进行缩进,并且用缩进标志代码块,这使得不同开发者合作时必须事先统一缩进空格的数量,不然会造成代码混乱。这是 Python 被争议最多的毛病之一。——译者注

还记得前面例子中的 Car 类吗?再次调用 exclaim() 方法:

  1. >>> car = Car()
  2. >>> car.exclaim()
  3. I'm a Car!

Python 在背后做了以下两件事情:

  • 查找 car 对象所属的类(Car);

  • car 对象作为 self 参数传给 Car 类所包含的 exclaim() 方法。

了解调用机制后,为了好玩,我们甚至可以像下面这样进行调用,这与普通的调用语法(car.exclaim())效果完全一致:

  1. >>> Car.exclaim(car)
  2. I'm a Car!

当然,我们没有理由使用这种臃肿的语法。

6.8 使用属性对特性进行访问和设置

有一些面向对象的语言支持私有特性。这些特性无法从对象外部直接访问,我们需要编写 getter 和 setter 方法对这些私有特性进行读写操作。

Python 不需要 getter 和 setter 方法,因为 Python 里所有特性都是公开的,使用时全凭自觉。如果你不放心直接访问对象的特性,可以为对象编写 setter 和 getter 方法。但更具 Python 风格的解决方案是使用属性(property)5。

5本书将 property 译作属性,而将 attribute 译作特性,请读者注意区分。——译者注

下面的例子中,首先定义一个 Duck 类,它仅包含一个 hidden_name 特性。(下一节会告诉你命名私有特性的一种更好的方式。)我们不希望别人能够直接访问这个特性,因此需要定义两个方法:getter 方法(get_name())和 setter 方法(set_name())。我们在每个方法中都添加一个 print() 函数,这样就能方便地知道它们何时被调用。最后,把这些方法设置为 name 属性:

  1. >>> class Duck():
  2. ... def __init__(self, input_name):
  3. ... self.hidden_name = input_name
  4. ... def get_name(self):
  5. ... print('inside the getter')
  6. ... return self.hidden_name
  7. ... def set_name(self, input_name):
  8. ... print('inside the setter')
  9. ... self.hidden_name = input_name
  10. ... name = property(get_name, set_name)

这两个新方法在最后一行之前都与普通的 getter 和 setter 方法没有任何区别,而最后一行则把这两个方法定义为了 name 属性。property() 的第一个参数是 getter 方法,第二个参数是 setter 方法。现在,当你尝试访问 Duck 类对象的 name 特性时,get_name() 会被自动调用:

  1. >>> fowl = Duck('Howard')
  2. >>> fowl.name
  3. inside the getter
  4. 'Howard'

当然,也可以显式调用 get_name() 方法,它就像普通的 getter 方法一样:

  1. >>> fowl.get_name()
  2. inside the getter
  3. 'Howard'

当对 name 特性执行赋值操作时,set_name() 方法会被调用:

  1. >>> fowl.name = 'Daffy'
  2. inside the setter
  3. >>> fowl.name
  4. inside the getter
  5. 'Daffy'

也可以显式调用 set_name() 方法:

  1. >>> fowl.set_name('Daffy')
  2. inside the setter
  3. >>> fowl.name
  4. inside the getter
  5. 'Daffy'

另一种定义属性的方式是使用装饰器(decorator)。下一个例子会定义两个不同的方法,它们都叫 name(),但包含不同的装饰器:

  • @property,用于指示 getter 方法;

  • @name.setter,用于指示 setter 方法。 实际代码如下所示:

  1. >>> class Duck():
  2. ... def __init__(self, input_name):
  3. ... self.hidden_name = input_name
  4. ... @property
  5. ... def name(self):
  6. ... print('inside the getter')
  7. ... return self.hidden_name
  8. ... @name.setter
  9. ... def name(self, input_name):
  10. ... print('inside the setter')
  11. ... self.hidden_name = input_name

你仍然可以像之前访问特性一样访问 name,但这里没有了显式的 get_name()set_name() 方法:

  1. >>> fowl = Duck('Howard')
  2. >>> fowl.name
  3. inside the getter
  4. 'Howard'
  5. >>> fowl.name = 'Donald'
  6. inside the setter
  7. >>> fowl.name
  8. inside the getter
  9. 'Donald'

第 6 章 对象和类 - 图1 实际上,如果有人能猜到我们在类的内部用的特性名是 hidden_name,他仍然可以直接通过 fowl.hidden_name 进行读写操作。下一节将看到 Python 中特有的命名私有特性的方式。

在前面几个例子中,我们都使用 name 属性指向类中存储的某一特性(在我们的例子中是 hidden_name)。除此之外,属性还可以指向一个计算结果值。我们来定义一个 Circle 类,它包含 radius 特性以及一个计算属性 diameter

  1. >>> class Circle():
  2. ... def __init__(self, radius):
  3. ... self.radius = radius
  4. ... @property
  5. ... def diameter(self):
  6. ... return 2 * self.radius
  7. ...

创建一个 Circle 对象,并给 radius 赋予一个初值:

  1. >>> c = Circle(5)
  2. >>> c.radius
  3. 5

可以像访问特性(例如 radius)一样访问属性 diameter

  1. >>> c.diameter
  2. 10

真正有趣的还在后面。我们可以随时改变 radius 特性的值,计算属性 diameter 会自动根据新的值更新自己:

  1. >>> c.radius = 7
  2. >>> c.diameter
  3. 14

如果你没有指定某一特性的 setter 属性(@diameter.setter),那么将无法从类的外部对它的值进行设置。这对于那些只读的特性非常有用:

  1. >>> c.diameter = 20
  2. Traceback (most recent call last):
  3. File "<stdin>", line 1, in <module>
  4. AttributeError: can't set attribute

与直接访问特性相比,使用 property 还有一个巨大的优势:如果你改变了某个特性的定义,只需要在类定义里修改相关代码即可,不需要在每一处调用修改。6

6如前面例子中,假如我们需要把特性 hidden_name 的名字改成 in_class_name。不设置属性(property)的话,我们需要在每一处访问 hidden_name 的地方将它替换成 in_class_name;而设置了属性的话,仅需在类的内部修改,其余部分的访问仍直接通过属性 name 即可。——译者注

6.9 使用名称重整保护私有特性

前面的 Duck 例子中,为了隐藏内部特性,我们曾将其命名为 hidden_name。其实,Python 对那些需要刻意隐藏在类内部的特性有自己的命名规范:由连续的两个下划线开头(__)。

我们来把 hidden_name 改名为 __name,如下所示:

  1. >>> class Duck():
  2. ... def __init__(self, input_name):
  3. ... self.__name = input_name
  4. ... @property
  5. ... def name(self):
  6. ... print('inside the getter')
  7. ... return self.__name
  8. ... @name.setter
  9. ... def name(self, input_name):
  10. ... print('inside the setter')
  11. ... self.__name = input_name
  12. ...

看看代码是否还能正常工作:

  1. >>> fowl = Duck('Howard')
  2. >>> fowl.name
  3. inside the getter
  4. 'Howard'
  5. >>> fowl.name = 'Donald'
  6. inside the setter
  7. >>> fowl.name
  8. inside the getter
  9. 'Donald'

看起来不错!现在,你无法在外部访问 __name 特性了:

  1. >>> fowl.__name
  2. Traceback (most recent call last):
  3. File "<stdin>", line 1, in <module>
  4. AttributeError: 'Duck' object has no attribute '__name'

这种命名规范本质上并没有把特性变成私有,但 Python 确实将它的名字重整了,让外部的代码无法使用。如果你实在好奇名称重整是怎么实现的,我可以偷偷地告诉你其中的奥秘,但不要告诉别人哦:

  1. >>> fowl._Duck__name
  2. 'Donald'

发现了吗?我们并没有得到 inside the getter,成功绕过了 getter 方法。尽管如我们所见,这种保护特性的方式并不完美,但它确实能在一定程度上避免我们无意或有意地对特性进行直接访问。

6.10 方法的类型

有些数据(特性)和函数(方法)是类本身的一部分,还有一些是由类创建的实例的一部分。

在类的定义中,以 self 作为第一个参数的方法都是实例方法(instance method)。它们在创建自定义类时最常用。实例方法的首个参数是 self,当它被调用时,Python 会把调用该方法的对象作为 self 参数传入。

与之相对,类方法(class method)会作用于整个类,对类作出的任何改变会对它的所有实例对象产生影响。在类定义内部,用前缀修饰符 @classmethod 指定的方法都是类方法。与实例方法类似,类方法的第一个参数是类本身。在 Python 中,这个参数常被写作 cls,因为全称 class 是保留字,在这里我们无法使用。下面的例子中,我们为类 A 定义一个类方法来记录一共有多少个类 A 的对象被创建:

  1. >>> class A():
  2. ... count = 0
  3. ... def __init__(self):
  4. ... A.count += 1
  5. ... def exclaim(self):
  6. ... print("I'm an A!")
  7. ... @classmethod
  8. ... def kids(cls):
  9. ... print("A has", cls.count, "little objects.")
  10. ...
  11. >>>
  12. >>> easy_a = A()
  13. >>> breezy_a = A()
  14. >>> wheezy_a = A()
  15. >>> A.kids()
  16. A has 3 little objects.

注意,上面的代码中,我们使用的是 A.count(类特性),而不是 self.count(可能是对象的特性)。在 kids() 方法中,我们使用的是 cls.count,它与 A.count 的作用一样。

类定义中的方法还存在着第三种类型,它既不会影响类也不会影响类的对象。它们出现在类的定义中仅仅是为了方便,否则它们只能孤零零地出现在代码的其他地方,这会影响代 码的逻辑性。这种类型的方法被称作静态方法(static method),用 @staticmethod 修饰,它既不需要 self 参数也不需要 class 参数。下面例子中的静态方法是一则 CoyoteWeapon 的广告:

  1. >>> class CoyoteWeapon():
  2. ... @staticmethod
  3. ... def commercial():
  4. ... print('This CoyoteWeapon has been brought to you by Acme')
  5. ...
  6. >>>
  7. >>> CoyoteWeapon.commercial()
  8. This CoyoteWeapon has been brought to you by Acme

注意,在这个例子中,我们甚至都不用创建任何 CoyoteWeapon 类的对象就可以调用这个方法,句法优雅不失风格!

6.11 鸭子类型

Python 对实现多态(polymorphism)要求得十分宽松,这意味着我们可以对不同对象调用同名的操作,甚至不用管这些对象的类型是什么。

我们来为三个 Quote 类设定同样的初始化方法 __init__(),然后再添加两个新函数:

  • who() 返回保存的 person 字符串的值;

  • says() 返回保存的 words 字符串的内容,并添上指定的标点符号。

它们的具体实现如下所示:

  1. >>> class Quote():
  2. ... def __init__(self, person, words):
  3. ... self.person = person
  4. ... self.words = words
  5. ... def who(self):
  6. ... return self.person
  7. ... def says(self):
  8. ... return self.words + '.'
  9. ...
  10. >>> class QuestionQuote(Quote):
  11. ... def says(self):
  12. ... return self.words + '?'
  13. ...
  14. >>> class ExclamationQuote(Quote):
  15. ... def says(self):
  16. ... return self.words + '!'
  17. ...
  18. >>>

我们不需要改变 QuestionQuote 或者 ExclamationQuote 的初始化方式,因此没有覆盖它们的 __init__() 方法。Python 会自动调用父类 Quote 的初始化函数 __init__() 来存储实例变量 personwords,这就是我们可以在子类 QuestionQuoteExclamationQuote 的对象里访问 self.words 的原因。

接下来创建一些对象:

  1. >>> hunter = Quote('Elmer Fudd', "I'm hunting wabbits")
  2. >>> print(hunter.who(), 'says:', hunter.says())
  3. Elmer Fudd says: I'm hunting wabbits.
  4. >>> hunted1 = QuestionQuote('Bugs Bunny', "What's up, doc")
  5. >>> print(hunted1.who(), 'says:', hunted1.says())
  6. Bugs Bunny says: What's up, doc?
  7. >>> hunted2 = ExclamationQuote('Daffy Duck', "It's rabbit season")
  8. >>> print(hunted2.who(), 'says:', hunted2.says())
  9. Daffy Duck says: It's rabbit season!

三个不同版本的 says() 为上面三种类提供了不同的响应方式,这是面向对象的语言中多态的传统形式。Python 在这方面走得更远一些,无论对象的种类是什么,只要包含 who()says(),你便可以调用它。我们再来定义一个 BabblingBrook 类,它与我们之前的猎人猎物(Quote 类的后代)什么的没有任何关系:

  1. >>> class BabblingBrook():
  2. ... def who(self):
  3. ... return 'Brook'
  4. ... def says(self):
  5. ... return 'Babble'
  6. ...
  7. >>> brook = BabblingBrook()

现在,对不同对象执行 who()says() 方法,其中有一个(brook)与其他类型的对象毫无关联:

  1. >>> def who_says(obj):
  2. ... print(obj.who(), 'says', obj.says())
  3. ...
  4. >>> who_says(hunter)
  5. Elmer Fudd says I'm hunting wabbits.
  6. >>> who_says(hunted1)
  7. Bugs Bunny says What's up, doc?
  8. >>> who_says(hunted2)
  9. Daffy Duck says It's rabbit season!
  10. >>> who_says(brook)
  11. Brook says Babble

这种方式有时被称作鸭子类型(duck typing),这个命名源自一句名言:

如果它像鸭子一样走路,像鸭子一样叫,那么它就是一只鸭子。7

—— 一位智者

7源于“鸭子测试”。在鸭子类型中,并不关注对象本身的具体类型,只关注它能实现的功能。——译者注

6.12 特殊方法

到目前为止,你已经能创建并使用基本对象了。现在再往深钻研一些。

当我们输入像 a = 3 + 8 这样的式子时,整数 38 是怎么知道如何实现 + 的?同样,a 又是怎么知道如何使用=来获取计算结果的?你可以使用 Python 的特殊方法(special method),有时也被称作魔术方法(magic method),来实现这些操作符的功能。别担心,不需要甘道夫 8 的帮助,它们一点也不复杂。

8英国作家 J. R. R. 托尔金小说《指环王》《霍比特人》中的巫师。——译者注

这些特殊方法的名称以双下划线(__)开头和结束。没错,你已经见过其中一个: __init__,它根据类的定义以及传入的参数对新创建的对象进行初始化。

假设你有一个简单的 Word 类,现在想要添加一个 equals() 方法来比较两个词是否一致,忽略大小写。也就是说,一个包含值 'ha'Word 对象与包含 'HA' 的是相同的。

下面的代码是第一次尝试,创建一个普通方法 equals()self.text 是当前 Word 对象所包含的字符串文本,equals() 方法将该字符串与 word2(另一个 Word 对象)所包含的字符串做比较:

  1. >>> class Word():
  2. ... def __init__(self, text):
  3. ... self.text = text
  4. ...
  5. ... def equals(self, word2):
  6. ... return self.text.lower() == word2.text.lower()
  7. ...

接着创建三个包含不同字符串的 Word 对象:

  1. >>> first = Word('ha')
  2. >>> second = Word('HA')
  3. >>> third = Word('eh')

当字符串 'ha''HA' 被转换为小写形式再进行比较时(我们就是这么做的),它们应该是相等的:

  1. >>> first.equals(second)
  2. True

但字符串 'eh' 无论如何与 'ha' 也不会相等:

  1. >>> first.equals(third)
  2. False

我们成功定义了 equals() 方法来进行小写转换并比较。但试想一下,如果能通过 if first == second 进行比较的话岂不更妙?这样类会更自然,表现得更像一个 Python 内置的类。好的,来试试吧,把前面例子中的 equals() 方法的名称改为 __eq__()(请先暂时接受,后面我会解释为什么这么命名):

  1. >>> class Word():
  2. ... def __init__(self, text):
  3. ... self.text = text
  4. ... def __eq__(self, word2):
  5. ... return self.text.lower() == word2.text.lower()
  6. ...

修改就此结束,来看看新的版本能否正常工作:

  1. >>> first = Word('ha')
  2. >>> second = Word('HA')
  3. >>> third = Word('eh')
  4. >>> first == second
  5. True
  6. >>> first == third
  7. False

太神奇了!是不是如同魔术一般?仅需将方法名改为 Python 里进行相等比较的特殊方法名 __eq__() 即可。表 6-1 和表 6-2 列出了最常用的一些魔术方法。

表6-1:和比较相关的魔术方法

方法名 使用
__eq__(self, other) self == other
__ne__(self, other) self != other
__lt__(self, other) self < other
__gt__(self, other) self > other
__le__(self, other) self <= other
__ge__(self, other) self >= other

表6-2:和数学相关的魔术方法

方法名 使用
__add__(self, other) self + other
__sub__(self, other) self - other
__mul__(self, other) self * other
__floordiv__(self, other) self // other
__truediv__(self, other) self / other
__mod__(self, other) self % other
__pow__(self, other) self ** other

不仅数字类型可以使用像 +(魔术方法 __add__())和 -(魔术方法 __sub__())的数学运算符,一些其他的类型也可以使用。例如,Python 的字符串类型使用 + 进行拼接,使用 * 进行复制。关于字符串的魔术方法还有很多,你可以在 Python 3 在线文档的 Special method names(https://docs.python.org/3/reference/datamodel.html#special-method-names)里找到,其中最常用的一些参见下面的表 6-3。

表6-3:其他种类的魔术方法

方法名 使用
__str__(self) str(self)
__repr__(self) repr(self)
__len__(self) len(self)

除了 ___init__() 外,你会发现在编写类方法时最常用到的是 __str__(),它用于定义如何打印对象信息。print() 方法,str() 方法以及你将在第 7 章读到的关于字符串格式化的相关方法都会用到 __str__()。交互式解释器则用 __repr__() 方法输出变量。如果在你的类既没有定义 __str__() 也没有定义 __repr__(),Python 会输出类似下面这样的默认字符串:

  1. >>> first = Word('ha')
  2. >>> first
  3. <__main__.Word object at 0x1006ba3d0>
  4. >>> print(first)
  5. <__main__.Word object at 0x1006ba3d0>

我们将 __str__()__repr__() 方法都添加到 Word 类里,让输出的对象信息变得更好看些:

  1. >>> class Word():
  2. ... def __init__(self, text):
  3. ... self.text = text
  4. ... def __eq__(self, word2):
  5. ... return self.text.lower() == word2.text.lower()
  6. ... def __str__(self):
  7. ... return self.text
  8. ... def __repr__(self):
  9. ... return 'Word("' + self.text + '")'
  10. ...
  11. >>> first = Word('ha')
  12. >>> first # uses __repr__
  13. Word("ha")
  14. >>> print(first) # uses __str__
  15. ha

更多关于魔术方法的内容请查看 Python 在线文档(https://docs.python.org/3/reference/datamodel.html#special-method-names)。

6.13 组合

如果你想要创建的子类在大多数情况下的行为都和父类相似的话(子类是父类的一种特殊情况,它们之间是 is-a 的关系),使用继承是非常不错的选择。建立复杂的继承关系确实很吸引人,但有些时候使用组合(composition)或聚合(aggregation)更加符合现实的逻辑(x 含有 y,它们之间是 has-a 的关系)。一只鸭子是鸟的一种(is-a),它有一条尾巴(has-a)。尾巴并不是鸭子的一种,它是鸭子的组成部分。下个例子中,我们会建立 billtail 对象,并将它们都提供给 duck 使用:

  1. >>> class Bill():
  2. ... def __init__(self, description):
  3. ... self.description = description
  4. ...
  5. >>> class Tail():
  6. ... def __init__(self, length):
  7. ... self.length = length
  8. ...
  9. >>> class Duck():
  10. ... def __init__(self, bill, tail):
  11. ... self.bill = bill
  12. ... self.tail = tail
  13. ... def about(self):
  14. ... print('This duck has a', self.bill.description, 'bill and a', self.tail.length, 'tail')
  15. ...
  16. >>> tail = Tail('long')
  17. >>> bill = Bill('wide orange')
  18. >>> duck = Duck(bill, tail)
  19. >>> duck.about()
  20. This duck has a wide orange bill and a long tail

6.14 何时使用类和对象而不是模块

有一些方法可以帮助你决定是把你的代码封装到类里还是模块里。

  • 当你需要许多具有相似行为(方法)但不同状态(特性)的实例时,使用对象是最好的选择。

  • 类支持继承,但模块不支持。

  • 如果你想要保证实例的唯一性,使用模块是最好的选择。不管模块在程序中被引用多少次,始终只有一个实例被加载。(对 Java 和 C++ 程序员来说,如果读过 Erich Gamma 的《设计模式:可复用面向对象软件的基础》,可以把 Python 模块理解为单例。)

  • 如果你有一系列包含多个值的变量,并且它们能作为参数传入不同的函数,那么最好将它们封装到类里面。举个例子,你可能会使用以 sizecolor 为键的字典代表一张彩色图片。你可以在程序中为每张图片创建不同的字典,并把它们作为参数传递给像 scale() 或者 transform() 之类的函数。但这么做的话,一旦你想要添加其他的键或者函数会变得非常麻烦。为了保证统一性,应该定义一个 Image 类,把 sizecolor 作为特性,把 scale()transform() 定义为方法。这么一来,关于一张图片的所有数据和可执行的操作都存储在了统一的位置。

  • 用最简单的方式解决问题。使用字典、列表和元组往往要比使用模块更加简单、简洁且快速。而使用类则更为复杂。

创始人 Guido 的建议:

不要过度构建数据结构。尽量使用元组(以及命名元组)而不是对象。尽量使用简单的属性域而不是 getter/setter 函数……内置数据类型是你最好的朋友。尽可能多地使用数字、字符串、元组、列表、集合以及字典。多看看容器库提供的类型,尤其是双端队列。

—— Guido van Rossum

命名元组

由于 Guido 刚刚提到了命名元组(named tuple),那么我们就在这里谈一谈关于它的事情。命名元组是元组的子类,你既可以通过名称(使用 .name)来访问其中的值,也可以通过位置进行访问(使用 [offset])。

我们来把前面例子中的 Duck 类改写成命名元组,简洁起见,把 billtail 当作简单的字符串特性而不当作类。我们可以通过将下面两个参数传入 namedtuple 函数来创建命名元组:

  • 名称;

  • 由多个域名组成的字符串,各个域名之间由空格隔开。

命名元组并不是 Python 自动支持的类型,使用之前需要加载与其相关的模块,下面例子中的第一行就是在进行模块加载工作:

  1. >>> from collections import namedtuple
  2. >>> Duck = namedtuple('Duck', 'bill tail')
  3. >>> duck = Duck('wide orange', 'long')
  4. >>> duck
  5. Duck(bill='wide orange', tail='long')
  6. >>> duck.bill
  7. 'wide orange'
  8. >>> duck.tail
  9. 'long'

也可以用字典来构造一个命名元组:

  1. >>> parts = {'bill': 'wide orange', 'tail': 'long'}
  2. >>> duck2 = Duck(**parts)
  3. >>> duck2
  4. Duck(bill='wide orange', tail='long')

注意,上面例子中的 **parts,它是个关键词变量(keyword argument)。它的作用是将 parts 字典中的键和值抽取出来作为参数提供给 Duck() 使用。它与下面这行代码的功能一样:

  1. >>> duck2 = Duck(bill = 'wide orange', tail = 'long')

命名元组是不可变的,但你可以替换其中某些域的值并返回一个新的命名元组:

  1. >>> duck3 = duck2._replace(tail='magnificent', bill='crushing')
  2. >>> duck3
  3. Duck(bill='crushing', tail='magnificent')

假设我们把 duck 定义为字典:

  1. >>> duck_dict = {'bill': 'wide orange', 'tail': 'long'}
  2. >>> duck_dict
  3. {'tail': 'long', 'bill': 'wide orange'}

可以向字典里添加新的域(键值对):

  1. >>> duck_dict['color'] = 'green'
  2. >>> duck_dict
  3. {'color': 'green', 'tail': 'long', 'bill': 'wide orange'}

但无法对命名元组这么做:

  1. >>> duck.color = 'green'
  2. Traceback (most recent call last):
  3. File "<stdin>", line 1, in <module>
  4. AttributeError: 'dict' object has no attribute 'color'

作为总结,我列出了一些使用命名元组的好处:

  • 它无论看起来还是使用起来都和不可变对象非常相似;

  • 与使用对象相比,使用命名元组在时间和空间上效率更高;

  • 可以使用点号(.)对特性进行访问,而不需要使用字典风格的方括号;

  • 可以把它作为字典的键。

6.15 练习

(1) 创建一个名为 Thing 的空类并将它打印出来。接着,创建一个属于该类的对象 example,同样将它打印出来。看看这两次打印的结果是一样的还是不同的?

(2) 创建一个新的类 Thing2,将 'abc' 赋值给类特性 letters,打印 letters

(3) 再创建一个新的类,叫作 Thing3。这次将 'xyz' 赋值给实例(对象)特性 letters,并试着打印 letters。看看你是不是必须先创建一个对象才可以进行打印操作?

(4) 创建一个名为 Element 的类,它包含实例特性 namesymbolnumber。使用 'Hydrogen''H'1 实例化一个对象。

(5) 创建一个字典,包含这些键值对:'name': 'Hydrogen''symbol': 'H''number': 1。然后用这个字典实例化 Element 类的对象 hydrogen

(6) 为 Element 类定义一个 dump() 方法,用于打印对象的特性(namesymbolnumber)。使用这个新定义的类创建一个对象 hydrogen 并用 dump() 打印。

(7) 调用 print(hydrogen),然后修改 Element 的定义,将 dump 方法的名字改为 __str__。再次创建一个 hydrogen 对象并调用 print(hydrogen),观察输出结果。

(8) 修改 Element 使得 namesymbolnumber 特性都变成私有的。为它们各定义一个 getter 属性来返回各自的值。

(9) 定义三个类 BearRabbitOctothorpe。对每个类都只定义一个方法 eats(),分别返回 'berries'Bear)、'clover'Rabbit) 和 'campers'Octothorpe)。为每个类创建一个对象并输出它们各自吃的食物(调用 eats())。

(10) 定义三个类 LaserClaw 以及 SmartPhone。每个类都仅有一个方法 does(),分别返回 'disintegrate'Laser)、'crush'Claw)以及 'ring'SmartPhone)。接着,定义 Robot 类,包含上述三个类的实例(对象)各一个。给 Robot 定义 does() 方法用于输出它各部分的功能。