Python之元類筆記

輕舟輕發表於2016-12-09

轉載:http://kissg.me/2016/04/25/python-metaclass/

引文

自上一次寫部落格到現在已經過去整整15天了.這期間,我看過許多材料,也有許多想付諸筆端與大家分享的.但苦於前人幾乎已經把該講的不該講的都講了,而且講得非常透徹,鞭僻入裡,有他們的珠玉在前,加上我又希望堅持原創,一時竟不知從何落筆了.思前想後,不如就寫一篇”讀書筆記”吧.於是就選定了關於python3 metaclass這篇文章(中文版請看這裡).(為敘述方便,後文用“原文”來指代這篇文章)

正文

按照慣例,先介紹一些預備知識,以便讀者能更好地理解.

首先,我想簡單講下關鍵字(keyword)的概念(有時也叫保留字(reserved word))

關鍵字,是程式語言的一類語法結構.它們在語言設計之初就被定義了.

說白了,關鍵字就是程式語言已經為我們準備好的工具,我們可以直接拿過來用.舉一個例子,比如,要定義一個類,我們會這樣寫:

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

此處class關鍵字就相當於告訴python直譯器:”直譯器大哥,使用者自定義了一個類,類名就叫Myclass,引數是…麻煩您給構造一下”.然後,Myclass類就自動被建立了.

另一個需要澄清的概念是動態程式語言(dynamic programming language)

動態程式語言,是一類在執行時可以改變程式結構的語言,例如新的函式,物件甚至程式碼可以被引進,已有的函式可以被刪除或者其他結構上的變化.

下面是一個簡單的例子:

>>> class Myclass(object):
...     pass
...
>>> mc = Myclass()
>>> mc.name = "kissg" # Myclass類本身並沒有name屬性,在執行期間為其新增了name屬性
>>> print(mc.name)
kissg

(注:動態程式語言 != 動態型別語言.動態型別語言是指在執行時確定變數的型別)


眾所周知,python是物件導向的程式語言.對此,我們要清楚並始終牢記一點:

在python的世界裡,一切皆為物件.

整型浮點型字串型變數是物件,函式也是物件,類還是物件。只不過,類作為物件有點特殊,它是自身具有建立物件(類例項)能力的物件.

那麼,類是一個物件有何意義呢?這意味著,我們完全可以像操縱普通物件一樣操縱一個類:

  • 可以將它賦給一個變數
  • 可以對它進行拷貝
  • 可以為它增加屬性
  • 可以將它作為引數傳遞給某個函式

正是由於python的這些特點,為動態程式設計提供一個可能,一種思路。

前文已經提到,使用class關鍵字,python直譯器就為我們自動地建立了一個類。其實,我們還可以手動地建立一個類,即呼叫type函式。

實際上,當我們使用class關鍵字定義好一個類,python直譯器就是通過呼叫type函式來構造類的,它以我們寫好的類定義(包括類名,父類,屬性)作為引數,並返回一個類。官方文件對此描述如下:

class type(name, bases, dict)
With three arguments, return a new type object. This is essentially a dynamic form of the class statement. The name string is the class name and becomes the __name__ attribute; the bases tuple itemizes the base classes and becomes the__bases__ attribute; and the dict dictionary is the namespace containing definitions for class body and becomes the __dict__ attribute.

以下2種方法建立類的方法完全相同。

>>> class X(object):
...     a = 1
...
>>> X = type('X', (object,), dict(a=1))

那麼,如何為動態建立的類新增method(為了不混淆視聽,此處用method來表示類的方法)呢?有兩種方法,一種是在建立類的時候指定,一種是在後期動態地新增。方法與前文介紹的新增屬性基本類似。實際上,完全可以將method看作是特殊一點的屬性,這樣,處理method的時候,想想屬性是如何處理的,就會簡單許多。

# 動態建立類時,指定method
>>> def echo_bar(self):
...       print(self.bar)
...
>>> Foo = type('Foo', (object,), {'echo_bar': echo_bar})

#=======================================================

# 動態地新增method
>>> def echo_bar_more(self):
...       print('yet another method')
...
>>> Foo.echo_bar_more = echo_bar_more

值得注意的是,我們使用class關鍵字定義類的時候,method的第一個引數一般總是self,它指向呼叫method的物件(類例項)本身。因此,在我們動態地新增method之前,定義函式時,千萬別忘了self關鍵字。否則,錯誤將超乎你的想象.(思考一下,這個method並沒有繫結到類例項上,這是你想要的效果嗎?如果是,當我沒說)

細心的同學可能已經發現了,type(name, bases, dict)其實是一個建構函式(見上文官方文件的引用: return a new type object)。而我之前卻說,它返回一個類。其實,一個類就是一個type的物件。這個結果不算驚世駭俗:我們已經知道類例項是由類建立的一個物件,那麼既然類也是一個物件,理應有一個更加強大的存在能夠建立類。我們稱這個更加強大的存在為元類(metaclass),即類的類。

(注如果你在其他地方接觸過“元xx”,應該很容易就能理解。比如“後設資料”就是描述資料的資料)

無疑,type就是一個元類,並且它還是所有類的元類:

# 通過__class__屬性可獲得物件的類
>>> 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__,可以看出所有類的類都是type
>>> age.__class__.__class__
<type 'type'>
>>> name.__class__.__class__
<type 'type'>
>>> foo.__class__.__class__
<type 'type'>
>>> b.__class__.__class__
<type 'type'>

現在你知道,為什麼是type,而不是Type了吧。(提示,對比下strint你就懂啦)

注意到,上面的程式碼段有一句<type 'function'>,這表示存在一個內建的function
所以為什麼說函式也是一個物件,因為它們都是function類的一個例項。
聯絡關鍵字的知識,當我們使用def關鍵字時,就是在告訴python直譯器“請給我一個function例項”

除了使用type(name, bases, dict)來動態地建立類外,我們還可以自定義元類,並用自定義的元類來控制類的建立行為

比如說,我希望類的所有屬性都加上字首kissg_。當然我可以在定義類的時候為每個屬性手動地加上kiss_字首。而另一種行之有效的方法就是自定義一個元類,由它在建立類的時候,自動地給每個屬性加上kissg_字首。這就是所謂的“控制類的建立行為”,並且它是自動進行的。

那麼,如何來自定義元類呢?其實只要我們明白了type是如何建立類的,自定義元類就是非常簡單的一件事,無非就是接收類定義,並修改類定義,再返回一個類。而且,我們完全可以呼叫type函式來返回這個類,從而簡化操作。

# 我們已經知道type是一個元類,因此自定義元類應繼承自type或其子類
# 有一個約定俗成的習慣,自定義元類一般以Metaclass作為字尾,以明確表示這是一個元類
class AddPrefixMetaclass(type):
    # __new__方法在__init__方法之前被呼叫
    # 因此,當我們想要控制類的建立行為時,一般使用__new__方法
    # 定義普通類的方法時,我們用self作為第一個引數,來指向呼叫方法的類例項本身
    # 此處addprefix_metaclass的意義與self類似,用於指向使用該元類建立的類本身
    # 其他引數就是類的定義了,依次是類名,父類的元組,屬性的字典
    def __new__(addprefix_metaclass, class_name, class_bases, class_dict):
        prefix = "kissg_"
        addprefix_dict = {} # 我們用一個新的字典來儲存加了字首的屬性
        # 遍歷類的屬性,為所有非特殊屬性與私有屬性加上字首
        for name, val in class_dict.items():
            if not name.startswith('_'):
                addprefix_dict[prefix + name] = val
            else:
                addprefix_dict[name] = val

        # 呼叫type函式來返回類,此時我們使用的是加了字首的屬性字典
        return type(class_name, class_bases, addprefix_dict)

# 指定metaclass為自定義的元類,將在建立類時使用該自定義元類
class Myclass(object, metaclass=AddPrefixMetaclass):
    name = "kissg"

kg = Myclass()
print(hasattr(Myclass, "name"))
# 輸出: False
print(hasattr(Myclass, "kissg_name"))
# 輸出: True
print(kg.kissg_name)
# 輸出: kissg

如你所見,自定義元類就這麼簡單,而元類的使用同樣簡單,只需在類定義時像使用關鍵字引數一樣,指定metaclass為自定義的元類即可。

按照原文的說法,以上自定義元類不是物件導向程式設計(Object-oriented programming,簡稱OOP)的正確寫法。正確的寫法應該是這樣的:

class AddPrefixMetaclass(type):
    # 此處__new__的引數也是約定俗成的寫法,就像用**kw表示關鍵字引數一樣
    # cls - 使用自定義元類要建立的類,你可以就簡單地記成self
    # clsname - 類名
    # bases - 父類的元組的(tuple)
    # dct - 類屬性的字典
    def __new__(cls, clsname, bases, dct):
        prefix = "kissg_"
        addprefix_dict = {}
        for name, val in dct.items():
            if not name.startswith('_'):
                addprefix_dict[prefix + name] = val
            else:
                addprefix_dict[name] = val
        # 元類也是可以被繼承的。
        # 呼叫父類的__new__方法來建立類,簡化繼承
        return super(AddPrefixMetaclass, cls).__new__(cls, clsname, bases, addprefix_dict)

是不是比之前的優雅了許多?既避免了直接呼叫type函式,又使用super使繼承顯得更容易了,而且使用約定俗稱的命名方法立顯規範與高大上氣息。

最後,建立類的元類是可以被繼承的,有點拗口,但請區別於元類是可以被繼承的。這句話的意思是:定義子類時沒有指定元類(即沒有metaclass=XXXMetaclass),將自動使用其父類的元類來建立該子類。

注: python2還可以通過指定__metaclass__屬性為元類或其他任何能返回類的東西(比如函式)來控制類的建立。python3雖然保留了__metaclass__屬性,但其實並無用處,因此本文不展開講。有興趣的同學可以看看原文,但我覺得沒太大必要


小結

元類被譽為python的黑魔法(black magic)之一,一方面強調了元類使用的困難,另一方面也強調了元類的強大。如果你仔細看過正文的內容,會發現元類的使用似乎也不太難。如果覺得仍有難度,再看一遍,或者可以看下原文。當然本文所舉的例子都比較簡單,你完全可以用元類實現一些更加強大的功能,比如自定義一個ORM(Object Relational Mapping,物件關係對映),我也是基於此,才找了一些材料學習元類的。

總的來說,元類就做了以下3件事:

  1. 攔截類的建立
  2. 修改類定義
  3. 返回修改後的類

也許這樣將步驟拆分了,你能更好得理解記憶。而元類真的就這麼簡單。

引用原文的一句話作結:

Everything is an object in python, and they are all either instances of classes or instances of metaclasses.
(在python的世界裡,一切皆為物件,它們要麼是類的例項,要麼是元類的例項。)


相關文章