Python属性访问和描述符,property装饰器
一、属性访问
1.一个实例
看下面这个示例代码
class Animal(object):
run = True
class Dog(Animal):
fly = False
def __init__(self, age):
self.age = age
def sound(self):
return "wang wang~"
if __name__ == '__main__':
# 实例化一个对象dog
dog = Dog(1)
# 查看dog对象的属性
print('dog.__dict__:', dog.__dict__)
# 查看类Dog的属性
print('Dog.__dict__:', Dog.__dict__)
# 查看类Animal的属性
print('Animal.__dict__:', Animal.__dict__)
以下为控制台输出结果
dog.__dict__: {'age': 1}
Dog.__dict__: {'__module__': '__main__', 'fly': False, '__init__': <function Dog.__init__ at 0x000002586A2FF9D0>, 'sound': <function Dog.sound at 0x000002586A2FFA60>, '__doc__': None}
Animal.__dict__: {'__module__': '__main__', 'run': True, '__dict__': <attribute '__dict__' of 'Animal' objects>, '__weakref__': <attribute '__weakref__' of 'Animal' objects>, '__doc__': None}
- 下面对这些结果作一些总结:
- 类Animal定义的属性run只出现在自身而不出现在其子类dog中。
- 类Dog中定义的两个函数和定义的fly字段只出现在自身而不出现在具体实例dog中。
- 实例对象dog在初始化时(调用Dog类的构造函数)加上了一个age字段,但这个字段并不属于Dog类。除此之外,实例对象dog本身并不具有其类中的方法。
2.__getattribute__
方法
我们想要在访问对象属性的同时打印一些信息。在类中对于对象属性的访问都会调用__getattribute__
方法。我们先进行下面的尝试:
class Animal(object):
run = True
class Dog(Animal):
fly = False
def __init__(self, age):
self.age = age
def sound(self):
return "wang wang~"
def __getattribute__(self, item):
print("这里在访问狗的年龄")
return self.item
if __name__ == '__main__':
# 实例化一个对象dog
dog = Dog(1)
print(dog.age)
发现程序居然爆栈了!原因何在?
- 我们是重写了该方法。调用
__getattribute__
方法就相当于执行self.字段形式的代码。在返回self.item时,实际上还是调用的self.__getattribute__(item)
方法。然后再次调用,造成了爆栈。也就是说由于重写了该方法,每次对象调用的都是调用的重写以后的方法造成无限递归。 - 因此考虑不能够使用点运算符来访问。因此考虑采用下面的代码形式
class Animal(object):
run = True
class Dog(Animal):
fly = False
def __init__(self, age):
self.age = age
def __getattribute__(self, key):
print ("calling __getattribute__")
return super(Dog, self).__getattribute__(key)
def sound(self):
return "wang wang~"
以下为测试代码和控制台输出结果:
# 实例化对象dog
dog = Dog(1)
# 访问dog对象的age属性
print ('dog.age:',dog.age)
# 访问dog对象的fly属性
print ('dog.fly:',dog.fly)
# 访问dog对象的run属性
print ('dog.run:',dog.run)
# 访问dog对象的sound方法
print ('dog.sound:',dog.sound)
calling __getattribute__
dog.age: 1
calling __getattribute__
dog.fly: False
calling __getattribute__
dog.run: True
calling __getattribute__
dog.sound: <bound method Dog.sound of <__main__.Dog object at 0x00000253FA4CB7B8
在访问具体属性之前都进行了相对应的打印操作。
3.对象属性控制
3.1 __getattr__
方法
- 如果在第2节中介绍的
__getattribute__
方法找不到相应的属性,可以用来定义类的行为,增强了程序的健壮性。
看下面这个实例:
class Animal(object):
run = True
class Dog(Animal):
fly = False
def __init__(self, age):
self.age = age
def __getattr__(self, name):
print ("calling __getattr__")
if name == 'adult':
return True if self.age >= 2 else False
else:
raise AttributeError
以上的Dog类及其实例中并不存在name字段。如果注释掉上述的7-12行代码,并运行下面所示的代码:
if __name__ == '__main__':
# 实例化一个对象dog
dog = Dog(1)
print(dog.adult)
果不其然,抛出了以下的错误:
Traceback (most recent call last):
File "C:/Users/nth12/Desktop/machine learning/hw5/tester2.py", line 17, in <module>
print(dog.name)
AttributeError: 'Dog' object has no attribute 'name'
显然,这是由于dog中没有adult字段所致,__getattribute__
找不到相应的adult字段。如果去掉7-12行的注释,输出结果:
calling __getattr__
False
于是,推断__getattribute__
在找不到字段时将控制转移到了__getattr__
的位置,然后执行这个函数中的代码。
3.2 __setattr__(self, name, value)
方法
在3.1节中如果执行dog.age = 1
时便会执行__setattr__
方法,调用dog.fly = True
也会执行该方法。在Dog类中重写此方法加以验证。
class Animal(object):
run = True
class Dog(Animal):
fly = False
def __init__(self, age):
self.age = age
def __setattr__(self, name, value):
print("calling __setattr__,name = "+name+" value="+str(value))
super(Dog, self).__setattr__(name, value)
执行如下代码:
if __name__ == '__main__':
# 实例化一个对象dog
dog = Dog(1)
dog.age = 3
dog.fly = True
dog.run = False
-
同样,这种形式的赋值就等价于调用这个方法。防止无限递归的问题需要注意第十行代码的处理方法。
-
控制台输出的结果如下:
calling __setattr__,name = age value=1 calling __setattr__,name = age value=3 calling __setattr__,name = fly value=True calling __setattr__,name = run value=False
但是,一个很微妙的问题是,我们查看下Dog类和dog对象自身的字典,执行如下的代码:
if __name__ == '__main__':
# 实例化一个对象dog
dog = Dog(1)
print(dog.fly)
dog.age = 3
dog.fly = True
dog.run = False
print(dog.__dict__)
print(Dog.__dict__)
print(dog.fly)
print(Dog.fly)
控制台的输出结果如下:
calling __setattr__,name = age value=1
False
calling __setattr__,name = age value=3
calling __setattr__,name = fly value=True
calling __setattr__,name = run value=False
{'age': 3, 'fly': True, 'run': False}
{'__module__': '__main__', 'fly': False, '__init__': <function Dog.__init__ at 0x0000017C6852E9D0>, '__setattr__': <function Dog.__setattr__ at 0x0000017C6852EA60>, '__doc__': None}
True
False
- 从以上的输出结果可以看出:
- 第一次调用fly属性时,输出的是dog类中的false;第二次调用时,方法在具体dog实例中新生成了一个fly属性并赋值为True
- 所以,实际上我们调用的方法是在dog对象自身中加入了fly属性,run属性。对类中的属性并没有影响。
- 再次访问时,显然就是以实例中我们设置的属性为准了。
- 总结:
- 实例对象的
__setattr__
方法可以定义属性的赋值行为,不管属性是否存在。 - 当属性存在时,它会改变其值;
- 当属性不存在时,它会添加一个对象属性信息到对象的
__dict__
中,然而这并不改变类的属性。
- 实例对象的
3.3 __delattr__(self,name)
方法
在Dog类中添加如下的方法:
def __delattr__(self, name):
print ("calling __delattr__")
super(Dog, self).__delattr__(name)
在上述程序的基础上执行del dog.fly
语句,再观察dog实例的字典:
# 由于上面的例子中我们为dog设置了fly属性,现在删除它触发__delattr__方法
del dog.fly
# 再次查看dog对象的__dict__,发现和fly属性相关的信息被删除
print (dog.__dict__)
控制台输出结果如下:
calling __delattr__
{'age': 1}
但是这对类并无影响。此时如果执行dog.fly还是会显示类中的结果,即false。
二、描述符
1、定义
- 如果一个类中包含
__get__,__set__,__delete__
方法之一或全部则称这个类为描述符;- 不同于以上介绍的三种方式,描述符作用是对类或对象中某个成员进行详细的管理操作。
- 而以上三种方法可以管理所有的属性。
- 数据描述符:同时具备上述三种方法的类(优先级更高);否则称为非数据描述符
2、案例研究
class Email:
#成员属性
username = '123'
passport = '123456'
phone = 12345678
#成员方法
#登陆
def login(self):
print("denglu")
def logout(self):
print("tuichu")
if __name__ == '__main__':
e = Email()
print(e.username)
e.login()
运行上述代码,控制台输出下列结果
123
denglu
e实例自身并不具备username,于是__getarribute__
返回了我们的Email类中的Username;但是若想单独对Username进行管理是很不方便的。因此有了下面的描述符代码:
class Describor:
#定义描述符的三个成员
def __init__(self):
self.username_control = 1234
def __get__(self, instance, owner):
print("访问了用户名")
return self.username_control
def __set__(self, instance, value):
pass
def __delete__(self, instance):
pass
class Email:
#成员属性
username = Describor()
passport = '123456'
phone = 12345678
#成员方法
#登陆
def login(self):
print("denglu")
def logout(self):
print("tuichu")
if __name__ == '__main__':
e = Email()
print(e.username)
- 代码的第18行将username交付给一个描述符进行管理。
- 描述符中定义的username_control用于与原始的username产生关联。
- 控制台的输出结果如下所示:
访问了用户名 1234
- 显然调用了
__get__
函数,于是可以在__get__
中加入一些额外的东西进行装饰。
将36-38行换成如下的代码:
if __name__ == '__main__':
e = Email()
print(e.username)
e.username = '5678'
print(e.username)
控制台的输出结果如下:
访问了用户名
1234
访问了用户名
1234
显然我们的设置并没有成功。原因很简单,由于我们将username已经交付给描述符进行管理,执行第4行的语句是,此时描述符会调用__set__
方法进行设置。而此时__set__
处直接pass,什么都没做。在第5行执行e.username时还是像上面一样执行__get__
方法,因此结果会与上面一样。于是,修改上述代码的set模块处的操作:
def __set__(self, instance, value):
#设置与被管理成员在该描述符中对应的关联成员。在此例中为username_control;还可附加一些额外的修改信息
self.username_control = value
print('设置了用户名')
再次运行上述主函数的代码,可以得到预期的输出结果:
访问了用户名
1234
设置了用户名
访问了用户名
5678
同样,为了测试是否删除操作也是和前面两个类似,将主函数的代码修改成如下的形式:
if __name__ == '__main__':
e = Email()
print(e.username)
e.username = '5678'
print(e.username)
del e.username
print(e.username)
控制台输出结果如下:
访问了用户名
1234
设置了用户名
访问了用户名
5678
访问了用户名
5678
显然并没有删除成功,原因还是因为__delete函数直接pass掉了,删除过程中调用了delete函数。于是,将__delete__
函数修改成下面的形式:
def __delete__(self, instance):
#删除临时变量即可
del self.username_control
当然在删除过程中还可以加一些修饰。例如如果不允许该用户名被删除,则在该函数中直接pass掉或是输出一些信息。再例如,由于instance为被管理的实例,可以在instance中设置一些flag,在该函数中访问,以控制删除操作。
再次执行主函数代码观察运行结果:
访问了用户名
1234
设置了用户名
访问了用户名
5678
访问了用户名
Traceback (most recent call last):
File "C:/Users/nth12/Desktop/machine learning/hw5/tester.py", line 53, in <module>
print(e.username)
File "C:/Users/nth12/Desktop/machine learning/hw5/tester.py", line 18, in __get__
return self.username_control
AttributeError: 'Describor' object has no attribute 'username_control'
最后抛出异常,这是因为我们的self.username_control已经在__delete__
函数中被删除了。再次执行__get__
函数访问这一字段时抛出访问不到的异常。
2.1__get__(self, instance, owner)
方法
-
在访问对象成员属性(该成员已经交给描述符管理的时候)进行触发。例如上例中Email类中的username被交付给了描述符Describor进行管理。在主函数中访问username中触发了该方法。
-
功能:查看当前属性的值。
-
参数:
- self:描述符对象自身
- instance:被管理成员的类的所在实例对象(在上例中为Email类的实例对象e)
- owner:被管理成员的所在类(在上例中为Email类)
-
返回值:可有可无。若无则为None
2.2__set__(self, instance, value)
方法
- 在设置对象成员属性(该成员已经交给描述符管理的时候)进行触发。例如上例中Email类中的username被交付给了描述符Describor进行管理。在主函数中设置username中触发了该方法。
- 参数:
- self:描述符对象自身
- instance:被管理成员的类的所在实例对象(在上例中为Email类的实例对象e)
- value:要设置的值
- 设置的时候一定要设置关联的成员,否则没有意义。例如上面的实例中设置一定要设置与username关联的成员username_control,否则没有意义。
2.3__delete__(self, instance)
方法
- 在删除对象成员属性(该成员已经交给描述符管理的时候)进行触发。例如上例中Email类中的username被交付给了描述符Describor进行管理。在主函数中删除username中触发了该方法。
- 参数:
- self:描述符对象自身
- instance:被管理成员的类的所在实例对象(在上例中为Email类的实例对象e)
- 如果要删除,删除的时候一定要删除关联的成员,否则没有意义。
三、区分属性访问和描述符
- 描述符仅仅针对于某个特定的成员,可以对不同的类进行使用。(例如上述中的instance,可根据Instance中额外的成员或Instance的类型做不同的操作),具有良好的可复用性
- 属性访问针对当前类和对象所有成员的管理,仅对当前类有效。
四、property函数&property装饰器
1 、property函数提供了描述符类
在第二部分中,将描述符类与被管理的类相互分离,那么能否将两者合二为一呢?
property类是一个专用的描述符类。考虑下面的代码。
class Email:
# 成员属性
username = '匿名用户'
passport = '123456'
# phone = 12345678
def __init__(self):
self.username_control = '123456' # 用于与Username产生关联。类似于上面Describor类中的username_control
# 成员方法
# 登陆
def login(self):
print("denglu")
def logout(self):
print("tuichu")
# 委托给描述符部分的描述符的开始区域
def getusername(self):
pass
def setusername(self, value):
pass #改成和__set__方法类似的形式
def deleteusername(self):
pass #改成和__delete__方法类似的形式
# 委托给描述符部分的结束区域
# property是一个特有的描述符类,与上面的用法相同的
username = property(getusername, setusername, deleteusername, doc='username')
先执行下面的代码:
if __name__ == '__main__':
e = Email()
print(e.username)
控制台输出结果如下所示:
None
将getusername改成如下的形式:
def getusername(self):
return self.username_control
再次运行上述主函数,运行结果如下:
123456
- 显然在访问时,被property调用了我们的getusername方法。于是,以上定义的getusername,setusername,deleteusername和我们先前看到的get,set和delete应该具有相似的作用。只不过是property中调用了后者三个方法,而在调用后者三个方法的过程中,又委托给了我们定义的三个getusername,setusername,deleteusername进行了实现。
- 于是,在这个类中应该还有一个与username关联的成员变量
- 还需要注意三个定义的函数的形式。在代码中已经给出。
- 其中setusername中的value为欲设置的值
2、property装饰器
property装饰器的用法如下:
class C:
def __init__(self):
self._x = None #用于关联x的变量,类似于上面的username_control
@property#本质上进行描述符的关联,即定义一个成员变量x并与一个property描述符对象相关联。并类似于getusername方法。
def x(self):
"""I'm the 'x' property."""
return self._x
@x.setter
def x(self, value): #类似于setusername方法
self._x = value
@x.deleter
def x(self): #类似于deleteusername方法
del self._x
#如果 c 是 C 的实例,c.x 将调用getter,c.x = value 将调用setter, del c.x 将调用deleter。