Python很簡單?學會魔術方法才算入門!

中興開發者社群發表於2017-11-16

 點選上方“中興開發者社群”,關注我們

每天讀一篇一線開發者原創好文640?wx_fmt=png&wxfrom=5&wx_lazy=1

Python中的元類是什麼?

原問題地址:http://stackoverflow.com/questions/100003/what-is-a-metaclass-in-python

問題

什麼是元類?使用它們能做什麼?

答案 1

元類是類的一種。正如類定義了例項功能,元類也定義了類的功能。類是元類的例項。

在Python中你可以隨意呼叫元類(參考Jerub的回答),實際上更有用的方法是使其本身成為一個真正的類。type是Python中常用的元類。你可能覺得奇怪,是的,type本身就是一個類,而且它就是type型別。你無法在Python中重新創造出完全像type這樣的東西,但Python提供了一個小把戲。你只需要把type作為子類,就可以在Python中建立自己的元類。

元類是最常用的一類工廠。就像你通過呼叫類來建立類例項,Python也是通過呼叫元類來建立一個新類(當它執行class語句的時候),結合常規的 __init____new__方法,元類可以允許你在建立類的時候做一些“額外的事情”,like registering the new class with some registry(暫時不知道這句話的含義,不知道怎麼翻譯,字面意思是:就像用某個登錄檔來註冊新的類那樣),甚至可以用別的東西完全代替已有的類。

當Python執行class語句時,它首先把整個的class語句作為一個正常的程式碼塊來執行。由此產生的名稱空間(一個字典)具有待定類的屬性。元類取決於待定類的基類(元類是具有繼承性的)、或待定類的__metaclass__屬性(如果有的話)或__metaclass__全域性變數。接下來,用類的名稱、基類和屬性呼叫元類,從而把元類例項化。

然而,元類實際上定義的一個類的型別,而不只是類工廠,所以你可以用元類來做更多的事情。例如,你可以定義元類的一般方法。這些元類方法和類方法有相似之處,因為它們可以被沒有例項化的類呼叫。但這些元類方法和類方法也有不同之處,元類方法不能在類的例項中被呼叫。type.__subclasses__()是關於type的一個方法。你也可以定義常規的“魔法”函式,如__add__, __iter____getattr__,以便實現或修改類的功能。

摘抄一個例子:

def make_hook(f):
 """Decorator to turn 'foo' method into '__foo__'"""
 f.is_hook = 1
 return f

class MyType(type):
 def __new__(cls, name, bases, attrs):

if name.startswith('None'):
 return None

# Go over attributes and see if they should be renamed.
 newattrs = {}
 for attrname, attrvalue in attrs.iteritems():
 if getattr(attrvalue, 'is_hook', 0):
 newattrs['__%s__' % attrname] = attrvalue
 else:
 newattrs[attrname] = attrvalue

return super(MyType, cls).__new__(cls, name, bases, newattrs)

def __init__(self, name, bases, attrs):
 super(MyType, self).__init__(name, bases, attrs)

# classregistry.register(self, self.interfaces)
 print "Would register class %s now." % self

def __add__(self, other):
 class AutoClass(self, other):
 pass
 return AutoClass
 # Alternatively, to autogenerate the classname as well as the class:
 # return type(self.__name__ + other.__name__, (self, other), {})

def unregister(self):
 # classregistry.unregister(self)
 print "Would unregister class %s now." % self

class MyObject:
 __metaclass__ = MyType

class NoneSample(MyObject):
 pass

# Will print "NoneType None"
print type(NoneSample), repr(NoneSample)

class Example(MyObject):
 def __init__(self, value):
 self.value = value
 @make_hook
 def add(self, other):
 return self.__class__(self.value + other.value)

# Will unregister the class
Example.unregister()

inst = Example(10)
# Will fail with an AttributeError
#inst.unregister()

print inst + inst
class Sibling(MyObject):
 pass

ExampleSibling = Example + Sibling
# ExampleSibling is now a subclass of both Example and Sibling (with no
# content of its own) although it will believe it's called 'AutoClass'
print ExampleSibling
print ExampleSibling.__mro__
shareedit

答案 2

作為物件的類

在理解元類之前,你需要掌握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
...

這個物件(類)本身就能夠建立一些物件(例項),這就是為什麼它是類。

但它仍然是一個物件,因而:

  • 你可以將它分配給一個變數

  • 你可以複製它

  • 你可以增加它的屬性

  • 你可以把它作為一個功能引數來用

例如:

>>> 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>
Creating classes dynamically

類的動態建立

既然類是物件,你就能動態地建立它們,就像建立任何物件那樣。

首先,你可以在一個使用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是這樣應用的:

例如:

>>> 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

你現在明白了:在Python中,類就是物件,你可以動態地建立類。

這就是關鍵字class在Python中的應用,它是通過使用元類來發揮作用。

元類是什麼

元類是用來建立類的東西。

你通過定義類來建立物件,對吧?

但我們知道Python的類就是物件。

元類用於建立這些物件,元類是類的類,你可以這樣來描述它們:

MyClass = MetaClass()
MyObject = MyClass()

你已經看到了type可以這樣來用:

MyClass = type('MyClass', (), {})

這是因為type實際上是一個元類,作為元類的type在Python中被用於在後臺建立所有的類。

現在你感到疑惑的是為什麼這裡是小寫的type,而不是大寫的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__的屬性

當你建立類的時候,可以新增一個__metaclass__屬性:

class Foo(object):
 __metaclass__ = something...
 [...]

如果你這樣做,Python會使用元類來建立Foo這個類。

小心,這是棘手的。

這是你是首次建立class Foo(object),但是類物件Foo在記憶體中還沒有被建立。

Python會在類定義中尋找__metaclass__。如果找到它,Python會用它來建立物件類Foo。如果沒有找到它,Python將使用type來建立這個類。

把上面的話讀幾遍。

當你寫下:

class Foo(Bar):
 pass

Python會實現以下功能:

Foo有沒有__metaclass__的屬性?

如果有,通過借鑑__metaclass__,用Foo這個名字在記憶體中建立一個類物件(我說的是一個類物件,記住我的話)。

如果Python找不到__metaclass__,它會在模組層級尋找__metaclass__,並嘗試做同樣的事情(但這隻適用於不繼承任何東西的類,基本上是舊式類)。

如果它根本找不到任何__metaclass__,它將使用Bar(第一個父類)自己的元類(這可能是預設的type)來建立類物件。

小心點,__metaclass__屬性不會被繼承,而父類的元類(Bar.__class__)將會被繼承。如果Bar所用的__metaclass__屬性是用type()來建立Bar(而不是type.__new__()),它的子類不會繼承這種功能。

現在最大的問題是,你可以在__metaclass__中寫些什麼?

答案是:可以建立類的東西。

什麼可以建立類?type,或者父類。

自定義元類

一個元類的主要目的是當它被建立時,這個類可以自動改變。

通常在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_attr):
 """
 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_attr = {}
 for name, val in future_class_attr.items():
 if not name.startswith('__'):
 uppercase_attr[name.upper()] = val
 else:
 uppercase_attr[name] = val

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

__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'

print(hasattr(Foo, 'bar'))
# Out: False
print(hasattr(Foo, 'BAR'))
# Out: True

f = Foo()
print(f.BAR)
# Out: '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_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,我們無需覆蓋或呼叫父類__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

# reuse the type.__new__ method
 # this is basic OOP, nothing magic in there
 return type.__new__(upperattr_metaclass, future_class_name, future_class_parents, uppercase_attr)

你可能已經注意到額外的引數upperattr_metaclass,它沒有什麼特別之處:__new__upperattr_metaclass為第一引數,並且在upperattr_metaclass中被定義。就像你把self作為例項的第一個引數那樣,或者作為類方法的第一個引數。

當然,我在這裡為了清晰起見,使用了一個很長的名稱,但就像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)

我們可以用super使它更清楚,這樣將緩解繼承(因為,是的,你可以擁有元類,繼承metaclasses,繼承type):

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)

就是這樣。關於元類真的是沒有更多要講的了。

使用元類的程式碼複雜的原因並不在於元類,那是因為你通常用元類來實現一些奇怪的功能,而這些功能要依靠自省、繼承、變數,如:__dict__等。

的確,元類對於“魔法”特別有用,這些事務是複雜的,但元類本身很簡單:

  • 攔截類的建立

  • 修改類

  • 返回修改後的類

你為什麼要使用元類而不是函式?

__metaclass__可以被任意呼叫。既然類明顯更復雜,你為什麼還要使用它呢?

這樣做有幾個理由:

  • 目的明確。當你讀UpperAttrMetaclass(type)時,你知道要遵循的是什麼。

  • 可以使用物件導向程式設計。元類可以繼承元類、重寫父類的方法。元類甚至可以使用元類。

  • 可以優化程式碼。你從不為像上面例子中瑣碎的東西而使用元類。它通常用於複雜的東西。在一個類中有多種方法並且將它們優化組合,是非常有價值的,使得程式碼可讀性更強。

  • 你可能喜歡使用__new____init____call__。它們能幫你實現不同的功能。儘管你通常可以用__new__來實現所有的功能,有些人還是更喜歡使用__init__

  • 這些被稱為元類。可惡!它肯定意味著什麼!
    你為什麼會使用元類?

現在的大問題是:為什麼你會使用一些複雜難懂、容易出錯的特性?

嗯,通常你不會這樣做:

元類是更深層次的魔法,超過99%的使用者不需要擔心。不要懷疑你是否需要元類(那些真正需要元類的人對此確定無疑,並且不需要解釋為什麼)。
——Python專家蒂姆﹒彼得斯

元類的主要使用案例是建立API。一個典型的例子就是Django ORM。

它允許你定義類似這樣的東西:

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

但如果你這樣做:

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

它不會返回到一個IntegerField物件。它會返回到一個int,甚至可以直接從資料庫讀取。

這是可以實現的,因為models.Model定義了__metaclass__。它使用魔法把你剛才用簡單語句定義的Person轉化成一個複雜的鉤子連線到資料庫欄位。

通過顯示一個簡單的API和元類,Django使複雜的東西看起來簡單,再從API重構程式碼去完成真正的幕後工作。

最後的話

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

事實上,類本身就是例項。在元類中

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

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

但是type除外。

type實際上是自己的元類。你無法在純粹的Python中複製它,所以只能在實施層面做點小把戲。

其次,元類是複雜的。你可能不想把它們用於非常簡單的類。你可以用兩種不同的技術來改變類:

  • 猴子補丁monkey patching

  • 類裝飾器

當你需要改變類的時候,99%的情況下,使用它們是明智之舉。

但99%的時間,你根本不需要改變類。

轉自:https://github.com/qiwsir/StackOverFlowCn/blob/master/302.md

640?wx_fmt=jpeg

相關文章