每天一道 python 面試題 - Python中的元類(metaclass) 詳細版本

類作爲對象

在理解元類之前,您需要掌握Python的類。Python從Smalltalk語言中借用了一個非常特殊的類概念。

在大多數語言中,類只是描述如何產生對象的代碼段。在Python中也是如此:

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

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





但是類比Python中的更多。類也是對象。

一旦使用關鍵字class,Python就會執行它並創建一個對象

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

在內存中創建一個名稱爲“ ObjectCreator”的對象。

這個對象(類)本身具有創建對象(實例)的能力,這就是爲什麼它是一個類

但是,它仍然是一個對象,因此:

  • 您可以將其分配給變量

  • 你可以複製它

  • 您可以爲其添加屬性

  • 您可以將其作爲函數參數傳遞

例如:

>>> 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以下方法在函數中創建一個類

>>> 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中的大多數事情一樣,它爲您提供了一種手動進行操作的方法。

還記得功能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:班級名稱

  • bases:父類的元組(對於繼承,可以爲空)

  • attrs:包含屬性名稱和值的字典

例如:

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

可以通過以下方式手動創建:


>>> MyShinyClass = type('MyShinyClass', (), {}) # returns a class object
>>> print(MyShinyClass)
<class '__main__.MyShinyClass'>
>>> print(MyShinyClass()) # create an instance with the class
<__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使用in中的名稱__metaclass__

如果Python找不到__metaclass__,它將__metaclass__在MODULE級別查找,並嘗試執行相同的操作(但僅適用於不繼承任何內容的類,基本上是老式的類)。

然後,如果根本找不到任何對象__metaclass__,它將使用Bar的(第一個父對象)自己的元類(可能是默認值type)創建類對象。

請注意,該__metaclass__屬性將不會被繼承,而父(Bar.__class__的元類將被繼承如果Bar使用通過(而不是__metaclass__創建屬性,則子類將不會繼承該行爲。Bartype()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):
   ...


閱讀以下部分,瞭解python如何處理此問題。

自定義元類

元類的主要目的是在創建類時自動更改它。

通常,您要對API進行此操作,在API中要創建與當前上下文匹配的類。

想象一個愚蠢的示例,在該示例中,您決定模塊中的所有類的屬性都應大寫。有多種方法可以執行此操作,但是一種方法是__metaclass__在模塊級別進行設置

這樣,將使用此元類創建該模塊的所有類,而我們只需要告訴元類將所有屬性都轉換爲大寫即可。

幸運的是,__metaclass__實際上可以是任何可調用的,它不必是正式的類(我知道,名稱中帶有“ class”的東西不必是類,請弄清楚……但這很有用)。

因此,我們將從使用函數的簡單示例開始。

# the metaclass will automatically get passed the same argument
# that you usually pass to `type`
def upper_attr(future_class_name, future_class_parents, future_class_attrs):
   """
     Return a class object, with the list of its attribute turned
     into uppercase.
   """



   # pick up any attribute that doesn't start with '__' and uppercase it
   uppercase_attrs = {
       attr if attr.startswith("__") else attr.upper(): v
       for attr, v in future_class_attrs.items()
   }

   # let `type` do the class creation
   return type(future_class_name, future_class_parents, uppercase_attrs)

__metaclass__ = upper_attr # this will affect all classes in the module

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'

















讓我們檢查:


>>> hasattr(Foo, 'bar')
False
>>> hasattr(Foo, 'BAR')
True
>>> Foo.BAR
'bip'




現在,讓我們做完全一樣的操作,但是對元類使用真實的類:


# remember that `type` is actually a class like `str` and `int`
# so you can inherit from it
class UpperAttrMetaclass(type):
   # __new__ is the method called before __init__
   # it's the method that creates the object and returns it
   # while __init__ just initializes the object passed as parameter
   # you rarely use __new__, except when you want to control how the object
   # is created.
   # here the created object is the class, and we want to customize it
   # so we override __new__
   # you can do some stuff in __init__ too if you wish
   # some advanced use involves overriding __call__ as well, but we won't
   # see this
   def __new__(upperattr_metaclass, future_class_name,
               future_class_parents, future_class_attrs)
:

       uppercase_attrs = {
           attr if attr.startswith("__") else attr.upper(): v
           for attr, v in future_class_attrs.items()
       }
       return type(future_class_name, future_class_parents, uppercase_attrs)

















讓我們重寫上面的內容,但是現在有了更短,更實際的變量名,我們知道它們的含義了:


class UpperAttrMetaclass(type):
   def __new__(cls, clsname, bases, attrs):
       uppercase_attrs = {
           attr if attr.startswith("__") else attr.upper(): v
           for attr, v in attrs.items()
       }
       return type(clsname, bases, uppercase_attrs)





您可能已經注意到了額外的爭論cls它沒有什麼特別的:__new__始終將其定義的類作爲第一個參數。就像您有self將實例作爲第一個參數接收的普通方法一樣,還是爲類方法定義了類。

但這不是適當的OOP。我們正在type直接致電,而不是覆蓋或致電父母的__new__讓我們改爲:

class UpperAttrMetaclass(type):
   def __new__(cls, clsname, bases, attrs):
       uppercase_attrs = {
           attr if attr.startswith("__") else attr.upper(): v
           for attr, v in attrs.items()
       }
       return type.__new__(cls, clsname, bases, uppercase_attrs)





通過使用super我們可以使其更加整潔,這將簡化繼承(因爲是的,您可以具有元類,從元類繼承,從類型繼承):


class UpperAttrMetaclass(type):
   def __new__(cls, clsname, bases, attrs):
       uppercase_attrs = {
           attr if attr.startswith("__") else attr.upper(): v
           for attr, v in attrs.items()
       }
       return super(UpperAttrMetaclass, cls).__new__(
           cls, clsname, bases, uppercase_attrs)






在python 3中,如果您使用關鍵字參數進行此調用,例如:


class Foo(object, metaclass=MyMetaclass, kwarg1=value1):
   ...

它將在元類中轉換爲使用它:


class MyMetaclass(type):
   def __new__(cls, clsname, bases, dct, kwargs1=default):
       ...

而已。實際上,關於元類的更多信息。

使用元類編寫代碼的複雜性背後的原因不是因爲元類,而是因爲您通常使用元類依靠自省,操縱繼承和諸如var之類的變量來做扭曲的事情__dict__

確實,元類對於做黑魔法特別有用,因此也很複雜。但就其本身而言,它們很簡單:

  • 攔截class創建

  • 修改class

  • 返回修改後的類

爲什麼要使用元類類而不是函數?

既然__metaclass__可以接受任何可調用對象,那麼爲什麼要使用一個類,因爲它顯然更復雜?

這樣做有幾個原因:

  • 意圖很明確。閱讀時UpperAttrMetaclass(type),您會知道接下來會發生什麼

  • 您可以使用OOP。元類可以繼承元類,重寫父方法。元類甚至可以使用元類。

  • 如果您指定了元類類,但沒有元類函數,則該類的子類將是其元類的實例。

  • 您可以更好地構建代碼。絕對不要像上面的示例那樣將元類用於瑣碎的事情。通常用於複雜的事情。能夠製作幾種方法並將它們分組在一個類中的能力對於使代碼更易於閱讀非常有用。

  • 您可以勾上__new____init____call__這將允許您做不同的事情。即使通常您可以全部__new__使用,有些人也更習慣使用__init__

  • 這些被稱爲元類,該死!它一定意味着什麼!

爲什麼要使用元類?

現在是個大問題。爲什麼要使用一些晦澀的易錯功能?

好吧,通常您不會:

元類是更深層的魔術,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)
142630324

一切都是Python中的對象,它們都是類的實例或元類的實例。

除了type

type實際上是它自己的元類。這不是您可以在純Python中複製的東西,而是通過在實現級別上作弊來完成的。

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





發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章