本文翻譯自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>
>>>
可以看到,類的名稱被當作是參數傳給了type。type通過字典來定義類的屬性,比如
>>> 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呢?
我只能猜測這是為了和str,int這樣能創建對象的關鍵詞保持一致,所以首字母用了小寫。
通過查看__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__里面的內容可以是什么呢?
答案是:可以創建類的內容
什么可以創建一個類呢?type,type的子類,或者用到了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在實現層面,做了一些工作來實現這一點。
元類是很復雜的。比較簡單的場景不一定要用到它,要改變一個類,可以通過下面兩種技術實現
- monkey patching
- 裝飾器
如果需要改變類的行為,99%情況下應該使用上面的兩種方法。
但是99%情況下,根本就不需要改變類的行為
“本譯文僅供個人研習、欣賞語言之用,謝絕任何轉載及用于任何商業用途。本譯文所涉法律后果均由本人承擔。本人同意簡書平臺在接獲有關著作權人的通知后,刪除文章。”