学习Python:属性访问和描述符
在Python中,对于对象的属性访问,我们一般使用点(.)属性运算符来操作。例如,有一个类实例对象 foo
,它具有 name
属性。然后您可以使用 foo.name
来访问此属性。一般来说,点(.)属性操作符比较直观,也是我们经常遇到的一种属性访问方法。然而,点(.)属性运算符背后隐藏着一个值得我们讨论对象属性访问的秘密。
在分析对象属性访问之前,我们首先要了解对象如何表示其属性。为了便于说明,本文以新样式类为例。关于新样式类和旧类的区别,可以查看Python官方文档。
对象的属性
在Python中,“一切都是对象”。我们可以为对象设置不同的属性。我们先看一个简单的例子:
class Animal(object):
run = True
class Dog(Animal):
fly = False
def __init__(self, age):
self.age = age
def sound(self):
return "wang wang~"
在上面的例子中我们定义了两个类。类animals
定义了一个属性run
;类dog
继承自animal
,并在两个函数中定义了属性fly
。接下来,我们实例化一个对象。可以通过特殊属性__dict__
查看对象的属性。
# 实例化一个对象dog
>>> dog = Dog(1)
# 查看dog对象的属性
>>> dog.__dict__
{'age': 1}
# 查看类Dog的属性
>>> Dog.__dict__
dict_proxy({'__doc__': None,
'__init__': <function __main__.__init__>,
'__module__': '__main__',
'fly': False,
'sound': <function __main__.sound>})
# 查看类Animal的属性
>>> Animal.__dict__
dict_proxy({'__dict__': <attribute '__dict__' of 'Animal' objects>,
'__doc__': None,
'__module__': '__main__',
'__weakref__': <attribute '__weakref__' of 'Animal' objects>,
'run': True})
从上面的例子可以看出:在哪个对象上定义了属性,它出现在该对象的__dict__
中。例如:
- 类
animals
定义了一个属性run
,则该属性仅在类runanimals中__dict__
并且不会出现在其子类中。 - class
dog
在两个函数中定义了一个属性fly
,那么这些属性和方法就出现在♝D __dict__中,并且它们不会出现在出现实例__dict__
。实例对象dog
的__dict__
中仅出现一个属性age
。这是在初始化实例对象时添加的。它没有父类。属性和方法。 - 可见Python中对象的属性具有“层次结构”。这些属性出现在定义它们的对象的
__dict__
中。
这里我们首先明白的是,属性值存储在对象的__dict__
中,搜索也是在对象的__dict__
前面提到过,Python的属性访问方法非常直观,使用点属性运算符。在新样式类中,访问对象属性时会调用特殊方法 以上面的场景为例: 在上面的例子中,我们重写了 从上面的验证可以看出, 在继续介绍下面的相关内容之前,我们先来看看Python中与对象属性控制相关的相关方法。 我们还是用上面的例子来说明,不过这里需要重写三个属性控制方法。 验证以下内容。第一个是 可以看到,在属性访问过程中,当访问一个不存在的属性时,会触发 方法可以定义归因行为,无论属性是否存在。如果该属性存在,则会改变其值;如果属性不存在,则给对象 最后看 描述符是Python 2.2中引入的新概念。描述符一般用于实现对象系统的基本功能,包括绑定和非绑定方法、类方法、静态方法特性等。关于描述符的概念,官方并没有明确的定义。您可以上网查一下相关信息。在此我根据自己的了解分享一些想法。如有不当之处,敬请谅解。 之前我们学习了一些对象属性访问和行为控制的特殊方法,比如 类中设置属性的控制行为并不能很好的解决问题。 Python提供的解决方案是: 描述符对象通常作为其他对象类的属性存在。内部定义了三个方法来实现属性对象的查找、设置和删除行为。这三种方法是: 其中:instance是以描述符对象为属性的对象实例; 以下为官方示例: 上面定义了两个类。其中, 验证如下: 上面的例子在一定程度上解释了描述符,但描述符还需要进一步讨论和分析。这项工作留到以后再说。 最后需要注意的是:描述符分为数据描述和非数据描述。 在上面的讨论中,我们回避了一个问题,那就是属性访问的优先级规则。我们了解到,属性一般存储在 从上面的分析可以看出,属性访问的入口是 上图展示了搜索 作者:fanchunke1991中进行。那么在进行Python对象属性访问时,会按照什么规则来查找属性值呢?这个话题稍后会讨论。
对象属性访问及特殊方法
__getattribute__
__getattribute__
。 __getattribute__
允许我们自定义访问对象属性时的访问行为,但使用时要特别注意无限递归。 class Animal(object):
run = True
class Dog(Animal):
fly = False
def __init__(self, age):
self.age = age
# 重写__getattribute__。需要注意的是重写的方法中不能
# 使用对象的点运算符访问属性,否则使用点运算符访问属性时,
# 会再次调用__getattribute__。这样就会陷入无限递归。
# 可以使用super()方法避免这个问题。
def __getattribute__(self, key):
print "calling __getattribute__\n"
return super(Dog, self).__getattribute__(key)
def sound(self):
return "wang wang~"
__getattribute__
方法。请注意,我们使用 super()
方法来避免无限循环问题。下面我们实例化一个对象来说明访问对象属性时__getattribute__
的特点。 # 实例化对象dog
>>> dog = Dog(1)
# 访问dog对象的age属性
>>> dog.age
calling __getattribute__
1
# 访问dog对象的fly属性
>>> dog.fly
calling __getattribute__
False
# 访问dog对象的run属性
>>> dog.run
calling __getattribute__
True
# 访问dog对象的sound方法
>>> dog.sound
calling __getattribute__
<bound method Dog.sound of <__main__.Dog object at 0x0000000005A90668>>
__getattribute__
是查找属性或方法的示例对象的条目。当实例对象访问属性或方法时,必须调用__getattribute__
。然后按照一定的规则在每个__dict__
中查找对应的属性值或方法对象。如果没有找到,则调用__getattr__
(稍后介绍)。 __getattribute__
是Python中的内置方法。其基本实现可以查看相关官方文档。后面介绍的属性访问规则依赖于__getattribute__
。 对象属性控制
__getattr__(self, name)
__getattr__
可用于定义当用户尝试获取不存在(或暂时不存在)的属性时类的行为。如前所述,如果 __getattribute__
方法未找到该属性,则最终会调用 __getattr__
方法。可以用来捕获错误,灵活处理AttributeError。仅当您尝试访问不存在的属性时才会调用它。方法 __setattr__(self, name, value)
__setattr__
允许您自定义特定属性的设置行为。无论属性是否存在,您都可以为每个属性的所有更改定义自己的规则。 。 __setattr__
有两点需要注意:第一,使用时一定要小心。不能写成self.name = "Tom"
这样的形式,因为这样的赋值语句调用了__setattr__
方法,会导致无限递归;其次,需要区分对象属性和类属性这两个概念。这将在以下示例中进行解释。 __delattr__(self, name)
__delattr__
用于处理删除属性时的行为。请注意使用__setattr__
方法解决无限递归问题。如果重写这个方法,不要写成del self.name
。 class Animal(object):
run = True
class Dog(Animal):
fly = False
def __init__(self, age):
self.age = age
def __getattr__(self, name):
print "calling __getattr__\n"
if name == 'adult':
return True if self.age >= 2 else False
else:
raise AttributeError
def __setattr__(self, name, value):
print "calling __setattr__"
super(Dog, self).__setattr__(name, value)
def __delattr__(self, name):
print "calling __delattr__"
super(Dog, self).__delattr__(name)
__getattr__
:# 创建实例对象dog
>>> dog = Dog(1)
calling __setattr__
# 检查一下dog和Dog的__dict__
>>> dog.__dict__
{'age': 1}
>>> Dog.__dict__
dict_proxy({'__delattr__': <function __main__.__delattr__>,
'__doc__': None,
'__getattr__': <function __main__.__getattr__>,
'__init__': <function __main__.__init__>,
'__module__': '__main__',
'__setattr__': <function __main__.__setattr__>,
'fly': False})
# 获取dog的age属性
>>> dog.age
1
# 获取dog的adult属性。
# 由于__getattribute__没有找到相应的属性,所以调用__getattr__。
>>> dog.adult
calling __getattr__
False
# 调用一个不存在的属性name,__getattr__捕获AttributeError错误
>>> dog.name
calling __getattr__
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 10, in __getattr__
AttributeError
__getattr__
。接下来是__setattr__
:__setattr__
实例对象的# 给dog.age赋值,会调用__setattr__方法
>>> dog.age = 2
calling __setattr__
>>> dog.age
2
# 先调用dog.fly时会返回False,这时因为Dog类属性中有fly属性;
# 之后再给dog.fly赋值,触发__setattr__方法。
>>> dog.fly
False
>>> dog.fly = True
calling __setattr__
# 再次查看dog.fly的值以及dog和Dog的__dict__;
# 可以看出对dog对象进行赋值,会在dog对象的__dict__中添加了一条对象属性;
# 然而,Dog类属性没有发生变化
# 注意:dog对象和Dog类中都有fly属性,访问时会选择哪个呢?
>>> dog.fly
True
>>> dog.__dict__
{'age': 2, 'fly': True}
>>> Dog.__dict__
dict_proxy({'__delattr__': <function __main__.__delattr__>,
'__doc__': None,
'__getattr__': <function __main__.__getattr__>,
'__init__': <function __main__.__init__>,
'__module__': '__main__',
'__setattr__': <function __main__.__setattr__>,
'fly': False})
__dict__
一个对象属性信息,但这不会改变类的属性。从上面的例子就可以看出。 __delattr__
: # 由于上面的例子中我们为dog设置了fly属性,现在删除它触发__delattr__方法
>>> del dog.fly
calling __delattr__
# 再次查看dog对象的__dict__,发现和fly属性相关的信息被删除
>>> dog.__dict__
{'age': 2}
描述符
__getattribute__
、__getattr__❙❝ ,
__delattr__。以我的理解,这些方法对于属性来说应该是“通用的”,可以作为属性查找、设置和删除的通用方法。也就是说,所有的属性都可以使用该方法来实现属性的查找、设置和删除。删除等操作。但是,这对于强制执行特定属性的访问控制行为效果不佳。例如,在上面的例子中,如果你想改变属性
dog.age
的类型设置(只能是整数),如果你简单地改变__setattr__
方法如果要满足他们,那么这个方法可能无法支持其他属性设置。 __getattribute__
、__getattr__
、__setattr____setattr__❀lattr__
分别是用于实现属性搜索的一般逻辑,设置和删除,属性的控制行为是由属性对象来控制的。这里,提取出一个单独的属性对象,并在属性对象中定义了该属性的查找、设置和删除行为。这个属性对象就是描述符。
所有者是实例的类对象。 class RevealAccess(object):
def __init__(self, initval=None, name='var'):
self.val = initval
self.name = name
def __get__(self, obj, objtype):
print 'Retrieving', self.name
return self.val
def __set__(self, obj, val):
print 'Updating', self.name
self.val = val
class MyClass(object):
x = RevealAccess(10, 'var "x"')
y = 5
RevealAccess
类的实例作为MyClass
类属性的值而存在。
RevealAccess
类定义了__get__
、__set__
方法或。请注意,描述符对象的 __get__
、__set__
方法使用 self.val❝ 和
、、 __ 它可以作为一种方法被删除。 ; self.val❝ 等方法。
等语句,这些语句调用__getattribute__
,__setattr__
等方法。 __setattr__ 诸如 之类的方法控制对对象属性的访问。通用性(通用性是指它们的控制行为对于所有属性都是一致的),而特殊性如__get__
、__set__
等方法控制对对象属性的访问可以有不同的行为为特定属性定义)。 # 创建Myclass类的实例m
>>> m = MyClass()
# 查看m和MyClass的__dict__
>>> m.__dict__
{}
>>> MyClass.__dict__
dict_proxy({'__dict__': <attribute '__dict__' of 'MyClass' objects>,
'__doc__': None,
'__module__': '__main__',
'__weakref__': <attribute '__weakref__' of 'MyClass' objects>,
'x': <__main__.RevealAccess at 0x5130080>,
'y': 5})
# 访问m.x。会先触发__getattribute__方法
# 由于x属性的值是一个描述符,会触发它的__get__方法
>>> m.x
Retrieving var "x"
10
# 设置m.x的值。对描述符进行赋值,会触发它的__set__方法
# 在__set__方法中还会触发__setattr__方法(self.val = val)
>>> m.x = 20
Updating var "x"
# 再次访问m.x
>>> m.x
Retrieving var "x"
20
# 查看m和MyClass的__dict__,发现这与对描述符赋值之前一样。
# 这一点与一般属性的赋值不同,可参考上述的__setattr__方法。
# 之所以前后没有发生变化,是因为变化体现在描述符对象上,
# 而不是实例对象m和类MyClass上。
>>> m.__dict__
{}
>>> MyClass.__dict__
dict_proxy({'__dict__': <attribute '__dict__' of 'MyClass' objects>,
'__doc__': None,
'__module__': '__main__',
'__weakref__': <attribute '__weakref__' of 'MyClass' objects>,
'x': <__main__.RevealAccess at 0x5130080>,
'y': 5})
__get__
、__set__
、__get__
方法的对象是非数据描述符,这意味着它们只能在初始化后读取;同时实现 __get__
和 __set__
的 属性访问的优先级规则
__dict__
中,但是在访问属性时,使用什么规则来查询属性和对象属性、类类型、基类属性呢?下面对Python中属性访问的规则进行分析。 __getattribute__
方法。它的实现定义了 Python 中属性访问的优先规则。Python官方文档对__getattribute__
的底层实现有相关介绍。本文仅讨论属性搜索的规则。相关规则可以看下图: Python属性搜索
b.x
这样的属性过程。下面简单介绍一下这个图: type(b).__mro__
,直到找到该属性的第一个定义,并输入该属性的值descr
; 描述
的类型。其类型可分为数据描述符、非数据描述符、普通属性、未找到等类型。如果descr
是数据描述符,则调用desc.__get__(b, type(b))
,并返回结果,完成执行。否则,进行下一步; descr
是非数据描述、普通属性、未找到等。如果找到,则返回结果并结束执行。否则,进行下一步; b.__dict__
中没有找到相关属性,则返回判断描述值。
descr
为非数据描述符,则调用desc.__get__(b, type(b))
返回结果,并返回结果; descr
是普通属性,立即返回结果并终止执行; descr
为空(未找到),最终会抛出 AttributeError 异常并停止搜索。
链接:https://juejin.im/post/58ff7151da2f60005dd86f2d
来源:掘金属于作者所有。如需商业转载,请联系求作者授权。非商业转载请来源。
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。