类与继承

类与继承

1 多重继承

1.1 方法解析顺序MRO

提出问题:如果同级别的超类定义了同名属性,Python如何决定使用哪一个?

答案是:方法解析顺序(Method Resolution Order)

任何实现多重继承的语言都要处理潜在的命名冲突,这种冲突由不相关的祖先类实现同名方法引起。这种冲突叫做砖石(菱形)继承问题,如下图:
在这里插入图片描述在这里插入图片描述

第一个图是菱形问题的UML类图,第二个图橙色标记是示例的方法解析顺序。示例1

class A:
    def ping(self):
        print('ping:',self)

class B(A):
    def pong(self):
        print('pong:',self)

class C(A):
    """B和C都实现了pong()方法,唯一不同在pong与PONG"""
  	def pong(self):
		print('PONG:',self)

class D(B,C):
    def ping(self):
		super().ping()
        print('post-ping:',self)

直接调用d.pong()运行的是B类的pong方法;而且超类中的方法可以直接调用,只需要把实例作为显式参数传入。演示1

>>> d = D()
>>> d.ping()
ping: <__main__.D object at 0x000002A6544ABB00>
post-ping: <__main__.D object at 0x000002A6544ABB00>
>>> d.pong()
pong: <__main__.D object at 0x000002A6544ABB00>
>>> C.pong(d)
PONG: <__main__.D object at 0x000002A6544ABB00>
>>>

Python能区分d.pong()调用的是哪个方法,是因为Python会按特定的顺序遍历继承图。这个顺序叫方法解析顺序(Method Resolution Order)。

类有一个名为__mro__的属性(类实例没有),它是一个元组,按照方法解析顺序列出各个超类,从当前类一直到object类。如下D.__mro__

>>> D.__mro__
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)

运行结果所示,D类的方法解析顺序为D、B、C、A,所以调用d.pong()时,运行的是B类的pong方法。

查看类__mro__属性的例子:

>>> dict.__mro__
(<class 'dict'>, <class 'object'>)
>>> bool.__mro__
(<class 'bool'>, <class 'int'>, <class 'object'>)

1.2 super()函数

从演示1看到,可以绕过方法解析顺序(C.pong(d)),直接调用超类中的方法(直接在类上调用实例方法,必须显示传入self参数,因为这样访问的是未绑定方法);那么,D.ping方法可以改写成:

def ping(self):
	A.ping(self) #替换super().ping()
	print("post-ping:",self)

注意:使用super()最安全。super()调用方法时,会遵守方法解析顺序(MRO)。

1.3 如何处理多重继承?

2 抽象基类

抽象基类是用于封装框架引入的一般性概念和抽象的。基本上不需要自己编写新的抽象基类,只要正确使用现有的抽象基类,就能获得99.9%的好处,而不用冒着设计不当导致的巨大风险。

抽象类表示接口,协议是非正式的接口;抽象基类的常见用途:定义接口时作为超类使用。

继承抽象基类非常简单,只需要实现所需的方法(这样也能明确开发者的意图)。

协议与接口概念

  • 协议是Python中非正式的接口,是令Python这种动态类型语言实现多态的方式。
  • 接口:对象公开方法的子集,让对象在系统中扮演特定的角色。接口是实现特定角色的方法集合,这样理解也就是所说的协议。
  • 类的接口:类实现或继承的公开属性(方法或数据属性),包括特殊方法,如__getitem__

2.1 抽象基类的定义

声明抽象基类最简单的方式是继承abc.ABC或其它抽象基类。而抽象方法使用@abstractmethod装饰器标记,定义体中通常只有文档字符串;抽象方法也可以有实现,不过就算实现了,子类也必须覆盖抽象方法。但是在子类可以使用super()函数调用抽象方法,为它添加新功能,而不是重头开始实现。

当然,抽象基类中可以包括具体方法。

示例2:实现一个MyList抽象基类,该类提供两个抽象方法和一个具体方法。

import abc

class MyList(abc.ABC):
    @abc.abstractmethod
    def load(self, iterable):
        """从iterable中添加对象"""
    
    @abc.abstractmethod
    def pick(self):
        """随机删除元素,然后将其返回
        如果实例为空,应抛出'LookupError'
        """
    
    def getdata(self):
        """返回当前所有元素的元组"""
        data = []
        while True:
            try:
                data.append(self.pick())
            except LookupError:
                break
        self.load(data)
        return tuple(data)
              

抽象类无法实例化。如下演示2:Fake类继承了抽象基类MyList,但没有实现所需的抽象方法(只实现了load抽象方法,未实现pack抽象方法),所以Python认为Fake类仍然是抽象类,导致实例化异常。

>>> class Fake(MyList):
...     def load(self, iterable):
...         self.data = list(iterable)
... 
>>> f = Fake()
Traceback (most recent call last):
  File "<pyshell#7>", line 1, in <module>
    f = Fake()
TypeError: Can't instantiate abstract class Fake with abstract methods pick

2.2 @abstractmethod装饰器

abc模块定义了abstractmethod装饰器,用于声明抽象方法。

如果需要定义抽象类方法(抽象静态方法),可使用装饰器叠加,装饰器叠加使用时,@abstractmethod应放在最下层。

class MyABC(abc.ABC):
    @classmethod
    @abstractmethod
    def abstrct_cls_method(cls, ...):
        pass

2.3 register方法声明(注册)虚拟子类

注册虚拟子类的方式是在抽象基类上调用register方法。注册的类会变成抽象基类的虚拟子类,而且issubclassisinstance等函数也能识别,但是注册的类不会从抽象基类中继承任何方法和属性。

可以将虚拟子类理解为干儿子:大家都承认你是抽象基类子类,但并没有遗传其任何东西。

示例3:两种方式(方法调用与装饰器)声明虚拟子类。

from abc import ABC
class MyABC(ABC):
    pass

MyABC.register(tuple)#将tuple注册为MyABC的虚拟子类
from abc import ABC
class MyABC(ABC):
    pass

@MyABC.register#将MyTuple注册为MyABC的虚拟子类(python3.3+)
class MyTuple:
	pass

示例4:使用register注册为MyList的虚拟子类。

​ 虚拟子类不会继承注册的抽象基类,而且任何时候都不会检查是否符合抽象基类的接口。示例4展示了不符合抽象基类接口的虚拟子类(为了避免未知错误,最好实现所需的全部方法):

@MyList.register
class Virtual:
    def get(self, key):
        return 'Virtual SubClasses Of ABC MyList'

注册虚拟子类后,issubclassisinstance都判断Virtual类是MyList的子类。

>>> v = Virtual()
>>> issubclass(Virtual,MyList)
True
>>> isinstance(v,MyList)
True
>>> v.get(2)#v.get(any)
'Virtual SubClasses Of ABC MyList'

__mro__类属性中只包含“真实”子类,所以Virtual.__mro__中并不会存在注册的抽象基类。

>>> Virtual.__mro__
(<class '__main__.Virtual'>, <class 'object'>)

2.4 类属性__subclasses__()_abc_registry

  • __subclasses__()

    这个方法返回类的直接子类列表(内存中存在的直接子代),不包含虚拟子类。

  • _abc_registry

    只有抽象基类有这个属性,其值是一个WeakSet对象,即抽象类注册的虚拟子类的弱引用。

    >>> MyList._abc_registry
    <_weakrefset.WeakSet object at 0x000002412BC54390>
    >>> list(MyList._abc_registry)
    [<class '__main__.Virtual'>]
    

3 子类化内置类型会出乎你的意料

在Python2.2之后,内置类型都可以子类化,但是有一个注意事项:内置类型(使用C语言编写)不会调用用户定义的类覆盖的特殊方法。意思是内置类型的方法可能不会调用子类覆盖的方法,导致结果出现与预期不一致。

示例5:子类化内置类型dictdict.update方法会忽略DiyDict.__getitem__方法。

class DiyDict(dict):
    def __getitem__(self, key):
        return 'hello,world!'
>>> dd = DiyDict(one=1)
>>> dd
{'one': 1}
>>> dd['one']
'hello,world!'
>>> d = dict()
>>> d.update(dd)#忽略子类覆盖的__setitem__方法
>>> d
{'one': 1}
>>>

示例6:内置类型dict__init____update__方法会忽略子类覆盖的__setitem__方法。

class DIYDict(dict):
    def __setitem__(self, key, value):
        super().__setitem__(key, "hello world")
>>> dd = DIYDict(one=1)
>>> dd#dict.__init__方法忽略子类覆盖的__setitem__方法
{'one': 1}
>>> dd['two'] = 2#__setitem__方法正常运行
>>> dd
{'one': 1, 'two': 'hello world'}
>>> #dict.__init__方法忽略子类覆盖的__setitem__方法
>>> dd.update(three=3)
>>> dd
{'one': 1, 'two': 'hello world', 'three': 3}
>>>

注意:直接子类化内置类型(如strlistdict)容易出错,因为内置类型的方法通常会忽略用户覆盖的方法。如果需要子类化,用户定义的类应该继承colletcions模块中的类(如UserDictUserListUserString)。

示例7:子类化collections.UserDict,不会出现忽略子类覆盖方法的问题。

import collections

class MyDict(collections.UserDict):
    def __setitem__(self, key, value):
        super().__setitem__(key, "hello world")
>>> dd = MyDict(one=1)
>>> dd
{'one': 'hello world'}
>>> dd['two'] = 2
>>> dd
{'one': 'hello world', 'two': 'hello world'}
>>> dd.update(three=3)
>>> dd
{'one': 'hello world', 'two': 'hello world', 'three': 'hello world'}
>>>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值