文章目录
面向对象高级编程
前面一章介绍了OOP最基础的数据封装、继承和多态3个概念,还有一些类和实例的操作。而在Python中,OOP还有很多更高级的特性,这一章会讨论多重继承、定制类、元类等概念。
使用 __slots__
动态绑定属性
正常情况下,当我们定义了一个类,创建了一个类的实例后,我们可以给这个实例绑定任何属性和方法,这就是动态语言的灵活性。先定义类:
class Student(object):
pass
然后,创建实例并给这个实例绑定一个属性:
>>> s = Student()
>>> s.name = 'Michael' # 动态给实例绑定一个属性
>>> print(s.name)
Michael
动态绑定方法
还可以尝试给实例绑定一个方法:
>>> def set_age(self, age): # 定义一个函数
... self.age = age
...
>>> from types import MethodType
>>> s.set_age = MethodType(set_age, s) # 把函数绑定到实例上,变为实例的方法
>>> s.set_age(25) # 调用实例方法
>>> s.age # 测试结果
25
注意到这里使用types模块的 MethodType()
函数来给实例绑定方法,为什么要用 MethodType()
而不是直接用 s.set_age = set_age
直接绑定呢?这是因为我们采用后者绑定时,只是绑定了一个外部函数,它与实例本身没有任何关联,没法使用self变量,而使用 MethodType()
就会真正地为实例绑定一个方法,也因此绑定的函数的第一个参数要设置为self变量。做个对比:
>>> s.set_age = set_age # 直接绑定
>>> s.set_age(25) # 无法调用self变量
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: set_age() missing 1 required positional argument: 'age'
>>> s.set_age(s,25) # 必须显式地传入实例s自身
>>> s.age
25
>>> from types import MethodType
>>> s.set_age = MethodType(set_age, s) # 使用MethodType绑定
>>> s.set_age(30) # 可以调用self变量,只需传入一个参数
>>> s.age
30
但是,给一个实例绑定的方法,对另一个实例是不起作用的:
>>> s2 = Student() # 创建新的实例
>>> s2.set_age(25) # 尝试调用方法
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'set_age'
为了给所有实例都绑定方法,可以直接给类绑定方法:
>>> def set_score(self, score):
... self.score = score
...
>>> Student.set_score = set_score
给类绑定方法不需要使用 MethodType()
函数,并且所有实例均可调用绑定在类上的方法:
>>> s.set_score(100)
>>> s.score
100
>>> s2.set_score(99)
>>> s2.score
99
通常情况下,上面的 set_score
定义在类中,但动态绑定允许我们在程序运行的过程中动态给类加上功能,这在静态语言中很难实现。
限制可绑定的属性/方法
上面两个小节介绍了怎样绑定属性和方法,但是如果我们想要限制可以绑定到实例的属性/方法怎么办呢?比方说,只允许对Student类的实例绑定 name
和 age
属性。
为了达到限制的目的,Python允许在定义类的时候,定义一个特殊的 __slots__
变量,来限制该类实例能添加的属性:
>>> class Student(object):
... `__slots__` = ('name', 'age') # 用tuple定义允许绑定的属性名称
然后,我们试试:
>>> s = Student() # 创建新的实例
>>> s.name = 'Michael' # 绑定属性'name'
>>> s.age = 25 # 绑定属性'age'
>>> s.score = 99 # 绑定属性'score'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'score'
由于属性 score
没有被放到 __slots__
变量中,所以实例不能绑定 score
属性,试图绑定 score
将得到 AttributeError
错误。
使用 __slots__
要注意,__slots__
变量的属性限制仅对当前类的实例起作用,对继承的子类是不起作用的:
>>> class GraduateStudent(Student):
... pass
...
>>> s1 = GraduateStudent()
>>> s1.score = 9999 # 可以绑定任何属性
但是!**如果在子类中也定义 __slots__
,则子类实例允许定义的属性就既包括自身的 __slots__
也包括父类的 __slots__
**:
>>> class GraduateStudent(Student):
... __slots__ = ('score')
...
>>> s2 = GraduateStudent()
>>> s2.name = 'Angela'
>>> s2.age = 17
>>> s2.score = 99
>>> s2.sex = 'Female' # 无法绑定父类__slots__和当前类__slots__都没有的属性
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'GraduateStudent' object has no attribute 'sex'
使用@property
为何需要@property
在绑定属性时,如果我们直接把属性暴露出去供使用者修改,虽然写起来很简单,但是没办法检查设置的属性值是否合理,可以把成绩随便改:
s = Student()
s.score = 9999 # 直接通过属性修改
这显然不合逻辑。为了限制 score
的范围,可以通过一个 set_score()
方法来设置成绩,再通过一个 get_score()
来获取成绩,这样,在 set_score()
方法里,就可以检查参数:
class Student(object):
def get_score(self):
return self._score
def set_score(self, value):
if not isinstance(value, int):
raise ValueError('score must be an integer!')
if value < 0 or value > 100:
raise ValueError('score must between 0 ~ 100!')
self._score = value
现在,对任意的Student实例进行操作,就不能随心所欲地设置score了:
>>> s = Student()
>>> s.set_score(60) # 通过类的方法修改
>>> s.get_score()
60
>>> s.set_score(9999)
Traceback (most recent call last):
...
ValueError: score must between 0 ~ 100!
但是,通过类的方法修改,调用者使用时比较麻烦,没有直接使用属性进行修改简单,而且对调用者是否自觉也有要求,如果调用者依然直接使用属性修改,就没法检查属性值了。
有没有既能检查属性值,又可以直接使用属性修改的办法呢?答案是有的!
如何实现@property
在第四章-函数式编程中,我们学习到了装饰器(decorator),它可以给函数动态添加功能。事实上,不仅是对函数,装饰器对类的方法一样起作用。Python内置的 @property
装饰器就可以帮助我们实现前面的需求,把一个方法变成属性调用:
class Student(object):
@property
def score(self): # 对应getter方法,也即前面例子的get_score(self)
return self._score
@score.setter
def score(self, value): # 对应setter方法,也即前面例子的set_score(self, value)
if not isinstance(value, int):
raise ValueError('score must be an integer!')
if value < 0 or value > 100:
raise ValueError('score must between 0 ~ 100!')
self._score = value
@property
的实现比较复杂。准确地说,把一个getter方法变成属性,只需要加上 @property
装饰器就可以了,而把一个setter方法变成属性赋值,这要加上一个 @score.setter
装饰器,也即 @属性名.setter
。注意!属性名和方法名一定要区分开,否则会出错!这里我们把 score
属性改为 _score
属性,所以对内部来说 _score
是属性,score
是方法,对外部来说 score
是属性,_score
被封装起来了(因为我们使用了装饰器进行转换)。看看实际效果:
>>> s = Student()
>>> s.score = 60 # 实际转化为s.score(60)
>>> s.score # 实际转化为s.score()
60
>>> s.score = 9999 # 对外部来说可以直接使用属性赋值,同时也能检查属性值
Traceback (most recent call last):
...
ValueError: score must between 0 ~ 100!
还可以定义只读属性,只定义getter方法,不定义setter方法就是一个只读属性,只读属性只能获取属性值,无法设置属性值:
class Student(object):
@property
def birth(self):
return self._birth
@birth.setter
def birth(self, value):
self._birth = value
@property
def age(self): # 只读属性age,根据birth进行计算
return 2015 - self._birth
上面的birth是可读写属性,而age就是一个只读属性:。
>>> s = Student()
>>> s.birth = 2000 # 可读写属性birth可以进行赋值
>>> s.birth
2000
>>> s.age
16
>>> s.age = 17 # 只读属性age无法进行赋值
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: can't set attribute
注意必须先对属性 birth
进行赋值,然后才可以访问 birth
和 age
,否则就会出现:
>>> s.birth
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in birth
AttributeError: 'Student' object has no attribute '_birth'
>>> s.age
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 10, in age
AttributeError: 'Student' object has no attribute '_birth'
练习
请利用 @property
给一个 Screen
对象加上 width
和 height
属性,以及一个只读属性 resolution
:
class Screen(object):
@property
def width(self):
return self._width
@width.setter
def width(self, value):
self._width = value
@property
def height(self):
return self._height
@height.setter
def height(self, value):
self._height = value
@property
def resolution(self):
return self._width * self._height
测试:
>>> s = Screen()
>>> s.width = 1024
>>> s.height = 768
>>> print(s.resolution)
786432
>>> assert s.resolution == 786432, '1024 * 768 = %d ?' % s.resolution
小结
@property
广泛应用在类的定义中,可以让调用者写出简短的代码,同时又保证了对属性值进行必要的检查,这样,程序运行时就减少了出错的可能性。
多重继承
为何需要多重继承
在第六章-面向对象编程中,我们学习了面向对象编程的一个重要性质——继承。通过继承,子类可以获得父类的所有功能并进行进一步扩展。
假设我们设计了一个 Animal
类,并要为以下4种动物设计四个新的类:
- Dog - 狗狗;
- Bat - 蝙蝠;
- Parrot - 鹦鹉;
- Ostrich - 鸵鸟。
使用多重继承
因为能跑和能飞这两个类不受限于动物类,它们是独立的。我们单独实现这两个类,即使要再实现其他非动物的类,比如汽车和飞机,也能很轻松地继承它们的功能,而不需要再重复构造功能类似的新的类。而动物分类方面,假设我们加入宠物非宠物的分类,也不需再构造哺乳的能飞的宠物、鸟类的能飞的宠物等等类别,通过多重继承免去了很多麻烦。先进行动物分类的定义:
class Animal(object):
pass
# 大类:
class Mammal(Animal):
pass
class Bird(Animal):
pass
接下来定义好 Runnable
和 Flyable
的类:
class Runnable(object):
def run(self):
print('Running...')
class Flyable(object):
def fly(self):
print('Flying...')
对于需要 Runnable
功能的动物,只需要多继承一个 Runnable
,例如 Dog
class Dog(Mammal, Runnable):
pass
对于需要 Flyable
功能的动物,只需要多继承一个 Flyable
,例如 Bat
:
class Bat(Mammal, Flyable):
pass
通过多重继承,一个子类可以同时获得多个父类的所有功能。
MixIn
在设计类的继承关系时,通常主线都是单一继承下来的,例如,Ostrich
继承自 Bird
。但是,如果需要混入额外的功能,通过多重继承就可以实现,比如,让 Ostrich
除了继承自 Bird
外,再同时继承 Runnable
。种**(利用多重继承混入额外的功能)这种设计方式通常称之为MixIn**。
为了更好地看出继承关系,我们通常把用于添加额外功能的类命名带上一个后缀MixIn,例如把 Runnable
和 Flyable
改为 RunnableMixIn
和 FlyableMixIn
n 和 植食动物 HerbivoresMixIn
,让某个动物同时拥有好几个MixIn:
class Dog(Mammal, RunnableMixIn, CarnivorousMixIn):
pass
MixIn的目的就是给一个类增加多个功能,这样,在设计类的时候,我们可以优先考虑通过多重继承来组合多个MixIn的功能,而不是设计多层次的复杂的继承关系。
Python自带的很多库也使用了MixIn。举个例子,Python自带了 TCPServer
和 UDPServer
这两类网络服务,而要同时服务多个用户就必须使用多进程或多线程模型,这两种模型由 ForkingMixIn
和 ThreadingMixIn
提供。通过组合,我们就可以创造出合适的服务来。
比如,编写一个多进程模式的TCP服务,定义如下:
class MyTCPServer(TCPServer, ForkingMixIn):
pass
编写一个多线程模式的UDP服务,定义如下:
class MyUDPServer(UDPServer, ThreadingMixIn):
pass
如果你打算搞一个更先进的协程模型,可以编写一个 CoroutineMixIn
:
class MyTCPServer(TCPServer, CoroutineMixIn):
pass
这样一来,我们不需要复杂而庞大的继承链,只要选择组合不同的类的功能,就可以快速构造出所需的子类。
小结
-
由于Python允许使用多重继承,因此,MixIn就是一种常见的设计。
-
只允许单一继承的语言(如Java)不能使用MixIn的设计。