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

Class也是Object

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

對(duì)于大部分面向?qū)ο笳Z言來說,class是一段定義了如何產(chǎn)生object的代碼塊。在Python中這一定義也成立:

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

但是在Python中,class并不只有這一角色。class實(shí)際上也是object。當(dāng)我們使用class定義一個(gè)類的時(shí)候,Python會(huì)執(zhí)行相應(yīng)代碼并在內(nèi)存中創(chuàng)建一個(gè)名為example的object。

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

  • 我們可以將其賦給一個(gè)變量
  • 我們可以對(duì)其進(jìn)行拷貝
  • 我們可以賦給其新的變量
  • 我們可以將其作為參數(shù)賦給其他的函數(shù)

舉例如下:

# 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'>

動(dòng)態(tài)創(chuàng)建class

既然class也是object,那么我們就可以像創(chuàng)建普通的object一樣動(dòng)態(tài)創(chuàng)建class。

第一種方法,我們可以在方法中創(chuàng)建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>

但通過這種方式創(chuàng)建class并沒有特別動(dòng)態(tài)。我們?nèi)稳恍枰约憾x類的具體內(nèi)容。考慮到class也是object,那么也一定有某種方法能夠像產(chǎn)生instance一樣產(chǎn)生類。

當(dāng)我們使用class關(guān)鍵字創(chuàng)建類的時(shí)候,Python會(huì)自動(dòng)創(chuàng)建對(duì)應(yīng)的object。像Python中其他大多數(shù)情況一樣,我們也可以手動(dòng)創(chuàng)建這個(gè)class object。這一操作可以通過type()實(shí)現(xiàn)。

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

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

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

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

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

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

當(dāng)使用type創(chuàng)建class時(shí),其用法如下:

type(class_name, tuple_of_parent_class, dict_of_attribute_names_and_values)

其中第二個(gè)參數(shù)tuple_of_parent_class用來表示繼承關(guān)系,可以為空。第三個(gè)參數(shù)用來描述我們所要?jiǎng)?chuàng)建的類所應(yīng)該具有的attribute。如下面的例子所示:

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

上面定義的這個(gè)類可以由如下type函數(shù)創(chuàng)建:

>>>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>

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

我們也可以使用一個(gè)字典來定義所創(chuàng)建的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等同于下面這個(gè)class:

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

當(dāng)然,我們也可以用type返回一個(gè)繼承class_example的類:

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

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

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

我們甚至可以動(dòng)態(tài)創(chuàng)建包括方法的類。只要我們創(chuàng)建好方法并將其賦給相應(yīng)的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

同樣,我們也可以先動(dòng)態(tài)創(chuàng)建一個(gè)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其實(shí)是一個(gè)object,并且我們可以動(dòng)態(tài)創(chuàng)建class。事實(shí)上這也是我們?cè)谑褂胏lass關(guān)鍵字的時(shí)候Python所做的事情。Python通過使用metacalss來實(shí)現(xiàn)這一過程。

究竟什么是metaclass?

metaclass就是Python中用來創(chuàng)建class object的class。我們可以將其看做能夠產(chǎn)生class的類工廠。我們可以通過如下例子理解這個(gè)關(guān)系:

class = metaclass()
object = class()

從上文中我們知道了type()可以被用來動(dòng)態(tài)創(chuàng)建class,這是因?yàn)閷?shí)際上type是一個(gè)metaclass。而且type實(shí)際上是Python用在在幕后創(chuàng)建所有class的metaclass。

包括int, string, function, class在內(nèi),Python中所有的東西都是object,而所有的object都是被相應(yīng)的class創(chuàng)造的。我們可以通過__class__的值得知這一點(diǎn)。

>>> 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都來自于type。type,作為metaclass,創(chuàng)建了以上所有的class object。

type是Python定義好的metaclass。當(dāng)然,我們也可以自定義metaclass。

類的__metaclass__ attribute

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

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

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

當(dāng)Python試圖創(chuàng)建class Foo的時(shí)候,Python會(huì)首先在class的定義中尋找__metaclass__ attribute。如果存在__metaclass__,Python將會(huì)使用指定的__metaclass__來創(chuàng)建class Foo。如果沒有指定的話,Python就會(huì)使用默認(rèn)的type作為metaclas創(chuàng)建Foo。

所以,對(duì)于下面這個(gè)例子:

class Foo(Bar):
    pass

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

如果存在的話,Python將使用這個(gè)metaclass在內(nèi)存中創(chuàng)建一個(gè)名字為Foo的class object。如果Python

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

當(dāng)Python仍然沒有找到__metaclass__時(shí),Python將會(huì)使用當(dāng)前類的母類的metaclass來創(chuàng)建當(dāng)前類。在我們上面這個(gè)例子中,Python會(huì)使用Foo的母類Bar的metaclass來創(chuàng)建Foo的class object。

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

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

答案是:能夠創(chuàng)建class的東西。

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

自定義metaclass

metaclass的主要目的是在class被創(chuàng)建的時(shí)候?qū)ι傻腸lass進(jìn)行自動(dòng)的動(dòng)態(tài)修改。

一般來說,這一點(diǎn)主要應(yīng)用于API,例如我們想要根據(jù)當(dāng)前的內(nèi)容創(chuàng)建相匹配的class。

舉一個(gè)簡(jiǎn)單的例子如下:我們決定讓當(dāng)前module下所有的class的attribute的名字都是大寫。要實(shí)現(xiàn)這個(gè)功能有很多種方法。使用__metaclass__就是其中之一。

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

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

下面我們首先給出一個(gè)使用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的方式實(shí)現(xiàn)一個(gè)真正的class形式的metaclass。注意如果尚不清楚__new____init__的作用和區(qū)別的,請(qǐng)看這篇文章.

# 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。我們直接調(diào)用了type而非調(diào)用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__所接收的參數(shù)中有一個(gè)額外的upperattr_metaclass。這沒有什么特別的。如同__init__總是接收調(diào)用它的object作為第一個(gè)參數(shù)一樣(慣例上用self來命名__init__所接收的第一個(gè)參數(shù)),__new__總是接收其被定義在內(nèi)的class作為第一個(gè)參數(shù),就像類方法總是接收其被定義的class作為第一個(gè)參數(shù)一樣(慣例上用cls命名類方法所接收的第一個(gè)參數(shù))。

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

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)

通過應(yīng)用super,我們可以使得上面這段代碼更加干凈簡(jiǎn)潔,也使得繼承更加容易(我們可能有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à!上述基本就是關(guān)于metaclass的一切了。

使用metaclass之所以復(fù)雜,不是因?yàn)槠浯a實(shí)現(xiàn)復(fù)雜,而是因?yàn)槲覀円话闶褂胢etaclass來做一些邏輯上很復(fù)雜的操作,例如自省,修改繼承以及改變類的默認(rèn)attribute如__dict__等。

metaclass的確可以被用來實(shí)現(xiàn)一些奇妙的功能,也因此可以用來進(jìn)行極其復(fù)雜的邏輯操作。但是metaclass本身是很簡(jiǎn)單的:

  • 影響class初始化的過程
  • 修改class的內(nèi)容
  • 返回修改過的class

為什么我們要使用metaclass,而不是使用一些函數(shù)來實(shí)現(xiàn)類似的功能?

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

使用class作為metaclass有如下幾個(gè)理由:

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

為什么我們要使用metaclass呢?

那么究竟為什么我們要使用metaclass這樣一個(gè)難以理解且容易出錯(cuò)的實(shí)現(xiàn)方式呢?

答案是通常情況下我們不需要使用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主要的使用情況就是用來創(chuàng)建API。使用metaclass的一個(gè)典型的例子是Django ORM。

它是的我們可以使用如下當(dāng)時(shí)定義一個(gè)model:

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

同時(shí),如果我們調(diào)用這個(gè)model:

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

其并不會(huì)返回一個(gè)IntegerField對(duì)象,而是會(huì)返回一個(gè)int,甚至可以直接從數(shù)據(jù)庫中調(diào)用這個(gè)值。

正是因?yàn)?code>models.Model定義了__metaclass__,并使用了一些操作來將我們使用簡(jiǎn)單的語句定義的Person轉(zhuǎn)化成了與數(shù)據(jù)庫相應(yīng)的域相聯(lián)系的類,這種邏輯才成為可能。

Django使得很多復(fù)雜的邏輯僅暴露一個(gè)簡(jiǎn)單的API接口就可以調(diào)用,這正是通過metaclass實(shí)現(xiàn)的。metaclass會(huì)根據(jù)需要重新實(shí)現(xiàn)這些復(fù)雜操作所需要的真正的代碼。

再說兩句

首先我們知道了Python中的class實(shí)際上是object,同時(shí)class仍具有創(chuàng)建對(duì)應(yīng)的實(shí)例的能力。

實(shí)際上class本身也是metaclass的實(shí)例。

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

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

當(dāng)然,type除外。

type事實(shí)上是其自身的metaclass。我們使用Python是無法重復(fù)這種實(shí)現(xiàn)的。這一邏輯是在Python代碼實(shí)現(xiàn)的層面定義的。引用一下道德經(jīng)中的說法,我們可以說Python中type生metaclass,metaclass生class,class生萬物。

另外,metaclass的應(yīng)用一般頗為復(fù)雜,大多數(shù)情況下我們可以使用別的方法實(shí)現(xiàn)相同的功能。比如我們可以通過一下兩種技術(shù)修改class:

  • monkey patching
  • class decorators

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

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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

友情鏈接更多精彩內(nèi)容