【Python系列专栏】第二十篇 Python中的面向对象高级编程:多重继承

面向对象高级编程

前面一章介绍了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类的实例绑定 nameage 属性。

为了达到限制的目的,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 进行赋值,然后才可以访问 birthage,否则就会出现:

>>> 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 对象加上 widthheight 属性,以及一个只读属性 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

接下来定义好 RunnableFlyable 的类:

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,例如把 RunnableFlyable 改为 RunnableMixInFlyableMixInn 和 植食动物 HerbivoresMixIn,让某个动物同时拥有好几个MixIn:

class Dog(Mammal, RunnableMixIn, CarnivorousMixIn):
    pass

MixIn的目的就是给一个类增加多个功能,这样,在设计类的时候,我们可以优先考虑通过多重继承来组合多个MixIn的功能,而不是设计多层次的复杂的继承关系

Python自带的很多库也使用了MixIn。举个例子,Python自带了 TCPServerUDPServer 这两类网络服务,而要同时服务多个用户就必须使用多进程或多线程模型,这两种模型由 ForkingMixInThreadingMixIn 提供。通过组合,我们就可以创造出合适的服务来。

比如,编写一个多进程模式的TCP服务,定义如下:

class MyTCPServer(TCPServer, ForkingMixIn):
    pass

编写一个多线程模式的UDP服务,定义如下:

class MyUDPServer(UDPServer, ThreadingMixIn):
    pass

如果你打算搞一个更先进的协程模型,可以编写一个 CoroutineMixIn

class MyTCPServer(TCPServer, CoroutineMixIn):
    pass

这样一来,我们不需要复杂而庞大的继承链,只要选择组合不同的类的功能,就可以快速构造出所需的子类


小结

  • 由于Python允许使用多重继承,因此,MixIn就是一种常见的设计。

  • 只允许单一继承的语言(如Java)不能使用MixIn的设计。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Mrrunsen

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值