1. 程式人生 > >神級程序員通過兩句話帶你完全掌握Python最難知識點——元類!

神級程序員通過兩句話帶你完全掌握Python最難知識點——元類!

item 初始化 學習 字符 生命周期 定義 username awl 虛擬

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

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

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

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

在python世界,擁有一個永恒的道,那就是“type”,請記在腦海中,type就是道。如此廣袤無垠的python生態圈,都是由type產生出來的。在給大家分享之前呢,小編推薦一下一個挺不錯的交流寶地,裏面都是一群熱愛並在學習Python的小夥伴們,大幾千了吧,各種各樣的人群都有,特別喜歡看到這種大家一起交流解決難題的氛圍,群資料也上傳了好多,各種大牛解決小白的問題,這個Python群:330637182 歡迎大家進來一起交流討論,一起進步,盡早掌握這門Python語言。

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

道即是 type

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

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

三即是 instance(實例)

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

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

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

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

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

元類——道生一,一生二

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

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

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

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

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

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

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

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

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

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

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

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

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

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

挑戰一:通過元類創建ORM

準備工作,創建一個Field類

它的作用是

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

道生一

它做了以下幾件事

創建一個新的字典mapping

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

將剛剛傳入值為Field類的屬性刪除。

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

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

一生二

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()

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

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

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

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

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

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

試一試抓百度

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

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

批量處理抓取代理

道生一:元類的__new__中,做了四件事:

將“crawl_”開頭的類方法的名稱推入ProxyGetter.CrawlName

將“crawl_”開頭的類方法的本身推入ProxyGetter.CrawlFunc

計算符合“crawl_”開頭的類方法個數

刪除所有符合“crawl_”開頭的類方法

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

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

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

如果對yield用法不熟悉,可以查看:

二生三:創建實例對象crawler

三生萬物:遍歷每一個__CrawlFunc__

在ProxyGetter.__CrawlName__上面,獲取可以抓取的的網址名。

觸發類方法ProxyGetter.get_raw_proxies(site)

遍歷ProxyGetter.CrawlFunc,如果方法名和網址名稱相同的,則執行這一個方法

把每個網址獲取到的代理整合成數組輸出。

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

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

請記住揮動工具的口訣:

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

我是誰,我來自哪裏,我要到哪裏去

在理解元類之前,需要先掌握Python中的類,Python中類的概念與SmallTalk中類的概念相似。

在大多數語言中,類是用來描述如何創建對象的代碼段,這在Python中也是成立的

Python那些事——何為元類?道生一,一生二,二生三,三生萬物元

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

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

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

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

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

道 即是 type

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

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

三 即是 instance(實例)

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

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

Python

1

2

3

4

5

6

7

8

9

10

11

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

classHello():

defsay_hello(self,name=‘world‘):

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

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

hello=Hello()

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

hello.say_hello()

輸出效果:

Hello,world.

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

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

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

deffn(self,name=‘world‘):# 假如我們有一個函數叫fn

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

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

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

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

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

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

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

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

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

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

第二個參數:我從哪裏來

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

第三個參數:我要到哪裏去

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

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

classHello(object){

class 後聲明“我是誰”

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

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

defsay_hello(){

}

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

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

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

元類——道生一,一生二

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

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

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

classSayMetaClass(type):

def__new__(cls,name,bases,attrs):

attrs[‘say_‘+name]=lambdaself,value,saying=name:print(saying+‘,‘+value+‘!‘)

returntype.new(cls,name,bases,attrs)

記住兩點:

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

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

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

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

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

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

12

13

14

15

16

17

18

19

道生一:傳入type

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

創造“天賦”

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

一生二:創建類

classHello(object,metaclass=SayMetaClass):

pass

二生三:創建實列

三生萬物:調用實例方法

hello.say_Hello(‘world!‘)

輸出為

Hello,world!

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

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

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

classSayolala(object,metaclass=SayMetaClass):

s=Sayolala()

s.say_Sayolala(‘japan!‘)

輸出

Sayolala,japan!

也可以說中文

classNihao(object,metaclass=SayMetaClass):

n=Nihao()

n.say_Nihao(‘中華!‘)

Nihao,中華!

再來一個小例子:

道生一

classListMetaclass(type):

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

attrs[‘add‘]=lambdaself,value:self.append(value)

一生二

classMyList(list,metaclass=ListMetaclass):

二生三

L=MyList()

三生萬物

L.add(1)

現在我們打印一下L

print(L)

[1]

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

L2=list()

L2.add(1)

AttributeError:‘list‘objecthas no attribute‘add‘

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

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

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

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

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

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

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

挑戰一:通過元類創建ORM

準備工作,創建一個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名稱。

準備工作:創建StringField和IntergerField

classStringField(Field):

def__init__(self,name):

super(StringField,self).init(name,‘varchar(100)‘)

classIntegerField(Field):

super(IntegerField,self).init(name,‘bigint‘)

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

道生一

classModelMetaclass(type):

ifname==‘Model‘:

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# 假設表名和類名一致

它做了以下幾件事

創建一個新的字典mapping

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

將剛剛傳入值為Field類的屬性刪除。

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

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

一生二

20

21

22

23

24

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創建一個子類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的子類的實例,自動觸發元類的__new__,所以將IntergerField(‘id’)存入__mappings__並刪除這個鍵值對。

二生三、三生萬物

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

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

u.save()

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

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

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

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

通過u.save()模擬數據庫存入操作。這裏我們僅僅做了一下遍歷__mappings__操作,虛擬了sql並打印,在現實情況下是通過輸入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,[email protected])

ARGS:[‘Batman‘,‘iamback‘,12345,‘[email protected]‘]

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

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

挑戰二:網絡代理的爬取

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

請確保已安裝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)

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:\r\n‘,rs)

試一試抓代理

frompyquery importPyQuery aspq

start_url=‘//www.proxy360.cn/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)

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

批量處理抓取代理

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

fromgetpage importget_page

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

classProxyMetaclass(type):

"""

元類,在FreeProxyGetter類中加入

__CrawlFunc__和CrawlFuncCount

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

count=0

attrs[‘CrawlFunc‘]=[]

attrs[‘CrawlName‘]=[]

if‘crawl_‘ink:

attrs[‘CrawlName‘].append(k)

attrs[‘CrawlFunc‘].append(v)

count+=1

forkinattrs[‘CrawlName‘]:

attrs[‘CrawlFuncCount‘]=count

一生二:創建代理獲取類

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=‘//www.66ip.cn/{}.html‘

urls=[start_url.format(page)forpage inrange(1,page_count+1)]

forurl inurls:

print(‘Crawling‘,url)

html=get_page(url)

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):

defcrawl_goubanjia(self):

start_url=‘//www.goubanjia.com/free/gngn/index.shtml‘

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_”開頭的類方法

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

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

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

如果對yield用法不熟悉,可以查看:

廖雪峰的python教程:生成器

二生三:創建實例對象crawler

三生萬物:遍歷每一個__CrawlFunc__

在ProxyGetter.__CrawlName__上面,獲取可以抓取的的網址名。

觸發類方法ProxyGetter.get_raw_proxies(site)

遍歷ProxyGetter.CrawlFunc,如果方法名和網址名稱相同的,則執行這一個方法

把每個網址獲取到的代理整合成數組輸出。

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

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

神級程序員通過兩句話帶你完全掌握Python最難知識點——元類!