Python中的元類(譯)


add by zhj: 這是大stackoverflow上一位小白提出的問題,好吧,我承認我也是小白,元類這塊我也是好多次想搞明白,

但終究因為太難懂而敗下陣來。看了這篇文章明白了許多,再加下啄木鳥社區的 Python 類型和對象  。卧槽,

這簡直就是珠聯璧合,日月神劍啊,尼瑪。終於理解了元類。元類就是創建類對象的類,建議用__metaclass__用於指定元類,它的好

處也就是可以要創建類之前和之后修改類的屬性,創建后修改屬性也可以在__init__中做,不過完全可以用__new__代替,在元類的

__new__()方法中會直接或間接調用type(classname, parentclasses , attrs),我們可以控制這三個參數,type()就是實例化type類,

即在堆上創建一個類對象,任何類對象的創建都會調用這個接口,我們可以控制讓__new__()返回什么樣的類對象,比如我們可以在

__new__多次調用type(),讓他生成多個類對象,然后將這些類按一定的策略組合起來返回。說白了,__metaclass__是用於控制類的

生成過程。我們大多數情況下無需指定__metaclass__,這種情況下,解釋器會按一定的策略找到元類。元類的參數就是我們用class關鍵字

定義類時的類名,類的所有父類,類中定義的{屬性名:屬性對象}。

翻譯時部分地方有修改。

 

原文:http://stackoverflow.com/questions/100003/what-is-a-metaclass-in-python?answertab=votes#tab-top

1. 類對象

2. 通過type類創建類

3. 什么是元類

4. __metaclass__屬性

5. __metaclass__為什么要用類而不是函數

6. 到底為什么要使用元類

7. 最后

 

 

類是對象

      在理解元類之前,你需要先掌握Python中的類。在Python中,對類的定義比較特殊,這點借鑒了Smalltalk語言。在大多

數語言中,類只是用於描述如何創建對象的代碼片,在Python中,在一定程度上也是這樣:

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

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

不過,Python中的類不止如此。類也是對象,是的,我沒說錯,在《Python源碼剖析》中提到除了Python的內置類型外

其它對象都是在堆上創建的,而內置類型應該是在內存的全局數據區。在Python中,一切都是對象,對象對用戶來說只提供

了變量,變量就是那些標識符,變量以引用的方式操作對象,變量相當於對象暴露給用戶的接口。Python中的引用跟C++

的引用差不多相同,不過也有差異。

      當你使用關鍵字class時,Python解釋器執行時,會創建一個對象。如下,Python會在堆中創建一個對象,並在符號表

中增加一個標識符ObjectCreator,指向那個對象的地址,這就是Python中所說的引用,我們一般稱ObjectCreator為變量。

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

我們稱這個對象為類對象,因為該對象可以實例化,創建實例對象,因此我們稱它為類。

因為它是一個對象,所以:

  • 可以將它賦給一個變量
  • 可以copy它
  • 可以給它增加屬性
  • 可以作為函數參數

比如

>>> print(ObjectCreator) # 可以print it
<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>


通過type類創建類

      原文中說通過type類創建對象是動態,其實我們平時使用class關鍵字定義類也是動態的,當Python解釋器遇到class關鍵字的,

它就會執行,並創建類對象。其實,類對象也是實例化的結果,即類對象是由另一個類實例化得到的,我們稱創建類的類為元類,

反之也成立,如果類X實例化后得到是類對象,那X就是元類。在Python中,只有type類及其子類才可以當元類。多說

一句,這里會有雞生蛋、蛋生雞的問題。在Python中,一切都是對象,一切對象都是類實例化的結果。對象由類實例化得到,而類也是

對象,它也是由類(元類)實例化得到,繼續向上,元類也是對象,也要由另一個元類實例化得到,這樣下去就沒有盡頭了。在Python中,

這個追溯終止在type類。元類是type類或其子類,而type類的元類就是它自己,哈哈,type類自己創建自己,牛逼吧,當然type類的

這個特性是Python設計者設計並實現的。至於Python解釋器在定義類時是怎么找到該類的元類的,后面我們會講。

      還記得type()方法嗎?我們常用它看一個對象X所屬的類,即創建該對象X的類,當對象X是類對象時,看到的就是元類。如下,

>>> print(type(1))
<type 'int'>
>>> print(type("1"))
<type 'str'>
>>> print(type(ObjectCreator))     #查看創建ObjectCreator類的類
<type 'type'>
>>> print(type(ObjectCreator()))
<class '__main__.ObjectCreator'>

      type類還有另外一個功能,它可以創建類,它以類的一些信息做為type()的參數,並返回一個類。我知道,type根據輸入參數的不同而有不同的功能,

這種做法是愚蠢的。當type()只有一個參數時,它的功能就是返回創建該參數的類;當多於一個參數時,type()才是type類的實例化,實例化得到

一個類並返回該類。type創建類時,參數格式如下,classname是類名,字符串類型,parentclasses是類所有父類,元組類型,attrs是類的所有{屬性:值},

字典類型。如果用戶是用class關鍵字定義的類,那解釋器會自動轉為下面的格式執行。

type(classname, parentclasses , attrs)

比如,

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

當解釋器執行時,會轉為下面的語句,當然,你也可以直接這么寫。

MyShinyClass = type('MyShinyClass', (object,), {})

下面我們來定義一個類,並在類中定義屬性,如

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

我們可以用另一個類繼承它,so:

>>>   class FooChild(Foo):
...         pass

would be:

>>> FooChild = type('FooChild', (Foo,), {})
>>> print(FooChild)
<class '__main__.FooChild'>
>>> print(FooChild.bar) # bar is inherited from Foo
True

OK,你會想給你的類增加方法。定義一個函數,並將它分配給類的屬性

>>> 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創建類都是這么簡單嗎?都是直接用type元類實例化得到?

不是的,你可以指定元類,我們會在“__metaclass__屬性”一節會講。

 

什么是元類

元類就是創建類的類,當通過type.__new__(cls, classname, bases, attrs)創建類時,cls就是該類的元類,

它是type類或其子類。在上面,我們提到可以使用type()查看類的元類,你也可以使用__class__,他們是完全等價的。

一切類的創建最終都會調用type.__new__(cls, classname, bases, attrs),它會在堆中創建一個類對象,並返回

>>> 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__是什么呢?__class__返回的是一個類對象,再一次__class__返回創建類對象的類,

即類的元類,如下,這個例子中類的元類都是type類,我們平時用的絕大部分類的元類都是type,不過也有例外,

有些類的元類是type的子類。當然,如果你對某個類一直調用__class__,那在有限次之后,它返回的就是type類了。

>>> age.__class__.__class__
<type 'type'>
>>> name.__class__.__class__
<type 'type'>
>>> foo.__class__.__class__
<type 'type'>
>>> b.__class__.__class__
<type 'type'>

 

__metaclass__屬性

     你可以在類中添加__metaclass__屬性,用它指定創建該類的元類,前面我們說過,元類必須是type類或type子類。

注:其實__metaclass__只要是一個可調用對象就行,該可調用對象的參數格式為callable(classname, parentclasses , attrs),

可以看到,這跟上一節用type類創建類對象時,參數格式完全相同。Python要求該對象執行時必須要調用執行type()或type子類(),

當Python解釋器執行時,會調用這個可調用對象,參數也是Python解釋器添加上去的。一般的,__metaclass__用元類,在本

文中,作者分別用了函數和元類。這里還有一點,子類如果指定了__metaclass__,該__metaclass__必須與父類的元類相同或

是父類元類的子類,不然,呵呵,Python解釋器會選其中一個做元類,但選哪個,貌似沒有什么規律。

      如果我們創建類時,要指定__metaclass__屬性,那最好創建的這個類的父類的元類是type類(有點繞),這樣就不容易出錯,

不然的話,請慎重使用元類。

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

我們假定在繼承關系上,子類的元類是父類的元類或父類的元類的子類。在這個正確的前提下,我們說一下Python解釋器是怎么確定

一個類的元類的(只討論新式類的創建,舊式類不討論,對於新式類,Python解釋器的第三步是直接使用type類,作者討論了定義類時,

沒有父類的情況,這種類我們不會用到,不討論)。add by zhj:但后面我發現,下面這個規律並不成立,在"到底為什么要使用元類"

一節我猜想了更可能的規律,並初步驗證過了。

1、在類中查找__metaclass__屬性,如果找到就用,如果沒有,進入2

2、在繼承樹中查找__metaclass__屬性,如果還是沒有,進入3

3、使用type類

舉個例子吧,比如你想將一個類的屬性全部轉為大寫(以__開頭的屬性除外),其實一種辦法就是使用__metaclass__屬性。

前面我們提到過,__metaclass__可以是任意可調用對象,只要調用super()或super子類()就可以。

OK,我們先使用一個函數做__metaclass__,在實際上我們一般不會用函數,而是用類。

# 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.
  """
  print "call upper_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 # let `type` do the class creation return type(future_class_name, future_class_parents, uppercase_attr) class Foo(object): __metaclass__ = upper_attr bar = 'bip' print(hasattr(Foo, 'bar')) # Out: False print(hasattr(Foo, 'BAR')) # Out: True f = Foo() print(f.BAR) # Out: 'bip'

 

 執行輸出如下:

>>>
call upper_attr
False
True
bip
>>>

然后我們將函數換成類,__new__方法是靜態方法,當創建類時,Python解釋器會實例化元類

UpperAttrMetaclass(classname, parentclasses , attrs),進一步它會執行

UpperAttrMetaclass.__new__(UpperAttrMetaclass, classname, parentclasses , attrs)方法。我們在下面看到,__new__

會調用type類,其實指定__metaclass__的好處也就是可以要創建類之前修改類的屬性,即在調用type(classname, parentclasses , attrs)

之前修改這三個參數,前兩個參數貌似沒啥好修改的,主要是修改第三個參數,第三個參數是一個字典,我們可以修改鍵,

即修改屬性名稱,也可以修改鍵值,即屬性對象,屬性對象有value/property/method。

      Django的ORM就是這樣干的,我們在Model中定義的字段類型是各種field類實例,但當創建該model類時,會將這些field類實例轉為Python

內置的類型,如CharField()實例轉為unicode或str類型,IntegerField()實例轉為int型。

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

這還不是OOP,我們直接調用了type(),我們沒有使用super().__new__,下面我們修改一下

class UpperAttrMetaclass(type): 

    def __new__(cls, clsname, bases, attrs):

        uppercase_attrs = {}
        for name, val in attrs.items():
            if not name.startswith('__'):
                uppercase_attrs[name.upper()] = val
            else:
                uppercase_attrs[name] = val

        return super(UpperAttrMetaclass, cls).__new__(cls, clsname, bases, uppercase_attrs)

元類可以做到:

  • 攔截類的生成
  • 修改類
  • 返回修改后的類

 

__metaclass__為什么要用類而不是函數

__metaclass__可以接受任意可調用對象,為什么要使用類而不是函數呢?有下面幾個原因

 

  • 意圖清晰。當你讀到UpperAttrMetaclass(type), 你知道接下來要做什么
  • 你可以使用OOP
  • 你可以重定義 __new__, __init____call__,不過其實你完成都在 __new__方法中完成。有些人更喜歡用 __init__

 

到底為什么要使用元類

那么問題來了,用元類誰最強?哈哈,開個玩笑。為什么你要使用容易出錯的元類呢?

well,通常情況下你不需要使用

元類是一個高級特性,99%的用戶不需要關心。如果你還在想是否需要它,那你其實不需要(真正需要使用元類的人是非常確定自己的確需要的).

Python大師Tim Peters說過:元類主要的使用場景是創建API,一個典型的例子是Django ORM。

在Django ORM中,可以如下定義model

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類轉為數據庫的字段。Django通過暴露給用戶簡單的API,並通過使用元類,使復雜

的事情對用戶來說變得簡單。我看了一下源碼,如下。

class Model(six.with_metaclass(ModelBase)):
    _deferred = False

    def __init__(self, *args, **kwargs):
        signals.pre_init.send(sender=self.__class__, args=args, kwargs=kwargs)

        # Set up the storage for instance state
        self._state = ModelState()
        ……
        ……
def with_metaclass(meta, *bases):
    """Create a base class with a metaclass."""
    return meta("NewBase", bases, {})

 

class ModelBase(type): """ Metaclass for all models. """ def __new__(cls, name, bases, attrs): super_new = super(ModelBase, cls).__new__ …… ……

 

six.with_metaclass(ModelBase)等價於ModelBase("NewBase", (), {}),第二個參數為空時,Python解釋器會認為該類繼承於object類,

所以這句話等價於ModelBase("NewBase", (object,), {}),我在測試中發現創建的NewBase類的元類是ModelBase類,但Python

並沒有給NewBase創建__metaclass__字段,這讓人疑惑,看來上面說的一個類創建時查找元類的規律並不成立。那Python解釋器到底是怎

么確定NewBase的元類就是ModelBase呢?經我初步驗證,應該是靠調用type.__new__(cls, classname, bases, attrs)方法時,第一個參數,

因為一切類對象最終都是通過這個方法創建的,在創建時,它的第一個參數就是被創建的類的元類,第一個參數一般是type類或其子類,類創建時會記

錄下它的元類。你可能會問,如果一個類是由多個類對象組合而成的呢?沒問題,如果最終的類的id與其中一個類相同,那說明並沒有創建新的類,

類的id不變,類的元類就不變,如果它的id與每個類都不同,那就是創建了一個新的類,創建時還是會調用type.__new__方法,第一個參數就是

它的元類。

      我非常不喜歡ModelBase("NewBase", (object,), {})這種方式來定義類,因為這樣定義你很難知道NewBase的元類是誰,如果ModelBase

定義了__new__方法,且在調用type.__new__()時第一個參數是ModelBase,那NewBase的元類就是ModelBase,如果ModelBase沒有重定

義__new__,那對象創建時調用的type.__new__方法,第一個參數是type,即NewBase的元類就是type類。我操,好麻煩啊,我還是推薦傳統

的定義類的方式,如下,這樣才能一目了然的看到NewBase的元類是ModelBase,不過有一個要求,該NewBase的__metaclass__必須是NewBase

父類的元類的子類,或者相同,這樣才能確保NewBase的元類是__metaclass__指定的類。

 

class NewBase(object):

    __metaclass__ = ModelBase

 

 

 

最后

看了這么多,是不是暈了,反正我有點暈,再次提醒大家,最好不要指定元類,也不要用調用type()或其子類的方式來定義類,

因為這塊很容易出錯。推薦大家還是有class關鍵字去定義類。

首先,你要知道類是能產生實例的對象。

實際上,類是元類的實例。

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

Python中的一切皆對象,這些對象分為兩種:類對象和實例對象。type類的元類就是它自己。

其次,元類是復雜的,對於類的簡單的改動是不需要使用元類的。你可以通過下面兩種技術來改變類:

 

這里再說兩句吧,在gevent庫中就使用了monkey patching,它可以將Python標准庫中的IO接口

替換為自己的接口,感覺好牛逼的樣子,有時間看看它是怎么實現的。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM