python类的前世今生

python类的前世今生

python中关于类的传奇故事可谓不胜枚举,今天就聊一聊关于类的前世今生,话说五百年前…咳咳~言归正传,其实就是类是如何创建的以及怎样自定义一个类的创建过程。关于这个话题,笔者总结了五个部分。

一、metaclass  要自定义一个类的创建过程首先想到的就是通过元类来实现,先上个例子,然后通过这个例子来逐一解释类是如何创建的。

通过元类是如何创建一个类呢,总体分为四步:

  1. 首先确定要使用的元类,请参考第三部分。
  2. 执行元类的__prepare__方法,该方法必须返回一个类字典对象,只要实现了类似字典添加值和查询遍历方法的对象即可。所返回的类字典对象用来存放所创建的类的namespace。该方法是可选的,如果不重写该方法,默认会调用type.__prepare__,返回一个普通的dict,如上边例子所示。
  3. 然后解释器会根据类的定义(比如例子中的类A的定义)计算class body,搜集类的相关信息放到对应参数中:类的名称”A”传入name;父类列表传入bases(bases实际是一个元组);namespace相关的传入nsdict,namespace又包括两部分:一部分是类中所定义的方法或者类变量,比如test_a方法,另一部分是元类预定义或是在计算class body过程中搜集的应该放到这个类的namespace中的信息,这一部分不需要特别关注。
  4. 最后会调用元类的__new__方法来创建并返回这个类对象,name会成为类的__name__属性,bases成为类的__bases__属性,nsdict会被复制一份,该复制品被包装为只读的,并赋值为为类的__dict__属性。如果不重写该方法,则默认会调用type.__new__,如例子中所示。至此,一个热气腾腾的新鲜类就诞生了。(当然解释器还会执行些其他细微的操作,不在讨论范围内,直接忽略)

说明:

  • name,bases,nsdict这三个参数作为位置参数在相应的特殊方法中都是必传的,名字可以随便定义,没有固定要求,但最好定义为便于理解的形式。
  • __new__的第一个位置参数cls指代的是定义__new__方法的类,例子中就代表BaseMeta类。
  • 如果方法声明了可选的关键字参数**kwds,则定义类的时候可以传入关键字参数,会通过例子说明具体如何使用。

了解了类的创建过程,就可以改变某个步骤来实现自定义的需求了,下边通过几个例子简单演示一下:

1、标准例子

例子中将P的namespace定义为一个OrderedDict,并添加了default_ns,从输出中可以看出default_ns是第一个,也就是__prepare__先执行的,然后开始计算class body,然后依次添加到namespace中,一部分是解释器添加的必要属性,另一部分就是类定义体中的。

Tips:pyhton3.3及以上版本输出中才会有__qualname__

2、接收关键字参数的类定义

在定义类的时候,metaclass之后的关键字参数都会被添加到kwds中,有特殊用途的关键字参数可以提取出来,比如例子中的order。

3、小试牛刀,在元类中定义其他特殊方法实现特殊的功能

例子1-4演示了通过元类的方式如何禁止类实例化,当然实现该功能的方法有很多,这里不作过多说明

在元类中定义一个__init__特殊方法来给类做些初始化的操作,其实该方法与__new__很像,唯一不同的就是参数cls,__new__中代表的是元类,而__init__中代表的是被创建的类,也就是执行顺序问题。

二、通过type动态创建类。我们知道type用处最多的是用来获取对象的类型即type(object)。同时type也是所有元类的基类,定义类的时候没有声明元类的话默认则为type,当给它传入三个参数type(name, bases, nsdict)即可用来创建一个类,参数的意义同上所述。

通过type创建类的语法就可以看出,其实类就是元类的实例,从这个角度其实就很好理解某些特殊方法了,比如__new__声明在普通类中是用来创建实例的,而放在元类中则是用来创建类的;__call__声明在普通类中用来控制实例调用,而声明在元类中用来控制类调用;__init__声明在普通类是用来初始化实例,而声明在元类中则可以用来初始化类,就像例子1-4中所示的那样,只是在传参上有所区别。通过代码也可以很直观的验证这一点:

通过输出可以看到X是type的实例,当然type是默认的元类,即使不通过type创建类X,上边的判断也是成立的,我们基于例子1-1来看下:

通过输出可以很明显的看出类就是元类的实例,而且元类可以通过继承的方式传递,所以B也是BaseMeta的实例。

三、通过标准库types.new_class也可以创建类,这里不过多说明了,直接通过源码可以看到原理都是一样的。

有一点需要补充的:创建类的第一步是要确定使用的元类,用语言描述可能比较抽象,所以放到这里借助_calculate_meta这段代码来说明,代码逻辑很清晰,总结来说就是要么使用type,要么使用元类继承关系中最上层的子元类。

其实不论通过哪种方式创建类,原理都是一样的,只是表现形式不同。接下来要介绍的两部分跟类的创建过程没有关系,而是在类创建后再进行改造,即所谓的post-processing。

四、类装饰器。装饰器地球人都知道,类装饰器只是其中的一个应用分支,也比较简单易懂。

很简单的一个例子,有点需要强调下,一旦类被创建并初始化后,其namespace也就是类的__dict__属性就成为只读的了,无法修改赋值。

五、__init_subclass__,这个特殊方法是3.6新增的,当父类定义了这个方法,其子类在创建后会紧接着执行该方法,同样支持可选关键字参数。

关于以上这几点,这里更多的是想阐述清楚其基本原理,至于具体用途还是取决于真实的场景。就笔者遇到的实际情况而言,metaclass和类装饰器用到的比较多,尤其是类装饰器,因为遇到的大部分场景都是post-processing的,类装饰器可以很优雅的完成这一任务。而metaclass也可以实现很多脑洞大开的功能,关于metaclass的实际应用可以参考PythonCookbook中关于元编程的那一章。其他参考资料: 官档customizing-class-creationpep-3115。

说点什么

avatar
  订阅  
提醒