流畅的python笔记(二十一)类元编程

目录

前言

一、类工厂函数

二、定制描述符的类装饰器

三、导入时和运行时比较

场景练习

场景1解答

场景2解答

四、元类基础知识

场景练习 

场景3解答

场景4解答

五、定制描述符的元类

六、元类的特殊方法__prepare__

七、类作为对象


前言

类元编程是指在运行时创建或定制类的技艺。python中类是一等对象,因此任何时候都可以使用函数新建类,而无需使用class关键字。类装饰器也是函数,不过能够审查、修改甚至把被装饰的类替换成其他类。

        最后,元类是类元编程最高级的工具:使用元类可以创建具有某种特质的全新类种,比如抽象基类。

        除非开发框架,否则不要编写元类

一、类工厂函数

最常见的类工厂函数比如collections.namedtuple,我们把一个类名和几个属性名传给这个函数,它就会创建一个tuple的子类,其中元素可以通过名称获取,即具名元组,也可以认为是不可变的字典。

        假设编写一个宠物店应用程序,创建一个狗类:

我们可以看到,在编写上面代码的时候,各个字段的名称都出现了三次,参数列表中一次,=号两边各一次,这样很麻烦,且字符串表示形式也不好。

参考collections.namedtuple,下面我们创建一个record_factory函数,即时创建简单的类(比如Dog),record_factory函数的用法如下所示:

  1. 工厂函数record_factory的签名与namedtuple类似:先写类名,后面跟着写在一个字符串里的多个属性名,使用空格或逗号分开。
  2. 友好的字符串表示形式。
  3. 实例是可迭代对象,因此赋值时可以使用可迭代对象拆包。
  4. 传给format函数时也可以拆包。
  5. 实例是可变的对象。
  6. 新建的类继承自object,与我们的工厂函数record_factory没有关系。

  1.  这里体现鸭子类型:尝试在逗号或空格处拆分field_names,如果失败,那么假定field_names本就是可迭代的对象,一个迭代元素就对应一个属性名。
  2. 使用属性名构建元素,这将成为新建类的__slots__属性。此外,这么做还设定了拆包和字符串表示形式中各字段的顺序。
  3. 这个函数将成为新建类的__init__函数。参数有位置参数和关键字参数。
  4. 实现__iter__函数,把类的实例变成可迭代的对象。按照__slots__设定的顺序产出字段值。
  5. 迭代__slots__和self,生成友好的字符串表示形式。
  6. 组建类属性字典。
  7. 调用type构造方法,构建新类,然后将其返回,新类的名称是cls_name参数的值,唯一的直接超类是object,有__slots__,__init__、__iter__、__repr__四个类属性,其中后三个是实例方法。

把三个参数传给type是动态创建类的常用方式。通常我们把type当成函数用,type(my_object)获取对象所属的类,作用与my_object.__class__相同。然而type实际上是一个类,当成类使用时,传入三个参数可以新建一个类(type的三个参数分别是name、bases和dict,dict是一个字典,指定新类的属性名和值)

上述代码作用与下述代码相同:

type类的实例是类,比如这里的MyClass类。

        用record_factory函数创建的类,其实例有个局限------不能序列化,即不能使用pickle模块中的dump/load函数处理。此问题暂不解决。

二、定制描述符的类装饰器

在本书上一章节即第二十章处,LineItem示例还有问题尚待解决:储存属性的名称不具有描述性。即属性(如weight)的值存储在名为_Quantity#0的实例属性中,这样的名称不便调试。从托管实例中的描述符实例的storage_name属性能获取储存属性的名称:

如果储存属性的名称中能包含托管属性的名称更好:

上一章节中,之所以储存属性用了_Quantity#0泽中奇怪的名字,是因为实例化描述符时还无法得知托管属性的名称(比如weight)。但是一旦组建好整个类,而且把描述符绑定到类属性上之后,我们就可以审查类,并为描述符设置合理的储存属性名称。LineItem类的__new__方法可以做到这一点,因此在__init__方法中使用描述符时,储存属性已经设置了正确的名称。为了解决这个问题而使用__new__纯属白费力气:每次新建LineItem实例时都会运行__new__方法中的逻辑,可以,一旦LineItem类构建好了,描述符与托管属性之间的绑定就不会变了。即我们要在创建类时设置储存属性的名称,使用类装饰器或元类可以做到这一点。

        类装饰器与函数装饰器非常类似,是参数为类对象的函数,返回原来的类或修改后的类

 

  1.  装饰器函数的参数是一个类。
  2.  迭代存储类属性的字典。
  3.  如果属性是Validated描述符类的实例。
  4.  使用描述符类的名称type_name和托管属性的名称key来命名storage_name。
  5.  返回修改后的类。

类装饰器能以较简单的方式做到以前需要使用元类去做的事情------创建类时定制类。类装饰器有个重大缺点:只对直接装饰的类有效,这意味着被装饰的类的子类可能继承也可能不继承装饰器所作的改动,具体情况视改动的方式而定。

 

三、导入时和运行时比较

python程序员会区分导入时和运行时,但是这两个术语没有严格区分,且二者之间存在灰色地带。

        导入时,解释器会从上到下一次性解析完.py模块的源码,然后生成用于执行的字节码。如果句法有错误,就在此时报告。如果本地的__pycache__文件夹中有最新的.pyc文件,解释器会跳过上述步骤,因为已经有运行所需的字节码了。编译肯定是导入时的活动,但那个时期还会做些其他事,由于python中的语句几乎都是可执行的,也就是说语句可能会运行用户代码,修改用户程序的状态。尤其是import语句,它不只是声明,在进程中首次导入模块时,还会运行所导入模块中的全部顶层代码,以后导入相同的模块则使用缓存,只做名称绑定。那些顶层代码可以做任何事,包括通常在运行时做的事,例如连接数据库。因此导入时和运行时之间的界限是模糊的:import语句可以触发任何运行时行为

        导入模块时,解释器会执行顶层的def语句,首次导入模块是,解释器会编译函数的定义体,把函数对象绑定到对应的全局名称上,但是解释器不会执行函数的定义体。通常着意味着解释器在导入时定义顶层函数,但是仅当在运行时调用函数时才会执行函数的定义体。

        对类来说,情况不同:在导入时解释器会执行每个类的定义体,甚至会执行嵌套类的定义体。执行类定义体的结果是,定义了类的属性和方法,并构建了类对象。从这个意义上理解,类的定义体属于顶层代码,因为它在导入时运行。

场景练习

        下面是几个场景练习,假设在evaltime.py脚本中导入了evalsupport.py模块:

 

 

 

场景1解答

  1.  导入evaltime模块是,会运行其顶层代码,evaltime模块第一行就是导入evalsupport模块中的deco_alpha函数,因此先执行evalsupport模块中的顶层代码。但是,解释器会编译deco_alpha函数,却不会执行定义体。
  2.  MetaAleph类的定义体在导入evalsupport模块时被执行。
  3.  类的定义体在导入时都会被执行,这里是evaltime模块中Classone类的定义体被执行。
  4.  嵌套类的定义体也会被执行。
  5.  先执行被装饰的类ClassThree的定义体,然后运行装饰器函数。
  6.  在这个场景中evaltime模块是被导入的,因此不会运行if __name__ == '__main__'块。

对于场景1,有几点注意:

  • 这个场景由简单的import evaltime语句触发。
  • 解释器会执行所导入模块及其依赖中的每个类定义体。
  • 解释器先执行类定义体,然后调用依附在类上的装饰器函数,这是合理的,因为必须先构建类对象,装饰器才有类对象可处理。
  • 在这个场景中只运行了一个用户定义的函数或方法,即deco_alpha装饰器。

场景2解答

  1. 到此为止与场景1输出一样。
  2. 初始化了一个Classone对象,执行了其__init__方法。
  3. deco_alpha装饰器修改了ClassThree.method_y方法,因此执行three.method_y时运行inner_1函数的定义体。
  4. 程序结束时,绑定在全局变量one上的ClassOne实例会垃圾回收程序回收,执行了__del__方法。

场景2主要表明类装饰器可能对子类没有影响。示例中ClassFour定义为ClassThree的子类。ClassThree类上依附的@deco_alpha装饰器把method_y方法替换了,但是这对ClassFour类根本没有影响,但是如果ClassFour.method_y方法使用super()调用ClassThree.method_y方法,那么便会看到装饰器起作用,执行inner_1函数,综上,类装饰器究竟能不能影响到子类,关键看子类的具体实现

四、元类基础知识

元类是用于构建类的类。根据python对象模型,类也是对象,因此类肯定是另外某个类的实例。默认情况下,python中的类是type类的实例,即type是大多数内置类和用户定义的类的元类。为了避免无限回溯,type是其自身的实例。

object类和type类之间的关系很神奇:object类是type类的实例,而type类是object类的子类。这种神奇的关系没办法用python代码表述,因为定义其中一个之前另一个必须存在。type是自身的实例这一点也很神奇。

        除了type,标准库中还有一些别的元类,例如ABCMeta和Enum,collections.Iterable是一个抽象类,也是ABCMeta类的实例,ABCMeta类又是type类的实例。所有类都直接或间接地是type的实例,不过只有元类同时也是type的子类,即元类从type类继承了构建类的能力

 

 

所有类都直接或间接地是type的实例,但是元类还是type的子类,因此可以作为制造类的工厂。具体地说,元类可以通过实现__init__方法定制实例,元类的__init__方法可以做到类装饰器能做的任何事情,但作用更大。

场景练习 

 

场景3解答

  1.  创建ClassFive时调用了MetaAleph.__init__方法。
  2.  创建ClassFive的子类ClassSix时也调用了MetaAleph.__init__方法。

python解释器在计算ClassFive类的定义体时直接调用了MetaAleph类的__init__方法,其有四个参数:

  • cls,这是要初始化的类对象,例如这里的ClassFive。在编写元类时,通常把self参数改成cls,这样能清楚地表明要构建的实例是类。
  • name、bases、dic,与构建类时传给type的参数一样。

MetaAleph.__init__方法的定义体中定义了inner_2函数,然后将其绑定给cls.method_z。MetaAleph.__init__方法签名中的cls指代要创建的类(例如ClassFive),而inner_2函数签名中的self指代我们在创建的类的实例(例如ClassFive类的实例)。

场景4解答

  1. 装饰器依附到ClassThree类上之后,method_y方法被替换成inner_1方法。
  2. 虽然ClassFour是ClassThree的子类,但是没有依附装饰器的ClassFour类不受影响。
  3. MetaAleph类的__init__方法把ClassFive.method_z方法替换成inner_2函数。
  4. ClassFive的子类ClassSix也一样,method_z方法被替换成inner_2函数。

ClassSix类没有直接引用MetaAleph类,但是收到了影响,因为它是ClassFive的子类,进而也是MetaAleph类的实例,所以由MetaAleph.__init__方法初始化。

五、定制描述符的元类

  1. LineItem是model.Entity的子类。

model_v7模块中要定义一个元类,而model.Entity是元类的实例。

  1.  在超类type上调用__init__方法。
  2.  与上文中@entity装饰器逻辑一样。
  3. 这个类的存在是为了用起来便利:这个模块的用户直接继承Entity即可,无需关心EntityMeta元类,甚至不用知道它的存在。

 

 

六、元类的特殊方法__prepare__

某些应用中,可能需要知道类的属性(包括数据属性与方法)定义的顺序。

        type构造方法以及元类的__new__和__init__方法都会收到要计算的类的定义体,形式是名称到属性的映射,默认情况下该映射是字典。即,原来或类装饰器获取映射时,属性在类定义体中的顺序已经丢失了。

        该问题解决方法是python3引入的特殊方法__prepare__,该特殊方法只在元类中有用,且必须声明为类方法(@classmethod装饰器定义)。解释器调用元类的__new__方法之前会先调用__prepare__方法,使用类定义体中的属性创建映射。__prepare__方法的第一个参数是元类,第二个参数是要构建的类的名称,第三个参数是基类组成的元组,返回值必须是映射。元类构建新类时,__prepare__方法返回的映射会传给__new__方法的最后一个参数,然后再传给__init__方法。

  1.  返回一个空的OrderedDict实例,类属性将存储在里面。
  2.  在要构建的类中创建一个_field_names属性。
  3.  这里的attr_dict是OrderedDict对象,由解释器在调用__init__方法之前调用那个__prepare__方法时获得。因此这个for循环会按照添加属性的顺序迭代属性。
  4.  把找到的各个Validated字段添加到_field_names属性中。
  5. field_names类方法的作用就是按照添加字段的顺序产出字段的名称。

七、类作为对象

python数据模型为每个类定义了很多属性,常见的比如__mro__、__class__和__name__。

  • cls.__bases__,由类的基类组成的元素。
  • cls.__qualname__,类或函数的限定名称,即从模块的全局作用域到类的点分路径。
  • cls.__subclasses__(),返回一个列表,包含类的直接子类。这个方法的实现使用弱引用,防止在超类和子类之间出现循环引用。这个方法返回的列表中是内存里现存的子类。
  • cls.mro(),构建类时,如果需要获取储存在类属性__mro__中的超类元组,解释器会调用这个方法。元类可以覆盖这个方法,定制要构建的类解析方法的顺序。

 dir()函数不会列出本节提到的任何一个属性。

        

 

 

 

 

 

 

  • 4
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值