[轉]python-元類

轉載於:劉羽衝html

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

千萬不要被所謂「元類是99%的python程序員不會用到的特性」這類的說辭嚇住。由於每一箇中國人,都是天生的元類使用者python

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

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

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

1aa03f13fe494766870f4cfbccf0c586.jpg

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

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

道和一,是咱們今天討論的命題,而2、3、和萬物,則是咱們經常使用的類、實例、屬性和方法,用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()

輸出效果:sql

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寫法做用徹底相同,你能夠試試建立實例並調用segmentfault

# 從Hello類建立一個實例hello ----二生三,徹底同樣 hello = Hello() # 使用hello調用方法say_hello ----三生萬物,徹底同樣 hello.say_hello()

輸出效果:數組

Hello, world. ----調用結果徹底同樣。

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

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)

記住兩點:
一、元類是由「type」衍生而出,因此父類須要傳入type。【道生一,因此一必須包含道

二、元類的操做都在 __new__中完成,它的第一個參數是將建立的類,以後的參數便是三大永恆命題:我是誰,我從哪裏來,我將到哪裏去。 它返回的對象也是三大永恆命題,接下來,這三個參數將一直陪伴咱們。

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

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

它跟據類的名字,建立了一個類方法。好比咱們由元類建立的類叫「Hello」,那建立時就自動有了一個叫「say_Hello」的類方法,而後又將類的名字「Hello」做爲默認參數saying,傳到了方法裏面。而後把hello方法調用時的傳參做爲value傳進去,最終打印出來。

那麼,一個元類是怎麼從建立到調用的呢?
來!一塊兒根據道生1、一輩子2、二生3、三生萬物的準則,走進元類的生命週期吧!

# 道生一:傳入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世界的一切,盡在掌握。

7a4a39374e3e4d24ac16f8e9a48cc2f4_th.jpg

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

咱們選擇兩個領域,一個是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__並刪除這個鍵值對。

二生3、三生萬物

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

u = User(id=12345, name='Batman', email='batman@nasa.org', 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,batman@nasa.org) ARGS: ['Batman', 'iamback', 12345, 'batman@nasa.org']
  • 年輕的造物主,你已經和我一塊兒體驗了由「道」演化「萬物」的偉大曆程,這也是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. 把每一個網址獲取到的代理整合成數組輸出。

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

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

請記住揮動工具的口訣:

    • 道生一,一輩子二,二生三,三生萬物
    • 我是誰,我來自哪裏,我要到哪裏去
相關文章
相關標籤/搜索