5. 类

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

对象既包含数据(变量,更习惯称之为特性,attribute),也包含代码(函数,也成为方法)。它是某一类具体事务的特殊实例。例如,整数 7 就是一个包含了加法、乘法之类方法的对象。整数 8 则是另一个对象。这意味着在 Python 里,7和8都属于一个公共的类,我们称之为整数类。(字符串、list、dict)

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

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

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

术语

property 属性 attribute 特性

使用 class 定义类

想要在Python中创建属于自己的类使用关键字 class 来定义,我们先看个例子。

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

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

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

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

1
>>> someone = Person()

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

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

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

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

当你在类生命定义 init() 方法时,第一个参数必须为 self。尽管 self 并不是一个 Python 保留字,但它很常用。

尽管我们添加了初始化方法,但用这个 Person 类创建的对象仍然什么也做不了。接着我们在初始化方法中添加 name 参数:

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

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

1
>>> hunter = Person("xiao ming")

python执行: Person.init(huter, “xiao ming”) self 其实就是代表要实例化的对象,这个例子里是 hunter。

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

  • 查看 Person 类的定义;
  • 在内存中实例化(创建)一个新的对象
  • 调用对象的 init 方法, 将这个新创建的对象作为 self 传入,并将另一个参数(‘xiao ming’)作为 name 传入;
  • 将 name 的值存入对象;
  • 返回这个新的对象;
  • 将名字 hunter 与这个对象关联。

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

我们传入的 name 参数 作为对象的特性存储在了对象里。可以直接对它进行读写操作:

1
2
>>> print('The mighty hunter: ', hunter.name)
The mighty hunter:  xiao ming

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

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

继承

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

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

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

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

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

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

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

1
2
>>> give_me_a_car = Car()
>>> give_me_a_tesla = Tesla()

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

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

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

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

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

覆盖方法

新创建的子类会自动继承父类的所有信息。接下来我们来看子类如何替代——覆盖(override)父类的方法。

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

为每个类创建一个对象:

1
2
>>> give_me_a_car = Car()
>>> give_me_a_tesla = Tesla()

执行看下结果:

1
2
3
4
>>> give_me_a_car.exclaim()
I'm a Car!
>>> give_me_a_tesla.exclaim()
I'm a Tesla!

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

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

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

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

添加新方法

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
>>> class Car():
...     def exclaim(self):
...         print("I'm a Car!")
...
>>> class Tesla(Car):
...     def exclaim(self):
...         print("I'm a Tesla!")
...     def need_a_push(self):
...         print("A little help here?")
...

接着创建一个 Car 和一个 Tesla 对象:

1
2
>>> give_me_a_car = Car()
>>> give_me_a_tesla = Tesla()

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

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

但父类 Car 无法使用该方法:

1
2
3
4
5
>>> give_me_a_car.need_a_push()
Traceback (most recent call last):
  File "<ipython-input-98-61aca925ea27>", line 1, in <module>
    give_me_a_car.need_a_push()
AttributeError: 'Car' object has no attribute 'need_a_push'

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

super

我们已经知道如何在子类中覆盖父类的方法,但如果想要调用父类的方法就要使用 super()。下面的例子将新定义一个新的类 EmailPerson,用于表示有电子邮箱的 Person。首先,来定义熟悉的Person类:

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

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

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

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

  • 通过 super() 方法获取了父类 Person 的定义。
  • 子类的 init() 调用了 Person.init() 方法。它会自动将 self 参数传递给父类。因此,你只需传入其余参数即可。在上面的例子中,Person() 能接受的其余参数指的是 name。
  • self.email = email 这行新的代码才真正起到了将 EmailPerson 与 Person 区分开的作用。

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

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

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

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

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

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

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

子类可以按照自己的方式处理问题,但如果人需要借助父类的帮助,使用 super() 是最佳的选择。

self

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

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

Python 在背后做了一下两件事:

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

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

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

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

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

property 属性 attribute 特性

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

实际上,如果有人能猜到我们在类的内部用的特性名是 hidden_name,他仍然可以直接通过 fowl.hidden_name 进行读写操作。

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

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

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

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

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

1
2
>>> c.diameter
10

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

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

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

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

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

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

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12

>>> class Duck():
...     def __init__(self, input_name):
...         self.__name = input_name
...     @property
...     def name(self):
...         print('inside the getter')
...     @name.setter
...     def name(self, input_name):
...         print('inside the setter')
...         self.__name = input_name
...

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

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

看起来没问题,现在,你无法在外部访问 __name 特性了:

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

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

1
2
>>> fowl._Duck__name
'Donald'

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

方法的类型

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

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

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

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

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

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

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

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

鸭子类型

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

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

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

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

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

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

接下来创建一些对象:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
>>> hunter = Quote('Elmer Fudd', "I'm hunting rabbits")
>>> print(hunter.who(), 'says:', hunter.says())
Elmer Fudd says: I'm hunting wabbits.

>>> hunted1 = QuestionQuote('Bugs Bunny', "What's up, doc")
>>> print(hunted1.who(), 'says:', hunted1.says())
Bugs Bunny says: What's up, doc?

>>> hunted2 = ExclamationQuote('Daffy Duck', "It's rabbit season")
>>> print(hunted2.who(), 'says:', hunted2.says())
Daffy Duck says: It's rabbit season!

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

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

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

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

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

1
2
  如果它想鸭子一样走路,像样子一样叫,那么它就是一直鸭子。
                                                   ———— 以为智者

特殊方法

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

太神奇了!是不是如同魔术一般?仅需将方法名改为 Python 里进行相等比较的特殊方法名 eq() 即可。下面列出一些常用的魔术方法:

和比较相关的魔术方法

和数学相关的魔术方法

不仅数字类型可以使用像 + (魔术方法 add())和 - (魔术方法 sub())的数学运算符,一些其他的类型也可以使用。例如,Python 的字符类型使用 + 进行拼接,使用 * 进行复制。字符串常见的魔术方法如下:

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

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

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

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

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

组合

如果你想要创建的子类在大多数情况下的行为都和父类相似的话,使用继承是非常不错的选择,建立复杂的继承关系确实很吸引人,但有些时候使用组合(composition)或者聚合(aggregation)更加符合现实的逻辑。一只鸭子是鸟的一种,它有一条尾巴。尾巴并不是鸭子的一种,它是鸭子的组成部分。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
>>> class Tail():
...     def __init__(self, length):
...         self.length = length

>>> class Bill():
...     def __init__(self, description):
...         self.description = description

>>> class Duck():
...     def __init__(self, bill, tail):
...         self.bill = bill
...         self.tail = tail
...     def about(self):
...         print('This duck has a', bill.description, 'bill and a',
... tail.length, 'tail')
...
>>> tail = Tail('long')
>>> bill = Bill('wide orange')
>>> duck = Duck(bill, tail)
>>> duck.about()
This duck has a wide orange bill and a long tail
>>>

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

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

  • 当你需要许多具有相似行为(方法)但不同状态(特性)的实例时,使用对象是最好的选择。
  • 类支持继承,但模块不支持。
  • 如果你想要保证实例的唯一性,使用模块是最好的选择。不管模块在程序中被引用多少次,始终只有一个实例被加载(单例)。
  • 如果你有一系列包含多个值的变量,并且它们能作为参数传入不同的函数,那么最好将它们封装到类里面。举个例子,你可能会使用以 size 和 color 为键的字典代表一张彩色图片。你可以在程序中为每张图片创建不同的字典,并把它们作为参数传递给像 scale() 或者 transform() 之类的函数。但这么做的话,一但你想要添加其他的键或者函数会变得非常麻烦。为了确保统一性,应该定义一个 Image 类,把 size 和 color 作为特性,把 scale() 和 transform 定义为方法。这么一来,关于一张图片的所有数据和可执行的操作都存储在了统一的位置。
  • 用最简单的方式解决问题。使用字典、列表和元组往往要比使用模块更加简单、简洁且快速。而使用类则更为复杂。

创始人 Guido 的建议:

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

                     		                                          —— Guid van Rossum