1. 程式人生 > >python中Metaclass的理解

python中Metaclass的理解

bject call () 方法 情況 crawler ini -h 代碼

  今天在學習《python3爬蟲開發實戰》中看到這樣一段代碼3

 1 class ProxyMetaclass(type):
 2     def __new__(cls, name, bases, attrs):
 3         count = 0
 4         attrs[__CrawlFunc__] = []
 5         for k, v in attrs.items():
 6             if crawl in k:
 7                 attrs[__CrawlFunc__].append(k)
 8                 count += 1
 9
attrs[__CrawlFunc__] = count 10 return type.__new__(cls, name, bases, attrs) 11 12 class Crawler(object, metaclass=ProxyMetaclass)

從來沒有見過創建類的時候,傳入一個metaclass參數。於是通過查找資料得知,Metacalss為python中的元類。

簡單的講,元類創建了Python中所有的對象。

我們說Python是一種動態語言,而動態語言和靜態語言最大的不同,就是函數和類不是編譯時定義的,而是運行時動態創建的

比方說我們要定義一個HelloWorld

的class,就寫一個helloworld.py模塊:

class HelloWorld(object):
    def helloworld(self):
        print(‘Hello World!‘)

當Python解釋器載入helloworld模塊時,就會依次執行該模塊的所有語句,執行結果就是動態創建出一個HelloWorld的class對象,測試如下:

>>> from helloworld import HelloWorld
>>> h = HelloWorld()
>>> h.helloworld()
Hello, world!
>>> print(type(HelloWorld)) <class type> >>> print(type(h)) <class helloworld.HelloWorld>

type()函數用來查看一個類型或變量的類型,HelloWorld是一個class,它的類型就是type,而h是一個實例,它的類型就是class Helloworld

我們說class的定義是運行時動態創建的,而創建class的方法就是使用type()函數

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

type()函數既可以返回一個對象的類型,又可以創建出新的類型,比如,我們可以通過type()函數創建出HelloWorld類,而無需通過class HelloWorld(object)...的定義:

>>> def helloworld_outside(self): # 先定義函數
...     print(Hello World!)
...
>>> HelloWorld = type(HelloWorld, (object,), dict(helloworld=helloworld_outside)) # 創建HelloWorld class
>>> h = HelloWorld()
>>> h.helloworld()
Hello, world!
>>> print(type(HelloWorld))
<class type>
>>> print(type(h))
<class __main__.HelloWorld>

那麽要創建一個class對象,type()函數需要依次傳入3個參數:

  1. class的名稱;
  2. 繼承的父類集合,註意Python支持多重繼承,如果只有一個父類,別忘了tuple的單元素寫法;
  3. class的方法名稱與函數綁定,這裏我們把函數helloworld_outside綁定到方法名helloworld上。

通過type()函數創建的類和直接寫class是完全一樣的,因為Python解釋器遇到class定義時,僅僅是掃描一下class定義的語法,然後調用type()函數創建出class。

正常情況下,我們都用class Xxx...來定義類,但是,type()函數也允許我們動態創建出類來,也就是說,動態語言本身支持運行期動態創建類,這和靜態語言有非常大的不同,要在靜態語言運行期創建類,必須構造源代碼字符串再調用編譯器,或者借助一些工具生成字節碼實現,本質上都是動態編譯,會非常復雜。

metaclass

除了使用type()動態創建類以外,要控制類的創建行為,還可以使用metaclass

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

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

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

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

metaclass是Python面向對象裏最難理解,也是最難使用的魔術代碼。正常情況下,你不會碰到需要使用metaclass的情況,所以,以下內容看不懂也沒關系,因為基本上你不會用到。

我們先看一個簡單的例子,這個metaclass可以給我們自定義的MyList增加一個add方法:

class ListMetaclass(type):
    def __new__(cls, name, bases, attrs):
        attrs[‘add‘] = lambda self, value: self.append(value)
        return type.__new__(cls, name, bases, attrs)

class MyList(list, metaclass=ListMetaclass):
    pass

下面是運行結果,測試一下MyList是否可以調用add()方法:

>>> L = MyList()
>>> L.add(1)
>> L
[1]

通過這個例子我們可以看到,自定義我們的MyList分兩步:

1. 創建Metaclass,用來創建/修改類

2. 創建實際的MyList Class

首先我們來看第一步,創建Metaclass:

class ListMetaclass(type):
    def __new__(cls, name, bases, attrs):
        attrs[‘add‘] = lambda self, value: self.append(value)
        return type.__new__(cls, name, bases, attrs)

  • 類名的定義:定義ListMetaclass,按照默認習慣,metaclass的類名總是以Metaclass結尾,以便清楚地表示這是一個metaclass
  • Metaclass的父類:Metaclass是類的模板,所以必須從`type`類型派生:
  • 選擇__new__函數作為實現"修改類"的函數
    • 函數__new__(cls, name,bases,attrs)中,"cls"類似於類中其他函數的self參數,例如__init__(self),只不過self代表創建的對象,而cls代表類本身(__init__作為實例初始化的函數,需要把實例本身作為參數傳進去,這樣我們才能保證被修改的是實例;同理,__new__函數需要把類本身作為參數傳進去,才能保證被初始化的是當前類); name代表類的名稱;bases代表當前類的父類集合;attrs代表當前類的屬性,是狹義上屬性和方法的集合,可以用字典dict的方式傳入
    • 對__new__的定義def __new__(cls, name,bases,attrs),實際上,“new”方法在Python中是真正的構造方法(創建並返回實例),通過這個方法可以產生一個”cls”對應的實例對象所以說”new”方法一定要有返回,要把創建的實例對象返回回去。在此,我們把對類的修改放到__new__方法中,然後返回修改過後的實例對象。另外,很簡單的道理,選擇type.__new__函數作為return的值,是因為我們的ListMetaclass繼承自type,因此應該返回class type的__new__函數創建的對象。
class MyList(list, metaclass=ListMetaclass):
    pass

有了ListMetaclass,下一個問題是如何使用ListMetaclass?

首先我們需要先談一談Python創建class的機制:

當創建class的時候,python會先檢查當前類中有沒有__metaclass__,如果有,就用此方法創建對象;如果沒有,則會一級一級的檢查父類中有沒有__metaclass__,用來創建對象。創建的這個“對象”,就是當前的這個類。如果當前類和父類都沒有,則會在當前package中尋找__metaclass__方法,如果還沒有,則會調用自己隱藏的的type函數來創建對象

值得註意的是,如果我們在做類的定義時,在class聲明處傳入關鍵字metaclass=ListMetaclass,那麽如果傳入的這個metaclass有__call__函數,這個__call__函數將會覆蓋掉MyList class的__new__函數。這是為什麽呢?請大家回想一下,當我們實例化MyList的時候,用的語句是L1=MyList(),而我們知道,__call__函數的作用是能讓類實例化後的對象能夠像函數一樣被調用。也就是說MyList是ListMetaclass實例化後的對象,而MyList()調用的就是ListMetaclass的__call__函數。另外,值得一提的是,如果class聲明處,我們是讓MyList繼承ListMetaclass,那麽ListMetaclass的__call__函數將不會覆蓋掉MyList的__new__函數。

因此,我們在定義類的時候還要指示使用ListMetaclass來定制類(即在MyList class定義時,在class聲明處傳入關鍵字參數metaclass=ListMetaclass):我們傳入關鍵字參數metaclass後,python會在當前class裏創建屬性__metaclass__,因此它指示Python解釋器在創建MyList時,要通過ListMetaclass.__new__()來創建,在ListMetaclass.__new__()中,我們可以修改類的定義,比如,加上新的方法,然後,返回修改後的定義。

Ok,下面測試一下MyList是否可以調用add()方法:

>>> L = MyList()
>>> L.add(1)
>> L
[1]

而普通的list沒有add()方法:

>>> L2 = list()
>>> L2.add(1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: ‘list‘ object has no attribute ‘add‘

動態修改有什麽意義?直接在MyList定義中寫上add()方法不是更簡單嗎?正常情況下,確實應該直接寫,通過metaclass修改純屬變態。

但是,總會遇到需要通過metaclass修改類定義的。ORM就是一個典型的例子。

ORM全稱“Object Relational Mapping”,即對象-關系映射,就是把關系數據庫的一行映射為一個對象,也就是一個類對應一個表,這樣,寫代碼更簡單,不用直接操作SQL語句。

python中Metaclass的理解