Python -- 元類metaclass詳解

學習契機

項目中使用Elasticsearch(ES)存儲海量業務數據,基於ES向外提供的API進一層封裝,按需處理原始數據提供更精確、更多樣化的結果。在研究這一層的代碼時接觸到@six.add_metaclass(abc.ABCMeta),故而學習一下Python的元類。不過,雖然@six.add_metaclass(abc.ABCMeta)實現上與元類有關,但實際應用只須要調用其接口,並不須要接觸後幕後的元類操做。
翻譯這篇答案是爲了方便本身記憶理解,其實原文中一些地方我本身不是很明白,因此這個翻譯會根據本身理解的程度持續更新。python

原連接

stackoverflow-What are metaclasses in Python?數據庫

Python中的元類是什麼

類也是對象

在理解元類以前,須要掌握Python中類概念。Python的類概念稍有奇特,其借鑑於Smalltalk。
在大部分語言中,類用於描述如何生成一個對象,在Python中也是如此:函數

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

>>> my_object = ObjectCreator()
>>> print(my_object)
<__main__.ObjectCreator object at 0x8974f2c>

可是,在Python中類的意義更多,類同時也是對象。當你使用關鍵字class時,Python解釋器執行代碼時會生成一個對象。如下代碼會在內存中建立一個名爲「ObjectCreator」的對象:學習

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

這個對象(類)自身能夠建立對象(實例),這是爲何它是類的緣由。
不過它仍然是一個對象,你能夠:this

  • 能夠將它賦值給變量
  • 能夠複製
  • 能夠添加屬性 TODO 添加屬性只是對象的特性?
  • 能夠將其看成函數參數傳遞

舉例:翻譯

>>> print(ObjectCreator) # you can print a class because it's an object
<class '__main__.ObjectCreator'>
>>> def echo(o):
...       print(o)
...
>>> echo(ObjectCreator) # you can pass a class as a parameter
<class '__main__.ObjectCreator'>
>>> print(hasattr(ObjectCreator, 'new_attribute'))
False
>>> ObjectCreator.new_attribute = 'foo' # you can add attributes to a class
>>> print(hasattr(ObjectCreator, 'new_attribute'))
True
>>> print(ObjectCreator.new_attribute)
foo
>>> ObjectCreatorMirror = ObjectCreator # you can assign a class to a variable
>>> print(ObjectCreatorMirror.new_attribute)
foo
>>> print(ObjectCreatorMirror())
<__main__.ObjectCreator object at 0x8997b4c>

動態建立類

既然類也是對象,那就可像其餘對象那樣,動態建立類。
首先,可使用關鍵字class,在函數中建立類:code

>>> def choose_class(name):
...     if name == 'foo':
...         class Foo(object):
...             pass
...         return Foo # return the class, not an instance
...     else:
...         class Bar(object):
...             pass
...         return Bar
...
>>> MyClass = choose_class('foo')
>>> print(MyClass) # the function returns a class, not an instance
<class '__main__.Foo'>
>>> print(MyClass()) # you can create an object from this class
<__main__.Foo object at 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類能夠用如下方法手動建立:

>>> MyShinyClass = type('MyShinyClass', (), {}) # 返回一個類
>>> print(MyShinyClass)
<class '__main__.MyShinyClass'>
>>> print(MyShinyClass()) # 建立一個類對象
<__main__.MyShinyClass object at 0x8997cec>

應該注意到了,使用"MyShinyClass"做爲類名,也將其做爲一個變量名,賦值爲類的引用。類名和變量名是能夠不一樣的,可是不必把事情搞複雜。
type能夠接受一個定義類屬性的字典做爲參數:

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

以上定義等同於:

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

使用起來跟一個普通類同樣:

>>> print(Foo)
<class '__main__.Foo'>
>>> print(Foo.bar)
True
>>> f = Foo()
>>> print(f)
<__main__.Foo object at 0x8a9b84c>
>>> print(f.bar)  # 利用實例打印類屬性
True

固然,也能夠做爲基類,給其餘類繼承:

>>>   class FooChild(Foo):
...         pass

以上代碼等同於:

>>> FooChild = type('FooChild', (Foo,), {})
>>> print(FooChild)
<class '__main__.FooChild'>
>>> print(FooChild.bar) # bar屬性繼承自類Foo
True

你確定還想爲類添加方法。只須要定義一個名稱合理的函數,並將這個函數名做爲屬性傳遞給type就行:

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

在動態建立一個類以後,爲這個類添加更多的方法,就像爲一個正常建立的類添加方法同樣:

>>> def echo_bar_more(self):
...       print('yet another method')
...
>>> FooChild.echo_bar_more = echo_bar_more
>>> hasattr(FooChild, 'echo_bar_more')
True

如今已經看到:在Python中,類也是對象,能夠動態地建立類。當使用關鍵字class時,Python使用元類像這樣建立類的。

什麼是元類(終於講到了)

元類就是建立類的「東西」。咱們定義類是爲了建立對象,是吧?可是咱們認識到在Python中類也是對象,而元類就是建立類這種對象(類)的,它們是類的類,你能夠這樣理解:

MyClass = MetaClass()
MyObject = MyClass()

你已經看到type可讓你作以下操做:

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

這是由於type方法其實是一個元類。事實上,type是Python中用於建立全部類的元類。不過如今你必定很奇怪爲何這個類名首字母是小寫,而不是Type?我猜這是同str保持一致,str是用來建立string對象的類,int是用來建立integer對象的類,type則是建立類對象的類。
在Python中,一切,對就是一切,都是對象。包括整數、字符串、函數和類。全部東西都是對象,它們都是建立自某個類。
經過__class__屬性能夠驗證這一點:

>>> 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__屬性是什麼呢?

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

因而可知:元類確實就是建立類對象的東西。若是你以爲合適你就能夠稱之爲「類工廠」。type是Python使用的內置元類,固然,你也能夠本身建立元類。

__metaclass__屬性

編寫一個類時添加上__metaclass__屬性:

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

若是你像上面這樣作,Python就會使用元類建立一個Foo類。
要小心了,這裏有些小圈套。你先寫下了「class Foo(object)」,但此時內存中尚未建立Foo類對象。Python會在類的聲明中尋找屬性__metaclass_,若是找到了就會使用其建立Foo類;若是沒有,會使用type建立這個類。下面這段文字要多讀幾遍。
當你編寫如下代碼時:

class Foo(Bar):
    pass

Python作了這些事情:

在類Foo中有定義__metaclass__屬性嗎?
若是有,則繼續;
若是沒有,Python會在模塊層尋找__metaclass__屬性(這隻針對沒有繼承任何其餘類的狀況);
若是模塊層也沒有,則會在Bar(第一個父類)中尋找(這就有多是內置的type)。
這樣找到__metaclass__後,使用它在內存中建立名稱爲Foo的類對象(這邊跟上,一個類對象)

須要注意的是,__metaclass__屬性不會被繼承,可是父類的元類(Bar.__class__)能夠被繼承:若是Bar的__metaclass__屬性定義了使用type()(不是type.__new())建立Bar類,其子類不會繼承這個行爲。(Be careful here that the metaclass attribute will not be inherited, the metaclass of the parent (Bar.__class__) will be. If Bar used a metaclass attribute that created Bar with type() (and not type.__new__()), the subclasses will not inherit that behavior.)TODO 這邊不太理解
如今有個新問題,你能夠賦什麼值給__metaclass__?
答案是:能夠建立一個類的東西。
那什麼能夠建立類?type、子類化type或者使用type的東西。

自定義元類

元類的主要目的,是在建立類的時候動態地改變類。一般你會想建立符合當前上下文的類供API使用。舉一個簡單的例子,當你但願一個模塊中全部的類屬性都是小寫時,有幾種方法能夠實現,其中有一種方法就是在模塊層設置__metaclass__。使用這種方法,這個模塊中全部的類都將使用此元類建立,咱們只須要使元類將全部類屬性置爲小寫。
幸運的是,__metaclass__能夠被任意調用,不必定非要是一個正式的類(我知道,名稱包含「class」不必定非要是一個類,搞清楚了...這頗有用)。
如今先用一個函數舉一個簡單的例子:

# 元類會自動獲取一般傳給`type`的參數
def upper_attr(future_class_name, future_class_parents, future_class_attr):
    """
      返回一個類對象,將其屬性置爲大寫
    """

    # 過濾出全部開頭不爲'__'的屬性,置爲大寫
    uppercase_attr = {}
    for name, val in future_class_attr.items():
        if not name.startswith('__'):
            uppercase_attr[name.upper()] = val
        else:
            uppercase_attr[name] = val

    # 利用'type'建立類
    return type(future_class_name, future_class_parents, uppercase_attr)

__metaclass__ = upper_attr  # 這會影響此模塊中全部的類

class Foo():  # global __metaclass__ won't work with "object" though  == 沒看懂
    # but we can define __metaclass__ here instead to affect only this class
    # and this will work with "object" children
    bar = 'bip'

print(hasattr(Foo, 'bar'))
# Out: False
print(hasattr(Foo, 'BAR'))
# Out: True

f = Foo()
print(f.BAR)
# Out: 'bip'

如今,完成一樣的功能,可是爲元類定義一個真實的類:

# 記住`type`其實是一個像`str`和`int`的類,能夠用於被繼承
class UpperAttrMetaclass(type):
    # __new__放在__init__以前調用,此方法建立對象並反回
    # 而__init__則是初始化做爲參數傳遞給此方法的對象
    # 除非你想控制如何建立一個對象,不然不多用到__new__
    # 在這裏,被建立的對象是類,而咱們想自定義這個類,因此重寫了__new__
    # 若是須要的話你也能夠在__init__中作一些操做
    # 一些高級用法會包括重寫__call__,不過這裏還不須要
    def __new__(upperattr_metaclass, future_class_name,
                future_class_parents, future_class_attr):

        uppercase_attr = {}
        for name, val in future_class_attr.items():
            if not name.startswith('__'):
                uppercase_attr[name.upper()] = val
            else:
                uppercase_attr[name] = val

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

        uppercase_attr = {}
        for name, val in future_class_attr.items():
            if not name.startswith('__'):
                uppercase_attr[name.upper()] = val
            else:
                uppercase_attr[name] = val

        # 複用type.__new__方法
        # 這是基本的OOP,沒什麼深奧的
        return type.__new__(upperattr_metaclass, future_class_name,
                            future_class_parents, uppercase_attr)

你大概發現了傳給type的額外的參數upperattr_metaclass。這沒什麼奇怪的:__new__的第一個參數老是其定義的類。就像類方法中第一個參數老是self。固然,爲了清晰期間,這裏我起的名字比較長,可是像self這樣的參數一般有一個傳統的名字。因此真正的產品代碼中,元類是像這樣的:

class UpperAttrMetaclass(type):

    def __new__(cls, clsname, bases, dct):

        uppercase_attr = {}
        for name, val in dct.items():
            if not name.startswith('__'):
                uppercase_attr[name.upper()] = val
            else:
                uppercase_attr[name] = val

        return type.__new__(cls, clsname, bases, uppercase_attr)

使用super能夠更清晰,which will ease inheritance (because yes, you can have metaclasses, inheriting from metaclasses, inheriting from type)TODO:

class UpperAttrMetaclass(type):

    def __new__(cls, clsname, bases, dct):

        uppercase_attr = {}
        for name, val in dct.items():
            if not name.startswith('__'):
                uppercase_attr[name.upper()] = val
            else:
                uppercase_attr[name] = val

        return super(UpperAttrMetaclass, cls).__new__(cls, clsname, bases, uppercase_attr)

以上,關於元類也沒有更多了。使用元類的代碼比較複雜的緣由不在於元類,而在於你一般會依靠自省、操縱繼承、__dict__變量等,使用元類作一些晦澀的事情。(it's because you usually use metaclasses to do twisted stuff relying on introspection, manipulating inheritance, vars such as __dict__, etc.)TODO
元類來用於黑魔法時的確特別有用,由於也會將事情搞得很複雜。但就其自己而言,是簡單的:

  • 攔截一個類的建立
  • 修改類
  • 返回修改的類

爲何會用元類代替函數?

既然__metaclass__能夠被任意調用,爲何要使用明顯更復雜的類呢?有這樣一些理由:

  • 意圖明顯。當你看到UpperAttrMetaclass(type),你知道接下來會發生什麼。
  • 可使用OOP。元類能夠繼承自元類,重寫父類的方法,元類甚至可使用元類。
  • 若是爲一個類指定的元類是類而不是方法,這個類的子類將是元類的一個實例。Children of a class will be instances of its metaclass if you specified a metaclass-class, but not with a metaclass-function.TODO
  • 你能夠將代碼組織得更好。使用元類時確定不會僅想像上面舉的例子那樣簡單,一般是用於比較複雜的場景。將多個方法組織在一個類中有益於使代碼更容易閱讀。
  • 你可使用__new__,__init__ 和 __call__,這些方法能夠處理不用的事情。即便不少時候你能夠在__new__中完成全部工做,固然,一些人會更習慣用__init__。
  • 這些東西叫 「metaclass」,當心了!這必定很難搞!

爲何使用元類

好了,如今的問題是:爲何要是用這樣晦澀且容易出錯的特性?其實,一般你不會用:

元類是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的對象,而是返回一個整數,甚至能夠從數據庫中直接獲取數據。TODO
這是由於,在models.Model中定義了__metaclass__,使用了一些魔法將你定義的簡單的Person類轉換爲一個複雜的數據庫掛鉤。(turn the Person you just defined with simple statements into a complex hook to a database field.)TODO

Django使用元類對外提供簡單的API,簡化了一些複雜的東西,API中重建的代碼會去完成幕後真正的工做。

結語

首先,類是能夠建立實例的對象。類自己是元類的對象:

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

在Python中,除了type,一切皆對象,一切都是類或者元類的對象。事實上type是本身的元類,這在純Python中這是沒法實現的,這裏在實現層上作了一些手段。
其次,元類是複雜的。你可能不但願對很是簡單的類使用元類,那麼還有其餘兩種手段用來改變類:

  • monkey patching
  • 類裝飾器

若是你須要改變類,99%的狀況下使用這兩種方法。但其實98%的狀況你根本不須要改變類。

相關文章
相關標籤/搜索