楔子
Python除了給我提供了很多的類之外,還支持我們定義屬於自己的類,那么Python底層是如何做的呢?我們下面就來看看。
自定義class
老規矩,如果想知道底層是怎么做的,那么就必須要通過觀察字節碼來實現。
class Girl:
name = "夏色祭"
def __init__(self):
print("__init__")
def f(self):
print("f")
def g(self, name):
self.name = name
print(self.name)
girl = Girl()
girl.f()
girl.g("神樂mea")
"""
__init__
f
神樂mea
"""
通過之前對函數機制的分析中,我們知道對於一個包含函數定義的Python源文件,在編譯之后會得到一個和源文件對應的PyCodeObject對象,其內部的常量池中存儲了函數編譯之后的PyCodeObject對象。那么對於包含類的Python源文件,編譯之后的結果又是怎么樣的呢?
顯然我們可以照葫蘆畫瓢,根據以前的經驗我們可以猜測模塊對應的PyCodeObject對象的常量池中肯定存儲了類對應的PyCodeObject對象,類對應的PyCodeObject對象的常量池中則存儲了__init__、f、g三個函數對應的PyCodeObject對象。然而事實也確實如此。
在介紹函數的時候,我們看到函數的聲明(def語句)和函數的實現代碼雖然是一個邏輯整體,但是它們的字節碼指令卻是分離在兩個PyCodeObject對象中的。在類中,同樣存在這樣的分離現象。聲明類的class語句,編譯后的字節碼指令存儲在模塊對應的PyCodeObject中,而類的實現、也就是類里面的邏輯,編譯后的字節碼指令序列則存儲在類對應的的PyCodeObject中。所以我們在模塊級別中只能找到類,無法直接找到類里面的成員。
另外還可以看到,類的成員函數和一般的函數相同,也會有這種聲明和實現分離的現象。其實也很好理解,就把類和函數想象成變量就行了,類名、函數名就是變量名,而類、函數里面的邏輯想象成值,一個變量對應一個值。
s = """class Girl:
name = "夏色祭"
def __init__(self):
print("__init__")
def f(self):
print("f")
def g(self, name):
self.name = name
print(self.name)"""
# 此時的code顯然是模塊對應的PyCodeObject對象
code = compile(s, "class", "exec")
print(code) # <code object <module> at 0x000001B588101450, file "class", line 1>
# 常量池里面存儲了Girl對應的PyCodeObject對象
print(code.co_consts[0]) # <code object Girl at 0x0000024BB6C7ABE0, file "class", line 1>
# Girl的PyCodeObject對象的常量池里面存儲了幾個函數的PyCodeObject對象
# 至於它們的位置我們暫時不需要關心
print(code.co_consts[0].co_consts[6]) # <code object g at 0x000001FAC40A82F0, file "class", line 10>
print(code.co_consts[0].co_consts[6].co_varnames) # ('self', 'name')
class對象的動態元信息
class對象(class關鍵字創建的類)的元信息指的就是關於class的信息,比如說class的名稱、它所擁有的的屬性、方法,該class實例化時要為實例對象申請的內存空間大小等。對於模塊中定義的class Girl來說,我們必須知道相應的信息:比如在class Girl中,有一個符號f,這個f對應一個函數;還有一個符號g,這個g也對應了一個函數。有了這些元信息,才能創建class對象,否則我們是沒辦法創建的。元信息是一個非常重要的概念,比如說Hive,數據的元信息就是存儲在MySQL里面的,而在編程語言中,正是通過元信息才實現了反射等動態特性。而在Python中,元信息的概念被發揮的淋漓盡致,因此Python也提供了其他編程語言所不具備的高度靈活的動態特征。
老規矩,下面還是看一下字節碼:
class Girl:
name = "夏色祭"
def __init__(self):
print("__init__")
def f(self):
print("f")
def g(self, name):
self.name = name
print(self.name)
這里我們先不涉及調用,只看類的創建。
1 0 LOAD_BUILD_CLASS
2 LOAD_CONST 0 (<code object Girl at 0x0000026FB0B3ABE0, file "class", line 1>)
4 LOAD_CONST 1 ('Girl')
6 MAKE_FUNCTION 0
8 LOAD_CONST 1 ('Girl')
10 CALL_FUNCTION 2
12 STORE_NAME 0 (Girl)
14 LOAD_CONST 2 (None)
16 RETURN_VALUE
Disassembly of <code object Girl at 0x0000026FB0B3ABE0, file "class", line 1>:
1 0 LOAD_NAME 0 (__name__)
2 STORE_NAME 1 (__module__)
4 LOAD_CONST 0 ('Girl')
6 STORE_NAME 2 (__qualname__)
3 8 LOAD_CONST 1 ('夏色祭')
10 STORE_NAME 3 (name)
4 12 LOAD_CONST 2 (<code object __init__ at 0x0000026FB0961450, file "class", line 4>)
14 LOAD_CONST 3 ('Girl.__init__')
16 MAKE_FUNCTION 0
18 STORE_NAME 4 (__init__)
7 20 LOAD_CONST 4 (<code object f at 0x0000026FB095AB30, file "class", line 7>)
22 LOAD_CONST 5 ('Girl.f')
24 MAKE_FUNCTION 0
26 STORE_NAME 5 (f)
10 28 LOAD_CONST 6 (<code object g at 0x0000026FB0B472F0, file "class", line 10>)
30 LOAD_CONST 7 ('Girl.g')
32 MAKE_FUNCTION 0
34 STORE_NAME 6 (g)
36 LOAD_CONST 8 (None)
38 RETURN_VALUE
Disassembly of <code object __init__ at 0x0000026FB0961450, file "class", line 4>:
5 0 LOAD_GLOBAL 0 (print)
2 LOAD_CONST 1 ('__init__')
4 CALL_FUNCTION 1
6 POP_TOP
8 LOAD_CONST 0 (None)
10 RETURN_VALUE
Disassembly of <code object f at 0x0000026FB095AB30, file "class", line 7>:
8 0 LOAD_GLOBAL 0 (print)
2 LOAD_CONST 1 ('f')
4 CALL_FUNCTION 1
6 POP_TOP
8 LOAD_CONST 0 (None)
10 RETURN_VALUE
Disassembly of <code object g at 0x0000026FB0B472F0, file "class", line 10>:
11 0 LOAD_FAST 1 (name)
2 LOAD_FAST 0 (self)
4 STORE_ATTR 0 (name)
12 6 LOAD_GLOBAL 1 (print)
8 LOAD_FAST 0 (self)
10 LOAD_ATTR 0 (name)
12 CALL_FUNCTION 1
14 POP_TOP
16 LOAD_CONST 0 (None)
18 RETURN_VALUE
字節碼比較長,我們逐行分析,當然很多字節碼我們都見過了,因此有的字節碼介紹的時候就不會特別詳細了。我們仔細觀察一下字節碼,會發現分為五個部分:模塊的字節碼、class Girl的字節碼、class的三個函數的字節碼。
我們先來看看模塊的字節碼:
1 0 LOAD_BUILD_CLASS
2 LOAD_CONST 0 (<code object Girl at 0x0000026FB0B3ABE0, file "class", line 1>)
4 LOAD_CONST 1 ('Girl')
6 MAKE_FUNCTION 0
8 LOAD_CONST 1 ('Girl')
10 CALL_FUNCTION 2
12 STORE_NAME 0 (Girl)
14 LOAD_CONST 2 (None)
16 RETURN_VALUE
0 LOAD_BUILD_CLASS: 我們注意到這又是一條我們沒見過的新指令,從名字也能看出來這是要構建一個類;
2 LOAD_CONST: 加載Girl對應的PyCodeObject對象;
4 LOAD_CONST: 加載字符串"Girl"
6 MAKE_FUNCTION: 問題來了, 我們看到出現了MAKE_FUNCTION, 不是說要構建類嗎? 為什么是MAKE_FUNCTION呢? 別急, 往下看;
8 LOAD_CONST: 再次加載字符串"Girl";
10 CALL_FUNCTION: 你看到了什么?函數調用?是的, 這個CALL_FUNCTION是用來構建類的, 至於怎么構建我們后面會說;
12 STORE_NAME: 將上一步構建好的類使用符號Girl保存;
我們看一下LOAD_BUILD_CLASS這個指令都干了哪些事情吧。
case TARGET(LOAD_BUILD_CLASS): {
_Py_IDENTIFIER(__build_class__);
PyObject *bc;
if (PyDict_CheckExact(f->f_builtins)) {
//從f_builtins里面獲取PyId___build_class__
bc = _PyDict_GetItemIdWithError(f->f_builtins, &PyId___build_class__);
if (bc == NULL) {
if (!_PyErr_Occurred(tstate)) {
_PyErr_SetString(tstate, PyExc_NameError,
"__build_class__ not found");
}
goto error;
}
Py_INCREF(bc);
}
else {
PyObject *build_class_str = _PyUnicode_FromId(&PyId___build_class__);
if (build_class_str == NULL)
goto error;
bc = PyObject_GetItem(f->f_builtins, build_class_str);
if (bc == NULL) {
if (_PyErr_ExceptionMatches(tstate, PyExc_KeyError))
_PyErr_SetString(tstate, PyExc_NameError,
"__build_class__ not found");
goto error;
}
}
//入棧
PUSH(bc);
DISPATCH();
}
LOAD_BUILD_CLASS做的事情很簡單,就是從Python的內置函數中取得__build_class__將其入棧,然后下面的幾個指令很好理解,但是卻出現了一個CALL_FUNCTION,顯然它是調用__build_class__創建類的。我們看到它的參數個數是2個,這兩個參數分別是:A的PyFunctionObject、字符串"A",因此:
class A(object):
pass
# 在底層將會被翻譯成
A = __build_class__(<PyFunctionObject A>, "A")
# 如果是
class A(int):
pass
# 在底層將會被翻譯成
A = __build_class__(<PyFunctionObject A>, "A", int)
我們實際操作一下:
# 在Python中我們可以導入 builtins 來調用__build_class__,也可以直接使用
import builtins
# 構建一個類, 叫MyInt, 繼承自int
c = builtins.__build_class__(lambda: None, "MyInt", int)
print(c.__name__) # MyInt
print(c.__base__) # <class 'int'>
print(c(3) * c(5)) # 15
如果參數類型不正確的話,就會報出如下錯誤:
import sys
import builtins
try:
builtins.__build_class__()
except Exception as e:
exc_type, exc_value, _ = sys.exc_info()
print(exc_type, exc_value) # <class 'TypeError'> __build_class__: not enough arguments
try:
builtins.__build_class__("", "")
except Exception as e:
exc_type, exc_value, _ = sys.exc_info()
print(exc_type, exc_value) # <class 'TypeError'> __build_class__: func must be a function
try:
builtins.__build_class__(lambda: 123, 123)
except Exception as e:
exc_type, exc_value, _ = sys.exc_info()
print(exc_type, exc_value) # <class 'TypeError'> __build_class__: name is not a string
記住這幾個報錯信息,后面馬上就會看到。此外我們也看到,這個函數的一個參數叫func、第二個參數叫name。
所以現在就明白為什么會出現CALL_FUNCTION這條指令,__build_class__就是用來將一個函數對象變成一個class對象。
class對象的字節碼:
1 0 LOAD_NAME 0 (__name__)
2 STORE_NAME 1 (__module__)
4 LOAD_CONST 0 ('Girl')
6 STORE_NAME 2 (__qualname__)
3 8 LOAD_CONST 1 ('夏色祭')
10 STORE_NAME 3 (name)
4 12 LOAD_CONST 2 (<code object __init__ at 0x0000026FB0961450, file "class", line 4>)
14 LOAD_CONST 3 ('Girl.__init__')
16 MAKE_FUNCTION 0
18 STORE_NAME 4 (__init__)
7 20 LOAD_CONST 4 (<code object f at 0x0000026FB095AB30, file "class", line 7>)
22 LOAD_CONST 5 ('Girl.f')
24 MAKE_FUNCTION 0
26 STORE_NAME 5 (f)
10 28 LOAD_CONST 6 (<code object g at 0x0000026FB0B472F0, file "class", line 10>)
30 LOAD_CONST 7 ('Girl.g')
32 MAKE_FUNCTION 0
34 STORE_NAME 6 (g)
36 LOAD_CONST 8 (None)
38 RETURN_VALUE
對於一個類而言,調用其__module__屬性,可以獲取所在的模塊。所以開始的LOAD_NAME和STORE_NAME是將符號__module__和全局命名空間中符號__name__的值關聯了起來,並放入到該類的local名字空間中。
需要說明的是,我們在介紹函數的時候提過,當時我們說:"函數的局部變量是不可變的,在編譯的時候就已經確定了,是以一種靜態方式放在了運行時棧前面的那段內存中,並沒有放在f_locals中,f_locals其實是一個NULL,我們通過locals()拿到的只是對運行時棧前面的內存的一個拷貝,函數里面的局部變量是通過靜態方式來訪問的"。但是類則不一樣,類是可以動態修改的,可以隨時增加屬性、方法,這就意味着類是不可能通過靜態方式來查找屬性的。而事實上也確實如此,類也有一個f_locals,但它指向的就不再是NULL了,而和f_globals一樣,也是一個PyDictObject對象。然后是LOAD_CONST,將字符串"Girl"加載進來,和__qualname__組成一個entry存儲在Girl的local空間中。
class Girl:
name = "夏色祭"
def __init__(self):
print("__init__")
def f(self):
print("f")
def g(self, name):
self.name = name
print(self.name)
print(__name__) # __main__
print(Girl.__module__) # __main__
print(Girl.__qualname__) # Girl
所以整體過程就是:先將PyCodeObject構建成函數,再通過__build_class__將函數變成一個類,當然__build_class__結束之后我們的Girl這個類就橫空出世了。
因此剩下的來問題就是__build_class__是如何將一個函數變成類的,想要知道答案,那么只能去源碼中一探究竟了。不過在看源碼之前,我們還需要了解一樣東西:metaclass。
回顧metaclass
元類,被譽為是深度的魔法,但是個人覺得有點誇張了。首先元類是做什么的,它是用來控制我們類的生成過程的,默認情況下,我們自定義的類都是由type創建的。但是我們可以手動指定某個類的元類,但是在介紹元類之前,我們還需要看一下Python中的兩個特殊的魔法方法:__new__和__init__。
__new__和__init__
類在實例化的時候會自動調用__init__,但其實在調用__init__之前會先調用__new__。
__new__: 為實例對象申請一片內存;
__init__: 為實例對象設置屬性;
class A:
def __new__(cls, *args, **kwargs):
print("__new__")
def __init__(self):
print("__init__")
A() # __new__
然而我們看到只有__new__被調用了,__init__則沒有。原因就在於__new__中必須將A的實例對象返回,才會執行__init__,並且執行的時候會自動將__new__的返回值作為參數傳給self。
class A:
def __new__(cls, *args, **kwargs):
print("__new__")
# 這里的參數cls就表示A這個類本身
# object.__new__(cls) 便是根據cls創建cls的實例對象
return object.__new__(cls)
def __init__(self):
# 然后執行__init__, 里面的self指的就是實例對象
# 在執行__init__的時候, __new__的返回值會自動作為參數傳遞給這里的self
print("__init__")
A()
"""
__new__
__init__
"""
所以一個對象是什么,取決於其類型對象的__new__返回了什么。
class A:
def __new__(cls, *args, **kwargs):
print("__new__")
# 這里必須返回A的實例對象, 否則__init__函數是不會執行的
return 123
def __init__(self):
print("__init__")
a = A()
print(a + 1)
"""
__new__
124
"""
我們看到A在實例化之后得到的是一個整型,原因就是__new__返回了123。最后一個就是參數問題,首先我們說__new__是創建實例對象的,__init__是為實例對象綁定屬性的。
class A:
def __new__(cls, name, age):
# __new__里面的參數一定要和__init__是匹配的, 除了第一個參數之外
return object.__new__(cls)
def __init__(self, name, age):
self.name = name
self.age = age
a = A("夏色祭", -1)
# 我們這里傳入了兩個參數, 那么: A、"夏色祭"、-1 就會組合起來, 分別傳給__new__的 cls、name、age
# 然后__new__里面返回了一個實例對象
# 那么: object.__new__(cls)、__new__接收的name、__new__接收的age 會組合起來, 分別傳給__init__的 self、name、age
創建類的另一種方式
創建類的時候可以使用class關鍵字創建,除了class關鍵字之外,我們還可以使用type這個古老卻又強大的類來創建。
# type這個類里面可以接收一個參數或者三個參數
# 如果接收一個參數, 那么表示查看類型; 如果接收三個參數, 那么表示創建一個類
try:
A = type("A", "")
except Exception as e:
print(e) # type() takes 1 or 3 arguments
告訴我們type要么接收一個參數,要么接收三個參數。顯然接收一個參數查看類型不需要再說了,我們看看怎么用來用type創建一個類。
# type接收的三個參數: 類名、繼承的基類、屬性
class A(list):
name = "夏色祭"
# 上面這個類翻譯過來就是
val = type("A", (list, ), {"name": "夏色祭"})
print(val) # <class '__main__.A'>
print(val.__name__) # A
print(val.__base__) # <class 'list'>
print(val.name) # 夏色祭
所以還是很簡單的,我們還可以自定義一個類繼承自type。
class MyType(type):
def __new__(mcs, name, bases, attr):
print(name)
print(bases)
print(attr)
# 指定metaclass, 表示A這個類由MyType創建
# 我們說__new__是為實例對象開辟內存的, 那么MyType的實例對象是誰呢? 顯然就是這里的A
# 因為A指定了metaclass為MyType, 所以A的類型就是MyType
class A(int, object, metaclass=MyType):
name = "夏色祭"
"""
A
(<class 'int'>, <class 'object'>)
{'__module__': '__main__', '__qualname__': 'A', 'name': '夏色祭'}
"""
# 我們看到一個類在創建的時候會向元類的__new__中傳遞三個值
# 分別是類名、繼承的基類、類的屬性
# 但是此時A並沒有被創建出來
print(A) # None
"""
我們說__new__一定要將創建的實例對象返回才可以, 這里的MyType是元類
所以類對象A就等於MyType的實例對象, MyType的__new__就負責為類對象A分配空間
但是顯然我們這里並沒有分配, 而且返回的還是一個None, 如果我們返回的是123, 那么print(a)就是123
"""
所以元類和類之間的關系 和 類與實例對象的關系,之間是很相似的,因為完全可以把類對象看成是元類的實例對象。因此A既然指定了metaclass為MyType,表示A這個類由MyType創建,那么MyType的__new__函數返回了什么,A就是什么。
class MyType(type):
def __new__(mcs, name, bases, attr):
return "嘿嘿嘿"
class A(metaclass=MyType):
pass
print(A + "喲喲喲") # 嘿嘿嘿喲喲喲
這便是Python語言具備的高度動態特性,那么問題來了,如果我想把A創建出來、像普通的類一樣使用的話,該咋辦呢?因為默認情況下是由type創建,底層幫你做好了,但是現在是我們手動指定元類,那么一切就需要我們來手動指定了。顯然,這里創建還是要依賴於type,只不過需要我們手動指定,而且在手動指定的同時我們還可以增加一些我們自己的操作。
class MyType(type):
def __new__(mcs, name, bases, attr):
name = name * 2
bases = (list,)
attr.update({"name": "神樂mea", "nickname": "屑女仆"})
# 這里直接交給type即可, 然后type來負責創建
# 所以super().__new__實際上會調用type.__new__
# type(name, bases, attr) 等價於 type.__new__(type, name, bases, attr)
return super().__new__(mcs, name, bases, attr)
# 但是這里我們將__new__的第一個參數換成了mcs, 也就是這里的MyType
# 等價於type.__new__(mcs, name, bases, attr)表示將元類設置成MyType
# 注意: 不能寫type(name, bases, attr), 因為這樣的話類還是由type創建的
class Girl(metaclass=MyType):
pass
# 我們看到類的名字變了, 默認情況下是Girl, 但是我們在創建的時候將name成了個2
print(Girl.__name__) # GirlGirl
# 那么顯然Girl這里也要繼承自list
print(Girl("你好呀")) # ['你', '好', '呀']
# 同理Girl還有兩個屬性
print(Girl.name, Girl.nickname) # 神樂mea 屑女仆
我們之前還說過,一個類在沒有指定的metaclass的時候,如果它的父類指定了,那么這個類的metaclass等於父類的metaclass。
class MyType(type):
def __new__(mcs, name, bases, attr):
name = name * 2
bases = (list,)
attr.update({"name": "神樂mea", "nickname": "屑女仆"})
return super().__new__(mcs, name, bases, attr)
class Girl(metaclass=MyType):
pass
class A(Girl):
pass
print(A.__class__) # <class '__main__.MyType'>
print(A.__name__) # AA
我們之前還舉了個flask的例子,一種更加優雅的寫法。
class MyType(type):
def __new__(mcs, name, bases, attr):
return super().__new__(mcs, name, bases, attr)
def with_metaclass(meta, bases):
return meta("tmp", bases, {"gender": "female"})
# with_metaclass(MyType, (list,))便會返回一個類
# 這個類由MyType創建, 並且繼承自list
# 那么Girl再繼承這個類, 等價於Girl也是有MyType創建, 並且也會繼承自list
class Girl(with_metaclass(MyType, (list,))):
pass
print(Girl.__class__) # <class '__main__.MyType'>
print(Girl.__bases__) # (<class '__main__.tmp'>,)
print(Girl.__mro__) # (<class '__main__.Girl'>, <class '__main__.tmp'>, <class 'list'>, <class 'object'>)
# 所以with_metaclass(meta, bases)只是為了幫助我們找到元類和繼承的類
# 至於其本身並沒有太大的意義, 但我們畢竟繼承它了, 就意味着我們也可以找到它的屬性
print(Girl.gender) # female
注意:我們說創建類的對象是元類,元類要么是type、要么是繼承自type的子類。
class MyType(type):
def __new__(mcs, name, bases, attr):
return super().__new__(mcs, name, bases, attr)
# type直接加括號表示由type創建, 我們需要通過__new__手動指定
Girl = type.__new__(MyType, "GirlGirlGirl", (list,), {"foo": lambda self, value: value + 123})
print(Girl.__name__) # GirlGirlGirl
g = Girl()
print(g.foo(123)) # 246
try:
type.__new__(int, "A", (object,), {})
except TypeError as e:
# 指定為int則報錯, 告訴我們int不是type的子類
# 因為只有兩種情況: 要么是type、要么是type的子類
print(e) # type.__new__(int): int is not a subtype of type
怎么樣,是不是覺得元類很簡單呢?其實元類沒有什么復雜的。
再舉個例子:
class MyType(type):
def __new__(mcs, name, bases, attr):
if "f" in attr:
attr.pop("f")
return super().__new__(mcs, name, bases, attr)
class Girl(metaclass=MyType):
def f(self):
return "f"
def g(self):
return "g"
print(Girl().g()) # g
try:
print(Girl().f())
except AttributeError as e:
print(e) # 'Girl' object has no attribute 'f'
"""
驚了, 我們看到居然沒有f這個屬性, 我們明顯定義了啊, 原因就是我們在創建類的時候將其pop掉了
首先創建一個類需要三個元素: 類名、繼承的基類、類的一些屬性(以字典的形式, 屬性名: 屬性值)
然后會將這三個元素交給元類進行創建, 但是我們在創建的時候偷偷地將f從attr里面給pop掉了
因此創建出來的類是沒有f這個函數的
"""
元類確實蠻有趣的,而且也沒有想象中的那么難,可以多了解一下。
特殊的魔法函數
此外我們再來看兩個和元類有關的魔法函數:
__prepared__
class MyType(type):
@classmethod
def __prepare__(mcs, name, bases):
print("__prepared__")
# 必須返回一個mapping, 至於它是干什么的我們后面說
return {}
def __new__(mcs, name, bases, attr):
print("__new__")
return super().__new__(mcs, name, bases, attr)
class Girl(metaclass=MyType):
pass
"""
__prepared__
__new__
"""
我們看到__prepare__會在__new__方法之前被調用,那么它是做什么的呢?答案是添加屬性的,我們解釋一下。
class MyType(type):
@classmethod
def __prepare__(mcs, name, bases):
return {"name": "夏色祭"}
def __new__(mcs, name, bases, attr):
return super().__new__(mcs, name, bases, attr)
class Girl(metaclass=MyType):
def f(self):
return "f"
def g(self):
return "g"
print(Girl.name) # 夏色祭
# 現在你應該知道__prepare__是干什么的了吧, 它接收一個name、一個bases, 返回有個mapping
# 我們說name、bases、attr會傳遞給__new__, 但是在__new__之前會先經過__prepared__
# __prepared__返回一個字典(mapping), 假設叫m吧, 那會將attr和m合並, 相當於執行了attr.update(m)
# 然后再將 name、bases、attr交給__new__
此外__prepared__這個方法是被classmethod裝飾的,另外里面一定要返回一個mapping,否則報錯:TypeError: MyType.__prepare__() must return a mapping, not xxx
__init_subclass__
它類似於一個鈎子函數,在一些簡單地場景下可以代替元類。
class Base:
def __init_subclass__(cls, **kwargs):
print(cls, kwargs)
# 當A被創建的時候, 會觸發其父類的__init_subclass__
class A(Base):
pass
"""
<class '__main__.A'> {}
"""
class B(Base, name="夏色祭", age=-1):
pass
"""
<class '__main__.B'> {'name': '夏色祭', 'age': -1}
"""
所以父類的__init_subclass__里面的cls並不是父類本身,而是繼承它的類。kwargs,就是額外設置的一些屬性。因此我們可以實現一個屬性添加器。
class Base:
def __init_subclass__(cls, **kwargs):
for k, v in kwargs.items():
setattr(cls, k, v)
class A(Base, name="夏色祭", age=-1, __str__=lambda self: "__str__" ):
pass
print(A.name, A.age) # 夏色祭 -1
print(A()) # __str__
除了屬性添加器,我們還可以實現一個屬性攔截器。
class Base:
def __init_subclass__(cls, **kwargs):
if hasattr(cls, "yoyoyo") and hasattr(cls.yoyoyo, "__code__"):
raise Exception(f"{cls.__name__}不允許定義'yoyoyo'函數")
class A(Base):
yoyoyo = 123
# 由於在創建類的時候就會觸發, 所以必須加上try語句
try:
class B(Base):
def yoyoyo(self):
pass
except Exception as e:
print(e) # B不允許定義'yoyoyo'函數
有了這些元類相關的知識,我們后面在分析源碼的時候就會輕松一些。
源碼分析類機制與metaclass
我們說LOAD_BUILD_CLASS是將一個PyFunctionObject變成一個類,盡管它寫在最前面,但實際上是需要將class A對應的PyCodeObject對象包裝成一個PyFunctionObject對象之后才能執行。我們說__build_class__是用來將PyFunctionObject變成類的函數,我們來看看它長什么樣子。
//python/bltinmodule.c
static PyMethodDef builtin_methods[] = {
{"__build_class__", (PyCFunction)(void(*)(void))builtin___build_class__,
METH_FASTCALL | METH_KEYWORDS, build_class_doc},
//...
//...
}
static PyObject *
builtin___build_class__(PyObject *self, PyObject *const *args, Py_ssize_t nargs,
PyObject *kwnames)
{
PyObject *func, *name, *bases, *mkw, *meta, *winner, *prep, *ns, *orig_bases;
PyObject *cls = NULL, *cell = NULL;
int isclass = 0; /* initialize to prevent gcc warning */
//我們說了底層調用的是builtin___build_class__
//class A: 會被翻譯成builtin.__build_class__(PyFunctionObject, "class name")
//所以這個函數至少需要兩個參數
if (nargs < 2) {
//參數不足,報錯,還記的這個報錯信息嗎?上面測試過的
PyErr_SetString(PyExc_TypeError,
"__build_class__: not enough arguments");
return NULL;
}
//類對應的PyFunctionObject
func = args[0]; /* Better be callable */
if (!PyFunction_Check(func)) {
//如果不是PyFunctionObject,報錯,這個信息有印象嗎?
PyErr_SetString(PyExc_TypeError,
"__build_class__: func must be a function");
return NULL;
}
//類對應的名字,__build_class__的時候 總要給類起一個名字吧
name = args[1];
if (!PyUnicode_Check(name)) {
//如果不是一個PyUnicodeObject,報錯,這個有印象嗎?
PyErr_SetString(PyExc_TypeError,
"__build_class__: name is not a string");
return NULL;
}
//原始基類
orig_bases = _PyTuple_FromArray(args + 2, nargs - 2);
if (orig_bases == NULL)
return NULL;
//獲取class的基類列表
bases = update_bases(orig_bases, args + 2, nargs - 2);
if (bases == NULL) {
Py_DECREF(orig_bases);
return NULL;
}
if (kwnames == NULL) {
meta = NULL;
mkw = NULL;
}
else {
mkw = _PyStack_AsDict(args + nargs, kwnames);
if (mkw == NULL) {
Py_DECREF(bases);
return NULL;
}
//這里獲取meta
meta = _PyDict_GetItemIdWithError(mkw, &PyId_metaclass);
if (meta != NULL) {
Py_INCREF(meta);
if (_PyDict_DelItemId(mkw, &PyId_metaclass) < 0) {
Py_DECREF(meta);
Py_DECREF(mkw);
Py_DECREF(bases);
return NULL;
}
/* metaclass is explicitly given, check if it's indeed a class */
isclass = PyType_Check(meta);
}
else if (PyErr_Occurred()) {
Py_DECREF(mkw);
Py_DECREF(bases);
return NULL;
}
}
//如果meta為NULL,這意味着用戶沒有指定metaclass
if (meta == NULL) {
//然后嘗試獲取基類,如果沒有基類
if (PyTuple_GET_SIZE(bases) == 0) {
//指定metaclass為type
meta = (PyObject *) (&PyType_Type);
}
//否則獲取第一個繼承的基類的metaclass
else {
PyObject *base0 = PyTuple_GET_ITEM(bases, 0);//拿到第一個基類
meta = (PyObject *) (base0->ob_type);//拿到第一個基類的__class__
}
Py_INCREF(meta);//meta也是一個類
isclass = 1; /* meta is really a class */
}
//如果設置了元類, 那么isclass為1, 會執行下面的代碼
if (isclass) {
//既然已經選擇出了元類, 那么這一步是做什么的呢?
//這一步是為了解決元類沖突的, 假設有兩個繼承type的元類MyType1和MyType2, 然后Base1的元類是MyType1、Base2的元類是MyType2
//那么如果class A(Base1, Base2)的話, 就會報錯
//在Python中有一個要求, 假設class A(Base1, Base2, ..., BaseN), Base1的元類叫Type1、BaseN的元類叫TypeN
//那么必須滿足:
/*
Type1是Type2的子類或者父類;
Type1是Type3的子類或者父類;
Type1是Type4的子類或者父類;
....
Type1是TypeN的子類或者父類;
*/
//而之所以存在這一限制, 原因就是為了避免屬性沖突
winner = (PyObject *)_PyType_CalculateMetaclass((PyTypeObject *)meta,
bases);
if (winner == NULL) {
Py_DECREF(meta);
Py_XDECREF(mkw);
Py_DECREF(bases);
return NULL;
}
if (winner != meta) {
Py_DECREF(meta);
meta = winner;
Py_INCREF(meta);
}
}
/* else: meta is not a class, so we cannot do the metaclass
calculation, so we will use the explicitly given object as it is */
//尋找__prepare__方法
if (_PyObject_LookupAttrId(meta, &PyId___prepare__, &prep) < 0) {
ns = NULL;
}
//這個__prepare__方法必須返回一個mapping,如果返回None,那么默認返回一個空字典
else if (prep == NULL) {
ns = PyDict_New();
}
else {
//否則將字典返回
PyObject *pargs[2] = {name, bases};
//我們看到這里涉及到了一個函數調用, 這個函數應該有印象吧
ns = _PyObject_FastCallDict(prep, pargs, 2, mkw);
Py_DECREF(prep);
}
if (ns == NULL) {
Py_DECREF(meta);
Py_XDECREF(mkw);
Py_DECREF(bases);
return NULL;
}
if (!PyMapping_Check(ns)) {
//如果返回的不是一個字典,那么報錯,這個錯誤等信息我們也見過了
PyErr_Format(PyExc_TypeError,
"%.200s.__prepare__() must return a mapping, not %.200s",
isclass ? ((PyTypeObject *)meta)->tp_name : "<metaclass>",
Py_TYPE(ns)->tp_name);
goto error;
}
//......
}
可以看到,一個簡單的類定義,Python底層究竟做了多少事情啊,不過顯然這還沒完。
我們前面說,Python虛擬機獲得了關於class的屬性表(動態元信息),比如所有的方法、屬性,所以我們可以說,class的動態元信息包含了class的所有屬性。但是對於這個class對象的類型是什么,應該如何創建、要分配多少內存,卻沒有任何的信息。而在builtin___build_class__
中,metaclass正是關於class對象的另一部分元信息,我們稱之為靜態元信息。在靜態元信息中,隱藏着所有的類對象應該如何創建的信息,注意:是所有的類對象。
從源碼中我們可以看到,如果用戶指定了metaclass,那么會選擇指定的metaclass,如果沒有指定,那么會使用第一個繼承的基類的__class__作為該class的metaclass。
對於PyLongObject、PyDictObject這些Python中的實例對象,所有的元信息存儲在對應的類對象中(PyLong_Type,PyDict_Type)。但是對於類對象來說,其元信息的靜態元信息存儲在對應的元類(PyType_Type)中,動態元信息則存儲在本身的local名字空間中。但是為什么這么做呢?為什么對於類對象來說,其元信息要游離成兩部分呢?都存在metaclass里面不香嗎?這是因為,用戶在.py文件中可以定義不同的class,這個元信息必須、且只能是動態的,所以它是不適合保存在metaclass中的,因此類對象的創建策略等這些所有class都會共用的元信息,會存儲在metaclass里面。
像Python的內建對象都是Python靜態提供的,它們都具備相同的接口集合(底層都是PyTypeObject結構體實例),支持什么操作一開始就定義好了。只不過有的可以用,有的不能用。比如PyLongObject可以使用nb_add,但是PyDictObject不能。而PyDictObject可以使用mp_subscript,但是PyLongObject不可以。盡管如此,但這不影響它們的所有元信息都可以完全存儲在類型對象中。但是用戶自定義的class對象,接口是動態的,不可能在metaclass中靜態指定。
既然創建了元類,那么下面顯然就開始調用了。通過函數 PyObject_Call 調用。
//Objects/call.c
PyObject *
PyObject_Call(PyObject *callable, PyObject *args, PyObject *kwargs)
{
//...
else {
//調用了tp_call,指向type_call
call = callable->ob_type->tp_call;
if (call == NULL) {
PyErr_Format(PyExc_TypeError, "'%.200s' object is not callable",
callable->ob_type->tp_name);
return NULL;
}
//......
}
}
//Objects/typeobject.c
static PyObject *
type_call(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
PyObject *obj;
if (type->tp_new == NULL) {
PyErr_Format(PyExc_TypeError,
"cannot create '%.100s' instances",
type->tp_name);
return NULL;
}
//調用tp_new申請內存
obj = type->tp_new(type, args, kwds);
obj = _Py_CheckFunctionResult((PyObject*)type, obj, NULL);
if (obj == NULL)
return NULL;
//如果是PyType_Type, 那么執行完__new__之后直接返回
if (type == &PyType_Type &&
PyTuple_Check(args) && PyTuple_GET_SIZE(args) == 1 &&
(kwds == NULL ||
(PyDict_Check(kwds) && PyDict_GET_SIZE(kwds) == 0)))
return obj;
//還記得我們之前說過, __new__里面一定要返回類的實例對象, 否則是不會執行__init__函數的
//從這里我們也看到了, 如果obj的類型不是對應的類、或者其子類, 那么直接返回
if (!PyType_IsSubtype(Py_TYPE(obj), type))
return obj;
//然后獲取obj的類型
type = Py_TYPE(obj);
//如果存在__init__函數, 那么執行構造函數
if (type->tp_init != NULL) {
int res = type->tp_init(obj, args, kwds);
if (res < 0) {
assert(PyErr_Occurred());
Py_DECREF(obj);
obj = NULL;
}
else {
assert(!PyErr_Occurred());
}
}
//執行完構造函數之后, 再將實例對象返回
//注意: 執行了__init__說明obj是實例對象, 如果obj是類對象, 那么是不會走到這里來的
//執行完元類的__new__之后就返回了
return obj;
}
tp_new指向type_new,這個type_new是我們創建class對象的第一案發現場。我們看一下type_new的源碼,位於 Objects/typeobject.c 中,這個函數的代碼比較長,我們會有刪減,像那些檢測的代碼我們就省略掉了。
static PyObject *
type_new(PyTypeObject *metatype, PyObject *args, PyObject *kwds)
{
//都是類的一些動態元信息
PyObject *name, *bases = NULL, *orig_dict, *dict = NULL;
PyObject *qualname, *slots = NULL, *tmp, *newslots, *cell;
PyTypeObject *type = NULL, *base, *tmptype, *winner;
PyHeapTypeObject *et;
PyMemberDef *mp;
Py_ssize_t i, nbases, nslots, slotoffset, name_size;
int j, may_add_dict, may_add_weak, add_dict, add_weak;
_Py_IDENTIFIER(__qualname__);
_Py_IDENTIFIER(__slots__);
_Py_IDENTIFIER(__classcell__);
//如果metaclass是type的話
if (metatype == &PyType_Type) {
//獲取位置參數和關鍵字參數個數
const Py_ssize_t nargs = PyTuple_GET_SIZE(args);
const Py_ssize_t nkwds = kwds == NULL ? 0 : PyDict_GET_SIZE(kwds);
//位置參數為1,關鍵字參數為0,你想到了什么
//type(xxx),是不是這個呀
if (nargs == 1 && nkwds == 0) {
PyObject *x = PyTuple_GET_ITEM(args, 0);
Py_INCREF(Py_TYPE(x));
//這顯然是初學Python的時候,就知道的,查看一個變量的類型。
//獲取類型之后直接返回
return (PyObject *) Py_TYPE(x);
}
//如果上面的if不滿足,會走這里,表示現在不再是查看類型了,而是創建類
//而這里要求位置參數必須是3個,否則報錯。
//我們知道type查看類型,輸入一個參數即可,但是創建類需要3個
if (nargs != 3) {
PyErr_SetString(PyExc_TypeError,
"type() takes 1 or 3 arguments");
return NULL;
}
}
/* Check arguments: (name, bases, dict) */
//現在顯然是確定參數類型,對於type來說,你傳遞了三個參數,但是這三個參數是有類型要求的
//必須是PyUnicodeObject、PyTupleObject、PyDictObject
if (!PyArg_ParseTuple(args, "UO!O!:type.__new__", &name, &PyTuple_Type,
&bases, &PyDict_Type, &orig_dict))
/*
type(123, (object, ), {}) # TypeError: type.__new__() argument 1 must be str, not int
type("xx", [object], {}) # TypeError: type.__new__() argument 2 must be tuple, not list
type("xx", (object, ), []) # TypeError: type.__new__() argument 3 must be dict, not list
*/
return NULL;
//處理bases為空的情況,另外我們使用class關鍵字定義類,本質上會轉為type定義類的方式
nbases = PyTuple_GET_SIZE(bases);
if (nbases == 0) {
//如果發現我們沒有繼承基類,那么在Python3中會默認繼承object
base = &PyBaseObject_Type; //base設置為object
bases = PyTuple_Pack(1, base); //bases設置為(object,)
if (bases == NULL)
return NULL;
nbases = 1;
}
else {
_Py_IDENTIFIER(__mro_entries__);
//如果我們繼承了基類
//那么循環遍歷bases
for (i = 0; i < nbases; i++) {
//拿到每一個基類
tmp = PyTuple_GET_ITEM(bases, i);
//如果是PyType_Type類型,進行下一次循環
if (PyType_Check(tmp)) {
continue;
}
if (_PyObject_LookupAttrId(tmp, &PyId___mro_entries__, &tmp) < 0) {
return NULL;
}
if (tmp != NULL) {
PyErr_SetString(PyExc_TypeError,
"type() doesn't support MRO entry resolution; "
"use types.new_class()");
Py_DECREF(tmp);
return NULL;
}
}
/* Search the bases for the proper metatype to deal with this: */
//尋找父類的metaclass, 就是我們之前說的解決元類沖突所采取的策略
winner = _PyType_CalculateMetaclass(metatype, bases);
if (winner == NULL) {
return NULL;
}
if (winner != metatype) {
if (winner->tp_new != type_new) /* Pass it to the winner */
return winner->tp_new(winner, args, kwds);
metatype = winner;
}
/* Calculate best base, and check that all bases are type objects */
//確定最佳base,存儲在PyTypeObject *base中
base = best_base(bases);
if (base == NULL) {
return NULL;
}
Py_INCREF(bases);
}
/* Use "goto error" from this point on as we now own the reference to "bases". */
dict = PyDict_Copy(orig_dict);
if (dict == NULL)
goto error;
//處理用戶定義了__slots__屬性的邏輯,一旦程序猿定義了__slots__, 那么類的實例對象就沒有屬性字典了
slots = _PyDict_GetItemIdWithError(dict, &PyId___slots__);
nslots = 0;
add_dict = 0;
add_weak = 0;
may_add_dict = base->tp_dictoffset == 0;
may_add_weak = base->tp_weaklistoffset == 0 && base->tp_itemsize == 0;
if (slots == NULL) {
//.....
}
else {
//.....
}
/* Allocate the type object */
//為class對象申請內存
type = (PyTypeObject *)metatype->tp_alloc(metatype, nslots);
if (type == NULL)
goto error;
/* Keep name and slots alive in the extended type object */
et = (PyHeapTypeObject *)type;
Py_INCREF(name);
et->ht_name = name;
et->ht_slots = slots;
slots = NULL;
/* 初始化tp_flags */
type->tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HEAPTYPE |
Py_TPFLAGS_BASETYPE;
if (base->tp_flags & Py_TPFLAGS_HAVE_GC)
type->tp_flags |= Py_TPFLAGS_HAVE_GC;
//設置PyTypeObject中的各個域
type->tp_as_async = &et->as_async;
type->tp_as_number = &et->as_number;
type->tp_as_sequence = &et->as_sequence;
type->tp_as_mapping = &et->as_mapping;
type->tp_as_buffer = &et->as_buffer;
type->tp_name = PyUnicode_AsUTF8AndSize(name, &name_size);
if (!type->tp_name)
goto error;
if (strlen(type->tp_name) != (size_t)name_size) {
PyErr_SetString(PyExc_ValueError,
"type name must not contain null characters");
goto error;
}
/* 設置基類和基類列表 */
type->tp_bases = bases;
bases = NULL;
Py_INCREF(base);
type->tp_base = base;
/* 設置屬性表 */
Py_INCREF(dict);
type->tp_dict = dict;
//設置__module__
if (_PyDict_GetItemIdWithError(dict, &PyId___module__) == NULL) {
if (PyErr_Occurred()) {
goto error;
}
tmp = PyEval_GetGlobals();
if (tmp != NULL) {
tmp = _PyDict_GetItemIdWithError(tmp, &PyId___name__);
if (tmp != NULL) {
if (_PyDict_SetItemId(dict, &PyId___module__,
tmp) < 0)
goto error;
}
else if (PyErr_Occurred()) {
goto error;
}
}
}
//設置__qualname__,即"全限定名"
qualname = _PyDict_GetItemIdWithError(dict, &PyId___qualname__);
//......
//如果自定義的class中重寫了__new__方法,將__new__對應的函數改造為static函數
tmp = _PyDict_GetItemIdWithError(dict, &PyId___new__);
if (tmp != NULL && PyFunction_Check(tmp)) {
tmp = PyStaticMethod_New(tmp);
if (tmp == NULL)
goto error;
if (_PyDict_SetItemId(dict, &PyId___new__, tmp) < 0) {
Py_DECREF(tmp);
goto error;
}
Py_DECREF(tmp);
}
else if (tmp == NULL && PyErr_Occurred()) {
goto error;
}
//設置__init_subclass__,如果子類繼承了父類,那么會觸發父類的__init_subclass__方法
tmp = _PyDict_GetItemIdWithError(dict, &PyId___init_subclass__);
if (tmp != NULL && PyFunction_Check(tmp)) {
tmp = PyClassMethod_New(tmp);
if (tmp == NULL)
goto error;
if (_PyDict_SetItemId(dict, &PyId___init_subclass__, tmp) < 0) {
Py_DECREF(tmp);
goto error;
}
Py_DECREF(tmp);
}
else if (tmp == NULL && PyErr_Occurred()) {
goto error;
}
//設置__class_getitem__,這個是什么?類似於__getitem__
//__class_getitem__支持通過 類["xxx"] 的方式訪問
tmp = _PyDict_GetItemIdWithError(dict, &PyId___class_getitem__);
//......
//為class對象對應的instance對象設置內存大小信息
type->tp_basicsize = slotoffset;
type->tp_itemsize = base->tp_itemsize;
type->tp_members = PyHeapType_GET_MEMBERS(et);
//......
//調用PyType_Ready對class對象進行初始化
if (PyType_Ready(type) < 0)
goto error;
/* Put the proper slots in place */
fixup_slot_dispatchers(type);
if (type->tp_dictoffset) {
et->ht_cached_keys = _PyDict_NewKeysForClass();
}
if (set_names(type) < 0)
goto error;
if (init_subclass(type, kwds) < 0)
goto error;
Py_DECREF(dict);
return (PyObject *)type;
error:
Py_XDECREF(dict);
Py_XDECREF(bases);
Py_XDECREF(slots);
Py_XDECREF(type);
return NULL;
}
Python虛擬機首先會將類名、基類列表和屬性表從tuple對象中解析出來,然后會基於基類列表及傳入的metaclass(參數metatype)確定最佳的metaclass和base。
隨后,python虛擬機會調用metatype->tp_alloc
嘗試為要創建的類對象分配內存。這里需要注意的是,在PyType_Type中,我們發現tp_alloc是一個NULL,這顯然不正常。但是不要忘記,我們之前提到,在Python進行初始化時,會對所有的內建對象通過PyType_Ready進行初始化,在這個初始化過程中,有一項動作就是從基類繼承各種操作。由於type.__bases__中的第一個基類是object,所以type會繼承object中的tp_alloc操作,即 PyType_GenericAlloc 。對於我們的任意繼承自object的class對象來說, PyType_GenericAlloc 將申請metatype->tp_basicsize + metatype->tp_itemsize
大小的內存空間。從PyType_Type的定義中我們看到,這個大小實際就是 sizeof(PyHeapTypeObject) + sizeof(PyMemerDef) 。因此在這里應該就明白了PyHeapTypeObject這個老鐵到底是干嘛用的了,之前因為偏移量的問題,折騰了不少功夫,甚至讓人覺得這有啥用啊,但是現在意識到了,這個老鐵是為用戶自定義class准備的。
接下來就是設置class對象的各個域,其中包括了在tp_dict上設置屬性表,也就是__dict__。另外注意的是,這里還計算了類對象對應的實例對象所需要的內存大小信息,換言之,我們類創建一個實例對象時,需要為這個實例對象申請多大的內存空間呢?對於任意繼承object的class對象來說,這個大小為PyBaseObject_Type->tp_basicsize + 16
。其中的16是2 * sizeof(PyObject *)。為什么后面要跟着兩個PyObject *的空間,因為這些空間的地址被設置給了 tp_dictoffset 和 tp_weaklistoffset 了呢?這一點將在下一篇博客中進行解析,它是和實例對象的屬性字典密切相關的。
最后,Python虛擬機還會調用PyType_Ready對class定義的類對象(這里簡稱class對象)
進行和內建對象一樣的初始化動作,到此class對象才算正式創建完畢。那么內建對象和class對象在內存布局上面有什么區別呢?畢竟都是類對象。
本質上,無論用戶自定義的class對象還是內建對象,在Python虛擬機內部,都可以用一個PyTypeObject來表示。但不同的是,內建對象的PyTypeObject以及與其關聯的PyNumberMethods等屬性的內存位置都是在編譯時確定的,它們在內存中的位置是分離的。而用戶自定義的class對象的PyTypeObject和PyNumberMethods等內存位置是連續的,必須在運行時動態分配內存。
現在我們算是對python中可調用(callable)這個概念有一個感性認識了,在python中可調用這個概念是一個相當通用的概念,不拘泥於對象、大小,只要類型對象定義了tp_call操作,就能進行調用操作。我們已經看到,python中的對象class對象是調用metaclass創建。那么顯然,調用class對象就能得到實例對象。
小結
這一次我們介紹了自定義的類在底層是如何實現的,但是關於類的知識點還有很多,比如:魔法方法、描述符等等,我們可能還需要兩到三篇來進行介紹。