元类metaclass

元类metaclass

一 什么是元类

#### python中一切皆是对象。以如下代码为例分析:

# 元类=》OldboyTeacher类=》obj
class OldboyTeacher(object):
    school = 'oldboy'

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

    def say(self):
        print('%s says welcome to the oldboy to learn Python' % self.name)
#### 	所有的对象都是实例化或者说调用类而得到的(调用类的过程称为类的实例化) ,比如对象obj是调用类OldboyTeacher得到的

obj = OldboyTeacher('egon', 18)  # 调用OldboyTeacher类=》对象obj
#                                  调用元类=》OldboyTeacher类

# print(type(obj))  #查看对象obj的类是<class '__main__.OldboyTeacher'>
#### 如果一切皆为对象,那么类OldboyTeacher本质上也是一个对象, 既然所有的对象都是调用类得到的,那么OldboyTeacher必然也是调用一个类得到的,这个类称之为元类。

print(type(OldboyTeacher))  # 结果为<class 'type'>,证明是调用了type这个元类而产生的OldboyTeacher,即默认的元类为type
# 结论:
	默认的元类是type,默认的情况下,我们使用class关键字定义的类都是由type产生的。

![实例化](https://gitee.com/chaochaofan/img/raw/master/001 实例化.png)

1 类定义被执行时的步骤

1) 解析 MRO 条目;	  
	# 如果在类定义中出现的基类不是 type 的实例,则使用 __mro_entries__ 方法对其进行搜索,当找到结果时,它会以原始基类元组做参数进行调用。此方法必须返回类的元组以替代此基类被使用。元组可以为空,在此情况下原始基类将被忽略。

2) 确定适当的元类;		
	# 为一个类定义确定适当的元类是根据以下规则:
    2.1) 如果没有基类且没有显式指定元类,则使用 type();
    2.2) 如果给出一个显式元类而且 不是 type() 的实例,则其会被直接用作元类;
    2.3) 如果给出一个 type() 的实例作为显式元类,或是定义了基类,则使用最近派生的元类。
	- 最近派生的元类会从显式指定的元类(如果有)以及所有指定的基类的元类(即 type(cls))中选取。最近派生的元类应为 所有 这些候选元类的一个子类型。如果没有一个候选元类符合该条件,则类定义将失败并抛出 TypeError。

3) 准备类命名空间;		
	- 一旦确定了适当的元类,则将准备好类命名空间。 如果元类具有 __prepare__ 属性,它会以 namespace = metaclass.__prepare__(name, bases, **kwds) 的形式被调用(其中如果有任何额外的关键字参数,则应当来自类定义)。 __prepare__ 方法应该被实现为 classmethod()。 __prepare__ 所返回的命名空间会被传入 __new__,但是当最终的类对象被创建时,该命名空间会被拷贝到一个新的 dict 中。
	- 如果元类没有 __prepare__ 属性,则类命名空间将初始化为一个空的有序映射。

4) 执行类主体;		 
	- 类主体会以(类似于) exec(body, globals(), namespace) 的形式被执行。普通调用与 exec() 的关键区别在于当类定义发生于函数内部时,词法作用域允许类主体(包括任何方法)引用来自当前和外部作用域的名称。
	- 但是,即使当类定义发生于函数内部时,在类内部定义的方法仍然无法看到在类作用域层次上定义的名称。类变量必须通过实例的第一个形参或类方法来访问,或者是通过下一节中描述的隐式词法作用域的 __class__ 引用。

5) 创建类对象。		 
	- 一旦执行类主体完成填充类命名空间,将通过调用 metaclass(name, bases, namespace, **kwds) 创建类对象(此处的附加关键字参数与传入 __prepare__ 的相同)。
	- 如果类主体中有任何方法引用了 __class__ 或 super,这个类对象会通过零参数形式的 super(). __class__ 所引用,这是由编译器所创建的隐式闭包引用。这使用零参数形式的 super() 能够正确标识正在基于词法作用域来定义的类,而被用于进行当前调用的类或实例则是基于传递给方法的第一个参数来标识的。

二 class关键字创建类的流程分析

	class 关键字帮我们创建类的时候,必然帮我们调用了元类 `OldboyTeacher=type(...)`,那调用type时传入的参数是类的三大组成部分。

1 类的三大组成部分

1).类名class_name
	class_name = 'OldboyTeacher'

2).基类们class_bases
	class_bases = (object, )

3).类的名称空间class_dic
	类的名称空间是执行类体代码而得到的

2 class关键字创建类的四个过程

1).先拿到类名
	class_name = "OldboyTeacher"

2).然后拿到类的基类/父类们
	class_bases = (object,)

3).再运行类体代码,拿到类的名称空间
	class_dic = {}

4).调用元类(传入三大要素:类名,基类,类的名称空间) 得到一个元类的对象
	然后将元类的对象赋值给变量名OldboyTeacher,OldboyTeacher就是我们用class自定义的那个类。
	OldboyTeacher = type(class_name, class_bases, class_dic)

![](https://gitee.com/chaochaofan/img/raw/master/002 创建类的过程.png)

3 补充:exec的用法

	运行类体代码,拿到类的名称空间的过程中,实际上使用了exec() 得到了class_dic
#exec:三个参数#参数一:包含一系列python代码的字符串#参数二:全局作用域(字典形式) ,如果不指定,默认为globals()#参数三:局部作用域(字典形式) ,如果不指定,默认为locals()#可以把exec命令的执行当成是一个函数的执行,会将执行期间产生的名字存放于局部名称空间中exec(class_body,{},class_dic)

三 自定义元类

	一个类没有声明自己的元类,默认他的元类就是type,除了使用内置元类type,我们也可以通过继承type来自定义元类, 然后使用metaclass关键字参数为一个类指定元类。
class Mymeta(type):  # 只有继承了type类的类才是自定义的元类,否则就是一个普通类    passclass OldboyTeacher(object, metaclass=Mymeta):    school = 'oldboy'    def __init__(self, name, age):        self.name = name        self.age = age    def say(self):        print('%s says welcome to the oldboy to learn Python' % self.name)# 1、先拿到一个类名:"OldboyTeacher"# 2、然后拿到类的父类:(object,)# 3、再运行类体代码,将产生的名字放到名称空间中{...}# 4、调用元类(传入类的三大要素:类名、基类、类的名称空间) 得到一个元类的对象,然后将元类的对象赋值给变量名OldboyTeacher,oldboyTeacher就是我们用class自定义的那个类OldboyTeacher = Mymeta("OldboyTeacher",(object,),{...})

四 自定义元类来控制类的产生

# 自定义的元类可以控制类的产生过程,类的产生过程其实就是元类的调用过程,即:    OldboyTeacher=Mymeta('OldboyTeacher',(object),{...}),调用Mymeta会产生一个空对象OldboyTeacher,然后连同调用Mymeta括号内的参数一同传给Mymeta下的__ init __ 方法,完成初始化。
import reclass Mymeta(type):  # 只有继承了type类的类才是自定义的元类    def __init__(self, class_name, class_bases, class_dic):        # print(self)  # 类<class '__main__.OldboyTeacher'>        # print(class_name)        # print(class_bases)        # print(class_dic)        if not re.match("[A-Z]", class_name):            raise BaseException("类名必须用驼峰体")        if len(class_bases) == 0:            raise BaseException("至少继承一个父类")        # print("文档注释:",class_dic.get('__doc__'))        doc=class_dic.get('__doc__')        if not (doc and len(doc.strip()) > 0):            raise BaseException("必须要有文件注释,并且注释内容不为空")# OldboyTeacher = Mymeta("OldboyTeacher",(object,),{...})class OldboyTeacher(object,metaclass=Mymeta):    """    adsaf    """    school = 'oldboy'    def __init__(self, name, age):        self.name = name        self.age = age    def say(self):        print('%s says welcome to the oldboy to learn Python' % self.name)

五 自定义元类来控制类的调用

1 前置知识:__call__

	调用一个对象,就是触发对象所在类中的 `__call__ `方法的执行。​	自定义元类,必须使用`__call__ `方法,不然类无法被调用,也没有实例化过程中的各个步骤,以及也无法有返回值。
class Foo:    def __call__(self, *args, **kwargs):        print(self)        print(args)        print(kwargs)obj=Foo()#1、要想让obj这个对象变成一个可调用的对象,需要在该对象的类中定义一个方法__call__方法,该方法会在调用对象时自动触发#2、调用obj的返回值就是__call__方法的返回值res=obj(1,2,3,x=1,y=2)
# python官方文档解释:# 模拟可调用对象object.__call__(self[, args...])# 此方法会在实例作为一个函数被“调用”时被调用;如果定义了此方法,则 x(arg1, arg2, ...) 就相当于 x.__call__(arg1, arg2, ...) 的快捷方式。

2 自定义元类来控制类OldboyTeacher的调用

	如果把OldboyTeacher也当作一个对象,那么OldboyTeacher这个对象也必然存在一个`__call__ ` 。
import reclass Mymeta(type):  # 只有继承了type类的类才是自定义的元类    def __init__(self, class_name, class_bases, class_dic):        # print(self)  # 类<class '__main__.OldboyTeacher'>        # print(class_name)        # print(class_bases)        # print(class_dic)        if not re.match("[A-Z]", class_name):            raise BaseException("类名必须用驼峰体")        if len(class_bases) == 0:            raise BaseException("至少继承一个父类")        # print("文档注释:",class_dic.get('__doc__'))        doc = class_dic.get('__doc__')        if not (doc and len(doc.strip()) > 0):            raise BaseException("必须要有文件注释,并且注释内容不为空")    # res = OldboyTeacher('egon',18)    def __call__(self, *args, **kwargs):        # 1、先创建一个老师的空对象        tea_obj = object.__new__(self)        # 2、调用老师类内的__init__函数,然后将老师的空对象连同括号内的参数的参数一同传给__init__        self.__init__(tea_obj, *args, **kwargs)        tea_obj.__dict__ = {"_%s__%s" %(self.__name__,k): v for k, v in tea_obj.__dict__.items()}        # 3、将初始化好的老师对象赋值给变量名res        return tea_obj# OldboyTeacher = Mymeta("OldboyTeacher",(object,),{...})class OldboyTeacher(object, metaclass=Mymeta):    """    adsaf    """    school = 'oldboy'    #            tea_obj,'egon',18    def __init__(self, name, age):        self.name = name  # tea_obj.name='egon'        self.age = age  # tea_obj.age=18    def say(self):        print('%s says welcome to the oldboy to learn Python' % self.name)res = OldboyTeacher('egon', 18)print(res.__dict__)# print(res.name)# print(res.age)# print(res.say)# 调用OldboyTeacher类做的事情:# 1、先创建一个老师的空对象# 2、调用老师类内的__init__方法,然后将老师的空对象连同括号内的参数的参数一同传给__init__# 3、将初始化好的老师对象赋值给变量名res

3 调用类做的三件事情:

1).产生一个空对象obj;2).调用类内的`__init__ `方法,然后将空对象连同括号内的参数的参数一同传给`__init__ `;3).将初始化好的对象赋值给变量名obj。

六 单例模式

1 什么是单例模式

	单例模式(Singleton Pattern) 是最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。​	这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

2 为何要有单例模式

## 1.意图:	保证一个类仅有一个实例,并提供一个访问它的全局访问点。## 2.主要解决:	一个全局使用的类频繁地创建与销毁。

3 如何使用单例模式

## 1.何时使用:	当您想控制实例数目,节省系统资源的时候。## 2.如何解决:	判断系统是否已经有这个单例,如果有则返回,如果没有则创建。## 3优点:    (1) 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存) 。    (2) 避免对资源的多重占用(比如写文件操作) 。## 4缺点:	没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

4 三种实现单例模式的方法

# 如果我们从配置文件中读取配置来进行实例化,在配置相同的情况下,就没必要重复产生对象浪费内存了# settings.py文件内容如下HOST = '1.1.1.1'PORT = 3306

4.1 实现方式一:classmethod

	可以使用classmethod,使用类调用类内的单例函数,来完成唯一的值产生。
import settingsclass MySQL:    __instance = None  # 设置变量,    def __init__(self, ip, port):        self.ip = ip        self.port = port    @classmethod    def singleton(cls):        if cls.__instance:  # 如果已实例化,则不再进行新的实例化            return cls.__instance        cls.__instance = cls(settings.IP, settings.PORT)  # 未实例化,开始实例化        return cls.__instance# obj1=MySQL("1.1.1.1",3306)# obj2=MySQL("1.1.1.2",3306)# print(obj1)# print(obj2)obj3 = MySQL.singleton()print(obj3)obj4 = MySQL.singleton()print(obj4)

4.2 实现方式二:元类

import settingsclass Mymeta(type):    __instance = None    def __init__(self, class_name, class_bases, class_dic):  # 定义类Mysql时就触发        # 事先先从配置文件中取配置来造一个Mysql的实例出来        self.__instance = object.__new__(self)  # Mysql类的对象        self.__init__(self.__instance, settings.IP, settings.PORT)  # 初始化对象        # 上述两步可以合成下面一步        # self.__instance=super().__call__(*args,**kwargs)    def __call__(self, *args, **kwargs):  # Mysql(...)时触发        if args or kwargs:  # args或kwargs内有值            obj = object.__new__(self)            self.__init__(obj, *args, **kwargs)            return obj        else:            return self.__instance# MySQL=Mymeta(...)class MySQL(metaclass=Mymeta):    def __init__(self, ip, port):        self.ip = ip        self.port = port# obj1 = MySQL("1.1.1.1", 3306)# obj2 = MySQL("1.1.1.2", 3306)# print(obj1)# print(obj2)obj3 = MySQL()obj4 = MySQL()print(obj3 is obj4)

4.3 实现方式三:装饰器

import settingsdef outter(func):  # func = MySQl类的内存地址    _instance = func(settings.IP,settings.PORT)    def wrapper(*args,**kwargs):        if args or kwargs:            res=func(*args,**kwargs)            return res        else:            return _instance    return wrapper@outter  # MySQL=outter(MySQl类的内存地址)  # MySQL=》wrapperclass MySQL:    def __init__(self, ip, port):        self.ip = ip        self.port = port# obj1 = MySQL("1.1.1.1", 3306)# obj2 = MySQL("1.1.1.2", 3306)# print(obj1)# print(obj2)obj3 = MySQL()obj4 = MySQL()print(obj3 is obj4)

4.4 方式二类中的 __call__方法做的三件事情:

    1).调用`__new__`产生一个空对象obj;    2).调用`__init__`初始化空对象obj,然后将空对象连同括号内的参数的参数一同传给`__init__`;    3).将初始化好的对象赋值给变量名obj。

七 了解:属性查找

1 对象OldboyTeacher里的属性查找

	我们用class自定义的类也全都是对象(包括object类本身也是元类type的 一个实例,可以用type(object)查看) ,如果把类当成对象去看,将下述继承应该说成是:对象OldboyTeacher继承对象Foo,对象Foo继承对象Bar,对象Bar继承对象object
class Mymeta(type): #只有继承了type类才能称之为一个元类,否则就是一个普通的自定义类    n=444    def __call__(self, *args, **kwargs): #self=<class '__main__.OldboyTeacher'>        obj=self.__new__(self)        self.__init__(obj,*args,**kwargs)        return objclass Bar(object):    n=333class Foo(Bar):    n=222class OldboyTeacher(Foo,metaclass=Mymeta):    n=111    school='oldboy'    def __init__(self,name,age):        self.name=name        self.age=age    def say(self):        print('%s says welcome to the oldboy to learn Python' %self.name)print(OldboyTeacher.n) #自下而上依次注释各个类中的n=xxx,然后重新运行程序,发现n的查找顺序为OldboyTeacher->Foo->Bar->object->Mymeta->type
# 属性查找应该分两层,一层是对象层(基于c3算法的MRO) 的查找,另外一个则是元类(即元类层) 的查找。

![](https://gitee.com/chaochaofan/img/raw/master/003 属性查找1.png)

![](https://gitee.com/chaochaofan/img/raw/master/004 属性查找2.png)

#查找顺序:#1、先对象层:OldoyTeacher->Foo->Bar->object#2、然后元类层:Mymeta->type

2 元类Mymeta中__call__里的self.__new__的查找

class Mymeta(type):     n=444    def __call__(self, *args, **kwargs): #self=<class '__main__.OldboyTeacher'>        obj=self.__new__(self)        print(self.__new__ is object.__new__) #Trueclass Bar(object):    n=333    # def __new__(cls, *args, **kwargs):    #     print('Bar.__new__')class Foo(Bar):    n=222    # def __new__(cls, *args, **kwargs):    #     print('Foo.__new__')class OldboyTeacher(Foo,metaclass=Mymeta):    n=111    school='oldboy'    def __init__(self,name,age):        self.name=name        self.age=age    def say(self):        print('%s says welcome to the oldboy to learn Python' %self.name)    # def __new__(cls, *args, **kwargs):    #     print('OldboyTeacher.__new__')OldboyTeacher('egon',18) 	# 触发OldboyTeacher的类中的__call__方法的执行,进而执行self.__new__开始查找
# 总结:	Mymeta下的__call__里的self.__new__在OldboyTeacher、Foo、Bar里都没有找到__new__的情况下,会去找object里的__new__,而object下默认就有一个__new__,所以即便是之前的类均未实现__new__,也一定会在object中找到一个,根本不会、也根本没必要再去找元类Mymeta->type中查找__new__
posted @ 2021-06-25 15:00  越关山  阅读(49)  评论(0)    收藏  举报