元类


引入

  • python中一切皆对象,那么本质上一也是一个对象

一、什么是元类

类既然也是对象,那么就应该有另一个类来实例化得到它, 实例化得到类的类就是元类

默认情況下, 元类是 type 这个类, 并且所有的类都是由元类实例化得到的, 包括他自己

1、先定义一个类来进行分析

class Immortal(object):
    def __init__(self, name, age):
        self.name = name
        self.age = age


P = Immortal("赵总", 3345)
print(type(P))  # <class '__main__.Immortal'>

所有的对象都是通过 [类名] + ( ) 得到的, 也叫做实例化, p 对象就是由 Immortal 类实例化得到的

一切皆对象,那么 Immortal 应该也是一个类实例化的结果, 于是我们可以推导出 元类+( ) —> Immortal

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

通过 type 函数我们发现 Immortal 的类就是 type 类, 并惊奇的发现 type 类的类是自身

由此可以判断出:元类实例化得到类, 类实例化得到对象(实例), 并且验证了所有类都是由type实例化得到的,

元类_Python教程

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

上面我们都是使用 class 这个关键字来产生类的, class 关键字在帮我们创建类的时候必然调用了 Immortal = type( ) 这种方法, 那么 type 里的参数应该是什么呢?

1.内置函数exec的用法

在分析class工作流程之前我们先来了解一下exec函数作为储备知识:exec有三个参数

  • 第一个参数是包含的要执行的一系列 Python 代码(字符串格式)
  • 第二个参数是全局名称空间 (字典形式), 默认为 globals( )
  • 第三个参数是局部名称空间 (字典形式), 默认为 local( )

作用:可以将字符串当做python语句来执行, 并将期间产生的名字存放于局部名称空间中

flag = '''
name="淘小欣"
age=22
dic = {"sex":"man"}
def test():
    print("I am 淘小欣")
'''
globals_dic = {}  # 全局名称空间
locals_dict = {}  # 局部名称空间
exec(flag, globals_dic, locals_dict)
print(locals_dict)
'''
{'name': '淘小欣', 'age': 22, 'dic': {'sex': 'man'}, 'test': <function test at 0x000001E4C9B061F0>}
'''
print(locals_dict["name"])  # 淘小欣
locals_dict["test"]()  # I am 淘小欣

2.调用type+()来实现创建类

原来我们使用 class 来创建类, 其中必然调用了元类 type, type中需要传入三个参数, 这三个参数是类的三大组成部分 :

  • 类名:Immortal = type( )
  • 父类们(基类们):class_bases = (object , )
  • 类的名称空间: class_namespace (类的名称空间是执行类体代码而得到的)

接下来我们开始动手创建一个类了

# 设置类名
class_name = "Immortal"

# 设置基类们
class_bases = (object,)

# 设置类体(类的名称空间)
class_body = '''
name="淘小欣"
age=22
def print_name(self):
    print(f"I am {self.name}"}
'''
# 设置类名
class_name = "Immortal"

# 设置基类们
class_bases = (object,)

# 设置类体(类的名称空间)
class_body = '''
name = "jiji"
age = 22
def print_name(self):
    print(f"I am {self.name}")
'''
class_namespace = {}

# 运行"exec"函数
exec(class_body, {}, class_namespace)

# 创建类
Immortal = type(class_name, class_bases, class_namespace)

# 实例化
p = Immortal()
print(p.name, p.age)  # jiji 22
p.print_name()  # I am jiji

以上操作就是class关键字的工作流程, 相当于下面的效果

class Immortal(object):
    name="淘小欣"
    age=22
    def print_name(self):
        print(f"i am {self.name}")
p=Immortal()

由此可知 “class” 关键字的本质就是Immortal = type('Immortal',(object,),dic)

三.自定义元类来控制类的创建过程

既然我们已经了解了 class 类的工作流程, 那我们就可以使用这种原理来自定义自己的元类

1.metaclass 关键字指定元类

首先我们得了解, 一个类如果没有声明自己的元类, 那么它默认的元类就是 type, 除了使用默认的元类, 我们还可以通过继承 type 类来自定义元类, 指定元类的关键字是 : metaclass

class Monster(metaclass=type):  # 一个类默认的元类是type, 像左边这样
    pass

class Demon(metaclass=Mytype):  # 像这样, 我们可以使用metaclass关键字来指定一个类的元类
    pass 

2.自定义元类

值得注意的是:如果是元类, 那么必须继承 type, 否则就是一个普通的自定义类

class Mytype(type):  # 只有继承了 type 的类才能称之为一个元类, 否则就是一个普通的自定义类
    ...


class Monster(metaclass=Mytype):  # 使用 metaclass 关键字来指定元类 Mytype
    def __init__(self, name, age):
        self.name = name
        self.age = age
# "class Monster(metaclass=Mytype):" = "Monster = Mytype('Monster',(object,),{})"

上面是一个简单版的自定义元类, 我们可以知道的是 class Monster(metaclass=Mytype): 这一句等于 Monster = Mytype('Monster',(object,),{}),看到这是不是清晰了很多呢?

下面我们在Mytype中进行一些初始化类的设置

class Mytype(type):  # 只有继承了 type 的类才能称之为一个元类
    def __init__(self,class_name,class_bases,class_namespace):
        print(self)             # <class '__main__.Monster'>
        print(class_name)       # Monster
        print(class_bases)      # (<class 'object'>,)
        print(class_namespace)  # {'__module__': '__main__', '__qualname__': 'Monster', '__init__': <function Monster.__init__ at 0x000002A528B4CDC8>}
        super().__init__(class_name,class_bases,class_namespace)  # 使用父类的__init__来完成初始化

class Monster(metaclass=Mytype):  # 指定元类
    def __init__(self,name,age):
        self.name = name
        self.age = age

print(type(Monster))  # <class '__main__.Mytype'> (可以发现是自定义的元类创建出来的)

接下来我们再对这个元类添加一些对创建类的限制功能

  • 需求1 : 类名必须首字母大写, 否则抛出异常
  • 需求2 : 类中必须有文档注释, 否则抛出异常
class Mytype(type):
    def __init__(self,class_name,class_bases,class_namespace):
        super().__init__(class_name,class_bases,class_namespace)
		
        # 设置需求1
        if not class_name.istitle():
            raise Exception('类名首字母必须大写')
		
        # 设置需求2
        doc = class_namespace.get("__doc__")
        if not doc or len(doc) == 0:
            raise Exception('注释文档不能为空')

class Monster(metaclass=Mytype):
    '''
    我是Monster的注释
    '''
    def __init__(self,name,age):
        self.name = name
        self.age = age

上面创建的类首字母大写了,也有注释,是正常运行的, 接下来我们来测试一下两种异常

1、 首字母没有大写
class monster(metaclass=Mytype):
    '''
    我是Monster的注释
    '''
    def __init__(self,name,age):
        self.name = name
        self.age = age
# 抛出异常 : Exception: 类名首字母必须大写

2、没有注释信息
class Monster(metaclass=Mytype):
    def __init__(self,name,age):
        self.name = name
        self.age = age
# 抛出异常 : Exception: 注释文档不能为空
  • 需求3 : 在元类中控制把自定义类的数据属性都变成大写 (有坑)

    # 刚学过使用元类来控制类的产生, 解决这个问题是不是非常简单呢? 直接干!
    class Mytype(type):
        def __init__(self,name,bases,dic):   # 初始化类
            super().__init__(name,bases,dic)
            new_dic = {}  # 创一个字典存放修改后的值
            for k,v in dic.items():
                if k.startswith("__") or callable(k):  # "__"开头和可执行方法不修改
                    new_dic[k] = v
                else:
                    new_dic[k.upper()] = v
            self.__dict__ = new_dic  # 将字典赋值给类的属性字典
    
    class Person(metaclass=Mytype):
        name = "Jack"
        age = 19
        sex = "man"
    
    #  AttributeError: ..... is not writable (抛出异常,提示我们根本不可写)
    # 我们在初始化方法的下方打印下"self.__dict__"的类型
    print(type(self.__dict__))  # <class 'mappingproxy'>
    #   发现并不是"dict"类型, 所以我们无法对他进行修改
    
    
  • 需求3解决方法 : 既然在**“init"里面无法修改, 那我们就应该想到可以在对象出来的那一刻对其进行修改, 创建空对象的方法是"new”**

    class Mytype(type):
        def __new__(self,name,bases,dic):
            new_dic = {}  # 先创建一个空字典用来接收修改后的属性
            for k,v in dic.items():
                if k.startswith("__") or callable(k):  # "__"开头以及可执行的属性不进行大写操作
                    new_dic[k] = v
                else:
                    new_dic[k.upper()] = v
            return super(Mytype, self).__new__(self,name,bases,new_dic)  # 重新调用父类的"__new__"方法以新的名称空间创建一个对象并返回
    
    class Person(metaclass=Mytype):  # 实际进行的操作 : Person = Mytype('Person',(object,),dic)
        name = "Jack"
        age = 12
        sex = "man"
    
    print(Person.__dict__)
    '''
    {'__module__': '__main__', 'NAME': 'Jack', 'AGE': 12, 'SEX': 'man', '__dict__': <attribute '__dict__' of 'Person' objects>, '__weakref__': <attribute '__weakref__' of 'Person' objects>, '__doc__': None}
    '''
    #  因为在"__new__"里接收的"dic"是一个字典类型(名称空间), 我们可以对其进行修改, 或者传入新的字典进行创建
    
    

四.自定义元类来控制类的调用 (类实例化)

1、__call__方法的触发

在进行操作之前我们先复习一下 call 方法来进行储备

  • 触发条件:
  • [对象] + ( ) 就触发 __call__ 的执行
class Person:

    def __call__(self, *args, **kwargs):
        print(self)
        print(args)
        print(kwargs)

P1 = Person()

P1(1,2,3,4,name="JiJi")  # 对象 + ( )
'''
<__main__.Person object at 0x000001AC732E6520>
(1, 2, 3, 4)
{'name': 'JiJi'}
'''

由上可知, 调用一个对象最先触发的就是类中的 __call__ 方法, 一切皆对象, 那么调用这个类的时候是不是应该触发这个类的类的__call__方法呢?

下面我们简单验证一下:

class Mytype(type):
    def __call__(self, *args, **kwargs):
        print(self)    # <class '__main__.Monster'>
        print(args)    # (1, 2, 3, 'a')
        print(kwargs)  # {'name': 'shawn'}
        return 111111

class Monster(metaclass=Mytype):
    ...

p = Monster(1,2,3,"a",name = "吉吉王国")
print(p)  # 111111
'''
<class '__main__.Monster'>
(1, 2, 3, 'a')
{'name': '吉吉王国'}
111111
'''

显而易见的结论:

  • 调用 Monster 就是在调用 Mytype 中的__call__方法
  • 然后将 Monster 传给 Mytype 中的 self, 溢出的位置参数传给 *,溢出的关键字参数传给 **
  • 调用 Monster 的返回值就是调用 __call__ 的返回值

2.重写元类中的 call 方法来控制 Monster 的调用过程

默认的,一个类的调用 (p = Monster(“name”,29)像这种) 会发生三件事 :

  • 调用__new__ 方法创建一个空对象 obj
  • 调用__init__方法初始化对象 obj
  • return 初始化后的对象 obj
class Mytype(type):
    def __call__(self, *args, **kwargs):
        print("i am Mytype_call")           # 添加测试
        obj = self.__new__(self)            # 创建空对象 obj
        self.__init__(obj,*args,**kwargs)   # 通过父类来完成obj的初始化 (self=Monster,obj=p)
        return obj                          # 返回初始化之后的 obj

class Monstar(metaclass=Mytype):
    def __init__(self,name,age):
        self.name = name
        self.age = age

p = Monstar("小宋同学",22)  # i am Mytype_call
print(p.name)            # 小宋同学
print(p.age)             # 22
print(p)                 # <__main__.Monstar object at 0x000001881394AD08>

当整个流程已经明了之后我们就可以随心所欲的对调用过程做一些"手脚"

  • 需求1 : 在元类中控制自定义的类无需使用__init__方法

    class Mytype(type):
        def __call__(self, *args, **kwargs):
            obj = self.__new__(self)
            for k,v in kwargs.items():  # 我们直接遍历 kwargs 取出 key 和 value
                obj.__dict__[k] = v     # 再将其放入到对象的属性字典中去
    
            return obj
    
    class Person(metaclass=Mytype):
        def print_dict(self):
            print(self.__dict__)
    
    p = Person(name="小宋同学",age=22)    # 这种设计方式就需要你调用传参的时候按照键值对的形式来传
    print(p.name,p.age)  # 小宋同学 22
    p.print_dict()       # {'name': '小宋同学', 'age': 22}
    
  • 需求2 : 在元类中控制自定义的类产生的对象相关的属性全部为隐藏属性

    class Mytype(type):
        def __call__(self, *args, **kwargs):
            obj = self.__new__(self)  # 得到空对象obj (self = Person)
            self.__init__(obj, *args, **kwargs)  # 初始化对象obj (obj = p)
    
            # 循环取出对象 p 属性字典里面的 key 和 value 进行隐藏属性操作(这里使用的是字典推导式)
            obj.__dict__ = {f"_{self.__name__}__{k}": v for k, v in obj.__dict__.items()}
            return obj  # 返回处理后的对象
    
    
    class Person(metaclass=Mytype):
        def __init__(self, name, age, sex):
            self.name = name
            self.age = age
            self.sex = sex
    
        def print_name(self):  # 设置一个查看 name 的方法
            print(self.__name)
    
    
    p = Person("lihua", 22, "man")
    # print(p.name)   # 抛出异常 : "AttributeError" 没有该属性
    # print(p.age)    # 抛出异常 : "AttributeError" 没有该属性
    p.print_name()  # lihua
    print(p.__dict__)  # {'_Person__name': 'lihua', '_Person__age': 22, '_Person__sex': 'man'}
    
    

    至此, 自定义元类来控制类的创建过程以及**自定义元类来控制类的调用 (类实例化)**已经介绍完毕了

五.加入元类之后的属性查找

1.类属性查找顺序 :

  • 先对象层 : Person -----> Foo -----> Bar -----> object (mro列表)
  • 再元类层 : Mytype -----> type
  • 对象的属性查找: 只会找到object. 不会找元类.

元类_Python开发_02

class Mytype(type):
    n = 555
    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 Person(Foo,metaclass=Mytype):
    # n=111
    def __init__(self,name,age):
        self.name=name
        self.age=age

print(Person.n)

2.使用__new__创建空对象的两种方式

  • obj = self.new(self) : 就是上面我们使用的方法, 推荐使用这种, 如果 self 中没有**new**方法, 它是按照继承关系去查找 new 的, 一层层往上找, 最终能在 object 中找到, 不必到 元类中去找

  • obj = object.new(self) : 这种方法是直接跳过前面三个类去找 objectnew 方法, 如下图 :应该是这样画

    元类_Python教程_03