zoukankan      html  css  js  c++  java
  • 元类(metaclass)

    元类(metaclass)

    一、什么是元类

    • 一切源自于一句话:python中一切皆为对象。既然如此类是不是也是对象呢?
    class Teacher(object):
        school='tsinghua'
    
        def __init__(self,name,age):
            self.name=name
            self.age=age
    
        def say(self):
            print('%s says welcome to the Beijing' %self.name)
            
    t1=Teacher('egon',18)
    print(type(t1)) #查看对象t1的类是<class '__main__.Teacher'>
    
    • 所有的对象都是实例化或者说调用类而得到的(调用类的过程称为类的实例化),比如对象t1是调用类Teacher得到的
    • 一切皆对象的话 类也必然是一个对象,验证一下
    tcls = Teacher
    li = [Teacher]
    def func(cls):
        print(cls)
    func(Teacher)
    #完全没问题把他当做对象来使用 和其他对象没有任何区别
    
    • 思考,t1是通过Teacher实例化得到的,那Teacher对象是哪个类实例化的呢?
    print(type(Teacher))
    #<class 'type'>
    
    • 可以推导出===>产生Teacher的过程一定发生了:Teacher=type(...)

    元类type--------------->Teacher类--------------->t1对象

    • 用于实例化产生类的类称之为元类 就是此时的type类

    • 只要继承了type类,那么这个类就是元类

    • Teacher是通过type实例化得到的,既然如此,是不是可以自己调用type来实例化一个calss呢?

    二、创建类的流程分析

    ​ class关键字在帮我们创建类时,必然帮我们调用了元类Teacher=type(...),那调用type时传入的参数是什么呢?必然是类的关键组成部分,一个类有三大组成部分,分别是

    • 类名class_name='Teacher'
    • 基类们class_bases=(object,)
    • 类的名称空间class_dic,类的名称空间是执行类体代码而得到的

    调用type时会依次传入以上三个参数

    class_name = "Teacher"
    class_code = """
    def __init__(self,name,age):
        self.name=name
        self.age=age
    def say(self):
        print('%s says welcome to the Beijing' %self.name)
    """
    class_dict = {}
    exec(class_code,None,class_dict)
    
    bases = (object,)
    
    Teacher = type(class_name,bases,class_dict)
    print(Teacher)
    

    综上,class关键字帮我们创建一个类应该细分为一下四个过程

    1. 获取类名
    2. 获取基类
    3. 获取名称空间
    4. 实例化元类得到类

    元类中的__call__方法

    • 当你调用类对象时会自动珍惜元类中的__call__方法 ,并将这个类本身作为第一个参数传入,以及后面的一堆参数
    • 覆盖元类中的call之后,这个类就无法产生对象,必须调用super().__call__来完成对象的创建 ,并返回其返回值

    当你想要控制对象的创建过程时,就覆盖call方法

    当你想要控制类的创建过程时,就覆盖init方法

    三、自定义元类控制类的创建

    ​ 思考,如果我想高度定制一个类,该如何实现?例如要求所有方法名称必须小写,类名称必须大写开头,等等。创建类是由type完成的type中必然包含了创建了的具体代码,现在需要对这些代码进行修改,两种方法

    1. 修改type源代码>>>>不可取
    2. 创建新的元类,使用自己的元类来创建类,从而实现定制类

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

    class Mymeta(type): #只有继承了type类才能称之为一个元类,否则就是一个普通的自定义类
        pass
    class Teacher(object,metaclass=Mymeta): # Teacher=Mymeta('Teacher',(object),{...})
        school='tsinghua'
        def __init__(self,name,age):
            self.name=name
            self.age=age
    
        def say(self):
            print('%s says welcome to the Beijing' %self.name)
    

    需求:

    1. 规范类名必须大写
    2. 类中必须包含文档注释
    class MyMate(type):
        def __init__(self,name,bases,dic):
            print("run")
            if not dic.get("__doc__"):
                raise TypeError("类必须有文档注释!")
            if not name.istitle():
                raise TypeError("类名必须大写开头!")
            super().__init__(name,bases,dic)
    class Foo(object,metaclass=MyMate):
        pass
    

    项目中的应用

    ​ 在优酷系统中需要根据类的信息来生成创建表的语句,必须知道类何时被创建了,使用元类可以轻松的拦截类的创建过程,获取类相关信息来生成建表语句

    class MyMetaClass(type):
        def __init__(self,name,bases,dic):
            table_name = name
            columns = self.transfer_columns(dic)
            sql = "create table if not exists %s(%s)" % (table_name,columns)
            # 自动建表
            try:
                OBDB().conn.execute(sql)
            except Exception as e:
                pass
            super().__init__(name,bases,dic)
            
            
           
    s = 10  == s = int
    a = type("int")
    b = a
    b()
    c.__class__
    

    四、自定义元类控制类的调用

    __call__函数的执行时机

    • 该方法会在调用对象时自动触发执行(对象加括号)
    class Foo:
        def __call__(self, *args, **kwargs):
            print("run")
    f = Foo() #调用Foo得到f对象
    f()#调用对象时 触发__call__的执行
    
    • 通常调用一个对象是没有意义的,那__call__在什么时候用呢?

    • 我们说类也是一个对象,那么Foo()是不是也执行了Foo的类中的__call__函数呢?

    • Foo的类是谁呢?默认是元类type,通过metaclass来指定为自定义的元类来测试

    #测试
    class M(type):
        def __call__(self, *args, **kwargs):
            print("run mateclass __call__")
            pass
        pass
    class A(metaclass=M):
        pass
    print(A())
    #输出 run mateclass __call__
    #输出 None
    

    覆盖__call__函数时的注意事项

    • 第一行输出表明了,调用类A时,的确自动执行了__call__函数
    • 第二行输出一个空,这是为什么呢?将__call__注释起来

    必须明确创建对象的过程,先创建空对象,执行初始化将属性存储到对象的名称空间中!所以在__call__函数中必须完成这两步操作,同时将初始化完成的对象返回给调用者。

    • 一旦覆盖了__call__函数,就必须自己来完成上述的几个步骤
    class MyMate(type):
        def __call__(self, *args, **kwargs):
            # 创建空对象
            # 调用init
            # 返回初始化后的对象
            obj = object.__new__(self)
            self.__init__(obj,*args,**kwargs)
            return obj
    class Foo(metaclass=MyMate):
        def __init__(self):
            print("初始化对象")
    f = Foo()
    print(f)
    

    通过元类来控制一个类实例化对象的过程

    • 只需覆盖__call__函数我们就能完成对实例化过程的控制
    #需求:
    #2.要求实例化时传参必须为关键字形式,否则抛出异常TypeError: must use keyword argument
    #3.key作为用户自定义类产生对象的属性,且所有属性变成大写
    class Mymetaclass(type):
        def __call__(self, *args, **kwargs):
            if args:
                raise TypeError('must use keyword argument for key function')
            obj = object.__new__(self) #创建对象,self为类Chinese
    
            for k,v in kwargs.items():
                obj.__dict__[k.upper()]=v
            return obj
    class Chinese(metaclass=Mymetaclass):
        country='China'
        tag='Legend of the Dragon' #龙的传人
        def walk(self):
            print('%s is walking' %self.name)
    p=Chinese(name='egon',age=18,sex='male')
    print(p.__dict__)
    

    补充:

    ​ 产生来Teacher的过程就是在调用Mymeta,而Mymeta也是type类的一个对象,那么Mymeta之所以可以调用,一定是实现了__call__方法,但是我们就算自己写该方法,类也可以被创建,这是因为type中已经有默认的__call__的实现了

    #伪代码
     class type:
         def __call__(self, *args, **kwargs): #self=<class '__main__.Mymeta'>
             obj=self.__new__(self,*args,**kwargs) # 产生Mymeta的一个对象
             self.__init__(obj,*args,**kwargs) 
             return obj
    

    五、元类实现单例

    什么是单例

    • 单例是指的是单个实例,指一个类只能有一个实例对象

    为什么要用单例

    • 当一个类的实例中的数据不会变化时使用单例,数据是不变的
    • 例如开发一个音乐播放器程序,音乐播放器可以封装为一个对象,那你考虑一下,当你切歌的时候,是重新创建一个播放器,还是使用已有的播放器?
    • 因为播放器中的数据和业务逻辑都是相同的没有必要创建新的,所以最好使用单例模式,以节省资源
    • 当两个对象的数据完全相同时 则没有必要占用两份资源
    #使用classmethod 实现单例
    class Player():
        def __init__(self):
            print("创建播放器了")
        __play = None
        @classmethod
        def get_player(cls):
            if not cls.__play:
                cls.__play = Player()
            return cls.__play
    
    
    p1 = Player.get_player();
    p1 = Player.get_player();
    p1 = Player.get_player();
    p1 = Player.get_player();
    
    • 该方法无法避免使用者直接调用类来实例化,这样就不是单例了
    • 使用元类实现单例模式
    #在类定义时 自动执行init 在init中创建实例 call中直接返回已有实例
    class MyMeta(type):
        __instance = None
    
        def __init__(self,name,bases,dic):
            if not self.__instance:
                self.__instance = object.__new__(self)
                self.__init__(self.__instance)
    
            super().__init__(name, bases, dic)
    
    
        def __call__(cls):
            return cls.__instance
    
    class Player(metaclass=MyMeta):
        def __init__(self):
            print("创建播放器了")
    Player()
    Player()
    # 仅执行一次创建播放器
    

    六、元类之属性查找

    • 当一个类既有父类又有元类时属性的查找顺序是什么样的?

    • 回顾一下,在没有元类时属性的查找是基于MRO列表的顺序,这个点还是相同的,那我们为某个类增加元类后,元类中的属性,什么时候会被使用到呢?来看一个例子

    class Mymeta(type): #只有继承了type类才能称之为一个元类,否则就是一个普通的自定义类
        n=444
        def __new__(cls, *args, **kwargs):
            pass
    class Bar(object):
        n = 333
        def __new__(cls, *args, **kwargs):
            pass
    class Foo(Bar):
        n=222
        def __new__(cls, *args, **kwargs):
            pass
    class Teacher(Foo,metaclass=Mymeta):
        n=111
        def __new__(cls, *args, **kwargs):
            pass
        school='Tsinghua'
    print(Teacher.__new__)
    print(Teacher.n)
    

    ​ 测试结果表明:属性查找的顺序依然是遵循MRO列表顺序,当顶级类object中不存在时会查找元类,元类没有时查找元类的父类也就是type类

    七、令人迷惑的__new__函数与__init__函数

    ​ 当你要创建类对象时,会首先执行元类中的__new__方法,拿到一个空对象,然后会自动调用__init__来对这个类进行初始化操作

    注意:如果你覆盖了该方法则必须保证,new方法必须有返回值且必须是,对应的类对象

    class M(type):
        def __init__(self,clsname,bases,namespace):
            print("init")
        def __call__(self, *args, **kwargs):
            pass
        pass
    class A(metaclass=M):
        n = 1
        pass
    print(A.__name__)
    print(A.__bases__)
    print(A.__dict__)
    """输出
    init
    A
    (<class 'object'>,)
    {'__module__': '__main__', 'n': 1, '__dict__': <attribute '__dict__' of 'A' objects>, '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None}
    """
    

    ​ 我们已经知道__init__可以控制类的创建过程,但是现在我们看到的是,init中没有任何代码但是类的三个基本信息已经都有了,这说明类的创建其实已经完成了

    class M(type):
        def __new__(cls, *args, **kwargs):
            print("new")
            #return type.__new__(cls,*args,**kwargs)
        def __init__(self,clsname,bases,namespace):
            print("init")
    class A(metaclass=M):
        n = 1
    print(A.__name__)
    print(A.__bases__)
    print(A.__dict__)
    """输出
    new
    Traceback (most recent call last):
      File "/Users/jerry/PycharmProjects/元类属性查找.py", line 43, in <module>
        print(A.__name__)
    AttributeError: 'NoneType' object has no attribute '__name__'"""
    

    ​ 执行了__new__函数但是并没有执行__init__,因为__new__函数是真正用于创建类的方法,只有创建类成功了才会执行init函数,new必须要有返回值且返回值类型为__type__时才会执行__init__函数,将__new__中被注释的代码打开 一切正常! 再一次印证了第四节中的伪代码

    总结:元类中__new__是用于创建类对象的 __init__是用于初始化类的其他信息的

  • 相关阅读:
    详解 字符转换流
    详解 编码和解码
    详解 字符流
    详解 序列输入流
    详解 数据输入输出流
    八皇后
    这次我们从底层把线程说清楚
    四数之和递归
    PCB 内网实现《OCR文字识别》实现逻辑
    PCB 线路板人生
  • 原文地址:https://www.cnblogs.com/DcentMan/p/11307250.html
Copyright © 2011-2022 走看看