1. 程式人生 > >兩句話掌握python最難知識點——元類

兩句話掌握python最難知識點——元類

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

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

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

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

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

  1. 道 即是 type
  2. 一 即是 metaclass(元類,或者叫類生成器)
  3. 二 即是 class(類,或者叫例項生成器)
  4. 三 即是 instance(例項)
  5. 萬物 即是 例項的各種屬性與方法,我們平常使用python時,呼叫的就是它們。Python入門到實踐學習教程請加群

道和一,是我們今天討論的命題,而二、三、和萬物,則是我們常常使用的類、例項、屬性和方法,用hello world來舉例:219539519零基礎,進階實戰,歡迎加入

# 建立一個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=name: print(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=name: print(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, value: self.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 model: User
Found mapping: name ==> <StringField:username>
Found mapping: password ==> <StringField:password>
Found mapping: id ==> <IntegerField:id>
Found mapping: email ==> <StringField:email>
SQL: insert into User (username,password,id,email) values (Batman,iamback,12345,[email protected])
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用法不熟悉,可以檢視:

二生三:建立例項物件crawler

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

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