Pyhton中的元類

前言

本片文章主要是StackOverflow上關於元類解釋的高贊文章的譯文,順便加了一點個人的理解。 想看原文的同學可以直接點擊 What-are-metaclasses-in-python

環境使用Python2

一切皆對象

在Python中,一切皆對象。 字符串、列表、字典和函數是對象,類也是對象。因此你可以:

  • 把類賦值給一個變量
  • 可以拷貝它
  • 可以爲它添加屬性
  • 可以把它作爲一個函數參數進行傳遞

下面是示例:

class ObjectCreator(object):
    pass

print (ObjectCreator)   # 你可以打印一個類,因爲它其實也是一個類

def echo(o):    # 作爲函數參數
    print (o)

echo(ObjectCreator)

print (hasattr(ObjectCreator, 'new_attribute'))

ObjectCreator.new_attribute = 'foo' # 手動添加屬性
print (hasattr(ObjectCreator, 'new_attribute'))
print (ObjectCreator.new_attribute)

ObjectCreatorMirror = ObjectCreator # 賦值給變量
print (ObjectCreatorMirror())

運行結果爲:

<class '__main__.ObjectCreator'>
<class '__main__.ObjectCreator'>
False
True
foo
<__main__.ObjectCreator object at 0x000000000337B6A0>

動態的創建類

因爲類也是對象, 所以你可以在運行時動態的創建它們,就像其它對象一樣。 首先,你可以在函數中使用 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)     # the function returns a class, not an instance
print (MyClass())   # you can create an object from this class

運行結果:

<class '__main__.Foo'>
<__main__.Foo object at 0x00000000038FB710>

但是這還不夠動態,因爲你仍然需要自己編寫整個類的代碼。 由於類也是對象,所以它們必須通過一些東西生成。 當你使用 class 關鍵字時,Python解釋器自動創建這個對象。

但就和Python中大多數事情一樣,Python仍然給你提供了手動處理的辦法。 還記得內置函數 type 嗎? 這個古老但強大的函數能夠讓你知道一個對象的類型是什麼,就像這樣:

print(type(1))
print(type("1"))
print(type(ObjectCreator))
print(type(ObjectCreator()))

輸出結果:

<type 'int'>
<type 'str'>
<type 'type'>
<class '__main__.ObjectCreator'>

這裏 type 擁有一種完全不同的能力, 它能夠動態的創建類。也可以接受一個類的描述作爲參數,然後返回一個符合該描述的類。 (我知道, 根據傳入的參數不同,同一個函數擁有完全不同的用法是一件愚蠢的事情,但在Python中是爲了保持向後兼容性)

type 可以像這樣工作:

type(name of the class, tuple of the parent class(for inheritance, canbe empty), dictionary containing attributes names and values)

type(類名, 父類的元組(針對繼承的情況,可以爲空),包含屬性的字典(名稱和值))

比如下面的代碼:

class MyShinyClass(object):
    pass

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

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

# result
<class '__main__.MyShinyClass'>
<__main__.MyShinyClass object at 0x000000000318B710>

你會發現我們使用 MyShinyClass 作爲類名,並且也可以當作一個變量來作爲類的引用。 類和變量是不同的,這裏沒有理由把事情弄複雜。

type 接受一個字典來爲類定義屬性,因此:

class Foo(object):
    bar = True

可以翻譯爲:

Foo = type("Foo", (), {"bar": True})

並且可以將Foo當成一個普通的類一樣使用:

print (Foo)
f = Foo()
print (f)
print (f.bar)

# result
<__main__.MyShinyClass object at 0x0000000002CDB710>
<class '__main__.Foo'>
<__main__.Foo object at 0x0000000002CDB710>
True

當然,你可以像這個類繼承,就像下面的代碼一樣:

FooChild = type('FooChild', (Foo, ), {})
print (FooChild)
print (FooChild.bar)

# result
<class '__main__.FooChild'>
True

最終你會希望爲你的類增加方法,只需要定義一個有着恰當名稱的函數,並將其作爲屬性賦值就可以了。

def echo_bar(self):
    print (self.bar)

FooChild = type('FooChild', (Foo, ), {"echo_bar": echo_bar})
print (hasattr(Foo, 'echo_bar'))
print (hasattr(FooChild, 'echo_bar'))
my_foo = FooChild()
my_foo.echo_bar()

# result
False
True
True

你可以看到,在Python中,類也是對象,你可以動態的創建類。這就是當你使用關鍵字 class 時,Python在幕後做的事情,而這就是通過元類來實現的。

到底什麼是元類

重點來了!

元類就是用來創建類的東西。 你創建類就是爲了創建類的實例對象,不是嗎? 但是我們已經學習到Python中的類是對象。 好吧,元類就是用來創建這些類(對象) 的, 元類就是類的類,你可以理解爲:

MyClass = MetaClass()
MyObject = MyClass()

附圖-對象、類、元類的關係

在Python3中,object是所有類的基類,內置的類、自定義的類都直接或間接的繼承自object類(Python2中的新式類需要顯示繼承object)。如果去查看源碼,你會發現, type也繼承自object。這就對我們的理解造成了極大的困擾,主要表現在以下三點:

  • type是一個metaclass,而且是一個默認的metaclass。 也就是說,typeobject的類型, objecttype的一個實例。
  • typeobject的一個子類,繼承object的所有屬性和方法。
  • type還是一個callable,既實現了 __call__ 方法,可以當成一個函數實現。

我們使用一張圖來解釋typeobject的關係:

附圖-type和object關係

`type`和`object`有點像 "蛋生雞" 和 "雞生蛋" 的關係: `type`是`object`的子類, 同時`obejct`又是`type`的一個實例。`type`實例還是它自身。

你已經看到了type可以讓你像這樣做:

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

這是因爲函數type實際上是一個元類。type就是Python在背後用來創建所有類的元類。 現在你想知道那爲什麼type會全部採用小寫形式而不是Type呢? 好吧,我猜這是爲了和str保持一致性,str是用來創建字符串對象的類,而int是用來創建整數對象的類。 type就是創建對象的類。你可以通過檢查 __class__ 屬性來看到這一點。

回到開頭,Python中所有的東西都是對象,而且它們都是從同一個類創建而來。

age = 35
print (age.__class__)
name = 'bob'
print (name.__class__)

def foo():
    pass
print (foo.__class__)

class Bar(object):
    pass
b = Bar()
print (b.__class__)

# result
<type 'int'>
<type 'str'>
<type 'function'>
<class '__main__.Bar'>

現在,對於任何一個 __class____class__ 屬性又是什麼呢?

print (age.__class__.__class__)
print (foo.__class__.__class__)
print (b.__class__.__class__)

# result
<type 'type'>
<type 'type'>
<type 'type'>

因此,元類就是創建類這種對象的東西。 如果你喜歡的話, 可以把元類稱爲 “類工廠” (不要和工廠類搞混了) type就是Python的內建元類。 當然了,你也可以創建自己的元類。

metaclass in Python2

Python2 中指明元類的方法是,給類添加 __metaclass__ 屬性(Python3語法看下一小節)。 自定義的Metaclass通常以MetaclassMeta作爲後綴進行取名,以示區分。

class Foo(object):
    __metaclass__ = CustomMetaclass

如果你這麼做了,Python就會使用元類來創建類Foo

小心點,這裏有些技巧。你首先寫下 class Foo(object),但是類對象Foo還沒有在內存中創建。 Python會在類的定義中尋找metaclass屬性, 如果找到了,Python就會使用他來創建類Foo, 如果沒有找到,就會使用內建的type來創建這個類。

當你寫下如下內容時 把下面的內容讀上幾次。

class Foo(object):
    pass

Python 有 metaclass 這個屬性嗎?

  • 如果有,Python會在內存中通過 __metaclass__ 創建一個名字爲Foo的類對象(我說的是類對象,請跟緊我的思路)。

  • 如果Python沒有找到 __metaclass__ , 它會繼續在Bar(父類)中尋找 *__metaclass__*屬性,並嘗試座和前面同樣的操作。

  • 如果Python在任何父類中都找不到 __metaclass__,它就會在模塊層次中尋找 __metaclass__ , 並嘗試同樣的操作。

  • 如果還是找不到 __metaclass__ ,Python就會用內置的type來創建這個類對象。

補充:metaclass的查找循序可以下Python2文檔找到:

The appropriate metaclass is determined by the following precedence rules:

  • If dict[‘metaclass’] exists, it is used.
  • Otherwise, if there is at least one base class, its metaclass is used (this looks for a class attribute first and if not found, uses its type).
  • Otherwise, if a global variable named metaclass exists, it is used.
  • Otherwise, the old-style, classic metaclass (types.ClassType) is used.

現在的問題就是, 你可以在 __metaclass__ 中放置些什麼代碼呢? 答案就是: 可以創建一個類的東西。 那麼什麼可以用來創建一個類呢? type、任何使用到type或者子類化type的東西都可以。

metaclass in Python3

在Python3中設置元類的語法做出了一些該變,但目前仍然兼容Python2。__metaclass__ 屬性不再被使用, 而是使用基類列表中的關鍵字參數。

class Foo(object, metaclass=CustomMetaclss):
    [...]

自定義元類

元類的主要目的爲了在創建類時,能夠自動地改變類。通常,你會爲API做這樣的事情,你希望可以創建符合當前上下文的類。 假設一個很傻的例子,你決定修改模塊所有類裏的屬性爲大寫形式。有好幾種辦法可以辦到,但其中一種辦法就是通過設定metaclass。採用這種方法,這個模塊中的所有類都會通過這個元類創建。 我們只需告訴該元類把所有的類的屬性修改爲大寫形式就萬事大吉了。

幸運的是,metaclass實際上可以被任意調用, 它並不需要是一個正式的類(我知道,某些名字裏帶有"class"的東西並不需要是一個class,畫圖理解下,這很有幫助)。 所以,我們這裏就先以一個簡單的函數作爲例子。

# 元類會自動將你通常傳給‘type’的參數作爲自己的參數傳入
def upper_attr(future_class_name, future_class_parents, future_class_attr):
    '''返回一個類對象,將屬性都轉爲大寫形式'''
    #  選擇所有不以'__'開頭的屬性
    attrs = ((name, value) for name, value in future_class_attr.items() if not name.startswith('__'))

    # 將它們轉爲大寫形式
    uppercase_attr = dict((name.upper(), value) for name, value in attrs)
 
    # 通過'type'來做類對象的創建
    return type(future_class_name, future_class_parents, uppercase_attr)
 
__metaclass__ = upper_attr  #  這會作用到這個模塊中的所有類
 
class Foo(object):
    # 我們也可以只在這裏定義__metaclass__,這樣就只會作用於這個類中
    bar = 'bip'

print hasattr(Foo, 'bar')
# 輸出: False
print hasattr(Foo, 'BAR')
# 輸出:True
 
f = Foo()
print f.BAR
# 輸出:'bip'

現在讓我們再做一次,這一次用一個真正的class來當作元類。

# 請記住,'type'實際上是一個類,就像'str'和'int'一樣。 所以,你可以從type繼承
class UpperAttrMetaClass(type):
    # __new__ 是在__init__之前被調用的特殊方法
    # __new__是用來創建對象並返回之的方法
    # 而__init__只是用來將傳入的參數初始化給對象
    # 你很少用到__new__,除非你希望能夠控制對象的創建
    
    # 這裏,創建的對象是類,我們希望能夠自定義它,所以我們這裏改寫__new__
    # 如果你希望的話,你也可以在__init__中做些事情
    # 還有一些高級的用法會涉及到改寫__call__特殊方法,但是我們這裏不用
    def __new__(upperattr_metaclass, future_class_name, future_class_parents, future_class_attr):
        attrs = ((name, value) for name, value in future_class_attr.items() if not name.startswith('__'))
        uppercase_attr = dict((name.upper(), value) for name, value in attrs)
        return type(future_class_name, future_class_parents, uppercase_attr)

但是,這種方法其實不符合OOP。 我們直接調用了 type, 而且我們沒有改寫父類的 __new__ 方法。 現在讓我們這樣處理:

class UpperAttrMetaclass(type):
    def __new__(upperattr_metaclass, future_class_name, future_class_parents, future_class_attr):
        attrs = ((name, value) for name, value in future_class_attr.items() if not name.startswith('__'))
        uppercase_attr = dict((name.upper(), value) for name, value in attrs)
 
        # 複用type.__new__方法
        # 這就是基本的OOP編程,沒什麼魔法
        return type.__new__(upperattr_metaclass, future_class_name, future_class_parents, uppercase_attr)

如果使用super方法的畫,我們還可以使它更清晰一些,這會緩解繼承(是的,你可以擁有元類,從元類繼承,從type繼承)

class UpperAttrMetaclass(type):
    def __new__(cls, name, bases, dct):
        attrs = ((name, value) for name, value in dct.items() if not name.startswith('__'))
        uppercase_attr = dict((name.upper(), value) for name, value in attrs)
        return super(UpperAttrMetaclass, cls).__new__(cls, name, bases, uppercase_attr)

就是這樣,除此之外,關於元類真的沒有別的再好說的了。 使用到元類的代碼比較複雜,這背後的原因到並不是因爲元類本身,而是因爲你通常會使用元類去做一些晦澀的事情,依賴於自省,控制繼承等等。 確實,用元類來搞些“黑暗魔法”是特別有用的,因而會搞出些複雜的東西來。 但就元類本身而言,他們其實是很簡單的。

1、攔截類的創建

2、修改類

3、返回修改之後的類

爲什麼要用metaclass類而不是函數

由於 __metaclass__ 可以接受任何可調用的對象,那爲何還要使用類呢? 很顯然使用類會更加複雜。 這裏有好幾個原因:

1、 你的意圖會更加清晰, 當你讀到UpperAttrMetaclass(type)時,你會知道接下來發生什麼。

2、你可以使用OOP編程。 元類可以從元類繼承而來,改寫父類的方法。 元類甚至還可以使用元類。

3、你可以把代碼組織的更好。當你使用元類的時候肯定不會是像我上面舉的那種簡單場景,通常都是針對比較複雜的問題。將多個方法歸總到一個類總是很有幫助,也會使得代碼更容易閱讀。

4、你可以使用 __new____init__ 以及 __call__ 這樣的特殊方法。它們能幫你處理不同的任務。 就算通常你可以把所有東西都放在 __new__ 裏處理, 有些人還是覺得用 __init__ 更舒服。

5、這個東西的名字叫做metaclass, 肯定非善類,我要小心!

究竟爲什麼要使用元類

現在回到我們的大主題上來,究竟是爲什麼你會去使用一種容易出錯,而且晦澀的特性? 好吧,一般來說,你根本就用不上它:

元類就是深度的魔法,99%的用戶應該根本不必爲此操心。如果你想搞清楚究竟是否需要用到元類,那麼你就不需要它。那些實際用到元類的人都非常清楚地知道他們需要做什麼,而且根本不需要解釋爲什麼要用元類。 —— Python界的領袖 Tim Peters

元類的主要用途使創建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類轉變成對數據庫的一個複雜hook。 Django框架將這些看起來很複雜的東西通過簡單暴露出一個簡單的使用元類的API將其化簡,通過這個API重新創建代碼,在背後完成真正的工作。

結語

首先,你知道了類是能夠創建出類實例的對象。好吧,事實上,類也是實例,當然它們是元類的實例。

class Foo(object): pass
print(id(Foo))
58680680

Python中一切皆對象,它們要麼是類的實例,要麼是元類的實例,除了typetype實際上是自己的元類,在純Python環境中這可不是你能夠做到的,這是通過在實現層面耍的小手段做到的。 其次,元類是很複雜的。 對於非常簡單的類,你可能不希望通過使用元類來對類做修改。你可以通過其它兩種技術來修改類:

1、Monkey patching

2、class decorator

當你需要動態修改類時, 99%的時間裏面你最好使用以上兩種技術。當然了,其實在99%的時間裏你根本不需要動態修改類。

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