由表及里 探索元类(Python)的奥秘

        一. 文章为何来

        正如上一篇 ,我利用了元类实现了Enum的创建 ,但当时我对元类的理解仅仅停留在应用方面 ,所以说开此贴来好好道道元类(metaclass)

        其实本文的本意并不是做元类的讲解 ,而是里面的逻辑和易混点。 

        但为了让大家好理解 ,就连根拔起 ,从基础做起吧。

        如果有只想看逻辑和易混点的同志 ,就请从(三  -> !-> ④)开始阅读吧。

        二. 什么是元类:

        元类 ,可以说是Python基础中的最后一关 ,也是 “类和对象”的幕后黑手 ,是“神一般的存在” ,凤毛麟角 ,平时不可多见。 而操控元类的 ,居然是type

        说人话的话type 所有类型的元类(所有的类型的尽头就是type) ,而 object 是所有类的父类,所以说type的父类是object object 又是type类型的 ,type 的类型也是type 类型

        ... ... 没事 ,就是很绕 ,仅仅了解一下就行 ... ...

        说元类 ,不妨先从类的创建说起

# -*- coding :utf-8 -*-

class MyClass(object):
    pass

 以上就是 “最简单的类的创建”。

其实呢 ,还有一种类的创建方法 ,叫做 “一行流

#“一行流” 类中的函数
def echo():
    print(123456)

#“一行流” 类中的属性
boolean = True

#第一个参数叫name ,一般和类名相同就行。
#第二个参数叫bases ,就是父类 ,没有继承父类自然是() ,是一个元组。
#第三个参数叫attrs ,键值对分别是“名”与“值” ,是一个字典。
MyClass = type("MyClass" ,() ,{"echo" :echo ,"boolean" :boolean})

MyClass.echo()   #-> 123456
MyClass.boolean  #-> True

name :就是名字 ,一般和类名一样就行

bases :父类类名 ,元组形式传递

attrs  : 字典形式 ,储存这个类的属性和方法 ,一般照上面的例子写就行

 * PS :有这三个参数的存在 ,就是这么规定的 ,但为什么? 存在即合理 ,请继续往下看。

其实 ,这就是元类的雏形 ,尤其是指这三个参数(请记住它们三但并不是完全的元类。

那么 ,真正的元类怎么写? 以下 ,是 “最简单的元类”:

class MetaClass(type):
    pass
#参数metaclass ,指定的就是MyClass的元类。
class MyClass(metaclass = MetaClass):
    pass

看看 ,是不是和 “最简单的类” 有异曲同工之妙?仅仅是继承了type ,就摇身一变为元类。

有了元类 ,我们可以动态的修改类 ,可以在类实例化之前对它进行修改 ,在类实例化之前规范它 ,也就是在类出现的一瞬间。

听到这里 ,大家有可能感觉很迷茫 ,先不要急嘛~ 

上面只是介绍~

下面才开始讲解~

三. 来认真地谈一谈元类:

        1. 元类的常用方法:

        一般来说 ,元类常用的方法只有三个 ,当然 ,如果有需要也可以定制其他的方法。

        这三个方法 ,就是 “三板斧”: __new__   ->  __init__  ->  __call__

        这是它们的调用时序。(当然 ,只用其中一两个也是可以的 ,只不过这三个常见)

        (*)具体说:  (下文的“类”不是“元类” ,是MyClass那个“类”)

                1:在类创建完的一瞬间 ,元类里的 __new__第一个被调用 ,然后是元类里的__init__ ,这是情理之内。

                2:__call__方法是,在类被实例化的瞬间(广义上说 也就是加()的时候),被调用。例如:test = MyClass() ,就像这时候就会被触发。 

PS:(以前我在学习元类时 ,网络上有一种说法 ,说  先调用__call__ ,只不过只是开了个头 ,实际还是__new__第一 ,__init__第二 ,__call__第三)

        2. 先写一个元类:

看看是不是和上面的 “一行流” 的参数有相似之处~

   不需要细看 ,粗略看一下 ,有个印象就行 ,下面会用到再回头看~

class MetaClass(type):
    
    def __new__(cls ,name:str ,bases:tuple ,attrs:dict):
        print("new!")
        print(name ,bases ,attrs) #第一次
        cls.value1 = 1
        attrs['value2'] = 2
        print(name ,bases ,attrs) #第二次
        return type.__new__(cls ,name ,bases ,attrs)

    def __init__(self ,name:str ,bases:tuple ,attrs:dict):
        print("init!")
        self.value3 = 3
        type.__init__(self ,name ,bases ,attrs)

    def __call__(self):
        print("call!")
        return type.__call__(self)
class MyClass(metaclass = MetaClass):
    pass

        结合上面 ,这个name其实就是 MyClass(类名) , 这个bases其实就是MyClass的父类(现在是空元组或者说是(object ,) ),这个attrs其实就是MyClass 下的所有类属性与方法 (大家可以自己去实践一下)。

这三个参数全是针对MyClass的。

        __new__返回的 ,其实就是MyClass类 ,注意不是实例对象

        调用顺序是 __new__   ->   __init__    ->   __call__。

!理解的重点!:

所以说大家看出来元类的各个方法有什么用处了吧?

——                        ——                        ——                        ——                        ——         

:在__new__里 ,可以通过对attrs的修改 ,来 增添 / 删减 类方法和类属性。

attrs通过字典形式保存着MyClass下所有的类属性和方法 ,可以通过键值对增添新元素 ,或删减旧元素 ,然后传进type.__new__(cls ,name ,bases ,attrs)) ,从而改变类方法与属性。

就像:

1:上述代码的: attrs["value2"] = 2    ->   将 value2 变成了类MyClass的类属性

*(原理:元类中的__new__返回的是一块空间,这块空间其实就是MyClass这个类 ,return 的东西携带着attrs ,所以说attrs成了MyClass的初始条件的一部分,也就是类属性和方法

2:而上述代码的:cls.value1 = 1   ->   将value1变成了元类MetaClass的类属性

注意:元类的类属性 ,元类能访问到 ,类也能访问到 ,但类的实例对象访问不到!

*(原理:这是Python的保护机制 ,因为元类创建类 ,类实例化为实例对象 ,如果元类的类属性能随便被类的实例对象访问到 ,不就是“现在的我穿越到小时候干掉了我嘛?” 。所以说 ,一切能改变元类的事情 ,不能出现)

——                        ——                        ——                        ——                        ——     

:在__new__里 ,可以通过对bases的修改 ,来改变类的继承 ,也就是改变父类。

(:当attrs中出现了某一属性或方法时 ,那在__new__方法里识别到之后就可以改变bases ,然后传进type.__new__(cls ,name ,bases ,attrs)) ,从而改变MyClass的父类 ,改变继承关系)

就像: 假设我MyClass这么写:

class MyClass(metaclass = MetaClass):
    sign1 = True  #第一种方法

    def sign2(self):  #第二种方法
        pass

 在__new__里 ,attrs 里面会出现 "sign1" : True  和  "sign2" : <...>  这两对键值对。所以说可以写一个检测机制来识别 ,然后进行一系列对类的操作。

——                        ——                        ——                        ——                        —— 

在__call__里 ,__call__的参数其实就是MyClass实例化时所携带的参数。所以可以通过识别参数来进行一定的操作

先抛弃上面的元类,在此来新建一个元类 ,针对__call__的讲解:

#新建的元类 ,来针对__call__的讲解
def MetaClass(type):

    def __call__(cls ,*args ,**kwargs):
        # -此处是个性化处理- #
        return type.__call__(cls ,*args ,**kwargs)


def MyClass(metaclass = MetaClass):

    def __init__(self ,*args ,**kwargs):
        pass

         当我们实例化MyClass时 ,也就是myclass = MyClass()时 ,先调用元类的__call__方法 ,也就是说传进去MyClass的参数先被元类的__call__获取 ,进行了个性化处理后会返回实例对象 ,然后才被类的__init__方法获取并进行对象的初始化。

实例1

        就像在上文我创建的枚举类型 ,在元类的__call__方法里可以直接 raise 一个错误 ,以此不让Enum类进行初始化 ,从而实现枚举类型。  简单实现一下

class Enum(type):

    def __call__(cls ,*args ,**kwargs):
        raise AttributeError("XXX")
实例2

        再来个例子。假如你的需求是 “不允许传入某种类型的参数” ,一般来说 Type Hint “防君子而不防小人” ,那么我们可以用元类来强制识别。  假设是int类型吧

class MetaClass(type):

    def __call__(cls ,value):
        if isinstance(value ,int):
            raise TypeError("XXX")
        return type.__call__(cls ,value)


class MyClass(metaclass = MetaClass):

    def __init__(self ,value):
        self.value = value

         不仅如此 ,例如单例对象 ,也可以通过元类来进行实现。(其实没必要 ,通过类里的__new__就可以实现 ,不必要用到元类)

 

——                        ——                        ——                        ——                        —— 

在__init__方法里 ,可做的对象不多 ,但有些地方需要区分

        让我们回到前面的那个例子

        我们要清楚 ,元类里无论是__new__还是__init__ ,都是对 “类本身”(MyClass)的修饰 ,也就是说 :

       1:在元类里 ,__new__的第一个参数是 cls ,它指的是 “元类本身”__new__返回的是MyClass本身  ,所以上面的 cls.value1 = 1 ,实际上是给元类添加类类属性 ,这个类属性只能被元类和类访问 ,原因就是上面说的 "现在的我穿越到小时候干掉了我嘛?"
       2:__new__里的attrs ,指的是类的所有方法与属性 ,以键值对储存 。__new__返回的东西携带着的 attrs ,实际就是MyClass的初始条件
       3: __init__的第一个参数是 self ,self 实际上MyClass本身 ,就是它上面的__new__返回的 ,所以说给self添加属性 ,就如一开始的那个例子里的 self.value3 = 3 ,实际上是给MyClass设置属性 ,成为MyClass类属性

 

        *上面的2、3,其实讲的就是MyClass的初始化 ,就是类的初始化 *

        耳熟?

        * 因为类里的__init__方法 ,是对象的初始化 *

        * 那么,对于的初始化 ,还有什么方法?*
的初始化
class MetaClass(type):

    def __new__(cls ,name ,bases ,attrs):
        # target保存的本应是__new__返回的东西,这个东西是MyClass本身。
        target = type.__new__(cls ,name ,bases ,attrs)

        #实际上就是 MyClass.value=1
        target.value = 1
        
        #返回了“添加value:1 这个键值对的MyClass类”
        return target

 ——                        ——                        ——                        ——                        —— 

参照上面的 ,这时候我突发奇想 ,能不能这样实现对象的初始化呢:

class MetaClass(type):

    def __new__(cls ,name ,bases ,attrs):
        target = type.__new__(cls ,name ,bases ,attrs)
        
        #MyClass的实例对象
        Object = target()

        Object.a = 1
        return target

实际上是不行的 ,原因呢,我认为是Object终究只是一个形式参数 ,__new__方法结束后就自动释放了 ,所有不能进行对MyClass类的实例化对象的修改,所以不能初始化。

(在此插个眼 ,我以后打算试一试能不能将对象作为返回值)

对了 ,之前说过的 “除了这三板斧 ,还可以定制其它的方法” 。

对于这个问题感兴趣的同志 ,就请查阅我上一篇文章 “自创Enum” ,里面有具体的实例。

在这里就提一小小的嘴:

        就是 ,元类里实现的魔法方法会被 MyClass“继承” (查了资料,不是继承,但我发现就是继承的效果)

        对于在元类里的自定义方法 ,我姑且还没研究过 ,在此立个flag。

总结(Allover)

        行笔至此 ,这篇教学 ,也已缓步迈向尾声 ,与此同时希望大家还是有所启示。

        这样 ,这三天的键盘声 ,也会在欢声笑语中升华。

谢谢大家的阅读与支持!

有错误请指出 ,

有模棱两可的地方请批判,

有不太懂的地方请大家一起讨论。

2024/2/14    大年初五   无所事事中

  • 57
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值