Python进阶到高级:元类编程

2023年 8月 18日 43.7k 0

1、动态属性和属性描述符

有些同学可能知道 @property ,它的主要用于将一个方法变成属性,访问的时候直接通过名称访问,不需要加括号。注意加了 @property 函数不能有参数,你想嘛,人家调用的时候都不用括号,怎么传参,对吧。

举个小例子:

class Mikigo:

    @property
    def age(self):
        return "我晕,今年30了"

print(Mikigo().age)
我晕,今年30了

你看,调用 age 方法没加括号吧,那我要修改 age 的值怎么做呢?

class Mikigo:

    def __init__(self):
        self._age = 30

    @property
    def age(self):
        return self._age

    @age.setter
    def age(self, value):
        if not isinstance(value, int):
            raise ValueError
        self._age = value

mi = Mikigo()
mi.age = 25
print(mi.age)
25

注意上例中装饰器的写法,setter 是固定写法,setter 前面是你定义的函数名。

没什么问题哈,做了参数的类型检查,整体看起来不算复杂,其实了解到这里已经差不多了。但是,如果我们还有其他属性要处理,就得写好多个这样的,挺费劲不说,关键是不够优雅。

这时候就需要请出属性描述符。

这里又要介绍两个魔法函数:__get____set__

举个例子,讲解其用法:

class UserAttr:

    def __init__(self, user_age):
        self._age = user_age

    def __get__(self, instance, owner):
        print("get_instance:", instance)
        print("get_owner:", owner)
        return self._age

    def __set__(self, instance, value):
        print("set_instance:", instance)
        print("gse_value:", value)
        if not isinstance(value, int):
            raise ValueError
        self._age = value

真正使用的类:

class Mikigo:
    age = UserAttr(30)

mi = Mikigo()
print(mi.age)
get_instance: 
get_owner: 
30

在对象访问 age 的时候,首先是进入了 __get__ 方法,因为先打印了 get_instance 和 get_owner,instance 是 Mikigo 实例对象,也就是 mi,owner 是 Mikigo 类对象。

因此,到这里,我们知道了第一个小知识,在访问值的时候,调用的是 __get__

再赋值看看:

mi.age = 25
print(mi.age)
set_instance: 
set_value: 25
get_instance: 
get_owner: 
25

第二个小知识,赋值是调用的 __set__ 方法,一般为了使属性描述符成为只读的,应该同时定义 __get__()__set__() ,并在 __set__() 中引发 AttributeError

还有一个魔法函数 __delete__ 也是属性描述符,使用 del 会调用,由于不咋使用,不讲了,还有网上好多区分数据描述符和非数据描述符的,我感觉不用管也没必要,咱们是通俗易懂版,不整那些。

2、属性拦截器

属性拦截器就是在访问对象的属性时要做的一些事情,你想嘛,拦截就是拦路抢劫,拦截下来肯定要搞点事情才放你走。

主要介绍 2 个魔法函数:__getattr____getattribute__

这两个函数特别神奇,两个函数功能相反,一个是找到属性要做的事,另一个是没找到属性要做的事。

class Mikigo:

    def __init__(self):
        self.age = 30

    def __getattribute__(self, item):
        print(f"找到{item},我先搞点事情")

    def __getattr__(self, item):
        print(f"没找到{item},我想想能搞点啥事情")

定义了一个属性 age ,先来试试访问它

mi = Mikigo()
print(mi.age)
找到age,我先搞点事情
30

找到属性,会先调用 __getattribute__ ,并没有调用 __getattr__

好,现在访问一个不存在的属性:

mi.name
找到name,我先搞点事情
没找到name,我想想能搞点啥事情

这里就需要注意了,访问一个不存在的属性,首先还是会进入 __getattribute__ ,说明它是无条件进入的,然后才是调用 __getattr__

再扩展一个 __setattr__ 用于修改属性值的:

class Mikigo:
    def __init__(self):
        self.age = 30

    def __setattr__(self, key, value):
        print(f"修改{key}的值为{value}")
        self.__dict__[key] = value

mi = Mikigo()
mi.age = 25
print(mi.age)
修改age的值为30
修改age的值为25
25

你看,age 的值被修改了,但是 __setattr__ 貌似被调用了 2 次,那是因为在类实例化的时候就会进入一次,第一次是将 __init__ 里面的值添加到类实例的 __dict__ 属性中,第二次修改再次进入,将 __dict__ 属性中的值修改掉。

属性拦截一定要谨慎使用,一般情况下不建议使用,因为如果处理不好,会造成类里面属性关系的混乱,抛异常往往不容易定位。

项目实例,config 文件里面用到:

class Config:
    default = {
        # for cases
        "SMB_URL": "SMB://10.8.10.214",
        "SMB_IP": "10.8.10.214",
    }

    def __getattr__(self, key):
        try:
            return Config.default[key]
        except KeyError:
            raise AttributeError(f"{key} is not a valid option!") from KeyError

    def __setattr__(self, key, value):
        if key not in Config.default:
            raise AttributeError(f"{key} is not a valid option!") from KeyError
        Config.default[key] = value

试着分析下他们的作用吧,逻辑很简单的,你一定能看懂。

3、自定义元类

元类(metaclass)就是生成类的类,先定义metaclass,就可以创建类,最后创建实例。

其实最开始讲 type 的时候已经有所接触了,type 生成了所有类,它就是顶层元类,metaclass 也是要继承 type的,排行顶多老二,是不是应该叫“元二类”,或者“元类二”,爱谁谁吧。

来,咱们定义一个元类,用途是添加一个属性 age :

class AutoTestMetaClass(type):

    def __new__(cls, name, bases, dct):
        x = super().__new__(cls, name, bases, dct)
        x.age = 30
        return x

这里有 2 个知识点:

  • __new__ 也是构造函数,和 __init__ 有区别,__new__ 是用来构造类对象的,你看它的参数是 cls,必须 return 一个对象。
  • name, bases, dct 这三个参数和 type 的三个参数是一个意思,不清楚可以回看前面讲 type 的章节。

元类有了,咱们使用一下,既然元类是用来生成类的类,那咱们就来生成一个类:

class Mikigo(metaclass=AutoTestMetaClass):
    ...

mi = Mikigo()
print(mi.age)
print(Mikigo.age)
30
30

咱们定义一个类除了省略号没有任何属性,省略号也是一个对象,你也可以用 pass,但是仍然可以访问 age 属性。因为我们是通过元类,向 Mikigo 这个类添加了一个属性,元类有时称为类工厂。

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论