Python学习_基础_33_面向对象编程进阶(元类)

Python-面向对象编程进阶-元类

一、基本概念

什么是元类?

对象 → 通过调用实例化产生

→ 通过调用元类实例化产生

在这里插入图片描述

元类 :就是用来实例化产生类

我们使用class关键字来定义一个类,背后发生的事情实质就是调用了元类

class test:
    pass


print(type(test))  # <class 'type'>

如上所示,type就是元类,我们用class关键字定义的所有的以及内置的类都是由元类type实例化产生的。

至于type的妈是谁,不要去想,反正不可能是 Java 。如果非要想,不如认为它自己是自己的妈。


二、使用class关键字创建类的机制分析

我们使用class关键字来定义一个类,背后发生的事情实质就是调用了元类

类有三大基本特征:
  1. 类名
  2. 类的基类(父类),没有默认为object
  3. 类体代码(执行类体代码拿到的类的名称空间

满足了三个基本特征之后,就可以调用元类进行实例化。如下所示:

# 1、类名
name = "test"
# 2、类的基类
bases = ()  # bases = (object,) 是一个元组
# 3、执行类体代码拿到类的名称空间
dic = {}  # 包含属性名称和值的字典

# 4、调用元类
test = type(name, bases, dic)

print(test)  # <class '__main__.test'>
print(type(test))  # <class 'type'>

print(test.__name__)  # test
print(test.__bases__)  # (<class 'object'>,)

我们现在并没有使用class关键字,而是通过调用元类的方式创建了一个类。

而使用class关键字造类,class在底层帮我们做的,也是这四步。

type是python内置的元类,我们还可以自定义元类,来控制类的产生。


三、如何自定义元类

只有继承了元类type的类才可以作为元类使用

一个类没有声明自己的元类,则默认它的元类就是type。除了使用内置元类type,我们也可以通过继承type来自定义元类,然后使用metaclass关键字参数为一个类指定元类。

class Mymeta(type):
    def __init__(self, class_name, class_bases, class_dic):
        pass


class test(metaclass=Mymeta):
    pass

自定义元类可以控制类的产生过程,类的产生过程其实就是元类的调用过程。所以上面代码的实质即:

class Mymeta(type):
    def __init__(self, class_name, class_bases, class_dic):
        pass

test = Mymeta("test", (object,), {})

在上面的代码中,test = Mymeta("test", (object,), {})就是在调用元类Mymeta

而调用Mymeta发生的三件事:

  1. 先造一个空对象 => 名叫test
  2. 调用Mymeta这个类内的__init__方法,完成初始化对象的操作
  3. 返回初始化好的对象(这个对象就是类test)

我们来测试一下,在自定义元类的__init__方法中加入输出语句:

class Mymeta(type):
    def __init__(self, class_name, class_bases, class_dic):
        print('我波澜不惊的运行了')


class test(metaclass=Mymeta):
    pass

# 运行结果:
# 我波澜不惊的运行了

利用这一特性,我们可以实现一个检测小功能:检测类体内是否存在注释说明,无注释说明则报错。(使用raise来主动抛出异常)

代码如下:

class Mymeta(type):
    def __init__(self, class_name, class_bases, class_dic):
        if '__doc__' not in class_dic or len(class_dic['__doc__'].strip(' \n')) == 0:
            raise TypeError('类中必须有文档注释,并且文档注释不能为空')
        else:
            print('无事发生')



class test(metaclass=Mymeta):
    pass

# 运行结果:抛出异常
# TypeError: 类中必须有文档注释,并且文档注释不能为空
class Mymeta(type):
    def __init__(self, class_name, class_bases, class_dic):
        if '__doc__' not in class_dic or len(class_dic['__doc__'].strip(' \n')) == 0:
            raise TypeError('类中必须有文档注释,并且文档注释不能为空')
        else:
            print('无事发生')


class test(metaclass=Mymeta):
    '''
    铁憨憨到此一游
    '''
    pass

# 运行结果:
# 无事发生

四、__new____call__

我们知道,调用元类产生类发生了三件事,第一步就是造对象。而造对象,必定对应着某种方法,这个方法就是__new__

让我们来回顾一下这个过程:

在这里插入图片描述

⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔分⚔隔⚔线⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔

其实,在第一步(造空对象)时,实质上就是调用了元类Mymeta下的__new__方法。

然后,再调用Mymeta这个类内的__init__方法,完成初始化对象的操作。

最后,返回初始化好的对象。

我们来用一个案例演示:

class Mymeta(type):
    def __init__(self, class_name, class_bases, class_dic):
        print('__init__运行了')

    def __new__(cls, *args, **kwargs):
        print('__new__运行了')
        print(cls, args, kwargs)


test = Mymeta("test", (object,), {})
#运行结果:
# __new__运行了
# <class '__main__.Mymeta'> ('test', (<class 'object'>,), {}) {}

我们看到,__new__方法运行了,但是__init__方法并没有运行。这是因为我们覆盖重写的__new__没有返回值,即没有返回空对象,所以__init__没有执行。

我们看到,给__new__方法传入的参数,就是当前所在的类,以及调用类时所传入的参数。例中为Mymeta类、和定义的类名、基类、类体代码字典(名称空间)。

要想实现让__init__成功执行,有两种解决办法:

  1. 利用super()调用父类(type)的__new__方法

    class Mymeta(type):
        def __init__(self, class_name, class_bases, class_dic):
            print('__init__运行了')
    
        def __new__(cls, *args, **kwargs):
            print('__new__运行了')
            return super().__new__(cls, *args, **kwargs)  # 返回一个对象
    
    
    test = Mymeta("test", (object,), {})
    # 运行结果:
    # __new__运行了
    # __init__运行了
    
  2. 指名道姓调用type的__new__方法

    class Mymeta(type):
        def __init__(self, class_name, class_bases, class_dic):
            print('__init__运行了')
    
        def __new__(cls, *args, **kwargs):
            print('__new__运行了')
            return type.__new__(cls, *args, **kwargs)  # 返回一个对象
    
    
    test = Mymeta("test", (object,), {})
    # 运行结果:
    # __new__运行了
    # __init__运行了
    

其实两种方法的逻辑是一样的,只是形式上的不同。

强调: 只要是调用类,那么会一次调用

  1. 类内的__new__
  2. 类内的__init__

好了,我们现在已经知道__new__方法是干什么用的了,而还有一个方法不得不提,就是__call__方法。

我们说,test = Mymeta("test", (object,), {})就是在调用元类Mymeta。造对象(类)是应用了__new__方法,给对象进行初始化是应用了__init__方法,那么能让test通过加括号的方式就进行调用,就是因为__call__方法在背后疯狂输出。

class Foo:
    def __call__(self, *args, **kwargs):
        print(self)
        return '返回值'


obj = Foo()

# 1、要想让obj这个对象变成一个可调用的对象,需要在该对象的类中定义一个__call__方法,该方法会在调用对象时自动触发
# 2、调用obj的返回值就是Foo中__call__方法的返回值

res = obj()  # <__main__.Foo object at 0x000001FFF2E822B0>
print(res)  # 返回值

直接一点说,调用对象(x)这件事情的实质,是在调用实例化出对象(x)的那个对象(y)的__call__方法。(也就是对象他妈的__call__方法)
y —实例化—> x(y是x的妈)

那么我们就可以得出一个结论:如果想让一个对象可以加括号调用,就需要在该对象的类中添加一个__call__方法。

那么对应的:

  1. 对象() --> 类内的__call__
  2. 类() --> 自定义元类内的__call__
  3. 自定义元类() --> 内置元类type__call__

我们来梳理一遍整个流程:

我们定义一个类走的四步:

  1. 定义类名
  2. 定义类的基类
  3. 执行类体代码,拿到类的名称空间
  4. 调用元类,而调用元类也走了四步:
    1. 调用元类这件事情的本质是调用元类他妈的__call__方法,比如调用自定义元类Mymeta就是在调用元类type的__call__方法
    2. 造空对象(类),调用的是元类的__new__方法
    3. 初始化对象(类),调用的是元类的__init__方法
    4. 返回初始化好的对象(类)

五、自定义元类控制类的调用 ==》控制类的对象的产生

我们通过自定义一个元类,可以控制类的产生。那我们就可以通过调用产生的这个类,来实现控制对象的产生。

现在我们已经做的事情:定义了Mymeta元类,并调用Mymeta实例化出了test类。

然后我们来调用test类实例化出一个obj对象:

class Mymeta(type):
    def __init__(self, class_name, class_bases, class_dic):
        pass

    def __new__(cls, *args, **kwargs):
        return type.__new__(cls, *args, **kwargs)

    def __call__(self, *args, **kwargs):
        print('call哥我运行啦')


test = Mymeta("test", (object,), {})
obj = test()

# 运行结果:
# call哥我运行啦

相对应的,调用类test来实例化对象obj也走了四步:

  1. 调用类test就是在调用元类Mymeta__call__方法
  2. Mymeta.__call__函数内会先调用test内的__new__方法
  3. Mymeta.__call__函数内会再调用test内的__init__方法
  4. Mymeta.__call__函数内会返回一个初始化好的对象

为了证明,对象obj是由Mymeta.__call__函数返回的,我们来检验一下:

class Mymeta(type):
    def __init__(self, class_name, class_bases, class_dic):
        pass

    def __new__(cls, *args, **kwargs):
        return type.__new__(cls, *args, **kwargs)

    def __call__(self, *args, **kwargs):
        return '傻眼了吧'


test = Mymeta("test", (object,), {})
obj = test()
print(obj)  # 傻眼了吧

我们来理一下整个流程:

class Mymeta(type):
    def __call__(self, *args, **kwargs):
        # 1、Mymeta.__call__函数内会先调用test内的__new__
        test_obj = self.__new__(self)

        # 2、Mymeta.__call__函数内会调用test内的__init__
        self.__init__(test_obj, *args, **kwargs)

        # 3、Mymeta.__call__函数内会返回一个初始化好的对象
        return test_obj


class test(metaclass=Mymeta):
    def __new__(cls, *args, **kwargs):
        # 要产生真正的对象
        return object.__new__(cls)  # 调用的是object下的new方法,也就是为什么类会默认继承object类的重要原因。

    def __init__(self, name, say):
        self.name = name
        self.say = say


obj = test('山炮','致敬')
# test类的调用
    # 1、obj=test('山炮','致敬')就是在调用Mymeta.__call__
    # 2、Mymeta.__call__函数内会先调用test内的__new__创造obj空对象
    # 3、Mymeta.__call__函数内会调用test内的__init__进行初始化
    # 4、Mymeta.__call__函数内会返回一个初始化好的对象

流程分析:

  1. 首先我们自定义了一个元类Mymeta
  2. 我们调用Mymeta产生了一个类test(过程省略)
  3. 我们写下了obj=test('山炮','致敬')
  4. obj=test('山炮','致敬')调用了类test,实质上是调用自定义元类Mymeta__call__函数(方法)
  5. Mymeta.__call__函数内会先调用test内的__new__
  6. test.__new__实际上调用的是object类下的new方法,并返回给我们一个空对象
  7. 然后Mymeta.__call__函数调用test内的__init__给对象进行初始化
  8. 给对象添加self.name = '山炮'self.say = '致敬'两个属性
  9. Mymeta.__call__函数返回一个初始化好的对象给obj

其实这个流程所对应的代码就是一个,自定义元类控制类的对象生成,的模板。(例中为Mymeta控制test的对象生成)


六、属性查找

我们来将继承元类的知识结合起来,重新看一看对象的属性查找。(切记父类跟元类是两码事,可以理解为父类与子类是父子关系,元类与类是母子关系)

我们已知的属性查找的顺序为:对象 ——》类 ——》父类

如果加上元类,那么查找顺序是什么呢?

class Mymeta(type):
    n = 444

    def __call__(self, *args, **kwargs):
        obj = self.__new__(self)
        self.__init__(obj, *args, **kwargs)
        return obj


class Bar(object):
    n = 333


class Foo(Bar):
    n = 222


class test(Foo, metaclass=Mymeta):
    n = 111

    def __init__(self, name, age):
        pass


print(test.n)  # 自下而上依次注释各个类中的n=xxx,然后重新运行程序,发现n的查找顺序为test->Foo->Bar->object->Mymeta->type

结构图为:
在这里插入图片描述

查找顺序即为:

  1. 先找继承层:test -> Foo -> Bar -> object
  2. 后找元类层:Mymeta -> type

七、总结

元类定义的目的

  1. 控制类的产生
    __new__
    __init__
  2. 控制类的调用
    __call__
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值