元類的理解


 

兩句話掌握 Python 最難知識點——元類出處: 

每個中國人,都是天生的元類使用者

學懂元類,你只需要知道兩句話:

  • 道生一,一生二,二生三,三生萬物
  • 我是誰?我從哪來里?我要到哪里去?

在python世界,擁有一個永恆的道,那就是“type”,請記在腦海中,type就是道。如此廣袤無垠的python生態圈,都是由type產生出來的。

道生一,一生二,二生三,三生萬物。

  1.  即是 type
  2.  即是 metaclass(元類,或者叫類生成器)
  3.  即是 class(類,或者叫實例生成器)
  4.  即是 instance(實例)
  5. 萬物 即是 實例的各種屬性與方法,我們平常使用python時,調用的就是它們。

道和一,是我們今天討論的命題,而二、三、和萬物,則是我們常常使用的類、實例、屬性和方法,用hello world來舉例:

輸出效果:

這就是一個標准的“二生三,三生萬物”過程。 從類到我們可以調用的方法,用了這兩步。

那我們不由自主要問,類從何而來呢?回到代碼的第一行。
class Hello其實是一個函數的“語義化簡稱”,只為了讓代碼更淺顯易懂,它的另一個寫法是:

這樣的寫法,就和之前的Class Hello寫法作用完全相同,你可以試試創建實例並調用

輸出效果:

我們回頭看一眼最精彩的地方,道直接生出了二

Hello = type(‘Hello’, (object,), dict(say_hello=fn))

這就是“道”,python世界的起源,你可以為此而驚嘆。
注意它的三個參數!暗合人類的三大永恆命題:我是誰,我從哪里來,我要到哪里去。

  • 第一個參數:我是誰。 在這里,我需要一個區分於其它一切的命名,以上的實例將我命名為“Hello”
  • 第二個參數:我從哪里來
    在這里,我需要知道從哪里來,也就是我的“父類”,以上實例中我的父類是“object”——python中一種非常初級的類。
  • 第三個參數:我要到哪里去
    在這里,我們將需要調用的方法和屬性包含到一個字典里,再作為參數傳入。以上實例中,我們有一個say_hello方法包裝進了字典中。

值得注意的是,三大永恆命題,是一切類,一切實例,甚至一切實例屬性與方法都具有的。理所應當,它們的“創造者”,道和一,即type和元類,也具有這三個參數。但平常,類的三大永恆命題並不作為參數傳入,而是以如下方式傳入

 

  • 造物主,可以直接創造單個的人,但這是一件苦役。造物主會先創造“人”這一物種,再批量創造具體的個人。並將三大永恆命題,一直傳遞下去。
  • “道”可以直接生出“二”,但它會先生出“一”,再批量地制造“二”。
  • type可以直接生成類(class),但也可以先生成元類(metaclass),再使用元類批量定制類(class)。

元類——道生一,一生二

一般來說,元類均被命名后綴為Metalass。想象一下,我們需要一個可以自動打招呼的元類,它里面的類方法呢,有時需要say_Hello,有時需要say_Hi,有時又需要say_Sayolala,有時需要say_Nihao。

如果每個內置的say_xxx都需要在類里面聲明一次,那將是多么可怕的苦役! 不如使用元類來解決問題。

以下是創建一個專門“打招呼”用的元類代碼:

記住兩點:
1、元類是由“type”衍生而出,所以父類需要傳入type。【道生一,所以一必須包含道

2、元類的操作都在 __new__中完成,它的第一個參數是將創建的類,之后的參數即是三大永恆命題:我是誰,我從哪里來,我將到哪里去。 它返回的對象也是三大永恆命題,接下來,這三個參數將一直陪伴我們。

在__new__中,我只進行了一個操作,就是

它跟據類的名字,創建了一個類方法。比如我們由元類創建的類叫“Hello”,那創建時就自動有了一個叫“say_Hello”的類方法,然后又將類的名字“Hello”作為默認參數saying,傳到了方法里面。然后把hello方法調用時的傳參作為value傳進去,最終打印出來。

那么,一個元類是怎么從創建到調用的呢?
來!一起根據道生一、一生二、二生三、三生萬物的准則,走進元類的生命周期吧

輸出為

注意:通過元類創建的類,第一個參數是父類,第二個參數是metaclass

普通人出生都不會說話,但有的人出生就會打招呼說“Hello”,“你好”,“sayolala”,這就是天賦的力量。它會給我們面向對象的編程省下無數的麻煩。

現在,保持元類不變,我們還可以繼續創建Sayolala, Nihao類,如下:

輸出

也可以說中文

輸出

再來一個小例子:

現在我們打印一下L

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

太棒了!學到這里,你是不是已經體驗到了造物主的樂趣?

python世界的一切,盡在掌握。

年輕的造物主,請隨我一起開創新世界。

我們選擇兩個領域,一個是Django的核心思想,“Object Relational Mapping”,即對象-關系映射,簡稱ORM。

這是Django的一大難點,但學完了元類,一切變得清晰。你對Django的理解將更上一層樓!

另一個領域是爬蟲領域(黑客領域),一個自動搜索網絡上的可用代理,然后換着IP去突破別的人反爬蟲限制。

這兩項技能非常有用,也非常好玩!

挑戰一:通過元類創建ORM

准備工作,創建一個Field類

它的作用是
在Field類實例化時將得到兩個參數,name和column_type,它們將被綁定為Field的私有屬性,如果要將Field轉化為字符串時,將返回“Field:XXX” , XXX是傳入的name名稱。

准備工作:創建StringField和IntergerField

它的作用是
在StringField,IntegerField實例初始化時,時自動調用父類的初始化方式。

道生一

它做了以下幾件事

  1. 創建一個新的字典mapping
  2. 將每一個類的屬性,通過.items()遍歷其鍵值對。如果值是Field類,則打印鍵值,並將這一對鍵值綁定到mapping字典上。
  3. 將剛剛傳入值為Field類的屬性刪除。
  4. 創建一個專門的__mappings__屬性,保存字典mapping。
  5. 創建一個專門的__table__屬性,保存傳入的類的名稱。

一生二

 

如果從Model創建一個子類User:

這時
id= IntegerField(‘id’)就會自動解析為:

Model.__setattr__(self, ‘id’, IntegerField(‘id’))

因為IntergerField(‘id’)是Field的子類的實例,自動觸發元類的__new__,所以將IntergerField(‘id’)存入__mappings__並刪除這個鍵值對。

二生三、三生萬物

當你初始化一個實例的時候並調用save()方法時候

這時先完成了二生三的過程:

  1. 先調用Model.__setattr__,將鍵值載入私有對象
  2. 然后調用元類的“天賦”,ModelMetaclass.__new__,將Model中的私有對象,只要是Field的實例,都自動存入u.__mappings__。

接下來完成了三生萬物的過程:

通過u.save()模擬數據庫存入操作。這里我們僅僅做了一下遍歷__mappings__操作,虛擬了sql並打印,在現實情況下是通過輸入sql語句與數據庫來運行。

輸出結果為

 

  • 年輕的造物主,你已經和我一起體驗了由“道”演化“萬物”的偉大歷程,這也是Django中的Model版塊核心原理。
  • 接下來,請和我一起進行更好玩的爬蟲實戰(嗯,你現在已經是初級黑客了):網絡代理的爬取吧!

挑戰二:網絡代理的爬取

准備工作,先爬個頁面玩玩

請確保已安裝requests和pyquery這兩個包。

這里,我們利用request包,把百度的源碼爬了出來。

試一試抓百度

把這一段粘在get_page.py后面,試完刪除

 

試一試抓代理

把這一段粘在get_page.py后面,試完刪除

 

接下來進入正題:使用元類批量抓取代理

批量處理抓取代理

 

道生一:元類的__new__中,做了四件事:
  1. 將“crawl_”開頭的類方法的名稱推入ProxyGetter.__CrawlName__
  2. 將“crawl_”開頭的類方法的本身推入ProxyGetter.__CrawlFunc__
  3. 計算符合“crawl_”開頭的類方法個數
  4. 刪除所有符合“crawl_”開頭的類方法

怎么樣?是不是和之前創建ORM的__mappings__過程極為相似?

一生二:類里面定義了使用pyquery抓取頁面元素的方法

分別從三個免費代理網站抓取了頁面上顯示的全部代理。

如果對yield用法不熟悉,可以查看:
廖雪峰的python教程:生成器

二生三:創建實例對象crawler
三生萬物:遍歷每一個__CrawlFunc__
  1. 在ProxyGetter.__CrawlName__上面,獲取可以抓取的的網址名。
  2. 觸發類方法ProxyGetter.get_raw_proxies(site)
  3. 遍歷ProxyGetter.__CrawlFunc__,如果方法名和網址名稱相同的,則執行這一個方法
  4. 把每個網址獲取到的代理整合成數組輸出。

那么。。。怎么利用批量代理,沖擊別人的網站,套取別人的密碼,狂發廣告水貼,定時騷擾客戶? 呃!想啥呢!這些自己悟!如果悟不到,請聽下回分解!

年輕的造物主,創造世界的工具已經在你手上,請你將它的威力發揮到極致!

請記住揮動工具的口訣:

  • 道生一,一生二,二生三,三生萬物
  • 我是誰,我來自哪里,我要到哪里去
  • Python元類再談

    在Python中一切都是對象,類型也是對象;類比類型和實例的概念,類型也必然有自己的類型,十分合理。事實上,類型的類型其實就是術語元類型的意思,python里面所有類型的元類型都是type。默認情況下我們新建一個類,在不手動指定元類型的前提下,type會被指定為元類型 ,元類型能夠控制類型的創建和初始化。

    一般情況下我們能夠通過關鍵字class來定義一個新的自定義類型,但也能夠通過type動態的生成一個新的類型,下面的兩種實現方式等價:

    借助這個例子,我們還能順便看一下一些關於默認元類type的信息:

    新定義一個類型,當類型被解析的時候(比如當作模塊引入),元類會負責創建和初始化這個新的類型,背后的邏輯基本上包括:

    • 執行類的定義代碼收集所有的屬性和方法
    • 查找類的元類
    • 執行 Metaclass(name, bases, property_dict),參數分別新建的類的名稱,類的父類tuple,收集的屬性字典
    • 類型創建完成
    • 執行初始化

    在上面描述的過程中,自定義指定元類,然后重寫元類的__new__ 和 __init__方法,因為在指定元類的情況下,除去收集信息的過程,類型的創建和初始化兩個步驟:

    注意這里的表示方式是調用內部方法來表示的,一般來說在重寫的new或者init方法的最后都會調用type相應的方法來完成最終的類型創建或初始化工作,這時候也可以使用super關鍵字動態綁定,或者通過__dict__屬性字典訪問是一樣的:

    對於__init__這樣的method來說還可以這樣調用:

    插個題外話,這三種方式的使用其實涉及到python的描述符,使用super和get的時候會進行類型或者實例的綁定,相比直接調用內部方法__new____init__,由於綁定了self/cls上下文,在傳遞參數的時候就只用指定除上下文之后的參數了。

    從網上搜羅了一個例子,將元類創建和初始化新的類型的過程完整展示出來了:

    創建和初始化的過程只會發生一此,也就是會是說 __new__, __init__只會被執行一次,並且在執行完之前,類型MyKlass其實並沒有生成,直接通過名稱訪問會報錯:

    在使用元類的過程中,有時候我們會重寫他的__call__方法,這個方法的作用其實和__new__有點相似,只不過這次是控制類型實例對象的生成,因為這個方法恰好和生成類型實例時調用的構造方法吻合。關於重寫這個call方法的使用場景,一個比較常用的就是實現單例模式:

    例子很直接,__call__方法里面通過判斷是否已經有初始化過的實例,沒有就仿照正常未指定元類的情況下調用type的__call__方法(當然這里要么通過super binding要么手動指定cls上下文),生成一個Foo的實例存儲和返回出來。但是有一個注意點是,call方法每次初始化實例對象的時候都會被調用,這也和先前說的控制實例的生成一致:

    還有一個需要在意的地方是最后的兩行打印日志,Foo類型的元類是Metasingleton(調用new生成類型的時候默認指定元類是第一個參數);Foo的__call__方法是綁定了Foo(MetaSingleton的實例)實例的MetaSingleton的方法,也就是從另外的方面證實每次初始化Foo類型市里的時候,其實是在調用元類中重寫的__call__方法。

    元類這個特性大多數情況下確實使用的不多並且需要稍微花點時間來理解,但是需要使用的時候會非常好用,往往能夠實現很多優雅的功能,最典型的就是ORM的實現了,只不過會更加的復雜,且pythoning且學習吧, PEACE!

    參考資料中的鏈接里面有有幾個實際的例子,本文也是學習其中的內容后配合一些其它一些使用經驗以及碰到的問題理而成,希望對大家有用。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM