Python 中的元類到底是什麼?這篇恐怕是最清楚的了

Bigyoungs 發表於 2020-06-29

類作為物件

在理解元類之前,您需要掌握 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實際上是一個元類。typePython 用於在幕後建立所有類的元類

現在,您想知道為什麼用小寫而不是小寫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.Modeldefine __metaclass__並使用了一些魔術,這些魔術將使Person您使用簡單語句定義的物件變成與資料庫欄位的複雜掛鉤。

Django 通過公開一個簡單的 API 並使用元類,從該 API 重新建立程式碼來完成幕後的實際工作,使看起來複雜的事情變得簡單。

最後一點

首先,您知道類是可以建立例項的物件。

實際上,類本身就是元類的例項。

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

一切都是 Python 中的物件,它們都是類的例項或元類的例項。

除了type

type實際上是它自己的元類。

其次,元類很複雜。您可能不希望將它們用於非常簡單的類更改。您可以使用兩種不同的技術來更改類:

99%的時間,您需要更改類,最好使用這些。

但是 98%的時間根本不需要更改類。

本文首發於BigYoung小站