每个中国人,都是天生的元类使用者 学懂元类,你只需要知道两句话:道生一,一生二,二生三,三生万物我是谁?我从哪来里?我要到哪里去? 在python世界,拥有一个永恒的道,那就是type,请记在脑海中,type就是道。如此广袤无垠的python生态圈,都是由type产生出来的。 道生一,一生二,二生三,三生万物。道即是type一即是metaclass(元类,或者叫类生成器)二即是class(类,或者叫实例生成器)三即是instance(实例)万物即是实例的各种属性与方法,我们平常使用python时,调用的就是它们。道和一,是我们今天讨论的命题,而二、三、和万物,则是我们常常使用的类、实例、属性和方法,用helloworld来举例:1 2hr3hr4hr5hr6hr7hr8hr9hr10hr11创建一个Hello类,拥有属性sayhello二的起源 classHello(): defsayhello(self,nameworld): print(Hello,s。name) 从Hello类创建一个实例hello二生三 helloHello() 使用hello调用方法sayhello三生万物 hello。sayhello() 输出效果: 1hrHello,world。 这就是一个标准的二生三,三生万物过程。从类到我们可以调用的方法,用了这两步。 那我们不由自主要问,类从何而来呢?回到代码的第一行。 classHello其实是一个函数的语义化简称,只为了让代码更浅显易懂,它的另一个写法是:1 2hr3hr4deffn(self,nameworld):假如我们有一个函数叫fn print(Hello,s。name) Hellotype(Hello,(object,),dict(sayhellofn))通过type创建Helloclass神秘的道,可以点化一切,这次我们直接从道生出了二 这样的写法,就和之前的ClassHello写法作用完全相同,你可以试试创建实例并调用 Python1 2hr3hr4hr5从Hello类创建一个实例hello二生三,完全一样 helloHello() 使用hello调用方法sayhello三生万物,完全一样 hello。sayhello() 输出效果: 1hrHello,world。调用结果完全一样。 我们回头看一眼最精彩的地方,道直接生出了二: Hellotype(‘Hello’,(object,),dict(sayhellofn)) 这就是道,python世界的起源,你可以为此而惊叹。 注意它的三个参数!暗合人类的三大永恒命题:我是谁,我从哪里来,我要到哪里去。第一个参数:我是谁。在这里,我需要一个区分于其它一切的命名,以上的实例将我命名为Hello第二个参数:我从哪里来 在这里,我需要知道从哪里来,也就是我的父类,以上实例中我的父类是objectpython中一种非常初级的类。第三个参数:我要到哪里去 在这里,我们将需要调用的方法和属性包含到一个字典里,再作为参数传入。以上实例中,我们有一个sayhello方法包装进了字典中。 值得注意的是,三大永恒命题,是一切类,一切实例,甚至一切实例属性与方法都具有的。理所应当,它们的创造者,道和一,即type和元类,也具有这三个参数。但平常,类的三大永恒命题并不作为参数传入,而是以如下方式传入1 2hr3hr4hr5hr6hr7hr8classHello(object){ class后声明我是谁 小括号内声明我来自哪里 中括号内声明我要到哪里去 defsayhello(){ } }造物主,可以直接创造单个的人,但这是一件苦役。造物主会先创造人这一物种,再批量创造具体的个人。并将三大永恒命题,一直传递下去。道可以直接生出二,但它会先生出一,再批量地制造二。type可以直接生成类(class),但也可以先生成元类(metaclass),再使用元类批量定制类(class)。元类道生一,一生二 一般来说,元类均被命名后缀为Metalass。想象一下,我们需要一个可以自动打招呼的元类,它里面的类方法呢,有时需要sayHello,有时需要sayHi,有时又需要saySayolala,有时需要sayNihao。 如果每个内置的sayxxx都需要在类里面声明一次,那将是多么可怕的苦役!不如使用元类来解决问题。 以下是创建一个专门打招呼用的元类代码: Python1 2hr3hr4hr5classSayMetaClass(type): defnew(cls,name,bases,attrs): attrs〔sayname〕lambdaself,value,sayingname:print(saying,value!) returntype。new(cls,name,bases,attrs) 记住两点: 1、元类是由type衍生而出,所以父类需要传入type。【道生一,所以一必须包含道】 2、元类的操作都在new中完成,它的第一个参数是将创建的类,之后的参数即是三大永恒命题:我是谁,我从哪里来,我将到哪里去。它返回的对象也是三大永恒命题,接下来,这三个参数将一直陪伴我们。 在new中,我只进行了一个操作,就是 Python 1hrattrs〔sayname〕lambdaself,value,sayingname:print(saying,value!) 它跟据类的名字,创建了一个类方法。比如我们由元类创建的类叫Hello,那创建时就自动有了一个叫sayHello的类方法,然后又将类的名字Hello作为默认参数saying,传到了方法里面。然后把hello方法调用时的传参作为value传进去,最终打印出来。 那么,一个元类是怎么从创建到调用的呢? 来!一起根据道生一、一生二、二生三、三生万物的准则,走进元类的生命周期吧 Python1 2hr3hr4hr5hr6hr7hr8hr9hr10hr11hr12hr13hr14hr15hr16hr17hr18hr19道生一:传入type classSayMetaClass(type): 传入三大永恒命题:类名称、父类、属性 defnew(cls,name,bases,attrs): 创造天赋 attrs〔sayname〕lambdaself,value,sayingname:print(saying,value!) 传承三大永恒命题:类名称、父类、属性 returntype。new(cls,name,bases,attrs) 一生二:创建类 classHello(object,metaclassSayMetaClass): pass 二生三:创建实列 helloHello() 三生万物:调用实例方法 hello。sayHello(world!) 输出为 1hrHello,world! 注意:通过元类创建的类,第一个参数是父类,第二个参数是metaclass 普通人出生都不会说话,但有的人出生就会打招呼说Hello,你好,sayolala,这就是天赋的力量。它会给我们面向对象的编程省下无数的麻烦。 现在,保持元类不变,我们还可以继续创建Sayolala,Nihao类,如下: Python1 2hr3hr4hr5hr6hr7hr8hr9一生二:创建类 classSayolala(object,metaclassSayMetaClass): pass 二生三:创建实列 sSayolala() 三生万物:调用实例方法 s。saySayolala(japan!) 输出 Python 1hrSayolala,japan! 也可以说中文 Python1 2hr3hr4hr5hr6hr7hr8hr9一生二:创建类 classNihao(object,metaclassSayMetaClass): pass 二生三:创建实列 nNihao() 三生万物:调用实例方法 n。sayNihao(中华!) 输出 Python 1hrNihao,中华! 再来一个小例子: Python1 2hr3hr4hr5hr6hr7hr8hr9hr10hr11hr12hr13hr14hr15hr16道生一 classListMetaclass(type): defnew(cls,name,bases,attrs): 天赋:通过add方法将值绑定 attrs〔add〕lambdaself,value:self。append(value) returntype。new(cls,name,bases,attrs) 一生二 classMyList(list,metaclassListMetaclass): pass 二生三 LMyList() 三生万物 L。add(1) 现在我们打印一下L Python1 2hr3print(L) 〔1〕 而普通的list没有add()方法 Python1 2hr3hr4L2list() L2。add(1) AttributeError:listobjecthasnoattributeadd 太棒了!学到这里,你是不是已经体验到了造物主的乐趣? python世界的一切,尽在掌握。 年轻的造物主,请随我一起开创新世界。 我们选择两个领域,一个是Django的核心思想,ObjectRelationalMapping,即对象关系映射,简称ORM。 这是Django的一大难点,但学完了元类,一切变得清晰。你对Django的理解将更上一层楼! 另一个领域是爬虫领域(黑客领域),一个自动搜索网络上的可用代理,然后换着IP去突破别的人反爬虫限制。 这两项技能非常有用,也非常好玩!挑战一:通过元类创建ORM准备工作,创建一个Field类 Python1 2hr3hr4hr5hr6hr7hr8classField(object): definit(self,name,columntype): self。namename self。columntypecolumntype defstr(self): returnlt;s:s(self。class。name,self。name) 它的作用是 在Field类实例化时将得到两个参数,name和columntype,它们将被绑定为Field的私有属性,如果要将Field转化为字符串时,将返回Field:XXX,XXX是传入的name名称。准备工作:创建StringField和IntergerField Python1 2hr3hr4hr5hr6hr7hr8hr9classStringField(Field): definit(self,name): super(StringField,self)。init(name,varchar(100)) classIntegerField(Field): definit(self,name): super(IntegerField,self)。init(name,bigint) 它的作用是 在StringField,IntegerField实例初始化时,时自动调用父类的初始化方式。道生一 Python1 2hr3hr4hr5hr6hr7hr8hr9hr10hr11hr12hr13hr14hr15hr16classModelMetaclass(type): defnew(cls,name,bases,attrs): ifnameModel: returntype。new(cls,name,bases,attrs) print(Foundmodel:sname) mappingsdict() fork,vinattrs。items(): ifisinstance(v,Field): print(Foundmapping:ss(k,v)) mappings〔k〕v forkinmappings。keys(): attrs。pop(k) attrs〔mappings〕mappings保存属性和列的映射关系 attrs〔table〕name假设表名和类名一致 returntype。new(cls,name,bases,attrs) 它做了以下几件事创建一个新的字典mapping将每一个类的属性,通过。items()遍历其键值对。如果值是Field类,则打印键值,并将这一对键值绑定到mapping字典上。将刚刚传入值为Field类的属性删除。创建一个专门的mappings属性,保存字典mapping。创建一个专门的table属性,保存传入的类的名称。一生二 Python1 2hr3hr4hr5hr6hr7hr8hr9hr10hr11hr12hr13hr14hr15hr16hr17hr18hr19hr20hr21hr22hr23hr24classModel(dict,metaclassModelMetaclass): definit(self,kwarg): super(Model,self)。init(kwarg) defgetattr(self,key): try: returnself〔key〕 exceptKeyError: raiseAttributeError(Modelobjecthasnoattributeskey) defsetattr(self,key,value): self〔key〕value 模拟建表操作 defsave(self): fields〔〕 args〔〕 fork,vinself。mappings。items(): fields。append(v。name) args。append(getattr(self,k,None)) sqlinsertintos(s)values(s)(self。table,,。join(fields),,。join(〔str(i)foriinargs〕)) print(SQL:ssql) print(ARGS:sstr(args))如果从Model创建一个子类User: Python1 2hr3hr4hr5hr6classUser(Model): 定义类的属性到列的映射: idIntegerField(id) nameStringField(username) emailStringField(email) passwordStringField(password) 这时 idIntegerField(‘id’)就会自动解析为: Model。setattr(self,‘id’,IntegerField(‘id’)) 因为IntergerField(‘id’)是Field的子类的实例,自动触发元类的new,所以将IntergerField(‘id’)存入mappings并删除这个键值对。二生三、三生万物 当你初始化一个实例的时候并调用save()方法时候 Python1 2uUser(id12345,nameBatman,emailbatmannasa。org,passwordiamback) u。save() 这时先完成了二生三的过程:先调用Model。setattr,将键值载入私有对象然后调用元类的天赋,ModelMetaclass。new,将Model中的私有对象,只要是Field的实例,都自动存入u。mappings。 接下来完成了三生万物的过程: 通过u。save()模拟数据库存入操作。这里我们仅仅做了一下遍历mappings操作,虚拟了sql并打印,在现实情况下是通过输入sql语句与数据库来运行。 输出结果为 Python1 2hr3hr4hr5hr6hr7Foundmodel:User Foundmapping:name Foundmapping:password Foundmapping:id Foundmapping:email SQL:insertintoUser(username,password,id,email)values(Batman,iamback,12345,batmannasa。org) ARGS:〔Batman,iamback,12345,batmannasa。org〕 年轻的造物主,你已经和我一起体验了由道演化万物的伟大历程,这也是Django中的Model版块核心原理。接下来,请和我一起进行更好玩的爬虫实战(嗯,你现在已经是初级黑客了):网络代理的爬取吧!挑战二:网络代理的爬取准备工作,先爬个页面玩玩 请确保已安装requests和pyquery这两个包。 Python1 2hr3hr4hr5hr6hr7hr8hr9hr10hr11hr12hr13hr14hr15hr16hr17hr18hr19hr20hr21文件:getpage。py importrequests baseheaders{ UserAgent:Mozilla5。0(WindowsNT10。0;Win64;x64)AppleWebKit537。36(KHTML,likeGecko)Chrome54。0。2840。71Safari537。36, AcceptEncoding:gzip,deflate,sdch, AcceptLanguage:zhCN,zh;q0。8 } defgetpage(url): headersdict(baseheaders) print(Getting,url) try: rrequests。get(url,headersheaders) print(Gettingresult,url,r。statuscode) ifr。statuscode200: returnr。text exceptConnectionError: print(CrawlingFailed,url) returnNone 这里,我们利用request包,把百度的源码爬了出来。试一试抓百度 把这一段粘在getpage。py后面,试完删除 Python1 2hr3if(namemain): rsgetpage(https:www。baidu。com) print(result:r,rs)试一试抓代理 把这一段粘在getpage。py后面,试完删除 Python1 2hr3hr4hr5hr6hr7hr8hr9hr10hr11hr12if(namemain): frompyqueryimportPyQueryaspq starturlhttp:www。proxy360。cnRegionChina print(Crawling,starturl) htmlgetpage(starturl) ifhtml: docpq(html) linesdoc(p〔namelistproxyip〕)。items() forlineinlines: ipline。find(。tbBottomLine:nthchild(1))。text() portline。find(。tbBottomLine:nthchild(2))。text() print(ip:port) 接下来进入正题:使用元类批量抓取代理批量处理抓取代理 Python1 2hr3hr4hr5hr6hr7hr8hr9hr10hr11hr12hr13hr14hr15hr16hr17hr18hr19hr20hr21hr22hr23hr24hr25hr26hr27hr28hr29hr30hr31hr32hr33hr34hr35hr36hr37hr38hr39hr40hr41hr42hr43hr44hr45hr46hr47hr48hr49hr50hr51hr52hr53hr54hr55hr56hr57hr58hr59hr60hr61hr62hr63hr64hr65hr66hr67hr68hr69hr70hr71hr72hr73hr74hr75hr76hr77hr78hr79hr80hr81hr82hr83hr84hr85hr86fromgetpageimportgetpage frompyqueryimportPyQueryaspq 道生一:创建抽取代理的metaclass classProxyMetaclass(type): 元类,在FreeProxyGetter类中加入 CrawlFunc和CrawlFuncCount 两个参数,分别表示爬虫函数,和爬虫函数的数量。 defnew(cls,name,bases,attrs): count0 attrs〔CrawlFunc〕〔〕 attrs〔CrawlName〕〔〕 fork,vinattrs。items(): ifcrawlink: attrs〔CrawlName〕。append(k) attrs〔CrawlFunc〕。append(v) count1 forkinattrs〔CrawlName〕: attrs。pop(k) attrs〔CrawlFuncCount〕count returntype。new(cls,name,bases,attrs) 一生二:创建代理获取类 classProxyGetter(object,metaclassProxyMetaclass): defgetrawproxies(self,site): proxies〔〕 print(Site,site) forfuncinself。CrawlFunc: iffunc。namesite: thispageproxiesfunc(self) forproxyinthispageproxies: print(Getting,proxy,from,site) proxies。append(proxy) returnproxies defcrawldaili66(self,pagecount4): starturlhttp:www。66ip。cn{}。html urls〔starturl。format(page)forpageinrange(1,pagecount1)〕 forurlinurls: print(Crawling,url) htmlgetpage(url) ifhtml: docpq(html) trsdoc(。containerboxtabletr:gt(0))。items() fortrintrs: iptr。find(td:nthchild(1))。text() porttr。find(td:nthchild(2))。text() yield:。join(〔ip,port〕) defcrawlproxy360(self): starturlhttp:www。proxy360。cnRegionChina print(Crawling,starturl) htmlgetpage(starturl) ifhtml: docpq(html) linesdoc(p〔namelistproxyip〕)。items() forlineinlines: ipline。find(。tbBottomLine:nthchild(1))。text() portline。find(。tbBottomLine:nthchild(2))。text() yield:。join(〔ip,port〕) defcrawlgoubanjia(self): starturlhttp:www。goubanjia。comfreegngnindex。shtml htmlgetpage(starturl) ifhtml: docpq(html) tdsdoc(td。ip)。items() fortdintds: td。find(p)。remove() yieldtd。text()。replace(,) ifnamemain: 二生三:实例化ProxyGetter crawlerProxyGetter() print(crawler。CrawlName) 三生万物 forsitelabelinrange(crawler。CrawlFuncCount): sitecrawler。CrawlName〔sitelabel〕 myProxiescrawler。getrawproxies(site)道生一:元类的new中,做了四件事:将crawl开头的类方法的名称推入ProxyGetter。CrawlName将crawl开头的类方法的本身推入ProxyGetter。CrawlFunc计算符合crawl开头的类方法个数删除所有符合crawl开头的类方法 怎么样?是不是和之前创建ORM的mappings过程极为相似?一生二:类里面定义了使用pyquery抓取页面元素的方法 分别从三个免费代理网站抓取了页面上显示的全部代理。 如果对yield用法不熟悉,可以查看: 廖雪峰的python教程:生成器二生三:创建实例对象crawler三生万物:遍历每一个CrawlFunc在ProxyGetter。CrawlName上面,获取可以抓取的的网址名。触发类方法ProxyGetter。getrawproxies(site)遍历ProxyGetter。CrawlFunc,如果方法名和网址名称相同的,则执行这一个方法把每个网址获取到的代理整合成数组输出。 那么怎么利用批量代理,冲击别人的网站,套取别人的密码,狂发广告水贴,定时骚扰客户?呃!想啥呢!这些自己悟!如果悟不到,请听下回分解!年轻的造物主,创造世界的工具已经在你手上,请你将它的威力发挥到极致! 请记住挥动工具的口诀:道生一,一生二,二生三,三生万物我是谁,我来自哪里,我要到哪里去Python元类再谈原文出处:TypingQuietly在Python中一切都是对象,类型也是对象;类比类型和实例的概念,类型也必然有自己的类型,十分合理。事实上,类型的类型其实就是术语元类型的意思,python里面所有类型的元类型都是type。默认情况下我们新建一个类,在不手动指定元类型的前提下,type会被指定为元类型,元类型能够控制类型的创建和初始化。一般情况下我们能够通过关键字class来定义一个新的自定义类型,但也能够通过type动态的生成一个新的类型,下面的两种实现方式等价:1234type(SomeKlass,(object,),{foo:2,bar:4})classSomeKlass(object):foo2bar4借助这个例子,我们还能顺便看一下一些关于默认元类type的信息:Python12345someobjectSomeKlass()someobject。classSomeKlass。class新定义一个类型,当类型被解析的时候(比如当作模块引入),元类会负责创建和初始化这个新的类型,背后的逻辑基本上包括:执行类的定义代码收集所有的属性和方法查找类的元类执行Metaclass(name,bases,propertydict),参数分别新建的类的名称,类的父类tuple,收集的属性字典类型创建完成执行初始化在上面描述的过程中,自定义指定元类,然后重写元类的new和init方法,因为在指定元类的情况下,除去收集信息的过程,类型的创建和初始化两个步骤:12MyKlassMyMeta。new(MyMeta,name,bases,dct)MyMeta。init(MyKlass,name,bases,dct)注意这里的表示方式是调用内部方法来表示的,一般来说在重写的new或者init方法的最后都会调用type相应的方法来完成最终的类型创建或初始化工作,这时候也可以使用super关键字动态绑定,或者通过dict属性字典访问是一样的:12super(MyMeta,meta)。new(meta,name,bases,dct)type。new(meta,name,bases,dct)对于init这样的method来说还可以这样调用:1type。dict〔init〕。get(cls)(name,bases,dct)插个题外话,这三种方式的使用其实涉及到python的描述符,使用super和get的时候会进行类型或者实例的绑定,相比直接调用内部方法new和init,由于绑定了selfcls上下文,在传递参数的时候就只用指定除上下文之后的参数了。从网上搜罗了一个例子,将元类创建和初始化新的类型的过程完整展示出来了:1234567891011121314151617181920212223242526272829303132333435classMyMeta(type):defnew(meta,name,bases,dct):thisisnewdocoprintprintAllocatingmemoryforclass,nameprintmetaprintnameprintbasesprintdctreturnsuper(MyMeta,meta)。new(meta,name,bases,dct)returntype。new(meta,name,bases,dct)definit(cls,name,bases,dct):printprintInitializingclass,nameprintclsprintnameprintbasesprintdctsuper(MyMeta,cls)。init(name,bases,dct)type。init(cls,name,bases,dict)type。dict〔init〕。get(cls)(name,bases,dct)classMyKlass(object):metaclassMyMetadeffoo(self,param):passbarattr2mkMyKlass()创建和初始化的过程只会发生一此,也就是会是说new,init只会被执行一次,并且在执行完之前,类型MyKlass其实并没有生成,直接通过名称访问会报错:Python1234567891011AllocatingmemoryforclassMyKlassMyKlass(,){barattr:2,module:main,foo:,metaclass:}liInitializingclassMyKlassMyKlass(,){barattr:2,module:main,foo:,metaclass:}li在使用元类的过程中,有时候我们会重写他的call方法,这个方法的作用其实和new有点相似,只不过这次是控制类型实例对象的生成,因为这个方法恰好和生成类型实例时调用的构造方法吻合。关于重写这个call方法的使用场景,一个比较常用的就是实现单例模式:123456789101112131415161718192021222324252627282930classMetaSingleton(type):instanceNonedefcall(cls,args,kw):thisiscommentprintincallmethodifcls。instanceisNone:cls。instancesuper(MetaSingleton,cls)。call(args,kw)cls。instancetype。dict〔call〕。get(cls)(args,kw)cls。instancetype。call(cls,args,kw)printcls。instancereturncls。instanceclassFoo(object):metaclassMetaSingletondefinit(self,a,b):self。aaself。bbaFoo(1,2)bFoo(3,4)assertaisbprinta。a,a。b,b。a,b。bprinttype(Foo)printFoo。call例子很直接,call方法里面通过判断是否已经有初始化过的实例,没有就仿照正常未指定元类的情况下调用type的call方法(当然这里要么通过superbinding要么手动指定cls上下文),生成一个Foo的实例存储和返回出来。但是有一个注意点是,call方法每次初始化实例对象的时候都会被调用,这也和先前说的控制实例的生成一致:Python12345678(,,)liincallmethodmain。Fooobjectat0x00000000024BFA90incallmethodmain。Fooobjectat0x00000000024BFA901212还有一个需要在意的地方是最后的两行打印日志,Foo类型的元类是Metasingleton(调用new生成类型的时候默认指定元类是第一个参数);Foo的call方法是绑定了Foo(MetaSingleton的实例)实例的MetaSingleton的方法,也就是从另外的方面证实每次初始化Foo类型市里的时候,其实是在调用元类中重写的call方法。元类这个特性大多数情况下确实使用的不多并且需要稍微花点时间来理解,但是需要使用的时候会非常好用,往往能够实现很多优雅的功能,最典型的就是ORM的实现了,只不过会更加的复杂,且pythoning且学习吧,PEACE!参考资料中的链接里面有有几个实际的例子,本文也是学习其中的内容后配合一些其它一些使用经验以及碰到的问题理而成,希望对大家有用。ul