1. 程式人生 > >深刻理解Python中的元類(metaclass)

深刻理解Python中的元類(metaclass)

1.類也是物件

在理解元類之前,你需要先掌握Python中的類。Python中類的概念借鑑於Smalltalk,這顯得有些奇特。在大多數程式語言中,類就是一組用來描述如何生成一個物件的程式碼段。在Python中這一點仍然成立:

>>> class ObjectCreator(object):
…       pass
…
>>> my_object = ObjectCreator()
>>> print my_object
<__main__.ObjectCreator object at 0x8974f2c>

但是,Python中的類還遠不止如此。類同樣也是一種物件。是的,沒錯,就是物件。只要你使用關鍵字class,Python直譯器在執行的時候就會建立一個物件。下面的程式碼段:

>>> class ObjectCreator( object ) :
…        pass
…
將在記憶體中建立一個物件,名字就是ObjectCreator。這個物件(類)自身擁有建立物件(類例項)的能力,而這就是為什麼它是一個類的原因。但是,它的本質仍然是一個物件,於是乎你可以對它做如下的操作:

1)   你可以將它賦值給一個變數

2)   你可以拷貝它

3)   你可以為它增加屬性

4)   你可以將它作為函式引數進行傳遞

下面是示例:

# 你可以列印一個類,因為它其實也是一個物件
>>> print ObjectCreator
< class '__main__.ObjectCreator' >
>>> def echo ( o ) :
…        print o
…
# 你可以將類做為引數傳給函式
>>> echo ( ObjectCreator )
< class '__main__.ObjectCreator' >
>>> print hasattr ( ObjectCreator , 'new_attribute' )
Fasle
#  你可以為類增加屬性
>>> ObjectCreator . new_attribute = 'foo'
>>> print hasattr ( ObjectCreator , 'new_attribute' )
True
>>> print ObjectCreator . new_attribute
foo
# 你可以將類賦值給一個變數
>>> ObjectCreatorMirror = ObjectCreator
>>> print ObjectCreatorMirror ( )
< __main__ . ObjectCreator object at 0x8997b4c >

2.動態地建立類

因為類也是物件,你可以在執行時動態的建立它們,就像其他任何物件一樣。首先,你可以在函式中建立類,使用class關鍵字即可。

# 返回的是類,不是類的例項
>>> def choose_class( name ) :
…        if name =='foo' :
…            class Foo ( object ) :
…                pass
…            return Foo
…        else :
…            class Bar ( object ) :
…                pass
…            return Bar
…
# 函式返回的是類,不是類的例項
>>> MyClass =choose_class ( 'foo' )
>>> print MyClass
< class '__main__' . Foo >
# 你可以通過這個類建立類例項,也就是物件
>>> print MyClass( )
< __main__ . Foo objectat 0x89c6d4c >

但這還不夠動態,因為你仍然需要自己編寫整個類的程式碼。由於類也是物件,所以它們必須是通過什麼東西來生成的才對。當你使用class關鍵字時,Python直譯器自動建立這個物件。但就和Python中的大多數事情一樣,Python仍然提供給你手動處理的方法。還記得內建函式type嗎?這個古老但強大的函式能夠讓你知道一個物件的型別是什麼,就像這樣:

>>> print type ( 1 )
< type 'int' >
>>> print type ( "1" )
< type 'str' >
>>> print type ( ObjectCreator )
< type 'type' >
>>> print type ( ObjectCreator ( ) )
< class '__main__.ObjectCreator' >

這裡,type有一種完全不同的能力,它也能動態的建立類。type可以接受一個類的描述作為引數,然後返回一個類。(我知道,根據傳入引數的不同,同一個函式擁有兩種完全不同的用法是一件很傻的事情,但這在Python中是為了保持向後相容性)

type可以像這樣工作:

type (類名 , 父類的元組(針對繼承的情況,可以為空),包含屬性的字典(名稱和值))
比如下面的程式碼:
>>> class MyShinyClass( object ) :
…        pass

可以手動像這樣建立:

>>> MyShinyClass =type ( 'MyShinyClass' ,( ) , {} )    # 返回一個類物件
>>> print MyShinyClass
< class '__main__.MyShinyClass'>
>>> print MyShinyClass( )    #  建立一個該類的例項
< __main__ . MyShinyClassobject at 0x8997cec>

你會發現我們使用“MyShinyClass”作為類名,並且也可以把它當做一個變數來作為類的引用。類和變數是不同的,這裡沒有任何理由把事情弄的複雜。

type 接受一個字典來為類定義屬性,因此

>>> class Foo( object ) :
…        bar = True

可以翻譯為:

>>> Foo = type ( 'Foo' , () , { 'bar' : True } )

並且可以將Foo當成一個普通的類一樣使用:

>>> print Foo
< class '__main__.Foo' >
>>> print Foo. bar
True
>>> f = Foo ( )
>>> print f
< __main__ . Foo objectat 0x8a9b84c >
>>> print f. bar
True

當然,你可以向這個類繼承,所以,如下的程式碼:

>>> class FooChild( Foo ) :
…        pass

就可以寫成:

>>> FooChild =type ( 'FooChild' ,( Foo , ) , { } )
>>> print FooChild
< class '__main__.FooChild'>
>>> print FooChild. bar    # bar屬性是由Foo繼承而來
True

最終你會希望為你的類增加方法。只需要定義一個有著恰當簽名的函式並將其作為屬性賦值就可以了。

>>> def echo_bar( self ) :
…        print self . bar
…
>>> FooChild =type ( 'FooChild' ,( Foo , ) , { 'echo_bar' : echo_bar } )
>>> hasattr ( Foo, 'echo_bar' )
False
>>> hasattr ( FooChild, 'echo_bar' )
True
>>> my_foo =FooChild ( )
>>> my_foo . echo_bar( )
True

你可以看到,在Python中,類也是物件,你可以動態的建立類。這就是當你使用關鍵字class時Python在幕後做的事情,而這就是通過元類來實現的。


3.到底什麼是元類(終於到主題了)

元類就是用來建立類的“東西”。你建立類就是為了建立類的例項物件,不是嗎?但是我們已經學習到了Python中的類也是物件。好吧,元類就是用來建立這些類(物件)的,元類就是類的類,你可以這樣理解:

MyClass = MetaClass( )
MyObject = MyClass( )

你已經看到了type可以讓你像這樣做:

MyClass = type( 'MyClass' , ( ) , { } )

這是因為函式type實際上是一個元類。type就是Python在背後用來建立所有類的元類。現在你想知道那為什麼type會全部採用小寫形式而不是Type呢?好吧,我猜這是為了和str保持一致性,str是用來建立字串物件的類,而int是用來建立整數物件的類。type就是建立類物件的類。你可以通過檢查__class__屬性來看到這一點。Python中所有的東西,注意,我是指所有的東西——都是物件。這包括整數、字串、函式以及類。它們全部都是物件,而且它們都是從一個類建立而來。

>>> age = 35
>>> age . __class__
< type 'int' >
>>> name = 'bob'
>>> name . __class__
< type 'str' >
>>> def foo( ) : pass
>>> foo . __class__
< type 'function' >
>>> class Bar( object ) : pass
>>> b = Bar ( )
>>> b . __class__
< class '__main__.Bar' >

現在,對於任何一個__class__的__class__屬性又是什麼呢?

>>> a . __class__. __class__
< type 'type' >
>>> age . __class__. __class__
< type 'type' >
>>> foo . __class__. __class__
< type 'type' >
>>> b . __class__. __class__
< type 'type' >

因此,元類就是建立類這種物件的東西。如果你喜歡的話,可以把元類稱為“類工廠”(不要和工廠類搞混了)。type就是Python的內建元類,當然了,你也可以建立自己的元類。


4.__metaclass__屬性

你可以在寫一個類的時候為其新增__metaclass__屬性。

class Foo ( object) :
__metaclass__ = something…
[… ]

如果你這麼做了,Python就會用元類來建立類Foo。小心點,這裡面有些技巧。你首先寫下class Foo(object),但是類物件Foo還沒有在記憶體中建立。Python會在類的定義中尋找__metaclass__屬性,如果找到了,Python就會用它來建立類Foo,如果沒有找到,就會用內建的type來建立這個類。把下面這段話反覆讀幾次。當你寫如下程式碼時:

class Foo ( Bar) :
     pass

Python做了如下的操作:

Foo中有__metaclass__這個屬性嗎?如果是,Python會在記憶體中通過__metaclass__建立一個名字為Foo的類物件(我說的是類物件,請緊跟我的思路)。如果Python沒有找到__metaclass__,它會繼續在Bar(父類)中尋找__metaclass__屬性,並嘗試做和前面同樣的操作。如果Python在任何父類中都找不到__metaclass__,它就會在模組層次中去尋找__metaclass__,並嘗試做同樣的操作。如果還是找不到__metaclass__,Python就會用內建的type來建立這個類物件。

現在的問題就是,你可以在__metaclass__中放置些什麼程式碼呢?答案就是:可以建立一個類的東西。那麼什麼可以用來建立一個類呢?type,或者任何使用到type或者子類化type的東東都可以。


5.自定義元類

元類的主要目的就是為了當建立類時能夠自動地改變類。通常,你會為API做這樣的事情,你希望可以建立符合當前上下文的類。假想一個很傻的例子,你決定在你的模組裡所有的類的屬性都應該是大寫形式。有好幾種方法可以辦到,但其中一種就是通過在模組級別設定__metaclass__。採用這種方法,這個模組中的所有類都會通過這個元類來建立,我們只需要告訴元類把所有的屬性都改成大寫形式就萬事大吉了。

幸運的是,__metaclass__實際上可以被任意呼叫,它並不需要是一個正式的類(我知道,某些名字裡帶有‘class’的東西並不需要是一個class,畫畫圖理解下,這很有幫助)。所以,我們這裡就先以一個簡單的函式作為例子開始。

# 元類會自動將你通常傳給‘type’的引數作為自己的引數傳入
def upper_attr ( future_class_name, future_class_parents , future_class_attr ) :
     '''返回一個類物件,將屬性都轉為大寫形式'''
     # 選擇所有不以'__'開頭的屬性
     attrs = (( name , value ) for name ,value in future_class_attr. items ( ) ifnot name . startswith ( '__' ) )
     # 將它們轉為大寫形式
     uppercase_attr = dict ( ( name .upper ( ) , value) for name , value in attrs)
 
     # 通過'type'來做類物件的建立
     return type (future_class_name , future_class_parents, uppercase_attr )
 
__metaclass__ = upper_attr   #  這會作用到這個模組中的所有類
 
class Foo ( object) :
     # 我們也可以只在這裡定義__metaclass__,這樣就只會作用於這個類中
     bar = 'bip'

print hasattr ( Foo, 'bar' )
# 輸出: False
print hasattr ( Foo, 'BAR' )
# 輸出:True
 
f = Foo ( )
print f . BAR
# 輸出:'bip'

現在讓我們再做一次,這一次用一個真正的class來當做元類。

# 請記住,'type'實際上是一個類,就像'str'和'int'一樣
# 所以,你可以從type繼承
class UpperAttrMetaClass (type ) :
     # __new__ 是在__init__之前被呼叫的特殊方法
     # __new__是用來建立物件並返回之的方法
     # 而__init__只是用來將傳入的引數初始化給物件
     # 你很少用到__new__,除非你希望能夠控制物件的建立
     # 這裡,建立的物件是類,我們希望能夠自定義它,所以我們這裡改寫__new__
     # 如果你希望的話,你也可以在__init__中做些事情
     # 還有一些高階的用法會涉及到改寫__call__特殊方法,但是我們這裡不用
     def __new__ (upperattr_metaclass , future_class_name, future_class_parents , future_class_attr ) :
         attrs = (( name , value ) for name ,value in future_class_attr. items ( ) ifnot name . startswith ( '__' ) )
         uppercase_attr =dict ( ( name . upper ( ) , value) for name , value in attrs)
         return type ( future_class_name , future_class_parents, uppercase_attr )

但是,這種方式其實不是OOP。我們直接呼叫了type,而且我們沒有改寫父類的__new__方法。現在讓我們這樣去處理:

class UpperAttrMetaclass (type ) :
     def __new__ (upperattr_metaclass , future_class_name, future_class_parents , future_class_attr ) :
         attrs = (( name , value ) for name ,value in future_class_attr. items ( ) ifnot name . startswith ( '__' ) )
         uppercase_attr =dict ( ( name . upper ( ) , value) for name , value in attrs)
 
         # 複用type.__new__方法
         # 這就是基本的OOP程式設計,沒什麼魔法
         return type . __new__ ( upperattr_metaclass , future_class_name , future_class_parents, uppercase_attr )

你可能已經注意到了有個額外的引數upperattr_metaclass,這並沒有什麼特別的。類方法的第一個引數總是表示當前的例項,就像在普通的類方法中的self引數一樣。當然了,為了清晰起見,這裡的名字我起的比較長。但是就像self一樣,所有的引數都有它們的傳統名稱。因此,在真實的產品程式碼中一個元類應該是像這樣的:

class UpperAttrMetaclass (type ) :
     def __new__ (cls , name , bases , dct ):
         attrs = (( name , value ) for name ,value in dct . items ( ) ifnot name . startswith ( '__' )
         uppercase_attr    =dict ( ( name . upper ( ) , value) for name , value in attrs)
         return type . __new__ ( cls , name , bases ,uppercase_attr )

如果使用super方法的話,我們還可以使它變得更清晰一些,這會緩解繼承(是的,你可以擁有元類,從元類繼承,從type繼承)

class UpperAttrMetaclass (type ) :
     def __new__ (cls , name , bases , dct ):
         attrs = (( name , value ) for name ,value in dct . items ( ) ifnot name . startswith ( '__' ) )
         uppercase_attr =dict ( ( name . upper ( ) , value) for name , value in attrs)
         return super ( UpperAttrMetaclass , cls) . __new__ ( cls, name , bases, uppercase_attr )

就是這樣,除此之外,關於元類真的沒有別的可說的了。使用到元類的程式碼比較複雜,這背後的原因倒並不是因為元類本身,而是因為你通常會使用元類去做一些晦澀的事情,依賴於自省,控制繼承等等。確實,用元類來搞些“黑暗魔法”是特別有用的,因而會搞出些複雜的東西來。但就元類本身而言,它們其實是很簡單的:

1)   攔截類的建立

2)   修改類

3)   返回修改之後的類


6.為什麼要用metaclass類而不是函式?

由於__metaclass__可以接受任何可呼叫的物件,那為何還要使用類呢,因為很顯然使用類會更加複雜啊?這裡有好幾個原因:

1)意圖會更加清晰。當你讀到UpperAttrMetaclass(type)時,你知道接下來要發生什麼。

2)你可以使用OOP程式設計。元類可以從元類中繼承而來,改寫父類的方法。元類甚至還可以使用元類。

3)你可以把程式碼組織的更好。當你使用元類的時候肯定不會是像我上面舉的這種簡單場景,通常都是針對比較複雜的問題。將多個方法歸總到一個類中會很有幫助,也會使得程式碼更容易閱讀。

4)你可以使用__new__, __init__以及__call__這樣的特殊方法。它們能幫你處理不同的任務。就算通常你可以把所有的東西都在__new__裡處理掉,有些人還是覺得用__init__更舒服些。

5)哇哦,這東西的名字是metaclass,肯定非善類,我要小心!


7.究竟為什麼要使用元類?

現在回到我們的大主題上來,究竟是為什麼你會去使用這樣一種容易出錯且晦澀的特性?好吧,一般來說,你根本就用不上它:

“元類就是深度的魔法,99%的使用者應該根本不必為此操心。如果你想搞清楚究竟是否需要用到元類,那麼你就不需要它。那些實際用到元類的人都非常清楚地知道他們需要做什麼,而且根本不需要解釋為什麼要用元類。” —— Python界的領袖 Tim Peters

元類的主要用途是建立API。一個典型的例子是Django ORM。它允許你像這樣定義:

class Person ( models. Model ) :
     name = models. CharField ( max_length = 30 )
     age = models. IntegerField ( )

但是如果你像這樣做的話:

guy = Person(name = 'bob', age = '35')
print guy.age

這並不會返回一個IntegerField物件,而是會返回一個int,甚至可以直接從資料庫中取出資料。這是有可能的,因為models.Model定義了__metaclass__, 並且使用了一些魔法能夠將你剛剛定義的簡單的Person類轉變成對資料庫的一個複雜hook。Django框架將這些看起來很複雜的東西通過暴露出一個簡單的使用元類的API將其化簡,通過這個API重新建立程式碼,在背後完成真正的工作。

8.小結

首先,你知道了類其實是能夠創建出類例項的物件。好吧,事實上,類本身也是例項,當然,它們是元類的例項。

>>> class Foo (object) : pass
>>> id ( Foo)
142630324

Python中的一切都是物件,它們要麼是類的例項,要麼是元類的例項,除了type。type實際上是它自己的元類,在純Python環境中這可不是你能夠做到的,這是通過在實現層面耍一些小手段做到的。其次,元類是很複雜的。對於非常簡單的類,你可能不希望通過使用元類來對類做修改。你可以通過其他兩種技術來修改類:

2) Class decorators

當你需要動態修改類時,99%的時間裡你最好使用上面這兩種技術。當然了,其實在99%的時間裡你根本就不需要動態修改類!:)

相關推薦

pythonmetaclassattrs的使用

metaclass可以使用attrs檢視、修改子類的屬性 其中是class的屬性,不是(建立之後的,self.)物件的 可以從輸出carry在最後看到,Test定義之後馬上生成,而不是等到有語句。 self.d是不存在的,__init__也被看成屬性(attrs),然後

深刻理解Pythonmetaclass

1.類也是物件 在理解元類之前,你需要先掌握Python中的類。Python中類的概念借鑑於Smalltalk,這顯得有些奇特。在大多數程式語言中,類就是一組用來描述如何生成一個物件的程式碼段。在Python中這一點仍然成立: >>> class ObjectCreato

理解pythonmetaclass

本文是根據stackoverflow上關於“What are metaclasses in Python?”的回答翻譯總結而成的。原資料連線:https://stackoverflow.com/questions/100003/what-are-metaclasses-in-

pythonmetaclass

優先 裝飾器 target {} pass get tac 搜索 items 認識python中元類的準備工作。 1,首先需要明白一個概念就是python中一切皆為對象。 input: class Trick(object): pass print type(‘1234

Pythonmetaclass以及黑魔法__metaclass__

一、Python中的類 首先在這裡討論的python類,都是基於新式類進行討論。 在python中,一切皆為物件。 在理解元類之前我們先來重新理解一下python中的類。 class Joker: pass 當Python在執行帶clas

深刻理解Python(metaclass)

pytho light turn 理解 war highlight 參數 實例化 type 轉載地址:http://blog.jobbole.com/21351/ 另外有幾點理解記錄下: 創建一個實例時,有時會傳入參數,這些參數會同時傳入 __init__() 和 __

深刻理解Python(metaclass)

譯註:這是一篇在Stack overflow上很熱的帖子。提問者自稱已經掌握了有關Python OOP程式設計中的各種概念,但始終覺得元類(metaclass)難以理解。他知道這肯定和自省有關,但仍然覺得不太明白,希望大家可以給出一些實際的例子和程式碼片段以幫助理解

python

ech 變量 creat 類的定義 增加方法 sat variable 復雜 .... 元類 1. 類也是對象 在大多數編程語言中,類就是一組用來描述如何生成一個對象的代碼段。在Python中這一點仍然成立: >>> class ObjectCreator

python的作用

#-*- coding:utf-8 -*- def upper_attr(class_name, class_parents, class_attr): #遍歷屬性字典,把不是__開頭的屬性名字變為大寫 new_attr = {} for name,value

Python技巧:Metaclasses和利用Type構建的動態Dynamic Classes

`metaclass`和`type`關鍵字在Python程式碼中較少被使用(也正因如此,它們的作用也沒有很好的被理解)。在這篇文章中,我們將探究`type()`的型別(types)和跟`metaclasses`相關的`type`的用法。 這是我的型別麼?首先來看`ty

Python的列表listTuple,字典Dict和集合Set

一,列表(list) 定義一個列表使用一對中(方)括號”[ ] “。 python內建的一種資料型別是列表:list是一種有序的資料集合,可以隨意的新增和刪除其中的資料。比如列出班裡所有的同學的名字,列出所有工廠員工的工號等都是可以用到列表的。 不知道有沒

python的JSON1

welcome 定義 ack () found 存儲 remember nbsp python程序 很多程序都要求用戶輸入某種信息, 例如: 讓用戶存儲遊戲首選項或提供要可視化的數據,程序把用戶的信息存儲在列表和字典等數據結構中, 用戶關閉程序時,我們幾乎總要保存他們提

python的字串str操作

字串是python中資料型別。一般就單引號(‘’)或雙引號(“”)引起來的內容就是字串。 例如:下面兩個都是定義字串 str1 = "hello world" str2 = 'Hello World'  1、索引:就是下標,從0開始。預設是從左往右數;當索引為負數時,表示從右往左數。通過索引得

Python 2.7 (完結)

5 繼承 繼承語法 # 定義類 class C: pass # 繼承類 class CC(C): pass # 繼承模組類 class CCC(inheritance.MC): pass 在解析屬性引用時,如果請求屬性沒有在類中被找到,將會

Python去除列表list重複項的2種方式

方式一、轉換為集合再轉換為列表 list1=[11,22,11,22,33,44,55,55,66] print(list1) print(list(set(list1))) #輸出結果 #[11, 22, 11, 22, 33, 44, 55, 55, 66] #[33, 66, 11

深入理解java的介面 Interface

概念 我們知道java中是單繼承的,這就有了很多的限制,比如我們需要多繼承的時候但是不能用多繼承,這時候怎麼辦呢?就引入了介面的概念,它彌補了java中單繼承的缺陷,這一點非常的好,如果要約定子類的實現要求並避免單繼承侷限就需要使用介面。 那麼什麼是介面呢?

Python使用XMLRPC入門

一、簡介   RPC是Remote Procedure Call的縮寫,翻譯成中文為:遠端方法呼叫。 它是一種在本地機器上呼叫遠端機器上的一個過程(方法)的技術,這個過程也被大家稱為“分散式計算”,是為了提高各個分立機器的“互操作性”而發明出來的技術。   XML-

Python的容器container、迭代器iterator和生成器generator:yield

1、容器迭代器iterator  以list容器(tuple,range,file類似)為例,list容器可以呼叫自己的__iter__()方法獲取用來迭代自己內部元素的迭代器。例如: # list容器 myList = [1, 2, 3] # 獲取list容器的迭代器 it

python的函式

接觸過C語言的朋友對函式這個詞肯定非常熟悉,無論在哪門程式語言當中,函式(當然在某些語言裡稱作方法,意義是相同的)都扮演著至關重要的角色。今天就來了解一下Python中的函式用法。 一.函式的定義   在某些程式語言當中,函式宣告和函式定義是區分開的(在這些程式語言當中函式宣

Python的字典Dict

dict 格式: dict={key1:values1,key2:values,key3:values,...} dict={'name':'john','age':'22','address':'北京'} 字典的常用操作 (1)查詢字典中的元素 ①直接用變數