1. 程式人生 > >python3高階知識--元類(metaclass)深度剖析

python3高階知識--元類(metaclass)深度剖析

一、簡介

  在面向物件的程式設計中類和物件是其重要角色,我們知道物件是由類例項化而來,那麼類又是怎麼生成的呢?答案是通過元類。本篇文章將介紹元類相關知識,並剖析元類生成類的過程,以及元類的使用等內容,希望能幫助到正在學習python的同仁。 

一、一切皆物件

  在python中有這樣一句話“一切皆物件”,沒錯你所知道的dict、class、int、func等等都是物件,讓我們來看以下一段程式碼來進行說明:

#!/usr/bin/env python3
# -*- coding:utf-8 -*-
# Author:wd

class Foo(object):
    
pass def func(): print('func') print(Foo.__class__) print(func.__class__) print(int.__class__) print(func.__class__.__class__) 結果: <class 'type'> <class 'function'> <class 'type'> <class 'type'>

說明:__class__方法用於檢視當前物件由哪個類生成的,正如結果所見其中Foo和int這些類(物件)都是由type建立,而函式則是由function類建立,而function類則也是由type建立,究其根本所有的這些類物件都是由type穿件。這裡的type就是python內建的元類,接下來談談type。

二、關於type

  上面我們談到了所有的類(物件)都是由type生成,那麼不妨我們看看type定義,以下是python3.6中內建type定義部分摘抄:

class type(object):
    """
    type(object_or_name, bases, dict)
    type(object) -> the object's type
    type(name, bases, dict) -> a new type
    """
    def mro(self): # real signature unknown; restored from __doc__
""" mro() -> list return a type's method resolution order """ return []

從描述資訊中我們可以看到,type(object)->返回物件type型別,也就是我們常常使用該方法判斷一個物件的型別,而type(name, bases, dict) -> 返回一個新的類(物件)。 讓我們詳細描述下這個語法: 
type(類名,該類所繼承的父類元祖,該類對應的屬性字典(k,v))

利用該語法我們來穿件一個類(物件)Foo:

Foo=type('Foo',(object,),{'Name':'wd'})

print(Foo)
print(Foo.Name)

結果:
<class '__main__.Foo'>
wd

當然也可以例項化這個類(物件):

Foo=type('Foo',(object,),{'Name':'wd'})

obj=Foo()
print(obj.Name)
print(obj)
print(obj.__class__)

結果:
wd
<__main__.Foo object at 0x104482438>
<class '__main__.Foo'>

這樣建立方式等價於:

class Foo(object):
    Name='wd'

其實上面的過程也就是我們使用class定義類生成的過程,而type就是python中的元類。

三、元類

什麼是元類

經過以上的介紹,說白了元類就是建立類的類,有點拗口,姑且把這裡稱為可以建立類物件的類。列如type就是元類的一種,其他的元類都是通過繼承type或使用type生成的。通過元類我們可以控制一個類建立的過程,以及包括自己定製一些功能。 例如,下面動態的為類新增方法:

def get_name(self):
    print(self.name)


class MyType(type):
    def __new__(cls, cls_name, bases, dict_attr):
        dict_attr['get_name'] = get_name  #將get_name 作為屬性新增到類屬性中
        return super(MyType, cls).__new__(cls, cls_name, bases, dict_attr)


class Foo(metaclass=MyType):
    def __init__(self, name):
        self.name = name


obj = Foo('wd')
obj.get_name()#呼叫該方法
結果:
wd

以上示例說明: 1.MyType是繼承了type,也就是說繼承了其所有的功能與特性,所以它也具有建立類的功能,所以它也是元類; 2.類Foo中使用了metaclass關鍵字,表明該類由MyType進行建立。 3.建立Foo類時候會先執行MyType的__new__方法(後續會這些方法進行更詳細的說明),並接受三個引數,cls_name, bases, dict_attr,在改方法中我們在類屬性字典中添加了get_name屬性,並將它與函式繫結,這樣生成的類中就有了該方法。 

使用元類

  瞭解類元類的作用,我們知道其主要目的就是為了當建立類時能夠根據需求改變類,在以上的列子中我們介紹了使用方法,其中就像stackoverflow中關於對元類的使用建議一樣,絕大多數的應用程式都非必需使用元類,並且使用它可能會對你的程式碼帶來一定的複雜性,但是就元類的使用而言其實很簡單,其場景在於:

1.對建立的類進行校驗(攔截);

2.修改類;

3.為該類定製功能;

使用元類是時候經典類和新式類時候有些不同,新式類通過引數metaclass,經典類通過__metaclass__屬性:

class Foo(metaclass=MyType): #新式類
    pass


class Bar:  # 經典類
    __metaclass__ = MyType
    pass

  在解釋元類的時候有提到過,元類可以是type,也可以是繼承type的類,當然還可以是函式,只要它是可呼叫的。但是有個必要的前提是該函式使用的是具有type功能的函式,否則生成的物件可能就不是你想要的(在後續的原理在進行講解)。以下示例將給出使用函式作為元類來建立類:

def class_creater(cls_name, bases, dict_attr):
    return type(cls_name, bases, dict_attr)

class Foo(metaclass=class_creater):

    def __init__(self,name):
        self.name=name


obj=Foo('wd')
print(obj.name) #wd

原理

當我們使用class定義類時候,它會執行以下步驟:
  1. 獲取類名,以示例中class Foo為例,類名是Foo。
  2. 獲取父類,預設object,以元祖的形式,如(object,Foo)
  3. 獲取類的屬性字典(也叫名稱空間)
  4. 將這三個引數傳遞給元類(也就是metaclass引數指定的類),如果沒有metaclass引數則使用type生成類。
在這幾個步驟中,前三個步驟沒有什麼可說的,但是對於元類生成類的這一過程接下來我們將詳細介紹。 

元類建立類的過程

  其實如果你對面向物件非常熟悉的話,其過程也是非常容易理解的,在介紹類生成的過程之前,我們需要對三個方法做充分的理解:__init__、__new__、__call__。
  1. __init__ :通常用於初始化一個新例項,控制這個初始化的過程,比如新增一些屬性, 做一些額外的操作,發生在類例項被建立完以後。它是例項級別的方法。觸發方式為:類()
  2. __new__ :通常用於控制生成一個類例項的過程,依照Python官方文件的說法,__new__方法主要是當你繼承一些不可變的時(比如int, str, tuple), 提供給你一個自定義這些類的例項化過程的途徑。它是類級別的方法。
  3. __call__ :當類中有__call__方法存在時候,該類實列化的物件就是可呼叫的,觸發方式為:物件()。
  並且一個類在例項化的過程中執行順序是先執行__new__在執行__init__(這是重點),以下用一個示例來說明:
class Foo(object):
    def __init__(self, name):
        print('this is __init__')
        self.name = name

    def __new__(cls, *args, **kwargs):
        print('this is __new__')
        return object.__new__(cls)

    def __call__(self, *args, **kwargs):
        print("this is __call__")


obj=Foo('wd')  # 例項化
obj() # 觸發__call__

結果:
this is __new__
this is __init__
this is __call__

有了這個知識,再來看看使用元類生成類,以下程式碼定義來一個元類繼承來type,我們重寫__new__和__init__方法(其實什麼也沒幹),為了說明類的生成過程:

class MyType(type):
    def __init__(self, cls_name, bases, cls_attr):
        print("Mytype __init__", cls_name, bases)

    def __new__(cls, cls_name, bases, cls_attr):
        print("Mytype __new__", cls_name, bases)
        return super(MyType, cls).__new__(cls, cls_name, bases, cls_attr)


class Foo(metaclass=MyType):
    def __init__(self, name):
        print('this is __init__')
        self.name = name

    def __new__(cls, *args, **kwargs):
        print('this is __new__')
        return object.__new__(cls)


print("line -------")
obj = Foo('wd')  # 例項化


結果:
Mytype __new__ Foo ()
Mytype __init__ Foo ()
line -------
this is __new__
this is __init__

解釋說明:

  • 首先metaclass接受一個可呼叫的物件,而在這裡該物件是一個類,也就是說會執行MyType(),並把cls_name,bases,cls_attr傳遞給MyType,這不就是MyType的示例化過程嗎,所以你在結果中可以看到,分割線是在"Mytype __new__”和“Mytype __init__”之後輸出,接下來在看MyType。
  • MyType元類的例項化過程和普通類一樣,先執行自己__new__方法,在執行自己的__init__方法,在這裡請注意__new__方法是控制MyType類生成的過程,而__init__則是例項化過程,用於生成類Foo。這樣一來是不是對類的生成過程有了非常深刻的認識。 
這還不夠清楚,在以上的示例中Foo即是類,也是物件,它是由元類例項化的物件,那它執行Foo(‘wd’)相當於是執行:物件(),即執行的是元類的__call__方法,那麼在以上示例中我們在元類中加入__call__方法,看看在執行Foo(‘wd’)會不會呼叫__call__:
class MyType(type):
    def __init__(self, cls_name, bases, cls_attr):
        print("Mytype __init__", cls_name, bases)

    def __new__(cls, cls_name, bases, cls_attr):
        print("Mytype __new__", cls_name, bases)
        return super(MyType, cls).__new__(cls, cls_name, bases, cls_attr)

    def __call__(self, *args, **kwargs):
        print('Mytype __call__')


class Foo(metaclass=MyType):
    def __init__(self, name):
        print('this is __init__')
        self.name = name

    def __new__(cls, *args, **kwargs):
        print('this is __new__')
        return object.__new__(cls)

    def __call__(self, *args, **kwargs):
        print("this is __call__")


print("before -------")
obj = Foo('wd')  # 例項化
print("after -------")
print(obj)
結果:
Mytype __new__ Foo ()
Mytype __init__ Foo ()
before -------
Mytype __call__
after -------
None

  你會發現,當Foo例項化時候執行了元類的__call__,你從python的一切皆物件的方式來看,一切都是順理成章的,因為這裡的Foo其實是元類的物件,物件+()執行元類的__call__方法。請注意,在Foo進行例項化時候返回的物件是None,這是因為__call__方法返回的就是None,所以在沒有必要的前提下最好不要隨意重寫元類的__call__方法,這會影響到類的例項化。__call__方法在元類中作用是控制類生成時的呼叫過程。

  通過__call__方法我們能得出結果就是__call__方法返回什麼,我們最後得到的例項就是什麼。還是剛才栗子,我們讓Foo例項化以後變成一個字串:

class MyType(type):
    def __init__(self, cls_name, bases, cls_attr):
        print("Mytype __init__", cls_name, bases)

    def __new__(cls, cls_name, bases, cls_attr):
        return super(MyType, cls).__new__(cls, cls_name, bases, cls_attr)

    def __call__(self, *args, **kwargs):
        return 'this is wd'


class Foo(metaclass=MyType):
    def __init__(self, name):
        print('this is __init__')
        self.name = name

    def __new__(cls, *args, **kwargs):
        print('this is __new__')
        return object.__new__(cls)

    def __call__(self, *args, **kwargs):
        print("this is __call__")



obj = Foo('wd')  # 例項化

print(type(obj),obj)

結果:
Mytype __init__ Foo ()
<class 'str'> this is wd

既然__call__方法返回什麼,我們例項化生成的物件就是什麼,那麼在正常的流程是返回的是Foo的物件,而Foo的物件是由Foo的__new__和Foo的__init__生成的,所以在__call__方法的內部又有先後呼叫了Foo類的__new__方法和__init__方法,如果我們重寫元類的__call__方法,則應該呼叫物件的__new__和__init__,如下:

class MyType(type):
    def __init__(self, cls_name, bases, cls_attr):
        print("Mytype __init__", cls_name, bases)

    def __new__(cls, cls_name, bases, cls_attr):
        return super(MyType, cls).__new__(cls, cls_name, bases, cls_attr)

    def __call__(self, *args, **kwargs):
        print("Mytype __call__", )
        obj = self.__new__(self)
        print(self, obj)
        self.__init__(obj, *args, **kwargs)

        return obj


class Foo(metaclass=MyType):
    def __init__(self, name):
        self.name = name

    def __new__(cls, *args, **kwargs):
        return object.__new__(cls)


obj = Foo('wd')  # 例項化
print(obj.name)
結果:
Mytype __init__ Foo ()
Mytype __call__
<class '__main__.Foo'> <__main__.Foo object at 0x1100c9dd8>
wd

  同樣,當函式作為元類時候,metaclass關鍵字會呼叫其對應的函式生成類,如果這個函式返回的不是類,而是其他的物件,那麼使用該函式定義的類就得到的就是該物件,這也就是為什麼我說使用函式作為元類時候,需要有type功能,一個簡單的示例:

def func(cls_name, bases, dict_attr):
    return 'this is wd'


class Foo(metaclass=func):
    def __init__(self, name):
        self.name = name

    def __new__(cls, *args, **kwargs):
        return object.__new__(cls)


print(Foo, "|", type(Foo)) # 結果:this is wd | <class 'str'>

obj=Foo('wd') #報錯

結語

    現在說python一切皆物件可以說非常到位了,因為它們要不是類的物件,要不就是元類的物件,除了type。再者元類本身其實是複雜的,只是我們在對這元類生成類的這一過程做了深度的分析,所以在我們編寫的程式中可能極少會用到元類,除非有特殊的需求,比如動態的生成類、修改類的一些東西等,當然你想讓你的程式碼看來“複雜”也可以嘗試使用。但是在有些情況下(如在文章中提到的幾個場景中)使用元類能更巧妙的解決很多問題,不僅如此你會發現元類在很多開源框架中也有使用,例如django、flask,你也可以借鑑其中的場景對自己的程式進行優化改進。