類作為物件
在理解元類之前,您需要掌握 Python 的類。Python 從 Smalltalk 語言中借用了一個非常特殊的類概念。
在大多數語言中,類只是描述如何產生物件的程式碼段。在 Python 中也是如此:
>>> class ObjectCreator(object):
... pass
...
>>> my_object = ObjectCreator()
>>> print(my_object)
<__main__.ObjectCreator object at 0x8974f2c>
但是Python的類更甚。在Python中,Python的類也是物件。
對的,也是物件。
一旦使用關鍵字class
,Python 就會執行它並建立一個物件。示例程式碼:
>>> class ObjectCreator(object):
... pass
...
如上程式碼在記憶體中建立一個名稱為 “ObjectCreator” 的物件。
這個物件(類)本身具有建立物件(例項)的能力,這就是為什麼它也是一個類。
但是,它仍然是一個物件,因為:
- 您可以將其分配給變數
- 你可以複製它
- 您可以為其新增屬性
- 您可以將其作為函式引數傳遞
例如:
>>> print(ObjectCreator) # 你可以列印一個類,因為它是一個物件
<class '__main__.ObjectCreator'>
>>> def echo(o):
... print(o)
...
>>> echo(ObjectCreator) # 可以將類作為引數傳遞
<class '__main__.ObjectCreator'>
>>> print(hasattr(ObjectCreator, 'new_attribute'))
False
>>> ObjectCreator.new_attribute = 'foo' # 可以向類新增屬性
>>> print(hasattr(ObjectCreator, 'new_attribute'))
True
>>> print(ObjectCreator.new_attribute)
foo
>>> ObjectCreatorMirror = ObjectCreator # 可以為變數指定類
>>> print(ObjectCreatorMirror.new_attribute)
foo
>>> print(ObjectCreatorMirror())
<__main__.ObjectCreator object at 0x8997b4c>
動態建立類
由於類是物件,因此您可以像建立任何物件一樣即時建立它們。
首先,您可以使用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 0x89c6d4c>
但這並不是那麼動態,因為您仍然必須自己編寫整個類。
由於類是物件,因此它們必須由某種東西生成。
使用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'>
嗯,type
具有完全不同的功能,它也可以動態建立類。type
可以將類的描述作為引數,並返回一個類。
(我知道,根據傳遞給它的引數,同一個函式可以有兩種完全不同的用法是很愚蠢的。由於 Python 中的向後相容性,這是一個問題)
type
用法:
type(name, bases, attrs)
引數:
name
:Class名稱bases
:父類的元組(對於繼承,可以為空)attrs
:包含屬性名稱和值的字典
例如:
>>> class MyShinyClass(object):
... pass
可以通過以下方式手動建立:
>>> 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 is inherited from Foo
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 中,類是物件,您可以動態動態地建立一個類。
這是 Python 在使用關鍵字class
時所做的,並且是通過使用元類來完成的。
什麼是元類(最終)
元類是建立類的 “東西”。
您定義類是為了建立物件,對嗎?
但是我們瞭解到 Python 類是物件。
好吧,元類就是建立這些物件的原因。它們是類的類,您可以通過以下方式描繪它們:
MyClass = MetaClass()
my_object = MyClass()
您已經看到,type
您可以執行以下操作:
MyClass = type('MyClass', (), {})
這是因為該函式type
實際上是一個元類。type
是 Python 用於在幕後建立所有類的元類。
現在,您想知道為什麼用小寫而不是小寫Type
?
好吧,我想這與str
建立字串物件int
的類和建立整數物件的類的一致性有關。type
只是建立類物件的類。
您可以通過檢查__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
是 Python 使用的內建元類,但是您當然可以建立自己的元類。
該__metaclass__
屬性
在 Python 2 中,您可以__metaclass__
在編寫類時新增屬性(有關 Python 3 語法,請參見下一部分):
class Foo(object):
__metaclass__ = something...
[...]
如果這樣做,Python 將使用元類建立類Foo
。
小心點,這很棘手。
您class Foo(object)
先編寫,但Foo
尚未在記憶體中建立類物件。
Python 將__metaclass__
在類定義中尋找。如果找到它,它將使用它來建立物件類Foo
。如果沒有,它將 type
用於建立類。
讀幾次。
當您這樣做時:
class Foo(Bar):
pass
Python 執行以下操作:
中有__metaclass__
屬性Foo
嗎?
如果是的話,在記憶體中建立一個類物件(我說的是類物件,陪在我身邊在這裡),名稱Foo
使用是什麼__metaclass__
。
如果 Python 找不到__metaclass__
,它將__metaclass__
在 MODULE 級別上查詢,並嘗試執行相同的操作(但僅適用於不繼承任何內容的類,基本上是老式的類)。
然後,如果根本找不到任何物件__metaclass__
,它將使用Bar
的(第一個父物件)自己的元類(可能是預設值type
)建立類物件。
請注意,該__metaclass__
屬性將不會被繼承,父(Bar.__class__
)的元類將被繼承。如果Bar
使用通過(而不是)__metaclass__
建立的屬性,則子類將不會繼承該行為。Bar``type()``type.__new__()
現在最大的問題是,您可以輸入__metaclass__
什麼?
答案是:可以建立類的東西。
什麼可以建立一個類?type
,或任何繼承或使用它的內容。
Python 3 中的元類
設定元類的語法在 Python 3 中已更改:
class Foo(object, metaclass=something):
...
即__metaclass__
不再使用該屬性,而在基類列表中使用關鍵字引數。
在 python 3 中新增到元類的一件事是,您還可以將屬性作為關鍵字引數傳遞給元類,如下所示:
class Foo(object, metaclass=something, kwarg1=value1, kwarg2=value2):
...
為什麼要使用元類?
現在是個大問題。為什麼要使用一些晦澀的易錯功能?
好吧,通常您不會:
元類是更深層的魔術,99%的使用者永遠不必擔心。如果您想知道是否需要它們,則不需要(實際上需要它們的人肯定會知道他們需要它們,並且不需要解釋原因)。
Python 大師 Tim Peters
元類的主要用例是建立 API。一個典型的例子是 Django ORM。它允許您定義如下內容:
class Person(models.Model):
name = models.CharField(max_length=30)
age = models.IntegerField()
但是,如果您這樣做:
person = Person(name='bob', age='35')
print(person.age)
它不會返回IntegerField
物件。它將返回int
,甚至可以直接從資料庫中獲取它。
這是可能的,因為models.Model
define __metaclass__
並使用了一些魔術,這些魔術將使Person
您使用簡單語句定義的物件變成與資料庫欄位的複雜掛鉤。
Django 通過公開一個簡單的 API 並使用元類,從該 API 重新建立程式碼來完成幕後的實際工作,使看起來複雜的事情變得簡單。
最後一點
首先,您知道類是可以建立例項的物件。
實際上,類本身就是元類的例項。
>>> class Foo(object): pass
>>> id(Foo)
一切都是 Python 中的物件,它們都是類的例項或元類的例項。
除了type
。
type
實際上是它自己的元類。
其次,元類很複雜。您可能不希望將它們用於非常簡單的類更改。您可以使用兩種不同的技術來更改類:
- 猴子修補
- 類裝飾
99%的時間,您需要更改類,最好使用這些。
但是 98%的時間根本不需要更改類。
本文首發於BigYoung小站