Python基础入门自学——12--new方法和元类

上一篇讲到了元类:metaclass,不是很明白,并且测试代码也出现问题,这一章重点了解元类

首先,在元类的编写上用到了__new__()方法,这个方法与__init__()方法有什么不同,又有何关联呢:

说到__new__()方法,就要说到新式类:

新式类与经典类

在Python 2及以前的版本中,由任意内置类型派生出的类(只要一个内置类型位于类树的某个位置),都属于“新式类”,都会获得所有“新式类”的特性;反之,即不由任意内置类型派生出的类,则称之为“经典类”。

“新式类”和“经典类”的区分在Python 3之后就已经不存在,在Python 3.x之后的版本,因为所有的类都派生自内置类型object(即使没有显示的继承object类型),即所有的类都是“新式类”。

经典类在加载的时候采用的是深度优先算法,而新式类采用的是广度优先算法
1:经典类的深度优先,子类继承多个父类的时候,如果继承的多个类中有属性相同的,那么排在第一的父类的属性会覆盖后面继承的类的属性,也就是如果集成的多个父类属性相同,那么以继承的第一个父类的属性为主;
2:新式类的广度优先算法:子类继承多个父类的时候,如果继承的多个父类中有属性相同的,那么越往后继承的类将会覆盖前面的类的属性,也就是后来的继承的覆盖前面的;真正发挥了长江后浪推前浪的传统)
3. 新式类增加了__slots__内置属性, 可以把实例属性的种类锁定到__slots__规定的范围之中。
4. 新式类增加了__getattribute__方法
5.新式类内置有__new__方法而经典类没有__new__方法而只有__init__方法

新式类是在创建的时候继承内置object对象(或者是从内置类型,如list,dict等),而经典类是直接声明的。

Python所有内置对象都源自object对象。解释器内置的object对象定义了一系列特殊的方法,用于实现对象的默认行为:
• __new__
•__init__
•__delattr__
•__getattribute__
•__setattr__
•__hash__
•__repr__
•__str__
•@staticmethod
•@classmethod
•__slots__
•__getattribute__:所有属性和方法的访问操作都是通过__getattribute__完成

__new__方法和__init__的区别

在python中创建类的一个实例时,如果该类具有__new__方法,会先调用__new__方法__new__方法接受当前正在实例化的类作为第一个参数(这个参数的类型是type,这个类型在c和python的交互编程中具有重要的角色),其返回值是本次创建产生的实例,也就是我们熟知的__init__方法中的第一个参数self。那么就会有一个问题,这个实例怎么得到?

注意到有__new__方法的都是object类的后代,因此如果我们自己想要改写__new__方法(注意不改写时在创建实例的时候使用的是父类的__new__方法,如果父类没有则继续上溯)可以通过调用object的__new__方法类得到这个实例(这实际上也和python中的默认机制基本一致),如:

因此可以得到如下结论:

在实例创建过程中__new__方法先于__init__方法被调用,它的第一个参数类型为type。

如果不需要其它特殊的处理,可以使用object的__new__方法来得到创建的实例(也即self)。

于是我们可以发现,实际上可以使用其它类的__new__方法类得到这个实例,只要那个类或其父类或祖先有__new__方法。

所有我们发现__new__和__init__就像这么一个关系:__init__提供生产的原料self(但并不保证这个原料来源正宗,像上面那样它用的是另一个不相关的类的__new__方法类得到这个实例),而__init__就用__new__给的原料来完善这个对象(尽管它不知道这些原料是不是正宗的)

__new__生成一个对象(self),__init__则对这个对象(self)进行加工,即初始化操作。

__new__()必须要有返回值,返回实例化出来的实例,需要注意的是,可以return父类__new__()出来的实例,也可以直接将object的__new__()出来的实例返回。

__init__()有一个参数self,该self参数就是__new__()返回的实例,__init__()在__new__()的基础上可以完成一些其它初始化的动作,__init__()不需要返回值。

若__new__()没有正确返回当前类cls的实例,那__init__()将不会被调用,即使是父类的实例也不行。

我们可以将类比作制造商,__new__()方法就是前期的原材料购买环节,__init__()方法就是在有原材料的基础上,加工,初始化商品环节。

object类中对__new__()方法的定义:
class object: 
  @staticmethod # known case of __new__ 
  def __new__(cls, *more): # known special case of object.__new__ 
    """ T.__new__(S, ...) -> a new object with type S, a subtype of T """
        pass

object将__new__()方法定义为静态方法,并且至少需要传递一个参数cls,cls表示需要实例化的类,此参数在实例化时由Python解释器自动提供。

1.为什么__new__的第一个参数是cls而不是self。因为调用__new__的时候,实例对象还没有被创建,__new__是一个静态方法。第一个参数cls表示当前的class

2.绝大多数时间不需要我们实现__new__方法,python已经帮我们实现:使用父类的__new__()方法来创建对象并返回。所以下列的代码是等价的。

__new__与__init__的参数关系 :
__new__方法如果返回cls的对象(return super().__new__(cls)),则对象的__init__方法将自动被调用,相同的参数*args和**kwargs将被传入__init__方法。也既是说__new__和__init__方法共享同名的参数,除了第一个从cls变成了self。 如果__new__没有返回实例对象,则__init__方法不会被调用。

在调用Person(100)的时候,__new__中的age被赋值为100,__new__结束后会自动调用__init__,并把age传入给__init__

__new__比__init__参数多:

如果我们在__new__中有传入age,而在__init__中没有传入age则会报错:__init__定义时只给了一个固定位置的参数self但是却给了两个参数。很显然是__new__方法直接调用了__init__方法,并将self,age作为参数。 
__new__比__init__参数少:

实例初始化本质是向__new__中传参:

如果__init__有三个参数cls,age,name,而我们在初始化时(p = Person(100))只给了一个age参数,则会报错。所以没有打印print(f"__new__:age:{age}")。 这说明我们在初始化对象时Person(100)的参数,都会先经过__new__方法。

只用__new__而不用__init__来实例化对象实例:

这种方法是不建议的,通常使用以下方法:

上面常用的定义类写的方法,其实完整的写法是:

__new__的参数可以是*args和**kwargs,即可变的位置参数和可变的关键字参数,__init__的参数应该是明确的参数:

要保证new参数与init参数的一致,上例中的错误是age参数没有定义,改为下面的

参数传递到了init中,只不过是args数组中的元素。

当调用父类的new方法时,不需要在传递除cls外的其他参数:调用父类的new只是为了得到对应cls类的实例,得到实例后,本类的new方法调用init方法,将后续参数传递到init,所以这里的参数只对后续init方法有用,对父类的new方法无用。

什么是 metaclass:

metaclass翻译成元类,仅从字面理解, meta 的确是元,本源,但理解时,应该把元理解为描述数据的超越数据,metaclass 的 meta 起源于希腊词汇 meta,包含两种意思:
• “Beyond”,例如技术词汇 metadata,意思是描述数据的超越数据。
• “Change”,例如技术词汇 metamorphosis,意思是改变的形态。

因此可以理解 metaclass 为描述类的超类,同时可以改变子类的形态。这种特性在编程中有什么用?

用处非常大。在没有 metaclass 的情况下,子类继承父类,父类是无法对子类执行操作的,但有了 metaclass,就可以对子类进行操作,就像装饰器那样可以动态定制和修改被装饰的类,metaclass 可以动态的定制或修改继承它的子类。

metaclass 能解决什么问题? metaclass 可以像装饰器那样定制和修改继承它的子类:

在定义完Foo类时,就执行了Mymeta的new和init

从上面的运行结果可以发现在定义 class Foo() 定义时,会依次调用 MyMeta 的 __new__ 和 __init__ 方法构建 Foo 类,然后在调用 foo = Foo() 创建类的实例对象时,才会调用 MyMeta 的 __call__ 方法来调用 Foo 类的 __new__ 和 __init__ 方法。

通过上面的例子运行,正常情况下我们在父类中是不能对子类的属性进行操作,但是元类可以。换种方式理解:元类、装饰器、类装饰器都可以归为元编程。

这里对执行的过程在梳理一下:首先是对class Foo(),当遇到这个语句时,实际上也是执行一个实例化操作,与foo = Foo()是基本一样的过程,只不过,foo = Foo()是使用Foo类来实例化出一个对象,而class Foo()是使用type类来实例化出一个对象(如果定义Foo时没有指定metaclass,如果指定了metaclass,就使用metaclass实例化出一个类对象),只不过这个对象这时叫做类,类是type的实例,foo是Foo的实例。

当class Foo()定义完成后,Python解析器先进行一遍分析,知道要创建一个类Foo,同时这个Foo类的创建要使用Mymeta类来进行创建,而不是默认的type类创建,再就是分析出类的结构,形成一个字典对象args,即创建的类Foo,类继承的父类,这里是(),是空的,其他的属性,yaml_tag:‘!foo’,__init__:<function...>,__new__:<function...>,还有其他一些所有类共有的属性,如__module__,__qualname__等,作为Mymeta等元类的参数。

使用Mymeta元类创建Foo类,就是执行Mymeta的__new__方法,这个__new__方法又调用了type的new方法,创建Foo类,对Mymeta的new方法返回的类,实际上的调用是:

type.__new__(Mymeta,Foo,(),{'__modual__':'__main__','__qualname__':'Foo','yaml_tag':'!foo','__init__':<...>,'__new__':<>})

这样就创建了一个名字叫Foo,类型为Mymeta的类——Foo类。

因为new返回了Mymeta的一个实例,按照前面学习new和init的区别的内容,那么就会接着调用Mymeta的init方法,init方法的self就是Foo,后面的name,bases,dic就是args参数:Foo,(),{'__modual__':'__main__','__qualname__':'Foo','yaml_tag':'!foo','__init__':<...>,'__new__':<>}),在init中执行了一个super().__init__(name,bases,dic),这一步不太理解是什么作用,感觉这一步没有必要,init执行完毕后,Foo创建完毕,到这里,就是class Foo()的执行过程。

然后是foo = Foo(‘lll’),创建foo对象,因为是Foo()的形式,这就相当于调用__call__()方法,即Foo.__call__(),但是对于有自定义元类的类,则是直接调用其元类的Mymeta的call方法。要注意的是,虽然是 Mymeta的类的call方法,但是,方法的参数cls是Foo,在Mymeta的call方法中,先是调用Foo的new,创建一个实例,所以打印结果先打印的是Foo.__new__,然后是对创建的实例进行初始化,即调用Foo的init方法,注意,这里init的第一个参数是Foo,而不是实例obj,个人感觉是不对的,因为对类进行初始化,相当于给类加了一个name = ‘lll’,相当于所有的实例都将name赋值为‘lll’,这里应该改为obj才合理,不同的实例,赋予不同的值,看下面的测试,新的实例foo2将name赋值为aaa,但是foo的name值也变了。还有就是既然生成了实例obj,应该调用实例的init方法,可以改为obj.__init__(*args,**kwargs),这是对象方法调用,默认第一个参数会将自身传进去,所以不需要再写self参数,如果写了会提示参数过多,而cls.__init__(obj,*args,**kwargs)是类方法调用,必须显示指定要绑定的对象,否则提示缺少参数。

要理解 metaclass 的底层原理,需要深入理解 Python 类型模型。下面,分三点来说明。

第一,所有的 Python 的用户定义类,都是 type 这个类的实例。

可能会让你惊讶,事实上,类本身不过是一个名为 type 类的实例。在 Python 的类型世界里,type 这个类就是造物的上帝。这可以在代码中验证:

可以看到,inst 是 MyClass 的实例,而 MyClass 不过是“上帝” type 的实例。

第二,用户自定义类,只不过是 type 类的 __call__ 运算符重载

当我们定义一个类的语句结束时,真正发生的情况,是 Python 调用 type 的 __call__ 运算符。简单来说,当你定义一个类时,写成下面这样时:

class MyClass:
    data = 1

Python 真正执行的是下面这段代码:

class = type(classname, superclasses, attributedict)

这里等号右边的 type(classname, superclasses, attributedict),就是 type 的 __call__ 运算符重载,它会进一步调用:

type.__new__(typeclass, classname, superclasses, attributedict)
type.__init__(class, classname, superclasses, attributedict)

由此可见,正常的 MyClass 定义,和你手工去调用 type 运算符的结果是完全一样的。

第三,metaclass 是 type 的子类通过替换 type 的 __call__ 运算符重载机制,“超越变形”正常的类

其实,理解了以上几点,我们就会明白,正是 Python 的类创建机制,给了 metaclass 大展身手的机会。

一旦你把一个类型 MyClass 的 metaclass 设置成 MyMeta,MyClass 就不再由原生的 type 创建,而是会调用 MyMeta 的 __call__ 运算符重载。

class = type(classname, superclasses, attributedict) 
# 变为了
class = MyMeta(classname, superclasses, attributedict)

上一节最后的错误是因为在ModelMetaclass中,将attrs['__mappings__'] = mappings的key错写成了__mapping__导致出现错误。

通过这一节的学习,对metaclass有了一点了解,但是又出现很多疑问,需要进一步解决的是:object和type这两个类要仔细研究透彻,这是Python的类起源,然后是了解类的创建过程,如对于继承类创建,继承类的父类使用了元类,这种类的创建过程。

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值