Python 界的領袖 Tim Peters 說的:
元類就是深度的魔法,99% 的用戶應該根本不必為此操心。如果你想搞清楚究竟是否需要用到元類,那么你就不需要它。那些實際用到元類的人都非常清楚地知道他們需要做什么,而且根本不需要解釋為什么要用元類。
所以,這篇文章,認真閱讀一遍就好了。
目錄
一、Python 中類也是對象
在了解元類之前,我們先進一步理解 Python 中的類,在大多數編程語言中,類就是一組用來描述如何生成一個對象的代碼段。在 Python 中這一點也是一樣的。
這點在學習類的章節也強調過了,下面可以通過例子回憶一下:
但是,Python 中的類有一點跟大多數的編程語言不同,在 Python 中,可以把類理解成也是一種對象。對的,這里沒有寫錯,就是對象。
為什么呢?
因為只要使用關鍵字 class ,Python 解釋器在執行的時候就會創建一個對象。
如:
class (object): pass
當程序運行這段代碼的時候,就會在內存中創建一個對象,名字就是。這個對象(類)自身擁有創建對象(類實例)的能力,而這就是為什么它是一個類的原因。
但是,它的本質仍然是一個對象,于是我們可以對它做如下的操作:
# 可以直接打印一個類,因為它其實也是一個對象print()# 可以直接把一個類作為參數傳給函數(注意這里是類,是沒有實例化的)echo()# 也可以直接把類賦值給一個變量 = ()
輸出的結果如下:
?
二、使用 type() 動態創建類
因為類也是對象,所以我們可以在程序運行的時候創建類。
Python 是動態語言。
動態語言和靜態語言最大的不同,就是函數和類的定義,不是編譯時定義的,而是運行時動態創建的。
在之前,我們先了了解下 type() 函數。
首先我們新建一個 hello.py 的模塊,然后定義一個 Hello 的 class ,
然后在另一個模塊中引用 hello 模塊,并輸出相應的信息。
其中 type() 函數的作用是可以查看一個類型和變量的類型。
上面也提到過,type() 函數可以查看一個類型或變量的類型,Hello 是一個 class ,它的類型就是 type ,而 h 是一個實例,它的類型就是 com..hello.Hello。
前面的 com. 是我的包名,hello 模塊在該包名下。
在這里還要細想一下,上面的例子中,我們使用 type() 函數查看一個類型或者變量的類型。
其中查看了一個 Hello class 的類型,打印的結果是: 。
其實 type() 函數不僅可以返回一個對象的類型,也可以創建出新的類型。
class 的定義是運行時動態創建的,而創建 class 的方法就是使用 type() 函數。
比如我們可以通過 type() 函數創建出上面例子中的 Hello 類,具體看下面的代碼:
在這里,需先了解下通過 type() 函數創建 class 對象的參數說明:
1、class 的名稱,比如例子中的起名為 Hello
2、繼承的父類集合,注意 Python 支持多重繼承,如果只有一個父類,tuple 要使用單元素寫法;例子中繼承 object 類,因為是單元素的 tuple ,所以寫成 (object,)
3、class 的方法名稱與函數綁定;例子中將函數 綁定在方法名 hello 中
具體的模式如下:
type(類名, 父類的元組(針對繼承的情況,可以為空),包含屬性的字典(名稱和值))
好了,了解完具體的參數使用之外,我們看看輸出的結果,可以看到,通過 type() 函數創建的類和直接寫 class 是完全一樣的。
這是因為Python 解釋器遇到 class 定義時,僅僅是掃描一下 class 定義的語法,然后調用 type() 函數創建出 class 的。
不過一般的情況下,我們都是使用 class ***... 的方法來定義類的,不過 type() 函數也可以讓我們創建出類來。
也就是說,動態語言本身支持運行期動態創建類,這和靜態語言有非常大的不同,要在靜態語言運行期創建類,必須構造源代碼字符串再調用編譯器,或者借助一些工具生成字節碼實現,本質上都是動態編譯,會非常復雜。
可以看到,在 Python 中,類也是對象,你可以動態的創建類。
其實這也就是當你使用關鍵字 class 時 Python 在幕后做的事情,而這就是通過元類來實現的。
三、什么是元類
通過上面的介紹,終于模模糊糊的帶到元類這里來了。可是我們到現在還不知道元類是什么鬼東西。
我們創建類的時候,大多數是為了創建類的實例對象。
那么元類呢?
元類就是用來創建類的。也可以換個理解方式就是:元類就是類的類。
通過上面 type() 函數的介紹,我們知道可以通過 type() 函數創建類:
MyClass = type('MyClass', (), {})
實際上 type() 函數是一個元類。
type() 就是 Python 在背后用來創建所有類的元類。
那么現在我們也可以猜到一下為什么 type() 函數是 type 而不是 Type呢?
這可能是為了和 str 保持一致性,str 是用來創建字符串對象的類,而 int 是用來創建整數對象的類。
type 就是創建類對象的類。
你可以通過檢查 屬性來看到這一點。
Python 中所有的東西,注意喔,這里是說所有的東西,他們都是對象。
這包括整數、字符串、函數以及類。它們全部都是對象,而且它們都是從一個類創建而來。
輸出的結果如下:
可以看到,上面的所有東西,也就是所有對象都是通過類來創建的,那么我們可能會好奇, 的 會是什么呢?
換個說法就是,創建這些類的類是什么呢?
我們可以繼續在上面的代碼基礎上新增下面的代碼:
認真觀察,再理清一下,上面輸出的結果是我們把整形 age ,字符創 name ,函數 fu 和對象實例 mEat 里 的 打印出來的結果。
也可以說是他們類的類打印結果。發現打印出來的 class 都是 type 。
一開始也提到了,元類就是類的類。
也就是元類就是負責創建類的一種東西。
你也可以理解為,元類就是負責生成類的。
而 type 就是內建的元類。也就是 Python 自帶的元類。
四、自定義元類
到現在,我們已經知道元類是什么鬼東西了。
那么,從始至終我們還不知道元類到底有啥用。
只是了解了一下元類。
在了解它有啥用的時候,我們先來了解下怎么自定義元類。
因為只有了解了怎么自定義才能更好的理解它的作用。
首先我們來了解下 屬性
,直譯為元類,簡單的解釋就是:
當我們定義了類以后,就可以根據這個類創建出實例,所以:先定義類,然后創建實例。
但是如果我們想創建出類呢?
那就必須根據創建出類,所以:先定義,然后創建類。
連接起來就是:先定義,就可以創建類,最后創建實例。
所以, 允許你創建類或者修改類。
換句話說,你可以把類看成是 創建出來的“實例”。
class (object): = …[…]
如果是這樣寫的話,Python 就會用元類來創建類 。
當你寫下 class (object),但是類對象 還沒有在內存中創建。P
ython 會在類的定義中尋找 屬性,如果找到了,Python 就會用它來創建類 ,如果沒有找到,就會用內建的 type 函數來創建這個類。如果還不怎么理解,看下下面的流程圖:
再舉個實例:
class Foo(Bar): pass
它的判斷流程是怎樣的呢?
首先判斷 Foo 中是否有 這個屬性?如果有,Python 會在內存中通過 創建一個名字為 Foo 的類對象(注意,這里是類對象)。如果 Python 沒有找到 ,它會繼續在 Bar(父類)中尋找 屬性,并嘗試做和前面同樣的操作。如果 Python在任何父類中都找不到 ,它就會在模塊層次中去尋找 ,并嘗試做同樣的操作。如果還是找不到 ,Python 就會用內置的 type 來創建這個類對象。
其實 就是定義了 class 的行為。類似于 class 定義了 的行為, 則定義了 class 的行為。可以說,class 是 的 。
現在,我們基本了解了 屬性,但是,也沒講過如何使用這個屬性,或者說這個屬性可以放些什么?
答案就是:可以創建一個類的東西。那么什么可以用來創建一個類呢?type,或者任何使用到 type 或者子類化 type 的東東都可以。
元類的主要目的就是為了當創建類時能夠自動地改變類。
通常,你會為API 做這樣的事情,你希望可以創建符合當前上下文的類。假想一個很傻的例子,你決定在你的模塊里所有的類的屬性都應該是大寫形式。有好幾種方法可以辦到,但其中一種就是通過在模塊級別設定 。采用這種方法,這個模塊中的所有類都會通過這個元類來創建,我們只需要告訴元類把所有的屬性都改成大寫形式就萬事大吉了。
幸運的是, 實際上可以被任意調用,它并不需要是一個正式的類。所以,我們這里就先以一個簡單的函數作為例子開始。
# 元類會自動將你通常傳給‘type’的參數作為自己的參數傳入
# 請記住,'type'實際上是一個類,就像'str'和'int'一樣# 所以,你可以從type繼承class (type): # __new__ 是在之前被調用的特殊方法 # __new__是用來創建對象并返回之的方法 # 而只是用來將傳入的參數初始化給對象 # 你很少用到__new__,除非你希望能夠控制對象的創建 # 這里,創建的對象是類,我們希望能夠自定義它,所以我們這里改寫__new__ # 如果你希望的話,你也可以在中做些事情 # 還有一些高級的用法會涉及到改寫特殊方法,但是我們這里不用
# 復用type.__new__方法 # 這就是基本的OOP編程,沒什么魔法 return type.__new__(, , , )
你可能已經注意到了有個額外的參數 ,這并沒有什么特別的。類方法的第一個參數總是表示當前的實例,就像在普通的類方法中的 self 參數一樣。當然了,為了清晰起見,這里的名字我起的比較長。但是就像 self 一樣,所有的參數都有它們的傳統名稱。因此,在真實的產品代碼中一個元類應該是像這樣的:
如果使用 super 方法的話,我們還可以使它變得更清晰一些,這會緩解繼承(是的,你可以擁有元類,從元類繼承,從 type 繼承)
通常我們都會使用元類去做一些晦澀的事情,依賴于自省,控制繼承等等。確實,用元類來搞些“黑暗魔法”是特別有用的,因而會搞出些復雜的東西來。但就元類本身而言,它們其實是很簡單的:
五、使用元類
*請認真填寫需求信息,我們會在24小時內與您取得聯系。