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 这个类添加了一个属性,元类有时称为类工厂。