百科狗-知识改变命运!
--

Python之metaclass的原理和用法

乐乐1年前 (2023-11-21)阅读数 14#技术干货
文章标签实例

metaclass

metaclass的英文直译过来就是元类,这既是一个概念也可以认为是Python当中的一个关键字,不管怎么理解,对它的内核含义并没有什么影响。我们可以不必纠结,就认为它是类的类的意思即可。在这个用法当中,支持我们自己定义一个类,使得它是后面某一个类的元类。

之前使用type动态创建类的时候,我们传入了类名,和父类的tuple以及属性的dict。在metaclass用法当中,其实核心相差不大,只是表现形式有所区别。我们来看一个例子即可:

Python之metaclass的原理和用法

classAddInfo(type):

def__new__(cls,name,bases,attr):

attr['info']='addbymetaclass'

returnsuper().__new__(cls,name,bases,attr)

classTest(metaclass=AddInfo):

pass

在这个例子当中,我们首先创建了一个类叫做AddInfo,这是我们定义的一个元类。由于我们希望通过它来实现元类的功能,所以我们需要它继承type类。我们在之前的文章当中说过,在Python面向对象当中,所有的类的根本来源就是type。也就是说Python当中的每一个类都是type的实例。

我们在这个类当中重载了__new__方法,我们在__new__方法当中传入了四个参数。眼尖一点的小伙伴一定已经看出来了,这个函数的四个参数,正是我们调用type创建类的时候传入的参数。其实我们调用type的方法来创建类的时候,就是调用的__new__这个函数完成的,这两种写法对应的逻辑是完全一样的。

我们之后又创建了一个新的类叫做Test,这个当中没有任何逻辑,直接pass。但是我们在创建类的时候指定了一个参数metaclass=AddInfo,这里这个参数其实就是指定的这个类的元类,也就是指定这个类的创建逻辑。虽然我们用代码写了类的定义,但是在实际执行的时候,这个类是以metaclass为元类创建的。

根据上面的逻辑,我们可以知道,Test类在创建的时候就被赋予了类属性info。我们可以验证一下:

拓展类功能

上面这段就是元类的基本用法了,其实本质上和我们之前介绍的type的动态类创建是一样的,只不过展现的形式不同。那么我们就有一个问题要问了,我们使用元类究竟能够做什么呢?

这里有一个经典的例子,我们都知道Python原生的list是没有'add'这个方法的。假设我们习惯了Java当中list的使用,习惯用add来为它添加元素。我们希望创建一个新的类,在这个新的类当中,我们可以通过add来添加函数。通过元类可以很方便地使用这一点。

classListMeta(type):

def__new__(cls,name,bases,attrs):

#在类属性当中添加了add函数

#通过匿名函数映射到append函数上

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

returnsuper().__new__(cls,name,bases,attrs)

classMyList(list,metaclass=ListMeta):

pass

我们首先是定义了一个叫做ListMeta的元类,在这个元类当中我们给类添加了一个属性叫做add。它只是包装了一下而已,底层是通过append方法实现的。我们来实验一下:

从结果来看也没什么问题,我们成功通过调用add方法往list当中插入了元素。这里藏着一个小细节,我们在ListMeta当中为attrs添加了一个名叫'add'的属性。这个属性是添加给类的,而不是类初始化出来的实例的。所以如果我们print出MyList这个类当中的所有属性,也能看到add的存在。

如果我们直接去通过MyList去访问add方法的话会引起报错,因为我们实现add这个方法逻辑的匿名函数限制了需要传入两个参数。第一个参数是实例的对象self,第二个参数才是添加的元素value。如果我们通过MyList的类属性去访问它的话会触发一个错误,因为缺少了一个参数。因为类当中的属性实例也是可以调用的,并且Python会在参数前面自动添加self这个参数,就刚好满足了要求。

搞明白了这些我们只是解决了可能性问题,我们明白了元类可以实现这样的操作,但没有解决我们为什么必须要使用元类呢?就拿刚才的例子来说,我们完全可以继承list这个类,然后在其中再开发我们想要的方法,为什么一定要使用元类呢?

就刚才这个场景来说,的确,我们是找不出任何理由的。完全没有理由不使用继承,而非要用元类。但是在有些场景和有些问题当中,我们必须要使用元类不可。就是涉及类属性变更和类创建的时候,我们来看下面这个例子。

控制实例的创建

还记得我们上篇文章介绍的工厂设计模式的例子吗?就是我们可以通过参数来得到不同类的实例。

我们创建了三种游戏的类和一个工厂类,我们重载了工厂类的__new__函数。使得我们可以根据实例化时传入的参数返回不同类型的实例。

classLast_of_us:

defplay(self):

print('theLastOfUsisreallyfunny')

classUncharted:

defplay(self):

print('theUnchartedisreallyfunny')

classPSGame:

defplay(self):

print('PShasmanygames')

classGameFactory:

games={'last_of_us':Last_of_us,'uncharted':Uncharted}

def__new__(cls,name):

ifnameincls.games:

returncls.games[name]()

else:

returnPSGame()

uncharted=GameFactory('uncharted')

last_of_us=GameFactory('last_of_us')

假设这个需求完成得很好顺利上线了,但是运行了一段时间之后我们发现下游有的时候为了偷懒会不通过工厂类来创建实例,而是直接对需要的类做实例化。原本这没有问题,但是现在产品想要在工厂类当中加上一些埋点,统计出访问我们工厂的访问量。所以我们需要限制这些游戏类不能直接实例化,必须要通过工厂返回实例。

那么这个功能我们怎么实现呢?

我们分析一下问题就会发现,这一次不是需要我们在创建实例的时候做动态的添加,而是直接限制一些类不允许直接调用进行创建。限制的方法比较常用的一种就是抛出异常,所以我们希望可以给这些类加上一个逻辑,实例化类的时候传入一个参数,表明是否是通过工厂类进行的,如果不是,则抛出异常。

这里,我们需要用到另外一个默认函数,叫做__call__,它是允许将类实例当做函数调用。我们通过类名来实例化,其实也是一个调用逻辑。这个__call__的逻辑并不难写,我们随手就来:

def__call__(self,*args,**kwargs):

iflen(args)==0orargs[0]!='factory':

raiseTypeError("Can'tinstantiatedirectly")

但问题是这个__call__函数并不能直接加在类当中,因为它的应用范围是实例,而不是类。而我们希望的是在创建实例的时候进行限制,而不是对调用实例的时候进行限制,所以这段逻辑只能通过元类实现。

我们直接创建类的时候就会触发异常,因为不是通过工厂创建的。我们这里判断是否是工厂创建的逻辑简化掉了,只是通过一个简单的字符串来进行的判断,实际上会用一些更加复杂的逻辑,这不是本文的重点,我们了解即可。

整体运行的逻辑和我们设想的一样,说明这样实现是正确的。

总结

我们日常开发当中用到元类的情况非常罕见,一般都是在一些高端开发的场景当中。比如说开发一些框架或者是中间件,为了方便下游的使用,需要创建一些关于类属性的动态逻辑,才会用到元类。对于普通开发者而言,如果你无法理解元类的含义以及应用,也没有关系,使用频率非常低。

另外,元类的概念和动态类、动态语言的概念有关,Python语言的动态特性很多正是通过这一点体现的。所以随着我们对于Python动态特性理解的加深,理解元类也会变得越来越容易,同样也会理解越来越深刻。如果我们把Python的元类和装饰器做一个类比的话,会发现两者的核心逻辑是很类似的。本质上都是在原有的逻辑之外封装新的逻辑,只不过装饰器针对的是一段逻辑,而元类针对的是类的属性和创建过程。

以上内容为大家介绍了Python之metaclass的原理和用法,希望对大家有所帮助,如果想要了解更多Python相关知识,请关注IT培训机构:开发教育。

鹏仔微信 15129739599 鹏仔QQ344225443 鹏仔前端 pjxi.com 共享博客 sharedbk.com

免责声明:我们致力于保护作者版权,注重分享,当前被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理!邮箱:344225443@qq.com)

图片声明:本站部分配图来自网络。本站只作为美观性配图使用,无任何非法侵犯第三方意图,一切解释权归图片著作权方,本站不承担任何责任。如有恶意碰瓷者,必当奉陪到底严惩不贷!

内容声明:本文中引用的各种信息及资料(包括但不限于文字、数据、图表及超链接等)均来源于该信息及资料的相关主体(包括但不限于公司、媒体、协会等机构)的官方网站或公开发表的信息。部分内容参考包括:(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供参考使用,不准确地方联系删除处理!本站为非盈利性质站点,本着为中国教育事业出一份力,发布内容不收取任何费用也不接任何广告!)