python 面向对象之元类

python 面向对象之元类

type方法的应用

1.查看数据类型

s1 = 'hello world'  # str()
l1 = [11, 22, 33, 44]  # list()
d1 = {'name': 'jason', 'pwd': 123}  # dict()
t1 = (11, 22, 33, 44)  # tuple()
print(type(s1))  # <class 'str'>
print(type(l1))  # <class 'list'>
print(type(d1))  # <class 'dict'>
print(type(t1))  # <class 'tuple'>

2.查看对象是哪个类产生的

class Student:
    pass
obj = Student()
print(type(obj))  # <class '__main__.Student'>

3.类其实也是一种对象,类是由谁产生的呢

class Student:
    pass
obj = Student()
print(type(obj))  # <class '__main__.Student'>
print(type(Student))  # <class 'type'>
class A:pass
class B:pass
print(type(A), type(B))  # <class 'type'> <class 'type'>

我们发现我们的类都是由type产生的。

而type就是一切类的生产者(包括它自己)被称为元类

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

创建类的底层机制

我们平常都是用class关键字来创建类的:

class 类名(父类):
	类体代码

而我们打开产生类的type它的源码,可以看见type除了传入对象来判断产生它的类,还有另一种用法

image

即传入类名,父类,类体名称空间产生一个新类。

class_body_code = """
name = 'leethon'
"""
class_dict = {}
# exec会执行代码并将一些名称传入class_dict名称空间
exec(class_body_code, {}, class_dict)
cls = type('Student', (object,), class_dict)  # 传入类名,传入父类,传入名称空间
obj = cls()
print(obj.__class__)  # <class '__main__.Student'>
print(obj.name)  # leethon

这个过程很麻烦,而且和class关键字所实现的还有差距,所以这里只做了解。

即我们的类实际上都是由元类产生的,底层就是用了一个type方法。

通过元类控制类的产生

因为类是由元类产生的,所以可以理解为类实际上就是元类进行实例化产生的对象,

联系对比:

  • 对象--通过类名()的方式产生--触发了类体中__init__得到了独有的属性
  • 类---通过元类产生--触发了元类中的__init__得到了每个类所独有的类属性

所以我们可以通过派生元类中的__init__方法达到控制类产生的目的。

那么派生自然要用到继承和super关键字,将元类type作为父类得到的类也是元类,因为它有产生类的方法。

class MyMetaClass(type):
    def __init__(cls, what, bases=None, dict=None):
        if not what.istitle():  # 如果类名不大写开头
            raise TypeError(f'类定义必须要大写,你看看你写的{what}')  # 报错终止程序
        # 如果符合开头大写的命名风格就继续执行
        super().__init__(what, bases, dict)  # 继承父类中的方法

class A(metaclass=MyMetaClass):  # 修改默认元类变为我们的元类,也修改了产生本类的方式
    pass
class bbb(metaclass=MyMetaClass):  # 在这里报错,TypeError: 类定义必须要大写,你看看你写的bbb
    pass

如果我们选择修改产生类的元类(通过修改metaclass关键字参数的方式),那么这个元类可以是我们基于type派生的,我们通过派生type的双下init方法就可以控制类的产生过程了。

通过__new__方法控制产生类的过程

  • 实际上,类产生时触发的语法是:type(class_name,bases,dict)
  • 类似于,对象产生时,也是调用类时,触发了元类中的__call__()一样。

类的产生经历了以下三步:

  1. 通过元类的双下new方法产生一个新的对象(类也是对象)
  2. 调用元类的双下init方法为对象添加独有属性
  3. 返回创建好的类

那么type函数经历了类似以下的代码

# 以下代码只是简单描述了下type,与源代码有出入,只列举了一些特征。
def type(class_name,bases,dict):
	# 1.调用type的new产生一个新类
	cls = type.__new__(mcs,class_name,bases,dict)  # 传入元类,名、父类、名称空间
	# 2.调用双下给新类一些特有的属性
	type.__init__(cls, class_name, bases=None, dict=None)  # 传入新类、名、父类,名称空间
	# 3.返回出类
	return cls

所以我们除了通过双下init控制,还可以通过派生双下new来控制类的产生过程。(无论是什么方法都要指定元类为我们自己编写的继承派生于type的元类)

而且,通过派生双下new来修改产生类的过程,影响更加深远,因为new方法是会传入mcs元类的,这个元类属性会去父类里面找,所以new方法可以不仅可以控制产生的类,还可以控制产生的类的子类的过程。

而与之相比,双下init只能修改通过这个元类产生的类的过程,因为init方法只会传入新类和一些类属性,并不拥有元类属性。

形象来记忆的话,双下new是基因改造,双下init是后天整容。

控制某个类的子类的所有字符串属性全部归拢到words字典中
class MyMetaClass(type):
    def __new__(mcs, class_name, class_bases, class_attrs):
        if class_name == 'Models':
            return type.__new__(mcs, class_name, class_bases, class_attrs)
        words = {}
        for k, v in class_attrs.items():
            # 所有的表的字段属性都拿出来
            if isinstance(v, str):  # 如果某属性的值是字符串类型就归拢到words中
                words[k] = v
        for k in words:
            class_attrs.pop(k)  # 属性中就字符串类型移除
        class_attrs['words'] = words
        return type.__new__(mcs, class_name, class_bases, class_attrs)
		
class Models(metaclass=MyMetaClass):  # 类
    pass
		
class Leethon(Models):  # 子类
    name = 'leethon'
	age = 18
	hobby = 'eat'
	
print(Leethon.age)  # 18
print(Leethon.words)  
# {'__module__': '__main__', '__qualname__': 'Leethon', 'name': 'leethon', 'hobby': 'eat'}
print(Leethon.name)  # 报错,因为name已经不是这个类的属性了

通过元类控制类产生对象

看标题有些相似,但要注意区分,上一小节指控制类的产生过程,这一节要说明对象的产生过程。

  • 我们在上一篇博客中提到,类的魔法方法__call__是在对象被加括号调用时自动触发的。

  • 那么将类看成对象,产生它的元类中的__call__也就会在类名加括号调用时自动触发。

也就是我们平常习以为常的obj = 类名()的方式产生对象的过程会自动触发元类中的__call__

理解了上面这一点后,我们就可以尝试从元类的__call__做手脚,来控制对象的产生了。

我们可以直接通过派生的方式修改,也可以直接重写的一个call

一般来说类的元类都是type,它的__call__,主要做了以下几件事:

class MyMetaClass(type):
    def __call__(cls, *args, **kwargs):
        # 1.就是产生一个空对象
        obj = cls.__new__(cls)
        # 2.调用类的init传入对象和参数
        cls.__init__(obj, *args, **kwargs)
        # 3.返回创建好的对象
        return obj
# 以上的__call__只是对type中的简单模仿,建议还是直接用super派生

class A(metaclass=MyMetaClass):  # 修改默认元类变为我们的元类,也修改了产生本类的方式
    pass


obj = A()  # 触发了MyMetaClass的__call__
print(obj)  # <__main__.A object at 0x0000025DBB436610>
让产生对象时只能传入关键字参数
# 实现方式1
class MyMetaClass(type):
    def __call__(cls, *args, **kwargs):
        if args:
            raise TypeError('你只能传入关键字参数')  # 在这一步控制了只能传入关键字参数
        return super().__call__(*args, **kwargs)  # 自动传入调用的对象,方法是父类的方法
		# 并将值返回出去,与原本call的结构一致

class A(metaclass=MyMetaClass):  # 修改默认元类变为我们的元类,也修改了产生本类的方式
    def __init__(self, name):
        self.name = name


obj = A('leethon')
print(obj.name)

# 实现方式2(不建议)
class MyMetaClass(type):
    def __call__(cls, *args, **kwargs):
        # 1.就是产生一个空对象
        obj = cls.__new__(cls)
        # 2.调用类的init传入对象和参数
        if args:
            raise TypeError('你只能传入关键字参数')  # 在这一步控制了只能传入关键字参数
        cls.__init__(obj, *args, **kwargs)
        # 3.返回创建好的对象
        return obj
posted @   leethon  阅读(54)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
点击右上角即可分享
微信分享提示