本文始发于我的公众号:TechFlow,原创不易,求个关注web
今天是Python专题第18篇文章,咱们来继续聊聊Python当中的元类。面试
在上上篇文章当中咱们介绍了type元类的用法,在上一篇文章当中咱们介绍了__new__函数与__init__函数的区别,以及它在一些设计模式当中的运用。这篇文章咱们来看看metacalss与元类,以及__new__函数在元类当中的使用。设计模式
上一篇文章很是重要,是这一篇的基础,若是错过了上篇文章,推荐回顾一下:app
Python面试常见问题,__init__是构造函数吗?框架
metaclass的英文直译过来就是元类,这既是一个概念也能够认为是Python当中的一个关键字,无论怎么理解,对它的内核含义并无什么影响。咱们能够没必要纠结,就认为它是类的类的意思便可。在这个用法当中,支持咱们本身定义一个类,使得它是后面某一个类的元类。编辑器
以前使用type动态建立类的时候,咱们传入了类名,和父类的tuple以及属性的dict。在metaclass用法当中,其实核心相差不大,只是表现形式有所区别。咱们来看一个例子便可:函数
class AddInfo(type):
def __new__(cls, name, bases, attr): attr['info'] = 'add by metaclass' return super().__new__(cls, name, bases, attr) class Test(metaclass=AddInfo): pass 复制代码
在这个例子当中,咱们首先建立了一个类叫作AddInfo,这是咱们定义的一个元类。因为咱们但愿经过它来实现元类的功能,因此咱们须要它继承type类。咱们在以前的文章当中说过,在Python面向对象当中,全部的类的根原本源就是type。也就是说Python当中的每个类都是type的实例。url
咱们在这个类当中重载了__new__方法,咱们在__new__方法当中传入了四个参数。眼尖一点的小伙伴必定已经看出来了,这个函数的四个参数,正是咱们调用type建立类的时候传入的参数。其实咱们调用type的方法来建立类的时候,就是调用的__new__这个函数完成的,这两种写法对应的逻辑是彻底同样的。spa
咱们以后又建立了一个新的类叫作Test,这个当中没有任何逻辑,直接pass。可是咱们在建立类的时候指定了一个参数metaclass=AddInfo,这里这个参数其实就是指定的这个类的元类,也就是指定这个类的建立逻辑。虽然咱们用代码写了类的定义,可是在实际执行的时候,这个类是以metaclass为元类建立的。设计
根据上面的逻辑,咱们能够知道,Test类在建立的时候就被赋予了类属性info。咱们能够验证一下:
上面这段就是元类的基本用法了,其实本质上和咱们以前介绍的type的动态类建立是同样的,只不过展示的形式不一样。那么咱们就有一个问题要问了,咱们使用元类究竟可以作什么呢?
这里有一个经典的例子,咱们都知道Python原生的list是没有'add'这个方法的。假设咱们习惯了Java当中list的使用,习惯用add来为它添加元素。咱们但愿建立一个新的类,在这个新的类当中,咱们能够经过add来添加函数。经过元类能够很方便地使用这一点。
class ListMeta(type):
def __new__(cls, name, bases, attrs): # 在类属性当中添加了add函数 # 经过匿名函数映射到append函数上 attrs['add'] = lambda self, value: self.append(value) return super().__new__(cls, name, bases, attrs) class MyList(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__函数。使得咱们能够根据实例化时传入的参数返回不一样类型的实例。
class Last_of_us:
def play(self): print('the Last Of Us is really funny') class Uncharted: def play(self): print('the Uncharted is really funny') class PSGame: def play(self): print('PS has many games') class GameFactory: games = {'last_of_us': Last_of_us, 'uncharted': Uncharted} def __new__(cls, name): if name in cls.games: return cls.games[name]() else: return PSGame() uncharted = GameFactory('uncharted') last_of_us = GameFactory('last_of_us') 复制代码
假设这个需求完成得很好顺利上线了,可是运行了一段时间以后咱们发现下游有的时候为了偷懒会不经过工厂类来建立实例,而是直接对须要的类作实例化。本来这没有问题,可是如今产品想要在工厂类当中加上一些埋点,统计出访问咱们工厂的访问量。因此咱们须要限制这些游戏类不能直接实例化,必需要经过工厂返回实例。
那么这个功能咱们怎么实现呢?
咱们分析一下问题就会发现,这一次不是须要咱们在建立实例的时候作动态的添加,而是直接限制一些类不容许直接调用进行建立。限制的方法比较经常使用的一种就是抛出异常,因此咱们但愿能够给这些类加上一个逻辑,实例化类的时候传入一个参数,代表是不是经过工厂类进行的,若是不是,则抛出异常。
这里,咱们须要用到另一个默认函数,叫作__call__,它是容许将类实例当作函数调用。咱们经过类名来实例化,其实也是一个调用逻辑。这个__call__的逻辑并不难写,咱们随手就来:
def __call__(self, *args, **kwargs):
if len(args) == 0 or args[0] != 'factory': raise TypeError("Can't instantiate directly") 复制代码
但问题是这个__call__函数并不能直接加在类当中,由于它的应用范围是实例,而不是类。而咱们但愿的是在建立实例的时候进行限制,而不是对调用实例的时候进行限制,因此这段逻辑只能经过元类实现。
咱们直接建立类的时候就会触发异常,由于不是经过工厂建立的。咱们这里判断是不是工厂建立的逻辑简化掉了,只是经过一个简单的字符串来进行的判断,实际上会用一些更加复杂的逻辑,这不是本文的重点,咱们了解便可。
总体运行的逻辑和咱们设想的同样,说明这样实现是正确的。
咱们平常开发当中用到元类的状况很是罕见,通常都是在一些高端开发的场景当中。好比说开发一些框架或者是中间件,为了方便下游的使用,须要建立一些关于类属性的动态逻辑,才会用到元类。对于普通开发者而言,若是你没法理解元类的含义以及应用,也没有关系,使用频率很是低。
另外,元类的概念和动态类、动态语言的概念有关,Python语言的动态特性不少正是经过这一点体现的。因此随着咱们对于Python动态特性理解的加深,理解元类也会变得愈来愈容易,一样也会理解愈来愈深入。若是咱们把Python的元类和装饰器作一个类比的话,会发现二者的核心逻辑是很相似的。本质上都是在原有的逻辑以外封装新的逻辑,只不过装饰器针对的是一段逻辑,而元类针对的是类的属性和建立过程。
仔细思考,我相信必定会有灵光乍现的感受。
今天的文章就到这里,若是喜欢本文,能够的话,请点个关注,给我一点鼓励,也方便获取更多文章。
本文使用 mdnice 排版