属性管理是什么,为什么需要它
一个最一般的对象可以有各种属性,就像这样:
A=object()
A.x=114
print(A.x)
114
我们可以随意地给这个实例附加任何属性,属性可以是任何东西。
但在某些情况下,我们不希望属性如此自由,而需要加一些限制或做一些处理,例如:
- 限制取值范围,例如
student.score
限定为0-100的实数; - 禁止新增属性;
- 根据实例中其它属性的值来实时更新属性。
这就是属性管理的功能。
从底层原理来说,这种功能实际上就是运算符重载,即当我们打下
A.x=...
时,我们实际上希望将这转换为一个特定函数来执行:
def setX(self,value): …
还不熟悉运算符重(chong2)载的朋友可以参见https://blog.csdn.net/goodlixueyong/article/details/52589979
python中的属性管理方法
在python中,class里的属性管理有三种方式可以实现,它们是:
- property路子,对指定属性起效,在class内部定义;
- descriptor路子, 对指定属性起效,在class外部定义(外包);
- setattr路子,对所有属性起效(即
.
的运算符重载),在class内部定义。
这在下面各个会给出实例代码,说明它们的基本写法,以及使用上的区别。(这里为了行文简洁,只讨论了get和set的方法,没写delete方法)
先放一个对照组:一个没有属性管理的class
class ob1:
# 没有属性管理
def __init__(self):
self.x=114
self.y=514
# ====测试====
ins1=ob1()
print(ins1.x,ins1.y)
ins1.x=1919
print(ins1.x,ins1.y)
ins1.x=19.19
print(ins1.x,ins1.y)
ins1.z=810
print(ins1.x,ins1.y,ins1.z)
114 514
1919 514
19.19 514
19.19 514 810
下面我们会依次用这三种路子管理起x
,y
两种属性,并实现 x
只能赋值为整数的功能 。
property
property这条路是在class定义内部写的,有函数和装饰器两种等价的途径。话不多说上实例代码:
class ob2:
# 用property进行属性管理
def __init__(self):
self.x=114
self.y=514
# =====方法1:用property()函数=====
def getx(self):
return self._x
def setx(self,value):
print('>> setting x in ob2!')
if isinstance(value,int): # 进行赋值管理!
self._x=value # 这里属性名不能是x自己,会冲突;前加一下划线是通常做法
else:
print('>> ERROR! x必须是整数!')
x=property(getx,setx) # 用property进行属性管理
# =====方法2:用@property装饰器=====
@property # 相当于getter
def y(self): # 名称须为变量名
return self._y
@y.setter
def y(self,value): # 名称须为变量名
print('>> setting y in ob2!')
self._y=value
当我们用去调用属性时,就会被property函数/装饰器重载到相应的函数(例如getx()
,setx()
)上,从而实现特定的功能。特别是,如果不给set方法,那该属性就变成了一个只读属性。对于没有指定的属性,则没有任何处理。运行测试:
# ====测试====
ins2=ob2()
print(ins2.x,ins2.y)
ins2.x=1919
print(ins2.x,ins2.y)
ins2.x=19.19 # 会报错而不赋值
print(ins2.x,ins2.y)
ins2.z=810 # 后加的z没有属性管理
print(ins2.x,ins2.y,ins2.z)
>> setting x in ob2!
>> setting y in ob2!
114 514
>> setting x in ob2!
1919 514
>> setting x in ob2!
>> ERROR! x必须是整数!
1919 514
1919 514 810
这条路子的好处是,如果只需要个别变量需要管理,那么这样写的工作量最小。
官方文档:https://docs.python.org/zh-cn/3/library/functions.html?highlight=property#property
descriptor
descriptor这条路则是将属性管理的任务「外包」给了在class外定义的另一个descriptor类。实例代码:
class mydescrptrX:
# 创建x的descriptor
def __get__(self,instance,owner):
return instance._x
def __set__(self,instance,value):
print('>> setting x by mydescrptrX!')
if isinstance(value,int): # 进行赋值管理!
instance._x=value
else:
print('>> ERROR! x必须是整数!')
class mydescrptrY:
# 创建y的descriptor
def __get__(self,instance,owner):
return instance._y
def __set__(self,instance,value):
print('>> setting y by mydescrptrY!')
instance._y=value
class ob3:
# 用descriptor进行属性管理
def __init__(self):
self.x=114
self.y=514
x=mydescrptrX() # 把x托管给descriptor
y=mydescrptrY() # 把y托管给descriptor
descriptor类不需要特殊的名字——只要类定义中具有__get__()
,__set__()
,__delete__()
中任意一个就成为了descriptor。就像例子中看到的那样,如果一个属性指向一个descriptor,那么该属性在调用时就会被重载为descriptor里的 __get__()
, __set__()
。对于没有指定的属性,也没有任何处理。
__get__(self,instance,owner)
变量陌生让人迷糊!里面发生的对应关系是:ins3.x -> mydescrptrX.__get__(self=ob3.x, instance=ins3, owner=ob3)
数据都是放在实例
ins3
里的,所以主要玩弄的是这里的instance
。
结构上要麻烦一点,但是如果要定义很多类/属性而它们需要相同的属性管理方法的话,则反而比较方便。实际上descriptor就是property的底层方法。
运行测试:
# ====测试====
ins3=ob3()
print(ins3.x,ins3.y)
ins3.x=1919
print(ins3.x,ins3.y)
ins3.x=19.19 # 会报错而不赋值
print(ins3.x,ins3.y)
ins3.z=810 # 后加的z没有属性管理
print(ins3.x,ins3.y,ins3.z)
>> setting x in ob2!
>> setting y in ob2!
114 514
>> setting x in ob2!
1919 514
>> setting x in ob2!
>> ERROR! x必须是整数!
1919 514
1919 514 810
上面的例子为了对比其它路子,没有体现出「外包」的优越性,而下面是一个两个属性公用一个descriptor的例子,可见如果是许多类、许多属性只用写一个管理规则的话,能大大提高coding效率:
class mydescrptr:
# 创建公用的descriptor:赋值只能是整数
def __init__(self,name):
self.name=name
def __get__(self,instance,owner):
return instance.__dict__['_'+self.name]
def __set__(self,instance,value):
print('>> setting '+self.name+' by mydescrptr!')
if isinstance(value,int): # 进行赋值管理!
instance.__dict__['_'+self.name]=value
else:
print('>> ERROR! '+self.name+'必须是整数!')
class ob5:
# 用相同的descriptor进行属性管理
def __init__(self):
self.x=114
self.y=514
x=mydescrptr('x') # 把x托管给descriptor
y=mydescrptr('y') # 把y托管给descriptor
ins5=ob5()
print(ins5.x,ins5.y)
ins5.y=8.10
print(ins5.x,ins5.y)
>> setting x by mydescrptr!
>> setting y by mydescrptr!
114 514
>> setting y by mydescrptr!
>> ERROR! y必须是整数!
114 514
参考:https://zhuanlan.zhihu.com/p/42485483
__setattr__()
上面的两种路子都是以某一属性为核心的管理方法,你设置了某个属性纳入管理,它才被管理;
而该路子是全局性的:所有的.
都会被重载到__getattr__()
和__setattr__()
里,然后你再在这两个函数里面做道场。
class ob4:
# 用__setattr__()进行属性管理
def __init__(self):
self.x=114 # 这里的.会被__setatrr__()重载
self.y=514
def __getattr__(self,attr):
if attr == 'x':
return self._x # 这里没有被套娃重载为__getattr__(),是因为还有个更底层的__getattribute__()在起作用
elif attr == 'y':
return self._y
else:
print('>> ERROR! 没有 '+attr+' 这个属性!')
def __setattr__(self,attr,value):
print('>> setting '+attr+' by __setattr__() !')
if attr == 'x':
if isinstance(value,int): # 进行赋值管理!
self.__dict__['_'+attr]=value # 注意不能写「self._x=value」,因为只要是「.」就会被__setattr__()重载,无限套娃循环
else:
print('>> ERROR! x必须是整数!')
elif attr == 'y':
self.__dict__['_'+attr]=value
else:
print('>> ERROR! 不能设置x,y之外的属性!') # 这是一种禁止添加新属性的方法
可以看出,这样相当于手动设定所有的.
该怎么样处理,也就很自然地实现了禁止新增属性的功能。
此外,由于是全局接管了.
,所以__setattr__()
内部不能使用.
,否则会无限循环的。这里用的是另一种更底层的赋值方法:用隐藏属性__dict__
,这是实际储存着所有属性和值的一个dict,可读可写可擦。
测试例:
# ====测试====
ins4=ob4()
print(ins4.x,ins4.y)
ins4.x=1919
print(ins4.x,ins4.y)
ins4.x=19.19
print(ins4.x,ins4.y)
ins4.z=810 # 不允许后加z
print(ins4.x,ins4.y,ins4.z)
>> setting x by __setattr__() !
>> setting y by __setattr__() !
114 514
>> setting x by __setattr__() !
1919 514
>> setting x by __setattr__() !
>> ERROR! x必须是整数!
1919 514
>> setting z by __setattr__() !
>> ERROR! 不能设置x,y之外的变量!
>> ERROR! 没有 z 这个属性!
1919 514 None
官方文档:https://docs.python.org/zh-cn/3/reference/datamodel.html?highlight=__setattr__#object.__setattr__
附加:__slots__
与禁止添加新变量
__slots__
是python class里面的一个隐藏属性,可以预先设定该class只能有哪些属性,也就是「插槽」的本意。倒不如说反过来,对于C语言来说,是必须实现声明的变量才能使用才更合理一点!
但是,如果用了__slots__
属性的话,它就会替换掉__dict__
。也就是说属性值的底层储存方式都变了。
- 对照组:
class ob1:
# 没有属性管理
def __init__(self):
self.x=114
self.y=514
def func1(self):
return 1919
ins1=ob1()
print(dir(ins1))
print(ins1.__dict__)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'func1', 'x', 'y']
{'x': 114, 'y': 514}
- 实验组:
__dict__
没了
class ob6:
__slots__=('x','y') # __slots__替代了__dict__
def __init__(self):
self.x=114
self.y=514
def func1(self):
return 1919
ins6=ob6()
print(dir(ins6))
print(ins6.__slots__)
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__slots__', '__str__', '__subclasshook__', 'func1', 'x', 'y']
('x','y')
如果试图新增属性,会直接报错:
ins6.z=810
# AttributeError: 'ob6' object has no attribute 'z'
由于对class底层结构有了较大的改动,所以如果你仅仅只是想实现软性的不允许添加新变量的功能,不建议用__slots__
来实现,而建议用上面的重载__setattr__()
的方法:
class ONLY_OO_XX:
__myDecidedAttrs=('oo','xx')
def __setattr__(self,attr,value):
if attr in self.__myDecidedAttrs:
self.__dict__[attr]=value # 没写__getattr__()方法,所以这里只能用attr原名
else:
raise AttributeError('NO WAY!')
ooxx=ONLY_OO_XX()
ooxx.oo=1 # goes well
ooxx.aa=2 # ERROR! NO WAY!
官方文档:https://docs.python.org/zh-cn/3/reference/datamodel.html?highlight=__slots__#object.__slots__