python中Metaclass的理解
今天在學習《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 9attrs[‘__CrawlFunc__‘] = count 10 return type.__new__(cls, name, bases, attrs) 11 12 class Crawler(object, metaclass=ProxyMetaclass)
從來沒有見過創建類的時候,傳入一個metaclass參數。於是通過查找資料得知,Metacalss為python中的元類。
簡單的講,元類創建了Python中所有的對象。
我們說Python是一種動態語言,而動態語言和靜態語言最大的不同,就是函數和類不是編譯時定義的,而是運行時動態創建的。
比方說我們要定義一個HelloWorld
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個參數:
- class的名稱;
- 繼承的父類集合,註意Python支持多重繼承,如果只有一個父類,別忘了tuple的單元素寫法;
- 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的父類:M
etaclass是類的模板,所以必須從`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的理解