python中的属性管理——property, descriptor, setattr三种方法的简介与对比

文章介绍了Python中对象属性管理的三种方式:property、descriptor和__setattr__。property用于在class内部管理指定属性,descriptor则在class外部定义,适用于多个类共享相同管理规则的情况,__setattr__全局管理所有属性,禁止新增属性。同时提到了__slots__用于预设类的属性,防止动态添加属性。
摘要由CSDN通过智能技术生成

属性管理是什么,为什么需要它

一个最一般的对象可以有各种属性,就像这样:

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__

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值