一文帶你完全理解Python中的metaclass

Class也是Object

在理解metaclass之前,我們需要先理解Python中的class。從某種程度上來說,Python中的class的定位比較特殊。

對于大部分面向對象語言來說,class是一段定義了如何產生object的代碼塊。在Python中這一定義也成立:

>>> class example(object):
...     pass
...
>>> object1 = example()
>>> print(object1)
<__main__.example object at 0x102e26990>

但是在Python中,class并不只有這一角色。class實際上也是object。當我們使用class定義一個類的時候,Python會執行相應代碼并在內存中創建一個名為example的object。

但該object(class)是具有創建其他object(instance)的能力的。這也是這個object是一個class的原因。由于本質上class任然是一個object,所以我們可以對class做出以下操作:

  • 我們可以將其賦給一個變量
  • 我們可以對其進行拷貝
  • 我們可以賦給其新的變量
  • 我們可以將其作為參數賦給其他的函數

舉例如下:

# print a class since it's an object
>>> print(example)
<class '__main__.example'>

# assign an attribute to the class
>>> print(hasattr(example, 'new_attribute'))
False
>>> example.new_attribute = 'assign an attribute to the class'
>>> print(hasattr(example, 'new_attribute'))
True
>>> print(example.new_attribute)
assign an attribute to the class

# assign the class to a variable
>>> example_mirror = example
>>> print(example_mirror)
<class '__main__.example'>
>>> print(example_mirror())
<__main__.example object at 0x102e26a90>

# pass class as a parameter
>>> def echo(cls):
...     print(cls)
...
>>> echo(example)
<class '__main__.example'>

動態創建class

既然class也是object,那么我們就可以像創建普通的object一樣動態創建class。

第一種方法,我們可以在方法中創建class。如下面的例子所示:

>>> def dynamic_class_creater(name):
...     if name == 'name1':
...         class class1(object):
...             pass
...         return class1
...     else:
...         class class2(object):
...             pass
...         return class2
...
>>> first_class = dynamic_class_creater('name1')
>>> print(first_class)
<class '__main__.class1'>
>>> print(first_class())
<__main__.class1 object at 0x10e4149d0>

但通過這種方式創建class并沒有特別動態。我們任然需要自己定義類的具體內容??紤]到class也是object,那么也一定有某種方法能夠像產生instance一樣產生類。

當我們使用class關鍵字創建類的時候,Python會自動創建對應的object。像Python中其他大多數情況一樣,我們也可以手動創建這個class object。這一操作可以通過type()實現。

通常情況下我們可以調用type來得到一個object的類型是什么。如下面的例子所示:

>>> print(type(1))
<type 'int'>

>>> print(type('str'))
<type 'str'>

>>> print(type(example()))
<class '__main__.example'>

>>> print(type(example))
<type 'type'>

在這里我們看到我們所創建example類的type是'type'。這實際上也就是接下來要討論的內容。既type的完全不同的功能——type可以動態創建class。type()函數可以接收class的描述來作為參數并返回所生成的class object。type同時具有這兩個迥異的功能是由于Python兼容性問題導致的。在此我們不做深究。

當使用type創建class時,其用法如下:

type(class_name, tuple_of_parent_class, dict_of_attribute_names_and_values)

其中第二個參數tuple_of_parent_class用來表示繼承關系,可以為空。第三個參數用來描述我們所要創建的類所應該具有的attribute。如下面的例子所示:

>>>class class_example(object):
...     pass

上面定義的這個類可以由如下type函數創建:

>>>class_example = type('class_example', (), {}) # create a class on the fly
>>>print(class_example)
<class '__main__.class_example'>
>>> print(class_example()) # get a instance of the class
<__main__.class_example object at 0x10e414b10>

在這個例子中,type所接收的第一個參數'class_example'是該類的類名,同時我們使用了class_example作為存儲該class object引用的變量。這二者可以不同。但一般我們沒有理由采用不同的名字從而使得代碼更加復雜。

我們也可以使用一個字典來定義所創建的class的attribute:

>>> class_example = type('class_example', (), {'attr': 1})
>>> print(class_example)
<class '__main__.class_example'>
>>> print(class_example.attr)
1
>>> print(class_example())
<__main__.class_example object at 0x10e414a90>
>>> print(class_example().attr)
1

上面的例子中type返回的class等同于下面這個class:

>>> class class_example(object):
...     attr = 1

當然,我們也可以用type返回一個繼承class_example的類:

>>> child_example = type('child_example', (class_example,), {})
>>> print(child_example)
<class '__main__.child_example'>
>>> print(child_example.attr)
1

上面這個例子中type返回的class等同于如下class:

>>> class child_example(class_example):
...     pass

我們甚至可以動態創建包括方法的類。只要我們創建好方法并將其賦給相應的attribute即可:

>>> def echo(self):
...     print(self.attr)
...
>>> child_example = type('child_example', (class_example,), {'echo': echo})
>>> hasattr(class_example, 'echo')
False
>>> hasattr(child_example, 'echo')
True
>>> child_example().echo()
1

同樣,我們也可以先動態創建一個class,然后再賦給其新的方法:

>>> child_example = type('child_example', (class_example,), {})
>>> def another_method(self):
...     print('another method')
...
>>> child_example.another_method = another_method
>>> hasattr(child_example, 'another_method')
True
>>> child_example().another_method()
another method

綜上所述,Python中的class其實是一個object,并且我們可以動態創建class。事實上這也是我們在使用class關鍵字的時候Python所做的事情。Python通過使用metacalss來實現這一過程。

究竟什么是metaclass?

metaclass就是Python中用來創建class object的class。我們可以將其看做能夠產生class的類工廠。我們可以通過如下例子理解這個關系:

class = metaclass()
object = class()

從上文中我們知道了type()可以被用來動態創建class,這是因為實際上type是一個metaclass。而且type實際上是Python用在在幕后創建所有class的metaclass。

包括int, string, function, class在內,Python中所有的東西都是object,而所有的object都是被相應的class創造的。我們可以通過__class__的值得知這一點。

>>> age = 24
>>> age.__class__
<type 'int'>

>>> name = 'bob'
>>> name.__class__
<type 'str'>

>>> def foo(): pass
>>> foo.__class__
<type 'function'>

>>> class Bar(object): pass
>>> bar = Bar()
>>> bar.__class__
<class '__main__.Bar'>

那么,這些__class____class__又是什么呢?

>>> age.__class__.__class__
<type 'type'>
>>> name.__class__.__class__
<type 'type'>
>>> foo.__class__.__class__
<type 'type'>
>>> bar.__class__.__class__
<type 'type'>

可以看出,所有的class都來自于typetype,作為metaclass,創建了以上所有的class object。

type是Python定義好的metaclass。當然,我們也可以自定義metaclass。

類的__metaclass__ attribute

當定義class的時候,我們可以使用__metaclass__ attribute來指定用來初始化當前class的metaclass。如下面的例子所示:

class Foo(object):
    __metaclass__ = something
    [other statements...]

如果我們指定了__metaclass__,Python就是使用這個metaclass來生成class Foo

當Python試圖創建class Foo的時候,Python會首先在class的定義中尋找__metaclass__ attribute。如果存在__metaclass__,Python將會使用指定的__metaclass__來創建class Foo。如果沒有指定的話,Python就會使用默認的type作為metaclas創建Foo。

所以,對于下面這個例子:

class Foo(Bar):
    pass

Python首先在Foo中尋找是否存在__metaclass__ attribute。

如果存在的話,Python將使用這個metaclass在內存中創建一個名字為Foo的class object。如果Python

如果class定義中不存在__metaclass__的話,Python將會尋找MODULE級別的__metaclass__。如果存在的話鳩進行與前述相同的操作。但是只有我們定義的class沒有繼承任何類的情況下,Python才會在MODULE級別尋找__metaclass__?;蛘哒f,只有當該類是一個舊類的情況下,Python才會在MODULE級別尋找__metaclass__。(關于新類和舊類的區別,請看這篇文章).

當Python仍然沒有找到__metaclass__時,Python將會使用當前類的母類的metaclass來創建當前類。在我們上面這個例子中,Python會使用Foo的母類Bar的metaclass來創建Foo的class object。

同時需要注意的是,在class中定義的__metaclass__ attribute并不會被子類繼承。被子類繼承的是母類的metaclass,也就是母類的.__class__ attribute。比如上面的例子中,Bar.__class__將會被Foo繼承。也就是說,如果Bar定義了一個__metaclass__ attribute來使用type()創建Bar的class object(而非使用type.__new__()),那么Bar的子類,也就是Foo,并不會繼承這一行為。

那么問題來了:我們究竟應該在__metaclass__ attribute中定義什么?

答案是:能夠創建class的東西。

那么什么能夠創建class呢?type,或者任何type的子類。

自定義metaclass

metaclass的主要目的是在class被創建的時候對生成的class進行自動的動態修改。

一般來說,這一點主要應用于API,例如我們想要根據當前的內容創建相匹配的class。

舉一個簡單的例子如下:我們決定讓當前module下所有的class的attribute的名字都是大寫。要實現這個功能有很多種方法。使用__metaclass__就是其中之一。

設置了__metaclass__的話,class的創建就會由指定的metaclass處理,那么我們只需要讓這個metaclass將所有attribute的名字改成大寫即可。

__metaclass__可以是任何Python的callable,不必一定是一個正式的class。

下面我們首先給出一個使用function作為__metaclass__的例子。

# the metaclass will automatically get passed the same argument 
# that is passed to `type()`
def upper_attr(class_name, class_parents, class_attr):
    '''Return a class object, with the list of its attribute turned into 
    uppercase.
    '''
    # pick up any attribute that doesn't start with '__' and turn it into uppercase.
    uppercase_attr = {}
    for name, val in class_attr.items():
        if name.startswith('__'):
            uppercase_attr[name] = val
        else:
            uppercase_attr[name.upper()] = val
    
    # let `type` do the class creation
    return type(class_name, class_parents, uppercase_attr)


class Foo(object):
    # this __metaclass__ will affect the creation of this new style class
    __metaclass__ = upper_attr
    bar = 'bar'


print(hasattr(Foo), 'bar')
# False

print(hasattr(Foo), 'BAR')
# True

f = Foo()
print(f.BAR)
# 'bar'

接下來我們通過繼承type的方式實現一個真正的class形式的metaclass。注意如果尚不清楚__new____init__的作用和區別的,請看這篇文章.

# remember that `type` is actually a just a class like int or str
# so we can inherit from it.

class UpperAttrMetaclass(type):
    '''
    __new__ is the method called before __init__
    It's the function that actually creates the object and returns it.
    __init__ only initialize the object passed as a parameter.
    We rarely use __new__, except when we want to control how the object
    is created.
    For a metaclass, the object created is a class. And since we want to 
    customize it, we need to override __new__.
    We can also do something by overriding __init__ to get customized initialization
    process as well.
    Advanced usage involves override __call__, but we won't talk about this here.
    '''
    def __new__(upperattr_metaclass, class_name, class_parents, class_attr):
        uppercase_attr = {}
        for name, val in class_attr.items():
            if name.startswith('__'):
                uppercase_attr[name] = val
            else:
                uppercase_attr[name.upper()] = val
        return type(class_name, class_parents, uppercase_attr)

但這不是很OOP。我們直接調用了type而非調用type.__new__。那么OOP的做法如下。

class UpperAttrMetaclass(type):
    def __new__(upperattr_metaclass, class_name, class_parents, class_attr):
        uppercase_attr = {}
        for name, val in class_attr.items():
            if name.startswith('__'):
                uppercase_attr[name] = val
            else:
                uppercase_attr[name.upper()] = val
        # basic OOP. Reuse the parent's `__new__()`
        return type.__new__(upperattr_metaclass, class_name, class_parents, uppercase_attr)

我們注意到,__new__所接收的參數中有一個額外的upperattr_metaclass。這沒有什么特別的。如同__init__總是接收調用它的object作為第一個參數一樣(慣例上用self來命名__init__所接收的第一個參數),__new__總是接收其被定義在內的class作為第一個參數,就像類方法總是接收其被定義的class作為第一個參數一樣(慣例上用cls命名類方法所接收的第一個參數)。

清楚起見,這里給出的例子的變量和方法名都很長。但在實際的應用中,類似于使用selfcls代替第一個參數,我們可以將這些名字替換為更加簡潔的形式:

class UpperAttrMetaclass(type):
    def __new__(cls, cls_name, bases, attr_dict):
        uppercase_attr = {}
        for name, val in attr_dict.items():
            if name.startswith('__'):
                uppercase_attr[name] = val
            else:
                uppercase_attr[name.upper()] = val
        return type.__new__(cls, cls_name, bases, uppercase_attr)

通過應用super,我們可以使得上面這段代碼更加干凈簡潔,也使得繼承更加容易(我們可能有metaclass繼承別的一些metaclass,而這些metaclass又繼承type):

class UpperAttrMetaclass(type):
    def __new__(cls, cls_name, bases, attr_dict):
        uppercase_attr = {}
        for name, val in attr_dict.items():
            if name.startswith('__'):
                uppercase_attr[name] = val
            else:
                uppercase_attr[name.upper()] = val
        return super(UpperAttrMetaclass, cls).__new__(cls, cls_name, bases, uppercase_attr)

Voilà!上述基本就是關于metaclass的一切了。

使用metaclass之所以復雜,不是因為其代碼實現復雜,而是因為我們一般使用metaclass來做一些邏輯上很復雜的操作,例如自省,修改繼承以及改變類的默認attribute如__dict__等。

metaclass的確可以被用來實現一些奇妙的功能,也因此可以用來進行極其復雜的邏輯操作。但是metaclass本身是很簡單的:

  • 影響class初始化的過程
  • 修改class的內容
  • 返回修改過的class

為什么我們要使用metaclass,而不是使用一些函數來實現類似的功能?

就像前文所說,__metaclass__實際上可以是任何callable,那么為什么我們還要使用metaclass而不是直接調用這些函數呢?

使用class作為metaclass有如下幾個理由:

  • 使用class作為metaclass能夠使得我們代碼的動機更加明確。比如當我們讀到上面所定義的UpperAttrMetaclass(type)代碼時,我們清楚地知道接下來這段代碼想要干什么(自定義class object初始化的過程)。
  • 我們能夠使用OOP的思想進行處理。class作為metaclass可以繼承其他的metaclass,重載母類的方法,甚至可以使用別的metaclass。
  • 如果我們使用class作為metaclass,某一使用該metaclass的class的子類將仍是是其metaclass的實例。但這一功能無法通過使用函數作為metaclass實現。
  • 使用metaclass可以使得代碼結構更加優美。實際應用中我們很少使用metaclass來實現上面那樣簡單的功能。使用metaclass往往是為了實現非常復雜的操作。如果使用class作為metaclass,我們就可以把相應的方法封裝到這一個metaclass中,使得代碼更加易懂。
  • 使用class作為metaclass可以在class中容易的定義__new__,__init__,__call__方法。雖然我們在將所有的邏輯都放入__new__中,但有的時候根據需要使用其他幾個方法會使得邏輯更加清晰。
  • 額賊!人家名字就叫metaclass。這不是帶著個class嗎?

為什么我們要使用metaclass呢?

那么究竟為什么我們要使用metaclass這樣一個難以理解且容易出錯的實現方式呢?

答案是通常情況下我們不需要使用metaclass。

引用Python大師Tim Peters的話來說,就是:

Metaclasses are deeper magic that 99% of users should never worry about. If you wonder whether you need them, you don't (the people who actually need them know with certainty that they need them, and don't need an explanation about why).

metaclass主要的使用情況就是用來創建API。使用metaclass的一個典型的例子是Django ORM。

它是的我們可以使用如下當時定義一個model:

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

同時,如果我們調用這個model:

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

其并不會返回一個IntegerField對象,而是會返回一個int,甚至可以直接從數據庫中調用這個值。

正是因為models.Model定義了__metaclass__,并使用了一些操作來將我們使用簡單的語句定義的Person轉化成了與數據庫相應的域相聯系的類,這種邏輯才成為可能。

Django使得很多復雜的邏輯僅暴露一個簡單的API接口就可以調用,這正是通過metaclass實現的。metaclass會根據需要重新實現這些復雜操作所需要的真正的代碼。

再說兩句

首先我們知道了Python中的class實際上是object,同時class仍具有創建對應的實例的能力。

實際上class本身也是metaclass的實例。

>>> class Foo(object):
...     pass
...
>>> id(Foo)
4299321816

Python中的任何東西都是object,這些object不是class的實例就是metaclass的實例。

當然,type除外。

type事實上是其自身的metaclass。我們使用Python是無法重復這種實現的。這一邏輯是在Python代碼實現的層面定義的。引用一下道德經中的說法,我們可以說Python中type生metaclass,metaclass生class,class生萬物

另外,metaclass的應用一般頗為復雜,大多數情況下我們可以使用別的方法實現相同的功能。比如我們可以通過一下兩種技術修改class:

  • monkey patching
  • class decorators

99%我們需要改變class的情況下,我們使用上述兩種技術可以解決。

但事實是,99%的情況下我們根本不需要改變class。

原文鏈接:https://stackoverflow.com/a/6581949/6037083

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 1.元類 1.1.1類也是對象 在大多數編程語言中,類就是一組用來描述如何生成一個對象的代碼段。在Python中這...
    TENG書閱讀 1,315評論 0 3
  • 譯注:這是一篇在Stack overflow上很熱的帖子。提問者自稱已經掌握了有關Python OOP編程中的各種...
    LazzMan閱讀 603評論 0 1
  • 類也是對象 在理解元類之前,需要先掌握Python中的類。Python中類的概念借鑒于Smalltalk,這顯得有...
    蘭山小亭閱讀 907評論 0 5
  • 轉至元數據結尾創建: 董瀟偉,最新修改于: 十二月 23, 2016 轉至元數據起始第一章:isa和Class一....
    40c0490e5268閱讀 1,776評論 0 9
  • 人生路,有很多事的事最終都會云淡風輕,我沒有必要太多的掛心。有時候你所擔心的和你所關心的,在你最不想留住的那些時刻...
    一心小記閱讀 583評論 0 2