Python黑科技之元類

本文翻譯自stackoverflow的一篇回答,原地址是 what-is-a-metaclass-in-python

Python中的類

在理解元類之前,你需要了解Python中的類。Python中的類借鑒自Smalltalk。
在大多數編程語言中,類只是描述對象生成方式的一段代碼,在Python里面看起來也是這樣。比如下面的代碼

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

但在Python中,類也是對象。是的,類是對象
class關鍵字聲明了一個類,Python會執行class這一段代碼,生成一個對象,下面的操作在內存中創建一個對象,取名為"ObjectCreator"。

>>> class ObjectCreator(object): pass

這個類可以創建自己的對象,這也是類的功能。

但是它本身也是一個對象,因此

  • 可以將它賦值給一個變量
  • 可以復制
  • 可以往里邊添加屬性
  • 也可以將其作為參數傳入一個函數

舉個例子

>>> class ObjectCreator(object): pass
...
>>> print(ObjectCreator)
<class '__main__.ObjectCreator'>
>>> def echo(o): print(o)
...
>>> echo(ObjectCreator)
<class '__main__.ObjectCreator'>
>>> ObjectCreator.new_attribute = 'foo'
>>> print(hasattr(ObjectCreator, 'new_attribute'))
True
>>> print(ObjectCreator.new_attribute)
foo
>>> ObjectCreatorMirror = ObjectCreator
>>> print(ObjectCreatorMirror.new_attribute)
foo
>>> id(ObjectCreatorMirror)
140433925632016
>>> id(ObjectCreator)
140433925632016
>>> print(ObjectCreatorMirror())
<__main__.ObjectCreator object at 0x1072342d0>

動態生成類

就像對象一樣,類也可以動態生成,因為它本身就是對象。
可以在函數中用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 object at 0x107234290>

上面的函數這也并不是那么智能,因為還是要完成得定義一個類。
既然類也是對象,那么一定有辦法可以生成類。
當使用class關鍵字時,Python會自動創建類。和其他特性一樣,Python也提供了手動創建類的方式。
還記得type這個函數嗎?這個函數可以讓你知道一個對象的類型:

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

這個函數還有另外的功能,就是動態創建類,它通過傳入類的描述作為參數來做到這一點。
(同一個函數根據不同的參數有完全不同的兩個共同,這看起來確實有點奇怪。這是Python為了向后兼容而引入的一個問題)。

可以這樣使用type

type(name of the class, 
     tuple of the parent class (for inheritance, can be empty), 
     dictionary containing attributes names and values)

舉個例子

>>> class MyShinyClass(object): pass

可以這樣被創建

>>> MyShinyClass = type('MyShinyClass', (), {})
>>> print(MyShinyClass)
<class '__main__.MyShinyClass'>
>>> print(MyShinyClass())
<__main__.MyShinyClass object at 0x109250ad0>
>>>

可以看到,類的名稱被當作是參數傳給了typetype通過字典來定義類的屬性,比如

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

等同于

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

通過type定義的類可以像用class定義的類一樣使用

>>> print(Foo)
<class '__main__.Foo'>
>>> print(Foo.bar)
True
>>> f = Foo()
>>> print(f)
<__main__.Foo object at 0x109250b50>
>>> print(f.bar)
True
>>>

當然也可以編寫子類類繼承它

>>> class FooChild(Foo): pass

等同于

>>> FooChild = type('FooChild', (Foo,), {})
>>> print(FooChild)
<class '__main__.FooChild'>
>>> print(FooChild.bar)
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

在動態創建類之后,也可以添加方法,效果和在創建的時候添加方法一樣。

>>> 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函數實際上是一個元類,Python就是用type來創建類。

也許你會問,那為什么type不寫成Type呢?
我只能猜測這是為了和strint這樣能創建對象的關鍵詞保持一致,所以首字母用了小寫。
通過查看__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__是什么呢?

>>> age.__class__.__class__
<type 'type'>
>>> name.__class__.__class__
<type 'type'>
>>> foo.__class__.__class__
<type 'type'>
>>> b.__class__.__class__
<type 'type'>
>>> type.__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__這個屬性嗎?
  • 如果有,就用__metaclass__定義的元類來創建Foo這個類;
  • 如果找不到__metaclass__這個屬性,Python會在模塊中尋找__metaclass__,如果找到了,就用它來創建Foo這個類;
  • 如果還是找不到,Python會用Bar的元類(應該是type)來創建Foo這個類。

注意,子類不會繼承__metaclass__這個屬性,但是會繼承父類的元類。就是說,如果Bar使用__metaclass__這個屬性來創建Bar這個類,子類不會繼承這個行為。
現在問題來了,__metaclass__里面的內容可以是什么呢?
答案是:可以創建類的內容
什么可以創建一個類呢?typetype的子類,或者用到了type的類

自定義元類

元類的主要作用是在創建類的時候改變這個類。
根據當前的上下文創建類,這個特性可以用來開發API。
舉個簡單例子,現在你想要模塊中所有的類中的屬性都是大寫開頭的。有很多種方式來實現這一點,現在我們通過使用修改模版中的__metaclass__屬性來做到這一點。
這樣,這個模塊中所有的類都會用自定義的元類來創建,我們只需要在元類中將類中的所有屬性首字母改成大寫。
幸運的是,__metaclass__是可以被調用的,所以不必是“類”。
下面來看看例子吧

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
    return type(future_class_name, future_class_parents, uppercase_attr)

__metaclass__ = upper_attr


class Foo():
    bar = 'bip'

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

現在我們用類來實現一個元類

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
        return type(future_class_name, future_class_parents, uppercase_attr)

但是上面的方法沒有用到type類中的方法,我們可以通過調用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
        return type.__new__(upperattr_metaclass, future_class_name,
                            future_class_parents, uppercase_attr)

可能你注意到了上面代碼中的upperattr_metaclass參數,這沒有什么特別的,__new__方法總是會將定義它的類作為第一個參數傳入,這就和self一樣,上面的例子中,把
upperattr_metaclass打印出來,可以看到類似<class '__main__.UpperAttrMetaclass'>的結果。
當然,這里的取名只是為了說清明這些變量,但就和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)

為了讓UpperAttrMetaclass繼承自type這一特性表現的更清楚,可以使用super

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)

代碼中使用元類,可以實現一些黑科技。而元類本身只實現下面的功能。

  • 中斷類的創建
  • 修改類
  • 返回修改后的類

元類實現的取舍

既然__metaclass__可以是任意可調用的對象,為什么要用類而不是函數來實現它呢?

主要考慮到下面幾個原因

  • 語義上更清晰。
  • 面向對象。元類可以繼承自元類,元類甚至可以使用元類。
  • 代碼結構更清晰。
  • 可以根據不同想法,在__new__,__init__,__call__不同的函數中實現不同的功能
  • 既然都叫元類了,那肯定就是類嘛!

為什么要使用元類

現在還有一個問題,為什么要使用這樣一個令人費解的功能呢?
當然,一般情況下,這個功能不會被用到

99%的人都不會用到元類,如果你還在想是否要用它,那么你就不需要用到它(真正有需求用它的人不會問這個問題) Python Guru Tim Peters

元類的主要功能是用來開發API。一個典型的例子就是Django ORM,它可以這樣定義一個model

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

但是下面的代碼卻不會返回一個IntegerField的對象,而是返回一個int,甚至可以從數據庫中去取這個值。

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

之所以能這樣實現,是因為model.Model中定義了__metaclass__,通過定義的元類將Person類轉換成一條SQL語句。
Django通過元類將代碼改寫,這樣就可以只暴露簡單的API,而實現復雜的功能。

結語

類可以用來創建對象。
而事實上,類本身也是對象,元類的對象。

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

Python中萬物都是對象,它們要么是類的實例,要么是元類的實例。
除了type
type是它自己的元類,Python在實現層面,做了一些工作來實現這一點。
元類是很復雜的。比較簡單的場景不一定要用到它,要改變一個類,可以通過下面兩種技術實現

如果需要改變類的行為,99%情況下應該使用上面的兩種方法。
但是99%情況下,根本就不需要改變類的行為

“本譯文僅供個人研習、欣賞語言之用,謝絕任何轉載及用于任何商業用途。本譯文所涉法律后果均由本人承擔。本人同意簡書平臺在接獲有關著作權人的通知后,刪除文章。”

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

推薦閱讀更多精彩內容