1. 程式人生 > >深度解析並實現python中的super

深度解析並實現python中的super

概述

python中的super是一個神奇的存在。本文對python中的super進行深入的講解,首先說明super的定義,並列舉一下super的典型用法,然後會對和super相關的語言特性進行講解,比如mro(方法解析順序),descriptor描述器,函式繫結,最後嘗試自己動手實現一個super,並簡單探索一下python中對super的實現。

super的定義

首先看一下super的定義,當然是help(super)看一下文件介紹:

Help on class super in module builtins:

class super(object)
 |  super() ->
same as super(__class__, <first argument>) | super(type) -> unbound super object | super(type, obj) -> bound super object; requires isinstance(obj, type) | super(type, type2) -> bound super object; requires issubclass(type2, type) | Typical use to call a cooperative superclass method:
| class C(B): | def meth(self, arg): | super().meth(arg) | This works for class methods too: | class C(B): | @classmethod | def cmeth(cls, arg): | super().cmeth(arg)

從文件裡可以看出以下幾點:

1 super是一個類

super不是關鍵字,而是一個類, 呼叫super()會建立一個super物件:

>>> class A:
... def __init__(self): ... su = super() ... print(su) ... print(type(su)) ... >>> a = A() <super: <class 'A'>, <A object>> <class 'super'>

或者:

>>> class A:
...     pass
...
>>> a = A()
>>> su = super(A, a)
>>> su
<super: <class 'A'>, <A object>>
>>> type(su)
<class 'super'>
>>>

2 super支援四種呼叫方式

  1. super()
  2. super(type, obj)
  3. super(type)
  4. super(type, type1)

其中super(type)建立一個未繫結super物件(unbound),其餘三種方式建立的是繫結的super物件(bound)。super()是python3中支援的寫法,是一種呼叫上的優化,其實相當於第一個引數傳入呼叫super的當前的類,第二個引數傳入呼叫super的方法的第一個引數。

關於super的定義先介紹到這裡,下面介紹bound相關的概念,bound的概念又和描述器相關,所以接下來介紹函式bound和描述器

函式bound和描述器

要理解bound,首先要理解在python中,函式都是物件,並且是描述器。

函式都是物件:

>>> def test():
...     pass
...
>>> test
<function test at 0x10a989268>
>>> type(test)
<class 'function'>
>>>

test是一個函式,同時又是一個function物件。所以當我們使用def定義一個函式的時候,相當於建立一個function物件。因為function實現了__call__方法,所以可以被呼叫:

>>> getattr(test, '__call__')
<method-wrapper '__call__' of function object at 0x10a989268>
>>>

由於function實現了__get__方法,所以,函式物件又是一個描述器物件(descriptor):

>>> getattr(test, '__get__')
<method-wrapper '__get__' of function object at 0x10a989268>

因為根據python的定義,只要實現了__get__, __set__和__delete__中的一個或多個,就認為是一個描述器。

描述器的概念和bound的概念,在模組函式上提現不出來,但是如果一個函式定義在類中,這兩個概念會體現的很明顯。

下面我們在類中定義一個函式:

>>> class A:
...     def test(self):
...         pass
...

首先驗證在類中定義的函式也是一個function物件:

>>> A.__dict__['test']
<function A.test at 0x10aab4158>
>>>
>>> type(A.__dict__['test'])
<class 'function'>
>>>
>>>

下面驗證在類中定義的函式也是一個描述器,也就是驗證實現了__get__方法:

>>> getattr(A.__dict__['test'], '__get__')
<method-wrapper '__get__' of function object at 0x10aab4158>
>>>

從上面的驗證可以看到,在類中定義的函式,也是一個描述器物件。所以可以認為在類中定義函式,相當於定義一個描述器。所以當我們寫下面程式碼時:

class A:
    def test(self):
        pass

相當於這樣:

class A:
    test = function()

下面簡單講一下描述器的特性。看下面的程式碼:

class NameDesc:
    def __get__(self, instance, cls):
        print('NameDesc.__get__:', self, instance, cls)
        if instance is None: #通過類訪問描述器的時候,instance為None
            return self
        else:
            return instance.__dict__['_name']

    def __set__(self, instance, value):
        print('NameDesc.__set__:', self, instance, value)
        if not isinstance(value, str):
            raise TypeError('expect str')
        instance.__dict__['_name'] = value

class Person:
    name = NameDesc()

p = Person()

p.name = 'zhang'
print(p.name)
print(Person.name)

輸出結果為:

NameDesc.__set__: <__main__.NameDesc object at 0x10babaf60> <__main__.Person object at 0x10babaf98> zhang
NameDesc.__get__: <__main__.NameDesc object at 0x10babaf60> <__main__.Person object at 0x10babaf98> <class '__main__.Person'>
zhang
NameDesc.__get__: <__main__.NameDesc object at 0x10e8dbf98> None <class '__main__.Person'>
<__main__.NameDesc object at 0x10e8dbf98>

當一個類(Person)中存在一個描述器屬性(name), 當這個屬性被訪問時,會自動呼叫描述器的__get__和__set__方法:

  1. 當使用類名訪問描述器時(Person.name) , __get__方法返回描述器本身
  2. 當使用物件訪問描述器時(p.name), __get__方法會返回自定義的值(instance._name),我們可以自定義返回任何值,包括函式

回到上面的兩段等效程式碼:

class A:
    def test(self):
        pass
class A:
    test = function()

那麼既然test是一個描述器,那麼我通過A呼叫test和通過a呼叫test時,會返回什麼呢?下面直接看結果:

>>> class A:
...     def test(self):
...         pass
...
>>> A.test
<function A.test at 0x1088db0d0>
>>>
>>> A.test is A.__dict__['test']
True
>>>
>>> a = A()
>>> a.test
<bound method A.test of <__main__.A object at 0x1088d9780>>

通過類A訪問test(A.test),還是會返回test這個描述器自身,也就是A.dict[‘test’] 通過物件a訪問test(a.test), 返回一個bound method。

所以我們可以認為:

  1. function的__get__方法,當不傳入instance時(相當於A.test),會返回function本身
  2. 當傳入一個instance的時候(相當於a.test),會返回一個bound method。

下面的程式碼可以驗證這個結論:

>>> A.test.__get__(None, A)
<function A.test at 0x1088db158>
>>> A.test.__get__(None, A) == A.test
True
>>>
>>> A.test.__get__(a, A)
<bound method A.test of <__main__.A object at 0x1088d9860>>
>>> A.test.__get__(a, A) == a.test
True

所以我們可以認為描述器function的實現方式如下:

class function:

    def __get__(self, instance, cls):
        if instance is None: #通過類呼叫
            return self
        else: #通過物件呼叫
            return self._translate_to_bound_method(instance)

    def _translate_to_bound_method(self, instance):
        #
        # ...
        #


class A:
    test = function()

下面看一下繫結(bound)和非繫結(unbound)到底有什麼區別。 接著看下面的示例:

>>> class A:
...     def test(self):
...         print('*** test ***')
...
>>> a = A()
>>>
>>> A.test(a)
*** test ***
>>>
>>> a.test()
*** test ***
>>>

我們看到,在定義A的時候,test方法是有一個引數self的。 A.test返回一個function物件,是一個未繫結函式,所以呼叫的時候要傳物件(A.test(a)) a.test返回一個bound method物件,是一個繫結函式,所以呼叫的時候不需要再傳入物件(a.test())

可以看出,所謂繫結,就是把呼叫函式的物件,繫結到函式的第一個引數上。

做一個總結,本節主要講解了函式,描述器和繫結的概念。結論就是function是一個可以被呼叫(實現了__call__方法)的描述器(實現了__get__方法)物件,並且通過類獲取函式物件的時候,__get__方法會返回function本身,通過例項獲取函式物件的時候,__get__方法會返回一個bound method,也就是將例項繫結到這個function上。

下面再回到super。

super的典型用法

很多人對super直觀的理解是,呼叫父類中的方法:

class A:
    def test(self):
        print('A.test')

class B(A):
    def test(self):
        super().test()
        print('B.test')

b = B()
b.test()

執行結果為:

A.test
B.test

從上面的例子看來,super確實可以呼叫父類中的方法。但是看下面的程式碼:

class A:
    def test(self):
        print('A.test')

class TestMixin:
    def test(self):
        print('TestMixin.test')
        super().test()

class B(TestMixin, A):
    def test(self):
        print('B.test')
        super().test()


b = B()
b.test()

列印結果:

B.test
TestMixin.test
A.test

上面的程式碼先建立B的物件b,然後呼叫b.test(),但是B的test函式通過super(),會調到第一個父類TestMixin的test函式,因為TestMixin是B的第一個父類。

TestMixin中的test函式中通過super調到了A中的test函式,但是A不是TestMixin的父類。在這個繼承體系中,A和TestMixin都是B的父類,但是A和TestMixin沒有任何繼承關係。為什麼TestMixin中的super會調到A中的test函式呢?

super的本質

其實super不是針對呼叫父類而設計的,它的本質是在一個由多個類組成的有序集合中搜尋一個特定的類,並找到這個類中的特定函式,將一個例項繫結到這個函式上,生成一個繫結方法(bound method),並返回這個bound method。

上面提到的由多個類組成的有序集合,即是類的mro,即方法解析順序(method resolution ),它是為了確定在繼承體系中,搜尋要呼叫的函式的順序的。通過inspect.getmro或者類中的__mro__屬性可以獲得這個集合。還是以上面的A, TestMixin,B為例:

class A:
    def test(self):
        print('A.test')

class TestMixin:
    def test(self):
        print('TestMixin.test')
        super().test()

class B(TestMixin, A):
    def test(self):
        print('B.test')
        super().test()


#b = B()
#b.test()

print(B.__mro__)

輸出結果為:

(<class '__main__.B'>, <class '__main__.TestMixin'>, <class '__main__.A'>, <class 'object'>)

可見B的mro為(B, TestMixin, A, object)。這個列表的意義是B的例項b在呼叫一個函式時,首先在B類中找這個函式,如果B中呼叫了super,則需要從B的下一個類(即TestMixin)中找函式,如果在TestMixin中又呼叫了super,則從TestMixin的下一個類(即A)中找函式。

在python 2.x中,要成功呼叫super必須指定兩個引數才行,即super(type,obj)或super(type, type1)。為了直觀, 我們用這種帶引數的形式改寫上面的示例:

class A:
    def test(self):
        print('A.test')

class TestMixin:
    def test(self):
        print('TestMixin.test')
        super(TestMixin, self).test()

class B(TestMixin, A):
    def test(self):
        print('B.test')
        super(B, self).test()


print(B.__mro__)

b = B()
b.test()

其實這兩個引數很關鍵,第一個引數是當前呼叫super的類,這個引數就是為了在mro中找到下一個類,然後從這個類開始搜尋函式。第二個引數有兩個作用,一是確定從哪個類獲取mro列表,二是作為例項,繫結到要呼叫的函式上。

我們以TestMixin的super(TestMixin, self).test()為例,解釋這兩個引數的意義。

先看第二個引數,需要知道, 當從b.test()一層層的向上調時,self始終是例項b,所以不管調到哪個類中的super,self始終是b,通過這個self獲取的mro永遠都是B的mro。當獲取到mro後,就在mro中找第一個引數TestMixin的下一個類,這裡是A, 並且在A裡面查詢有沒有目標函式,如果沒有,就在A類的下一個類中找,依次類推。

還有,通過super(TestMixin, self)建立的是super物件,super並沒有test方法,那麼super(TestMixin)為什麼能呼叫test方法呢?

這是因為當一個物件呼叫類中沒有的方法時,會呼叫類的__getattr__方法,在super中只要實現這個方法,就會攔截到super(TestMixin, self)對test的訪問,根據上面的介紹,super中可以根據傳入的TestMixin和self,確認了要在A中查詢方法,所以這裡我們可以直接從A查詢test函式,如果A中沒有,那麼就從mro中A後面的類依次查詢。

等找到這個函式後,不能直接返回這個test函式,因為這個函式還沒有繫結,需要通過這個函式(也是描述器)的__get__函式,將self例項傳入,獲得一個繫結方法(bound method),然後將這個bound method返回。所以到此為止,super(TestMixin, self).test 就獲取了一個bound method, 這個是A中的函式,並且綁定了self例項(這個例項是b)。然後在後面加一個(), super(TestMixin, self).test()的意義就是呼叫這個bound method。所以就調到了A中的test函式:

class A:
    def test(self):
        print('A.test')

因為繫結的是例項b, 所以上面test中傳入的self就是例項b。

到此為止,super的原理就講完了。

自定義super

上面講解了super的本質,根據上面的講解,我們自己來實現一個my_super:

class my_super:
    def __init__(self, thisclass=None, target=None):
        self._thisclass = thisclass
        self._target = target


    def _get_mro(self):
        if issubclass(type, type(self._target)):
            return self._target.__mro__ #第二個引數是型別
        else:
            return self._target.__class__.__mro__ #第二個引數是例項


    def _get_function(self, name):
        mro = self._get_mro()
        if not self._thisclass in mro:
            return None

        index = mro.index(self._thisclass) + 1
        while index < len(mro):
            cls = mro[index]
            if hasattr(cls, name):
                attr = cls.__dict__[name]
                #不要用getattr,因為我們這裡需要獲取未繫結的函式
                #如果使用getattr, 並且獲取的是classmethod
                #會直接將cls繫結到該函式上
                #attr = getattr(cls, name)
                if callable(attr) or isinstance(attr, classmethod):
                    return attr
            index += 1
        return None

    def __getattr__(self, name):
        func = self._get_function(name)
        if not func is None:
            if issubclass(type, type(self._target)):
                return func.__get__(None, self._target)
            else:
                return func.__get__(self._target, None)

和super一樣,上面的my_super的__init__函式接收兩個引數,一個是呼叫super的當前類thisclass, 第二個引數target是呼叫my_super的函式的第一個引數,也就是self或cls。所以這個引數可能是物件例項,也可能是類(如果在classmethod中呼叫my_super,第二個引數要傳cls),在my_super中要分兩種情況。

my_super中的_get_mro函式,根據傳入的第二個引數獲取mro。如果第二個引數target是物件例項,就獲取它的__class__,然後獲取__class__的__mro__,如果target是類,則直接獲取target的__mro__。

my_super的_get_function函式,先獲取mro,然後在mro上獲取位於thisclass後的目標類,並且在目標類中查詢函式,引數name是要查詢的函式的名字。這裡要注意,如果位於thisclass後的類中沒有名為name的函式,則繼續在下各類中查詢,所以使用了while迴圈

my_super的__getattr__函式,用於截獲my_super物件對方法的呼叫,舉例來說,如果my_supe呼叫的是test,那麼這個name就是’test’。在__getattr__中,首先呼叫_get_function,獲取目標函式,然後呼叫函式的描述器方法__get__,將target例項繫結,然後將繫結後的方法返回。這裡也發要分target是例項還是類。如果是例項(這時呼叫my_super的是例項函式),則使用function.get(instance, None)繫結,如果是類(這是呼叫my_super的是類函式),則使用functon.get(None, cls)繫結。

我們改寫上面的例子,來驗證my_super功能是否正常:

from my_super import my_super

class A:
    def test(self):
        print('A.test')

class TestMixin:
    def test(self):
        print('TestMixin.test')
        my_super(TestMixin, self).test()

class B(TestMixin, A):
    def test(self):
        print('B.test')
        my_super(B, self).test()


print(B.__mro__)

b = B()
b.test()

執行後輸出如下:

B.test
TestMixin.test
A.test

和super的效果是一樣的。

下面我們在寫一個菱形繼承的例項來驗證,並且驗證類函式中使用my_super功能是否正常:

from my_super import my_super

class A:
    def test(self):
        print('A.test')

    @classmethod
    def test1(cls):
        print('A.test1')

class B(A):
    def test(self):
        print('B.test')
        my_super(B, self).test()

    @classmethod
    def test1(cls):
        print('B.test1')
        my_super(B, cls).test1()

class C(A):
    def test(self):
        print('C.test')
        my_super(C, self).test()

    @classmethod
    def test1(cls):
        print('C.test1')
        my_super(C, cls).test1()

class D(B,C):
    def test(self):
        print('D.test')
        my_super(D, self).test()

    @classmethod
    def test1(cls):