草根學Python(十二)元類

兩點水發表於2017-09-07

前言

第十二篇了,擼起袖子,就是幹。

目錄

草根學Python(十二)元類
草根學Python(十二)元類

一、Python 中類也是物件

在瞭解元類之前,我們先進一步理解 Python 中的類,在大多數程式語言中,類就是一組用來描述如何生成一個物件的程式碼段。在 Python 中這一點也是一樣的。

class ObjectCreator(object):
    pass


mObject = ObjectCreator()
print(mObject)複製程式碼

輸出結果:

<__main__.ObjectCreator object at 0x00000000023EE048>複製程式碼

但是,Python 中的類有一點跟大多數的程式語言不同,在 Python 中,可以把類理解成也是一種物件。對的,這裡沒有寫錯,就是物件。

為什麼呢?

因為只要使用關鍵字 class ,Python 直譯器在執行的時候就會建立一個物件。

如:

class ObjectCreator(object):
    pass複製程式碼

當程式執行這段程式碼的時候,就會在記憶體中建立一個物件,名字就是ObjectCreator。這個物件(類)自身擁有建立物件(類例項)的能力,而這就是為什麼它是一個類的原因。但是,它的本質仍然是一個物件,於是我們可以對它做如下的操作:

class ObjectCreator(object):
    pass


def echo(ob):
    print(ob)


mObject = ObjectCreator()
print(mObject)

# 可以直接列印一個類,因為它其實也是一個物件
print(ObjectCreator)
# 可以直接把一個類作為引數傳給函式(注意這裡是類,是沒有例項化的)
echo(ObjectCreator)
# 也可以直接把類賦值給一個變數
objectCreator = ObjectCreator
print(objectCreator)複製程式碼

輸出的結果如下:

<__main__.ObjectCreator object at 0x000000000240E358>
<class '__main__.ObjectCreator'>
<class '__main__.ObjectCreator'>
<class '__main__.ObjectCreator'>複製程式碼

二、使用 type() 動態建立類

因為類也是物件,所以我們可以在程式執行的時候建立類。Python 是動態語言。動態語言和靜態語言最大的不同,就是函式和類的定義,不是編譯時定義的,而是執行時動態建立的。在之前,我們先了瞭解下 type() 函式。

首先我們新建一個 hello.py 的模組,然後定義一個 Hello 的 class ,

class Hello(object):
    def hello(self, name='Py'):
        print('Hello,', name)複製程式碼

然後在另一個模組中引用 hello 模組,並輸出相應的資訊。其中 type() 函式的作用是可以檢視一個型別和變數的型別。

#!/usr/bin/env python3
# -*- coding: UTF-8 -*-

from com.twowater.hello import Hello

h = Hello()
h.hello()

print(type(Hello))
print(type(h))複製程式碼

輸出的結果是怎樣的呢?

Hello, Py
<class 'type'>
<class 'com.twowater.hello.Hello'>複製程式碼

上面也提到過,type() 函式可以檢視一個型別或變數的型別,Hello 是一個 class ,它的型別就是 type ,而 h 是一個例項,它的型別就是 com.twowater.hello.Hello。前面的 com.twowater 是我的包名,hello 模組在該包名下。

在這裡還要細想一下,上面的例子中,我們使用 type() 函式檢視一個型別或者變數的型別。其中檢視了一個 Hello class 的型別,列印的結果是: <class 'type'> 。其實 type() 函式不僅可以返回一個物件的型別,也可以建立出新的型別。class 的定義是執行時動態建立的,而建立 class 的方法就是使用 type() 函式。比如我們可以通過 type() 函式建立出上面例子中的 Hello 類,具體看下面的程式碼:

# -*- coding: UTF-8 -*-

def printHello(self, name='Py'):
    # 定義一個列印 Hello 的函式
    print('Hello,', name)


# 建立一個 Hello 類
Hello = type('Hello', (object,), dict(hello=printHello))

# 例項化 Hello 類
h = Hello()
# 呼叫 Hello 類的方法
h.hello()
# 檢視 Hello class 的型別
print(type(Hello))
# 檢視例項 h 的型別
print(type(h))複製程式碼

輸出的結果如下:

Hello, Py
<class 'type'>
<class '__main__.Hello'>複製程式碼

在這裡,需先了解下通過 type() 函式建立 class 物件的引數說明:

1、class 的名稱,比如例子中的起名為 Hello

2、繼承的父類集合,注意 Python 支援多重繼承,如果只有一個父類,tuple 要使用單元素寫法;例子中繼承 object 類,因為是單元素的 tuple ,所以寫成 (object,)

3、class 的方法名稱與函式繫結;例子中將函式 printHello 繫結在方法名 hello

具體的模式如下:

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

好了,瞭解完具體的引數使用之外,我們看看輸出的結果,可以看到,通過 type() 函式建立的類和直接寫 class 是完全一樣的,因為Python 直譯器遇到 class 定義時,僅僅是掃描一下 class 定義的語法,然後呼叫 type() 函式建立出 class 的 。

不過一般的情況下,我們都是使用 class ***... 的方法來定義類的,不過 type() 函式也可以讓我們建立出類來。也就是說,動態語言本身支援執行期動態建立類,這和靜態語言有非常大的不同,要在靜態語言執行期建立類,必須構造原始碼字串再呼叫編譯器,或者藉助一些工具生成位元組碼實現,本質上都是動態編譯,會非常複雜。

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

三、什麼是元類

通過上面的介紹,終於模模糊糊的帶到元類這裡來了。可是我們到現在還不知道元類是什麼東東。

我們建立類的時候,大多數是為了建立類的例項物件。那麼元類呢?元類就是用來建立類的。也可以換個理解方式就是:元類就是類的類。

通過上面 type() 函式的介紹,我們知道可以通過 type() 函式建立類:

MyClass = type('MyClass', (), {})複製程式碼

實際上 type() 函式是一個元類。type() 就是 Python 在背後用來建立所有類的元類。

那麼現在我們也可以猜到一下為什麼 type() 函式是 type 而不是 Type呢?

這可能是為了和 str 保持一致性,str 是用來建立字串物件的類,而 int 是用來建立整數物件的類。type 就是建立類物件的類。你可以通過檢查 __class__ 屬性來看到這一點。Python 中所有的東西,注意喔,這裡是說所有的東西,他們都是物件。這包括整數、字串、函式以及類。它們全部都是物件,而且它們都是從一個類建立而來。

# 整形
age = 23
print(age.__class__)
# 字串
name = '兩點水'
print(name.__class__)


# 函式
def fu():
    pass


print(fu.__class__)


# 例項
class eat(object):
    pass


mEat = eat()

print(mEat.__class__)複製程式碼

輸出的結果如下:

<class 'int'>
<class 'str'>
<class 'function'>
<class '__main__.eat'>複製程式碼

可以看到,上面的所有東西,也就是所有物件都是通過類來建立的,那麼我們可能會好奇,__class____class__ 會是什麼呢?換個說法就是,建立這些類的類是什麼呢?

我們可以繼續在上面的程式碼基礎上新增下面的程式碼:

print(age.__class__.__class__)
print(name.__class__.__class__)
print(fu.__class__.__class__)
print(mEat.__class__.__class__)複製程式碼

輸出的結果如下:

<class 'type'>
<class 'type'>
<class 'type'>
<class 'type'>複製程式碼

認真觀察,再理清一下,上面輸出的結果是我們把整形 age ,字元創 name ,函式 fu 和物件例項 mEat__class____class__ 列印出來的結果。也可以說是他們類的類列印結果。發現列印出來的 class 都是 type 。

一開始也提到了,元類就是類的類。也就是元類就是負責建立類的一種東西。你也可以理解為,元類就是負責生成類的。而 type 就是內建的元類。也就是 Python 自帶的元類。

四、自定義元類

到現在,我們已經知道元類是什麼東東了。那麼,從始至終我們還不知道元類到底有啥用。只是瞭解了一下元類。在瞭解它有啥用的時候,我們先來了解下怎麼自定義元類。因為只有瞭解了怎麼自定義才能更好的理解它的作用。

首先我們來了解下 __metaclass__ 屬性

metaclass,直譯為元類,簡單的解釋就是:

當我們定義了類以後,就可以根據這個類建立出例項,所以:先定義類,然後建立例項。

但是如果我們想建立出類呢?那就必須根據metaclass建立出類,所以:先定義metaclass,然後建立類。

連線起來就是:先定義metaclass,就可以建立類,最後建立例項。

所以,metaclass允許你建立類或者修改類。換句話說,你可以把類看成是metaclass建立出來的“例項”。

class MyObject(object):
    __metaclass__ = something…
[…]複製程式碼

如果是這樣寫的話,Python 就會用元類來建立類 MyObject。當你寫下 class MyObject(object),但是類物件 MyObject 還沒有在記憶體中建立。Python 會在類的定義中尋找 __metaclass__ 屬性,如果找到了,Python 就會用它來建立類 MyObject,如果沒有找到,就會用內建的 type 函式來建立這個類。如果還不怎麼理解,看下下面的流程圖:

__metaclass__的介紹
__metaclass__的介紹

再舉個例項:

class Foo(Bar):
    pass複製程式碼

它的判斷流程是怎樣的呢?

首先判斷 Foo 中是否有 __metaclass__ 這個屬性?如果有,Python 會在記憶體中通過 __metaclass__ 建立一個名字為 Foo 的類物件(注意,這裡是類物件)。如果 Python 沒有找到__metaclass__ ,它會繼續在 Bar(父類)中尋找__metaclass__ 屬性,並嘗試做和前面同樣的操作。如果 Python在任何父類中都找不到 __metaclass__ ,它就會在模組層次中去尋找 __metaclass__ ,並嘗試做同樣的操作。如果還是找不到` metaclass` ,Python 就會用內建的 type 來建立這個類物件。

其實 __metaclass__ 就是定義了 class 的行為。類似於 class 定義了 instance 的行為,metaclass 則定義了 class 的行為。可以說,class 是 metaclass 的 instance。

現在,我們基本瞭解了 __metaclass__ 屬性,但是,也沒講過如何使用這個屬性,或者說這個屬性可以放些什麼?

答案就是:可以建立一個類的東西。那麼什麼可以用來建立一個類呢?type,或者任何使用到 type 或者子類化 type 的東東都可以。

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

幸運的是,__metaclass__ 實際上可以被任意呼叫,它並不需要是一個正式的類。所以,我們這裡就先以一個簡單的函式作為例子開始。

# 元類會自動將你通常傳給‘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)複製程式碼

你可能已經注意到了有個額外的引數 upperattr_metaclass ,這並沒有什麼特別的。類方法的第一個引數總是表示當前的例項,就像在普通的類方法中的 self 引數一樣。當然了,為了清晰起見,這裡的名字我起的比較長。但是就像 self 一樣,所有的引數都有它們的傳統名稱。因此,在真實的產品程式碼中一個元類應該是像這樣的:

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 type.__new__(cls, name, bases, 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)複製程式碼

通常我們都會使用元類去做一些晦澀的事情,依賴於自省,控制繼承等等。確實,用元類來搞些“黑暗魔法”是特別有用的,因而會搞出些複雜的東西來。但就元類本身而言,它們其實是很簡單的:

  • 攔截類的建立
  • 修改類
  • 返回修改之後的類

五、使用元類

終於到了使用元類了,可是一般來說,我們根本就用不上它,就像Python 界的領袖 Tim Peters 說的:

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

元類的主要用途是建立 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 重新建立程式碼,在背後完成真正的工作。

Python 中的一切都是物件,它們要麼是類的例項,要麼是元類的例項,除了 type。type 實際上是它自己的元類,在純 Python 環境中這可不是你能夠做到的,這是通過在實現層面耍一些小手段做到的。

參考:

stackoverflow.com/questions/1…

最後如果對本文有興趣,可以關注公眾號:

公眾號
公眾號

相關文章