千萬不要被所謂“元類是99%的python程序員不會用到的特性”這類的說辭嚇住。因為每個中國人,都是天生的元類使用者
學懂元類,你只需要知道兩句話:
道生一,一生二,二生三,三生萬物
我是誰?我從哪來里?我要到哪里去?
在python世界,擁有一個永恒的道,那就是“type”,請記在腦海中,type就是道。如此廣袤無垠的python生態(tài)圈,都是由type產生出來的。
道生一,一生二,二生三,三生萬物。
道 即是 type
一 即是 metaclass(元類,或者叫類生成器)
二 即是 class(類,或者叫實例生成器)
三 即是 instance(實例)
萬物 即是 實例的各種屬性與方法,我們平常使用python時,調用的就是它們。
道和一,是我們今天討論的命題,而二、三、和萬物,則是我們常常使用的類、實例、屬性和方法,用hello world來舉例:
# 創(chuàng)建一個Hello類,擁有屬性say_hello ----二的起源
classHello():
defsay_hello(self,name='world'):
print('Hello, %s.' % name)
# 從Hello類創(chuàng)建一個實例hello ----二生三
hello = Hello()
# 使用hello調用方法say_hello ----三生萬物
hello.say_hello()
輸出效果:
Hello, world.
這就是一個標準的“二生三,三生萬物”過程。從類到我們可以調用的方法,用了這兩步。
那我們不由自主要問,類從何而來呢?回到代碼的第一行。
class Hello其實是一個函數的“語義化簡稱”,只為了讓代碼更淺顯易懂,它的另一個寫法是:
deffn(self,name='world'): # 假如我們有一個函數叫fn
print('Hello, %s.' % name)
Hello = type('Hello',(object,),dict(say_hello=fn))# 通過type創(chuàng)建Hello class ---- 神秘的“道”,可以點化一切,這次我們直接從“道”生出了“二”
這樣的寫法,就和之前的Class Hello寫法作用完全相同,你可以試試創(chuàng)建實例并調用
# 從Hello類創(chuàng)建一個實例hello ----二生三,完全一樣
hello = Hello()
# 使用hello調用方法say_hello ----三生萬物,完全一樣
hello.say_hello()
輸出效果:
Hello, world. ----調用結果完全一樣。
我們回頭看一眼最精彩的地方,道直接生出了二:
Hello = type(‘Hello’, (object,), dict(say_hello=fn))
這就是“道”,python世界的起源,你可以為此而驚嘆。
注意它的三個參數!暗合人類的三大永恒命題:我是誰,我從哪里來,我要到哪里去。
第一個參數:我是誰。 在這里,我需要一個區(qū)分于其它一切的命名,以上的實例將我命名為“Hello”
第二個參數:我從哪里來。在這里,我需要知道從哪里來,也就是我的“父類”,以上實例中我的父類是“object”——python中一種非常初級的類。
第三個參數:我要到哪里去。在這里,我們將需要調用的方法和屬性包含到一個字典里,再作為參數傳入。以上實例中,我們有一個say_hello方法包裝進了字典中。
值得注意的是,三大永恒命題,是一切類,一切實例,甚至一切實例屬性與方法都具有的。理所應當,它們的“創(chuàng)造者”,道和一,即type和元類,也具有這三個參數。但平常,類的三大永恒命題并不作為參數傳入,而是以如下方式傳入
classHello(object){
# class 后聲明“我是誰”
# 小括號內聲明“我來自哪里”
# 中括號內聲明“我要到哪里去”
defsay_hello(){
}
}
造物主,可以直接創(chuàng)造單個的人,但這是一件苦役。造物主會先創(chuàng)造“人”這一物種,再批量創(chuàng)造具體的個人。并將三大永恒命題,一直傳遞下去。
“道”可以直接生出“二”,但它會先生出“一”,再批量地制造“二”。
type可以直接生成類(class),但也可以先生成元類(metaclass),再使用元類批量定制類(class)。
元類——道生一,一生二
一般來說,元類均被命名后綴為Metalass。想象一下,我們需要一個可以自動打招呼的元類,它里面的類方法呢,有時需要say_Hello,有時需要say_Hi,有時又需要say_Sayolala,有時需要say_Nihao。
如果每個內置的say_xxx都需要在類里面聲明一次,那將是多么可怕的苦役! 不如使用元類來解決問題。
以下是創(chuàng)建一個專門“打招呼”用的元類代碼:
classSayMetaClass(type):
def__new__(cls,name,bases,attrs):
attrs['say_'+name] = lambdaself,value,saying=name: print(saying+','+value+'!')
returntype.__new__(cls,name,bases,attrs)
記住兩點:
元類是由“type”衍生而出,所以父類需要傳入type?!镜郎?,所以一必須包含道】
元類的操作都在 __new__中完成,它的第一個參數是將創(chuàng)建的類,之后的參數即是三大永恒命題:我是誰,我從哪里來,我將到哪里去。 它返回的對象也是三大永恒命題,接下來,這三個參數將一直陪伴我們。
在__new__中,我只進行了一個操作,就是
attrs['say_'+name] = lambda self,value,saying=name: print(saying+','+value+'!')
它跟據類的名字,創(chuàng)建了一個類方法。比如我們由元類創(chuàng)建的類叫“Hello”,那創(chuàng)建時就自動有了一個叫“say_Hello”的類方法,然后又將類的名字“Hello”作為默認參數saying,傳到了方法里面。然后把hello方法調用時的傳參作為value傳進去,最終打印出來。
那么,一個元類是怎么從創(chuàng)建到調用的呢?
來!一起根據道生一、一生二、二生三、三生萬物的準則,走進元類的生命周期吧!
# 道生一:傳入type
classSayMetaClass(type):
# 傳入三大永恒命題:類名稱、父類、屬性
def__new__(cls,name,bases,attrs):
# 創(chuàng)造“天賦”
attrs['say_'+name] = lambdaself,value,saying=name: print(saying+','+value+'!')
# 傳承三大永恒命題:類名稱、父類、屬性
returntype.__new__(cls,name,bases,attrs)
# 一生二:創(chuàng)建類
classHello(object,metaclass=SayMetaClass):
pass
# 二生三:創(chuàng)建實列
hello = Hello()
# 三生萬物:調用實例方法
hello.say_Hello('world!')
輸出為
Hello, world!
注意:通過元類創(chuàng)建的類,第一個參數是父類,第二個參數是metaclass
普通人出生都不會說話,但有的人出生就會打招呼說“Hello”,“你好”,“sayolala”,這就是天賦的力量。它會給我們面向對象的編程省下無數的麻煩。
現(xiàn)在,保持元類不變,我們還可以繼續(xù)創(chuàng)建Sayolala, Nihao類,如下:
# 一生二:創(chuàng)建類
classSayolala(object,metaclass=SayMetaClass):
pass
# 二生三:創(chuàng)建實列
s = Sayolala()
# 三生萬物:調用實例方法
s.say_Sayolala('japan!')
輸出
Sayolala, japan!
也可以說中文
# 一生二:創(chuàng)建類
classNihao(object,metaclass=SayMetaClass):
pass
# 二生三:創(chuàng)建實列
n = Nihao()
# 三生萬物:調用實例方法
n.say_Nihao('中華!')
輸出
Nihao, 中華!
再來一個小例子:
# 道生一
classListMetaclass(type):
def__new__(cls,name,bases,attrs):
# 天賦:通過add方法將值綁定
attrs['add'] = lambdaself,value: self.append(value)
returntype.__new__(cls,name,bases,attrs)
# 一生二
classMyList(list,metaclass=ListMetaclass):
pass
# 二生三
L = MyList()
# 三生萬物
L.add(1)
現(xiàn)在我們打印一下L
print(L)
>>> [1]
而普通的list沒有add()方法
L2 = list()
L2.add(1)
>>>AttributeError: 'list'objecthas no attribute'add'
太棒了!學到這里,你是不是已經體驗到了造物主的樂趣?
python世界的一切,盡在掌握。
年輕的造物主,請隨我一起開創(chuàng)新世界。
我們選擇兩個領域,一個是Django的核心思想,“Object Relational Mapping”,即對象-關系映射,簡稱ORM。
這是Django的一大難點,但學完了元類,一切變得清晰。你對Django的理解將更上一層樓!
另一個領域是爬蟲領域(黑客領域),一個自動搜索網絡上的可用代理,然后換著IP去突破別的人反爬蟲限制。
這兩項技能非常有用,也非常好玩!
挑戰(zhàn)一:通過元類創(chuàng)建ORM
準備工作,創(chuàng)建一個Field類
classField(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名稱。
準備工作:創(chuàng)建StringField和IntergerField
classStringField(Field):
def__init__(self,name):
super(StringField,self).__init__(name,'varchar(100)')
classIntegerField(Field):
def__init__(self,name):
super(IntegerField,self).__init__(name,'bigint')
它的作用是
在StringField,IntegerField實例初始化時,時自動調用父類的初始化方式。
道生一
classModelMetaclass(type):
def__new__(cls,name,bases,attrs):
ifname=='Model':
returntype.__new__(cls,name,bases,attrs)
print('Found model: %s' % name)
mappings = dict()
fork,vinattrs.items():
ifisinstance(v,Field):
print('Found mapping: %s ==> %s' % (k,v))
mappings[k] = v
forkinmappings.keys():
attrs.pop(k)
attrs['__mappings__'] = mappings# 保存屬性和列的映射關系
attrs['__table__'] = name# 假設表名和類名一致
returntype.__new__(cls,name,bases,attrs)
它做了以下幾件事
創(chuàng)建一個新的字典mapping
將每一個類的屬性,通過.items()遍歷其鍵值對。如果值是Field類,則打印鍵值,并將這一對鍵值綁定到mapping字典上。
將剛剛傳入值為Field類的屬性刪除。
創(chuàng)建一個專門的__mappings__屬性,保存字典mapping。
創(chuàng)建一個專門的__table__屬性,保存?zhèn)魅氲念惖拿Q。
一生二
classModel(dict,metaclass=ModelMetaclass):
def__init__(self, **kwarg):
super(Model,self).__init__(**kwarg)
def__getattr__(self,key):
try:
returnself[key]
exceptKeyError:
raiseAttributeError("'Model' object has no attribute '%s'" % key)
def__setattr__(self,key,value):
self[key] = value
# 模擬建表操作
defsave(self):
fields = []
args = []
fork,vinself.__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)foriinargs]))
print('SQL: %s' % sql)
print('ARGS: %s' % str(args))
如果從Model創(chuàng)建一個子類User:
classUser(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的子類的實例,自動觸發(fā)元類的__new__,所以將IntergerField(‘id’)存入__mappings__并刪除這個鍵值對。
二生三、三生萬物
當你初始化一個實例的時候并調用save()方法時候
u = User(id=12345,name='Batman',email='batman@nasa.org',password='iamback')
u.save()
這時先完成了二生三的過程:
先調用Model.__setattr__,將鍵值載入私有對象
然后調用元類的“天賦”,ModelMetaclass.__new__,將Model中的私有對象,只要是Field的實例,都自動存入u.__mappings__。
接下來完成了三生萬物的過程:
通過u.save()模擬數據庫存入操作。這里我們僅僅做了一下遍歷__mappings__操作,虛擬了sql并打印,在現(xiàn)實情況下是通過輸入sql語句與數據庫來運行。
輸出結果為
Found model: User
Found mapping: name ==>
Found mapping: password ==>
Found mapping: id ==>
Found mapping: 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版塊核心原理。
接下來,請和我一起進行更好玩的爬蟲實戰(zhàn)(嗯,你現(xiàn)在已經是初級黑客了):網絡代理的爬取吧!
挑戰(zhàn)二:網絡代理的爬取
準備工作,先爬個頁面玩玩
請確保已安裝requests和pyquery這兩個包。
# 文件:get_page.py
importrequests
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'
}
defget_page(url):
headers = dict(base_headers)
print('Getting',url)
try:
r = requests.get(url,headers=headers)
print('Getting result',url,r.status_code)
ifr.status_code == 200:
returnr.text
exceptConnectionError:
print('Crawling Failed',url)
returnNone
這里,我們利用request包,把百度的源碼爬了出來。
試一試抓百度
把這一段粘在get_page.py后面,試完刪除
if(__name__ == '__main__'):
rs = get_page('https://www.baidu.com')
print('result:',rs)
試一試抓代理
把這一段粘在get_page.py后面,試完刪除
if(__name__ == '__main__'):
frompyquery importPyQuery aspq
start_url = 'http://www.proxy#/Region/China'
print('Crawling',start_url)
html = get_page(start_url)
ifhtml:
doc = pq(html)
lines = doc('div[name="list_proxy_ip"]').items()
forline inlines:
ip = line.find('.tbBottomLine:nth-child(1)').text()
port = line.find('.tbBottomLine:nth-child(2)').text()
print(ip+':'+port)
接下來進入正題:使用元類批量抓取代理
批量處理抓取代理
fromgetpage importget_page
frompyquery importPyQuery aspq
# 道生一:創(chuàng)建抽取代理的metaclass
classProxyMetaclass(type):
"""
元類,在FreeProxyGetter類中加入
__CrawlFunc__和__CrawlFuncCount__
兩個參數,分別表示爬蟲函數,和爬蟲函數的數量。
"""
def__new__(cls,name,bases,attrs):
count = 0
attrs['__CrawlFunc__'] = []
attrs['__CrawlName__'] = []
fork,vinattrs.items():
if'crawl_'ink:
attrs['__CrawlName__'].append(k)
attrs['__CrawlFunc__'].append(v)
count += 1
forkinattrs['__CrawlName__']:
attrs.pop(k)
attrs['__CrawlFuncCount__'] = count
returntype.__new__(cls,name,bases,attrs)
# 一生二:創(chuàng)建代理獲取類
classProxyGetter(object,metaclass=ProxyMetaclass):
defget_raw_proxies(self,site):
proxies = []
print('Site',site)
forfunc inself.__CrawlFunc__:
iffunc.__name__==site:
this_page_proxies = func(self)
forproxy inthis_page_proxies:
print('Getting',proxy,'from',site)
proxies.append(proxy)
returnproxies
defcrawl_daili66(self,page_count=4):
start_url = 'http://www.66ip.cn/{}.html'
urls = [start_url.format(page)forpage inrange(1,page_count + 1)]
forurl inurls:
print('Crawling',url)
html = get_page(url)
ifhtml:
doc = pq(html)
trs = doc('.containerbox table tr:gt(0)').items()
fortr intrs:
ip = tr.find('td:nth-child(1)').text()
port = tr.find('td:nth-child(2)').text()
yield':'.join([ip,port])
defcrawl_proxy360(self):
start_url = 'http://www.proxy#/Region/China'
print('Crawling',start_url)
html = get_page(start_url)
ifhtml:
doc = pq(html)
lines = doc('div[name="list_proxy_ip"]').items()
forline inlines:
ip = line.find('.tbBottomLine:nth-child(1)').text()
port = line.find('.tbBottomLine:nth-child(2)').text()
yield':'.join([ip,port])
defcrawl_goubanjia(self):
start_url = 'http://www.goubanjia.com/free/gngn/index.shtml'
html = get_page(start_url)
ifhtml:
doc = pq(html)
tds = doc('td.ip').items()
fortd intds:
td.find('p').remove()
yieldtd.text().replace(' ','')
if__name__ == '__main__':
# 二生三:實例化ProxyGetter
crawler = ProxyGetter()
print(crawler.__CrawlName__)
# 三生萬物
forsite_label inrange(crawler.__CrawlFuncCount__):
site = crawler.__CrawlName__[site_label]
myProxies = crawler.get_raw_proxies(site)
道生一:元類的__new__中,做了四件事:
將“crawl_”開頭的類方法的名稱推入ProxyGetter.__CrawlName__
將“crawl_”開頭的類方法的本身推入ProxyGetter.__CrawlFunc__
計算符合“crawl_”開頭的類方法個數
刪除所有符合“crawl_”開頭的類方法
怎么樣?是不是和之前創(chuàng)建ORM的__mappings__過程極為相似?
一生二:類里面定義了使用pyquery抓取頁面元素的方法
分別從三個免費代理網站抓取了頁面上顯示的全部代理。
如果對yield用法不熟悉,可以查看:廖雪峰的python教程:生成器
二生三:創(chuàng)建實例對象crawler
略
三生萬物:遍歷每一個__CrawlFunc__
在ProxyGetter.__CrawlName__上面,獲取可以抓取的的網址名。
觸發(fā)類方法ProxyGetter.get_raw_proxies(site)
遍歷ProxyGetter.__CrawlFunc__,如果方法名和網址名稱相同的,則執(zhí)行這一個方法
把每個網址獲取到的代理整合成數組輸出。
那么。。。怎么利用批量代理,沖擊別人的網站,套取別人的密碼,狂發(fā)廣告水貼,定時騷擾客戶? 呃!想啥呢!這些自己悟!如果悟不到,請聽下回分解!
年輕的造物主,創(chuàng)造世界的工具已經在你手上,請你將它的威力發(fā)揮到極致!
請記住揮動工具的口訣:
道生一,一生二,二生三,三生萬物
我是誰,我來自哪里,我要到哪里去
-
Type
+關注
關注
1文章
137瀏覽量
22704 -
python
+關注
關注
56文章
4797瀏覽量
84786 -
Django
+關注
關注
0文章
44瀏覽量
10396
原文標題:兩句話掌握 Python 最難知識點:元類
文章出處:【微信號:magedu-Linux,微信公眾號:馬哥Linux運維】歡迎添加關注!文章轉載請注明出處。
發(fā)布評論請先 登錄
相關推薦
評論