深度解析並實現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支援四種呼叫方式
- super()
- super(type, obj)
- super(type)
- 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__方法:
- 當使用類名訪問描述器時(Person.name) , __get__方法返回描述器本身
- 當使用物件訪問描述器時(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。
所以我們可以認為:
- function的__get__方法,當不傳入instance時(相當於A.test),會返回function本身
- 當傳入一個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):