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

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

 

來源:劉羽衝 

segmentfault.com/a/1190000011447445

如有好文章投稿,請點擊 → 這裏瞭解詳情

本文轉載自 知乎:python開發者


千萬不要被所謂“元類是99%的python程序員不會用到的特性”這類的說辭嚇住。因爲每個中國人,都是天生的元類使用者


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


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

  • 我是誰?我從哪來裏?我要到哪裏去?


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



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


  1. 道 即是 type

  2. 一 即是 metaclass(元類,或者叫類生成器)

  3. 二 即是 class(類,或者叫實例生成器)

  4. 三 即是 instance(實例)

  5. 萬物 即是 實例的各種屬性與方法,我們平常使用python時,調用的就是它們。


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


# 創建一個Hello類,擁有屬性say_hello ----二的起源

class Hello():

    def say_hello(self, name='world'):

        print('Hello, %s.' % name)

 

 

# 從Hello類創建一個實例hello ----二生三

hello = Hello()

 

# 使用hello調用方法say_hello ----三生萬物

hello.say_hello()


輸出效果:


Hello, world.


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


那我們不由自主要問,類從何而來呢?回到代碼的第一行。


class Hello其實是一個函數的“語義化簡稱”,只爲了讓代碼更淺顯易懂,它的另一個寫法是:


def fn(self, name='world')# 假如我們有一個函數叫fn

    print('Hello, %s.' % name)

    

Hello = type('Hello', (object,), dict(say_hello=fn)) # 通過type創建Hello class ---- 神祕的“道”,可以點化一切,這次我們直接從“道”生出了“二”


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


# 從Hello類創建一個實例hello ----二生三,完全一樣

hello = Hello()

 

# 使用hello調用方法say_hello ----三生萬物,完全一樣

hello.say_hello()


輸出效果:


Hello, world. ----調用結果完全一樣。


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


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


這就是“道”,python世界的起源,你可以爲此而驚歎。


注意它的三個參數!暗合人類的三大永恆命題:我是誰,我從哪裏來,我要到哪裏去。


  • 第一個參數:我是誰。 在這裏,我需要一個區分於其它一切的命名,以上的實例將我命名爲“Hello”

  • 第二個參數:我從哪裏來。在這裏,我需要知道從哪裏來,也就是我的“父類”,以上實例中我的父類是“object”——python中一種非常初級的類。

  • 第三個參數:我要到哪裏去。在這裏,我們將需要調用的方法和屬性包含到一個字典裏,再作爲參數傳入。以上實例中,我們有一個say_hello方法包裝進了字典中。


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


class Hello(object){

# class 後聲明“我是誰”

# 小括號內聲明“我來自哪裏”

# 中括號內聲明“我要到哪裏去”

    def say_hello(){

        

    }

}


  • 造物主,可以直接創造單個的人,但這是一件苦役。造物主會先創造“人”這一物種,再批量創造具體的個人。並將三大永恆命題,一直傳遞下去。

  • “道”可以直接生出“二”,但它會先生出“一”,再批量地製造“二”。

  • type可以直接生成類(class),但也可以先生成元類(metaclass),再使用元類批量定製類(class)。


元類——道生一,一生二


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


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


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


class SayMetaClass(type):

 

    def __new__(cls, name, bases, attrs):

        attrs['say_'+name] = lambda self,value,saying=nameprint(saying+','+value+'!')

        return type.__new__(cls, name, bases, attrs)


記住兩點:


  1. 元類是由“type”衍生而出,所以父類需要傳入type。【道生一,所以一必須包含道】

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


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


attrs['say_'+name] = lambda self,value,saying=name: print(saying+','+value+'!')


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


那麼,一個元類是怎麼從創建到調用的呢?


來!一起根據道生一、一生二、二生三、三生萬物的準則,走進元類的生命週期吧!


# 道生一:傳入type

class SayMetaClass(type):

 

    # 傳入三大永恆命題:類名稱、父類、屬性

    def __new__(cls, name, bases, attrs):

        # 創造“天賦”

        attrs['say_'+name] = lambda self,value,saying=nameprint(saying+','+value+'!')

        # 傳承三大永恆命題:類名稱、父類、屬性

        return type.__new__(cls, name, bases, attrs)

 

# 一生二:創建類

class Hello(object, metaclass=SayMetaClass):

    pass

 

# 二生三:創建實列

hello = Hello()

 

# 三生萬物:調用實例方法

hello.say_Hello('world!')


輸出爲


Hello, world!


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


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


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


# 一生二:創建類

class Sayolala(object, metaclass=SayMetaClass):

    pass

 

# 二生三:創建實列

s = Sayolala()

 

# 三生萬物:調用實例方法

s.say_Sayolala('japan!')


輸出


Sayolala, japan!


也可以說中文


# 一生二:創建類

class Nihao(object, metaclass=SayMetaClass):

    pass

 

# 二生三:創建實列

n = Nihao()

 

# 三生萬物:調用實例方法

n.say_Nihao('中華!')


輸出


Nihao, 中華!


再來一個小例子:


# 道生一

class ListMetaclass(type):

    def __new__(cls, name, bases, attrs):

        # 天賦:通過add方法將值綁定

        attrs['add'] = lambda self, valueself.append(value)

        return type.__new__(cls, name, bases, attrs)

        

# 一生二

class MyList(list, metaclass=ListMetaclass):

    pass

    

# 二生三

L = MyList()

 

# 三生萬物

L.add(1)


現在我們打印一下L


print(L)

 

>>> [1]


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


L2 = list()

L2.add(1)

 

>>>AttributeError'list' object has no attribute 'add'


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


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



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


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


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


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


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


挑戰一:通過元類創建ORM


準備工作,創建一個Field類


class Field(object):

 

    def __init__(self, name, column_type):

        self.name = name

        self.column_type = column_type

 

    def __str__(self):

        return '<%s:%s>' % (self.__class__.__name__, self.name)


它的作用是


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


準備工作:創建StringField和IntergerField


class StringField(Field):

 

    def __init__(self, name):

        super(StringField, self).__init__(name, 'varchar(100)')

 

class IntegerField(Field):

 

    def __init__(self, name):

        super(IntegerField, self).__init__(name, 'bigint')


它的作用是


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


道生一


class ModelMetaclass(type):

 

    def __new__(cls, name, bases, attrs):

        if name=='Model':

            return type.__new__(cls, name, bases, attrs)

        print('Found model: %s' % name)

        mappings = dict()

        for k, v in attrs.items():

            if isinstance(v, Field):

                print('Found mapping: %s ==> %s' % (k, v))

                mappings[k] = v

        for k in mappings.keys():

            attrs.pop(k)

        attrs['__mappings__'] = mappings # 保存屬性和列的映射關係

        attrs['__table__'] = name # 假設表名和類名一致

        return type.__new__(cls, name, bases, attrs)


它做了以下幾件事


  1. 創建一個新的字典mapping

  2. 將每一個類的屬性,通過.items()遍歷其鍵值對。如果值是Field類,則打印鍵值,並將這一對鍵值綁定到mapping字典上。

  3. 將剛剛傳入值爲Field類的屬性刪除。

  4. 創建一個專門的__mappings__屬性,保存字典mapping。

  5. 創建一個專門的__table__屬性,保存傳入的類的名稱。


一生二


class Model(dict, metaclass=ModelMetaclass):

 

    def __init__(self, **kwarg):

        super(Model, self).__init__(**kwarg)

 

    def __getattr__(self, key):

        try:

            return self[key]

        except KeyError:

            raise AttributeError("'Model' object has no attribute '%s'" % key)

 

    def __setattr__(self, key, value):

        self[key] = value

 

    # 模擬建表操作

    def save(self):

        fields = []

        args = []

        for k, v in self.__mappings__.items():

            fields.append(v.name)

            args.append(getattr(self, k, None))

        sql = 'insert into %s (%s) values (%s)' % (self.__table__, ','.join(fields), ','.join([str(i) for i in args]))

        print('SQL: %s' % sql)

        print('ARGS: %s' % str(args))


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


class User(Model):

    # 定義類的屬性到列的映射:

    id = IntegerField('id')

    name = StringField('username')

    email = StringField('email')

    password = StringField('password')


這時


id= IntegerField(‘id’)就會自動解析爲:


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


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


二生三、三生萬物


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


u = User(id=12345, name='Batman', email='[email protected]', password='iamback')

u.save()


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


  1. 先調用Model.__setattr__,將鍵值載入私有對象

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


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


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


輸出結果爲


Found modelUser

Found mappingname ==> <StringField:username>

Found mappingpassword ==> <StringField:password>

Found mappingid ==> <IntegerField:id>

Found mappingemail ==> <StringField:email>

SQLinsert into User (username,password,id,email) values (Batman,iamback,12345,batman@nasa.org)

ARGS['Batman', 'iamback', 12345, '[email protected]']


  • 年輕的造物主,你已經和我一起體驗了由“道”演化“萬物”的偉大曆程,這也是Django中的Model版塊核心原理。

  • 接下來,請和我一起進行更好玩的爬蟲實戰(嗯,你現在已經是初級黑客了):網絡代理的爬取吧!


挑戰二:網絡代理的爬取


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


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


# 文件:get_page.py

import requests

 

base_headers = {

    'User-Agent''Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36',

    'Accept-Encoding''gzip, deflate, sdch',

    'Accept-Language''zh-CN,zh;q=0.8'

}

 

 

def get_page(url):

    headers = dict(base_headers)

    print('Getting', url)

    try:

        r = requests.get(url, headers=headers)

        print('Getting result', url, r.status_code)

        if r.status_code == 200:

            return r.text

    except ConnectionError:

        print('Crawling Failed', url)

        return None


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


試一試抓百度


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


if(__name__ == '__main__'):

    rs = get_page('https://www.baidu.com')

    print('result:\r\n', rs)


試一試抓代理


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


if(__name__ == '__main__'):

    from pyquery import PyQuery as pq

    start_url = 'http://www.proxy360.cn/Region/China'

    print('Crawling', start_url)

    html = get_page(start_url)

    if html:

        doc = pq(html)

        lines = doc('div[name="list_proxy_ip"]').items()

        for line in lines:

            ip = line.find('.tbBottomLine:nth-child(1)').text()

            port = line.find('.tbBottomLine:nth-child(2)').text()

            print(ip+':'+port)


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


批量處理抓取代理


from getpage import get_page

from pyquery import PyQuery as pq

 

 

# 道生一:創建抽取代理的metaclass

class ProxyMetaclass(type):

    """

        元類,在FreeProxyGetter類中加入

        __CrawlFunc__和__CrawlFuncCount__

        兩個參數,分別表示爬蟲函數,和爬蟲函數的數量。

    """

    def __new__(cls, name, bases, attrs):

        count = 0

        attrs['__CrawlFunc__'] = []

        attrs['__CrawlName__'] = []

        for k, v in attrs.items():

            if 'crawl_' in k:

                attrs['__CrawlName__'].append(k)

                attrs['__CrawlFunc__'].append(v)

                count += 1

        for k in attrs['__CrawlName__']:

            attrs.pop(k)

        attrs['__CrawlFuncCount__'] = count

        return type.__new__(cls, name, bases, attrs)

 

 

# 一生二:創建代理獲取類

 

class ProxyGetter(object, metaclass=ProxyMetaclass):

    def get_raw_proxies(self, site):

        proxies = []

        print('Site', site)

        for func in self.__CrawlFunc__:

            if func.__name__==site:

                this_page_proxies = func(self)

                for proxy in this_page_proxies:

                    print('Getting', proxy, 'from', site)

                    proxies.append(proxy)

        return proxies

 

 

    def crawl_daili66(self, page_count=4):

        start_url = 'http://www.66ip.cn/{}.html'

        urls = [start_url.format(page) for page in range(1, page_count + 1)]

        for url in urls:

            print('Crawling', url)

            html = get_page(url)

            if html:

                doc = pq(html)

                trs = doc('.containerbox table tr:gt(0)').items()

                for tr in trs:

                    ip = tr.find('td:nth-child(1)').text()

                    port = tr.find('td:nth-child(2)').text()

                    yield ':'.join([ip, port])

 

    def crawl_proxy360(self):

        start_url = 'http://www.proxy360.cn/Region/China'

        print('Crawling', start_url)

        html = get_page(start_url)

        if html:

            doc = pq(html)

            lines = doc('div[name="list_proxy_ip"]').items()

            for line in lines:

                ip = line.find('.tbBottomLine:nth-child(1)').text()

                port = line.find('.tbBottomLine:nth-child(2)').text()

                yield ':'.join([ip, port])

 

    def crawl_goubanjia(self):

        start_url = 'http://www.goubanjia.com/free/gngn/index.shtml'

        html = get_page(start_url)

        if html:

            doc = pq(html)

            tds = doc('td.ip').items()

            for td in tds:

                td.find('p').remove()

                yield td.text().replace(' ', '')

 

 

if __name__ == '__main__':

    # 二生三:實例化ProxyGetter

    crawler = ProxyGetter()

    print(crawler.__CrawlName__)

    # 三生萬物

    for site_label in range(crawler.__CrawlFuncCount__):

        site = crawler.__CrawlName__[site_label]

        myProxies = crawler.get_raw_proxies(site)


道生一:元類的__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技能



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章