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~"

上面的例子中,我们定义了两个类。类 Animal 定义了一个属性 run ;类 Dog 继承自 Animal ,定义了一个属性 fly 和两个函数。接下来,我们实例化一个对象。对象的属性可以从特殊属性 __dict__ 中查看。

# 实例化一个对象dog>>> dog = Dog(1)# 查看dog对象的属性>>> dog.__dict__

{’age’: 1}# 查看类Dog的属性>>> Dog.__dict__

dict_proxy({’__doc__’: None,

’__init__’:,

’__module__’: ’__main__’,

’fly’: False,

’sound’:})# 查看类Animal的属性>>> Animal.__dict__

dict_proxy({’__dict__’:,

’__doc__’: None,

’__module__’: ’__main__’,

’__weakref__’:,

’run’: True})

由上面的例子可以看出:属性在哪个对象上定义,便会出现在哪个对象的 __dict__ 中。例如:

· 类 Animal 定义了一个属性 run ,那这个 run 属性便只会出现在类 Animal 的 __dict__中,而不会出现在其子类中。

· 类 Dog 定义了一个属性 fly 和两个函数,那这些属性和方法便会出现在类 Dog 的 __dict__ 中,同时它们也不会出现在实例的__dict__ 中。

· 实例对象 dog 的 __dict__ 中只出现了一个属性 age ,这是在初始化实例对象的时候添加的,它没有父类的属性和方法。

· 由此可知:Python中对象的属性具有 “层次性” ,属性在哪个对象上定义,便会出现在哪个对象的__dict__ 中。

在这里我们首先了解的是属性值会存储在对象的 __dict__ 中,查找也会在对象的 __dict__中进行查找的。至于Python对象进行属性访问时,会按照怎样的规则来查找属性值呢?这个问题在后文中进行讨论。

对象属性访问与特殊方法 __getattribute__

正如前面所述,Python的属性访问方式很直观,使用点属性运算符。在新式类中,对对象属性的访问,都会调用特殊方法__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__ 的。

对象属性控制

在继续介绍后面相关内容之前,让我们先来了解一下Python中和对象属性控制相关的相关方法。

__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__’:,

’__doc__’: None,

’__getattr__’:,

’__init__’:,

’__module__’: ’__main__’,

’__setattr__’:,

’fly’: False})

# 获取dog的age属性>>> dog.age1# 获取dog的adult属性。# 由于__getattribute__没有找到相应的属性,所以调用__getattr__。>>> dog.adult

calling __getattr__

False

# 调用一个不存在的属性name,__getattr__捕获AttributeError错误>>> dog.name

calling __getattr__

Traceback (most recent call last):

File "", line 1, in <module>

File "", line 10, in __getattr__

AttributeError

可以看到,属性访问时,当访问一个不存在的属性时触发 __getattr__ ,它会对访问行为进行控制。接下来是 __setattr__ :

# 给dog.age赋值,会调用__setattr__方法>>> dog.age = 2

calling __setattr__>>> dog.age2

# 先调用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__’:,

’__doc__’: None,

’__getattr__’:,

’__init__’:,

’__module__’: ’__main__’,

’__setattr__’:,

’fly’: False})

实例对象的 __setattr__ 方法可以定义属性的赋值行为,不管属性是否存在。当属性存在时,它会改变其值;当属性不存在时,它会添加一个对象属性信息到对象的 __dict__ 中,然而这并不改变类的属性。从上面的例子可以看出来。

最后,看一下 __delattr__ :

# 由于上面的例子中我们为dog设置了fly属性,现在删除它触发__delattr__方法>>> del dog.fly

calling __delattr__# 再次查看dog对象的__dict__,发现和fly属性相关的信息被删除>>> dog.__dict__

{’age’: 2}

描述符

描述符是Python 2.2 版本中引进来的新概念。描述符一般用于实现对象系统的底层功能, 包括绑定和非绑定方法、类方法、静态方法特特性等。关于描述符的概念,官方并没有明确的定义,可以在网上查阅相关资料。这里我从自己的认识谈一些想法,如有不当之处还请包涵。

在前面我们了解了对象属性访问和行为控制的一些特殊方法,例如 __getattribute__ 、 __getattr__ 、__setattr__ 、__delattr__ 。以我的理解来看,这些方法应当具有属性的"普适性",可以用于属性查找、设置、删除的一般方法,也就是说所有的属性都可以使用这些方法实现属性的查找、设置、删除等操作。但是,这并不能很好地实现对某个具体属性的访问控制行为。例如,上例中假如要实现 dog.age 属性的类型设置(只能是整数),如果单单去修改 __setattr__ 方法满足它,那这个方法便有可能不能支持其他的属性设置。

在类中设置属性的控制行为不能很好地解决问题,Python给出的方案是: __getattribute__、 __getattr__、 __setattr__、 __delattr__ 等方法用来实现属性查找、设置、删除的一般逻辑,而对属性的控制行为就由属性对象来控制。这里单独抽离出来一个属性对象,在属性对象中定义这个属性的查找、设置、删除行为。这个属性对象就是描述符。

描述符对象一般是作为其他类对象的属性而存在。在其内部定义了三个方法用来实现属性对象的查找、设置、删除行为。这三个方法分别是:

· get (self, instance, owner):定义当试图取出描述符的值时的行为。

· set (self, instance, value):定义当描述符的值改变时的行为。

· delete (self, instance):定义当描述符的值被删除时的行为。

其中:instance为把描述符对象作为属性的对象实例;

owner为instance的类对象。

以下以官方的一个例子进行说明:

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 类属性 x 的值存在的。而且 RevealAccess类定义了__get__ 、 __set__ 方法,它是一个描述符对象。注意,描述符对象的 __get__ 、 __set__ 方法中使用了诸如 self.val 和self.val = val 等语句,这些语句会调用 __getattribute__ 、 __setattr__ 等方法,这也说明了__getattribute__ 、__setattr__ 等方法在控制访问对象属性上的一般性(一般性是指对于所有属性它们的控制行为一致),以及 __get__ 、 __set__等方法在控制访问对象属性上的特殊性(特殊性是指它针对某个特定属性可以定义不同的行为)。

以下进行验证:

# 创建Myclass类的实例m>>> m = MyClass()

# 查看m和MyClass的__dict__>>> m.__dict__

{}>>> MyClass.__dict__

dict_proxy({’__dict__’:,

’__doc__’: None,

’__module__’: ’__main__’,

’__weakref__’:,

’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__’:,

’__doc__’: None,

’__module__’: ’__main__’,

’__weakref__’:,

’x’:<__main__.revealaccess at="" 0x5130080="">,

’y’: 5})

上面的例子对描述符进行了一定的解释,不过对描述符还需要更进一步的探讨和分析,这个工作先留待以后继续进行。

最后,还需要注意一点:描述符有数据描述符和非数据描述符之分。

· 只要至少实现 __get__ 、 __set__ 、 __delete__ 方法中的一个就可以认为是描述符;

· 只实现 __get__ 方法的对象是非数据描述符,意味着在初始化之后它们只能被读取;

· 同时实现 __get__ 和 __set__ 的对象是数据描述符,意味着这种属性是可读写的。

属性访问的优先规则

在以上的讨论中,我们一直回避着一个问题,那就是属性访问时的优先规则。我们了解到,属性一般都在__dict__ 中存储,但是在访问属性时,在对象属性、类属型、基类属性中以怎样的规则来查询属性呢?以下对Python中属性访问的规则进行分析。

由上述的分析可知,属性访问的入口点是 __getattribute__ 方法。它的实现中定义了Python中属性访问的优先规则。Python官方文档中对 __getattribute__ 的底层实现有相关的介绍,本文暂时只是讨论属性查找的规则,相关规则可见下图:

Python属性查找

上图是查找 b.x 这样一个属性的过程。在这里要对此图进行简单的介绍:

1. 查找属性的第一步是搜索基类列表,即 type(b).__mro__ ,直到找到该属性的第一个定义,并将该属性的值赋值给 descr;

2. 判断 descr 的类型。它的类型可分为数据描述符、非数据描述符、普通属性、未找到等类型。若 descr为数据描述符,则调用 desc.__get__(b, type(b)) ,并将结果返回,结束执行。否则进行下一步;

3. 如果 descr 为非数据描述符、普通属性、未找到等类型,则查找实例b的实例属性,即 b.__dict__ 。如果找到,则将结果返回,结束执行。否则进行下一步;

4. 如果在 b.__dict__ 未找到相关属性,则重新回到 descr 值的判断上。

· 若 descr 为非数据描述符,则调用 desc.__get__(b, type(b)) ,并将结果返回,结束执行;

· 若 descr 为普通属性,直接返回结果并结束执行;

· 若 descr 为空(未找到),则最终抛出 AttributeError 异常,结束查找。

来源:稀土掘金

时间: 2024-10-12 23:01:56

Python学习之属性访问与描述符详解的相关文章

Python 中的属性访问与描述符

在Python中,对于一个对象的属性访问,我们一般采用的是点(.)属性运算符进行操作.例如,有一个类实例对象foo,它有一个name属性,那便可以使用foo.name对此属性进行访问.一般而言,点(.)属性运算符比较直观,也是我们经常碰到的一种属性访问方式.然而,在点(.)属性运算符的背后却是别有洞天,值得我们对对象的属性访问进行探讨. 在进行对象属性访问的分析之前,我们需要先了解一下对象怎么表示其属性.为了便于说明,本文以新式类为例.有关新式类和旧式类的区别,大家可以查看Python官方文档.

USB HID Report Descriptor 报告描述符详解

Report descriptors are composed of pieces of information. Each piece of information is called an Item.报告描述符由一些数据片组成.这些数据片被叫做Item.All items have a one-byte prefix that contains the item tag, item type, and item size. 每一个Item都包含一个字节的前缀,这个前缀中包含了三个信息--it

Python学习教程(Python学习路线):Pandas库基础分析-详解时间序列的处理

Python学习教程(Python学习路线):Pandas库基础分析-详解时间序列的处理 在使用Python进行数据分析时,经常会遇到时间日期格式处理和转换,特别是分析和挖掘与时间相关的数据,比如量化交易就是从历史数据中寻找股价的变化规律.Python中自带的处理时间的模块有datetime,NumPy库也提供了相应的方法,Pandas作为Python环境下的数据分析库,更是提供了强大的日期数据处理的功能,是处理时间序列的利器. 1.生成日期序列 主要提供pd.data_range()和pd.p

Python__new__方法、定制属性访问、描述符与装饰器

__new__方法的运行顺序 装饰器的概念的用法 三个内置装饰器 类中属性的访问过程 __new__方法 创建实例的方法 __new__方法是在类创建实例的时候自动调用的 实例是通过类里面的__new__方法创建出来的 先调用__new__方法创建实例,再调用 __init__方法初始化实例 __new__方法,后面括号里的cls代表的是类本身 必须有返回值 父类名.__new__(cls) 单例模式 创建多个实例的时候,每个实例所指向的内存地址不同 单例模式让多个实例引用的是同一个实例,是一个

python描述符详解

什么是描述符 官方的定义:描述符是一种具有"捆绑行为"的对象属性.访问(获取.设置和删除)它的属性时,实际是调用特殊的方法(_get_(),_set_(),_delete_()).也就是说,如果一个对象定义了这三种方法的任何一种,它就是一个描述符. 描述符的作用是用来代理一个类的属性,需要注意的是描述符不能定义在被使用类的构造函数中,只能定义为类的属性,它只属于类的,不属于实例,我们可以通过查看实例和类的字典来确认这一点. 描述符是实现大部分Python类特性中最底层的数据结构的实现手

【从头开始写操作系统系列】页表以及相关的描述符详解

在之前的文章中,我们介绍过 GDT(全局描述符表)以及一致代码段和非一致代码段,这篇文章我们再回到描述符,这次我们来以 ARM 架构为例了解一下页表描述符. 在这篇文章中,我们会看到以下内容: 页表是什么? 一级页表的地址变换过程 由一级描述符来获取二级描述符或者段地址的过程 页表 页表是什么? 页表是一种特殊的数据结构,放在系统空间的页表区,存放逻辑页与物理页帧的对应关系. 每一个进程都拥有一个自己的页表,PCB表中有指针指向页表. (来自百度百科) 通俗的来讲,页表的内容就是一个描述符(关于

usb命令格式、描述符详解

一.USB命令 在USB规范里,对命令一词提供的单词为“Request”,但这里为了更好的理解主机与设备之间的主从关系,将它定义成“命令”. 所有的USB设备都要求对主机发给自己的控制命令作出响应,USB规范定义了11个标准命令,它们分别是:Clear_Feature. Get_Configuration.Get_Descriptor.Get_Interface.Get_Status.Set_Address. Set_Configuration.Set_Descriptor.Set_Interf

Python学习入门教程,字符串函数扩充详解

因有用户反映,在基础文章对字符串函数的讲解太过少,故写一篇文章详细讲解一下常用字符串函数.本文章是对:程序员带你十天快速入门Python,玩转电脑软件开发(三)中字符串函数的详解与扩充. 如果您想学习并参与本教程的完善与写作.请在下方讨论区,回复相关问题.一起完善本文章教程的书写. Python字符串常用函数. 声明字符串变量: str = ‘关注做全栈攻城狮,写代码也要读书,爱全栈,更爱生活.’ 下面所有字符串函数函数,是对变量str进行操作: 求字符串长度: 函数使用: 运行结果: 值得注意

python高级编程之最佳实践,描述符与属性01

# -*- coding: utf-8 -*- # python:2.x __author__ = 'Administrator' #最佳实践 """ 为了避免前面所有的问题,必须考虑到几下几点: ~1:应该避免多重继承,可以一些设计模式来代替它 ~2:super使用必须一致,在类层次结构中,应该在所有地方都使用super或者彻底不使用它,滥用super和传统调用是一种滥用方法,建议使用super ~3:不要滥用经典类和新式类,两者都具备代码库将导致不同的mro表现 ~4:调