Python存取属性的方式特别不对等,通过实例读取属性时,通常返回的是实例中定义的属性,但如果实例未曾定义过该属性,就会获取类属性,而为实例的属性赋值时,通常会在实例中创建属性,而不会影响到类本身。这种不对等的方式对描述符类也有影响。

def cls_name(obj_or_cls):  # 传入一个实例,返回类名
cls = type(obj_or_cls)
if cls is type:
cls = obj_or_cls
return cls.__name__.split('.')[-1] def display(obj):
cls = type(obj)
if cls is type: # 如果obj是一个类,则进入该分支
return '<class {}>'.format(obj.__name__)
elif cls in [type(None), int]: # 如果obj是None或者数值,进入该分支
return repr(obj)
else: # 如果obj是一个实例
return '<{} object>'.format(cls_name(obj)) def print_args(name, *args):
pseudo_args = ', '.join(display(x) for x in args)
print('-> {}.__{}__({})'.format(cls_name(args[0]), name, pseudo_args)) class Overriding: # <1>
"""a.k.a. data descriptor or enforced descriptor""" def __get__(self, instance, owner):
print_args('get', self, instance, owner) def __set__(self, instance, value):
print_args('set', self, instance, value) class OverridingNoGet: # <2>
"""an overriding descriptor without ``__get__``""" def __set__(self, instance, value):
print_args('set', self, instance, value) class NonOverriding: # <3>
"""a.k.a. non-data or shadowable descriptor""" def __get__(self, instance, owner):
print_args('get', self, instance, owner) class Managed: # <4>
over = Overriding()
over_no_get = OverridingNoGet()
non_over = NonOverriding() def spam(self): # <5>
print('-> Managed.spam({})'.format(display(self)))

    

  1. 有__get__和__set__方法的典型覆盖型描述符,在这个示例中,各个方法都调用了print_args()函数
  2. 没有__get__方法的覆盖型描述符
  3. 没有__set__方法,所以这是非覆盖型描述符
  4. 托管类,使用各个描述符类的一个实例
  5. spam方法放这里是为了对比,因为方法也是描述符

覆盖型描述符

实现__set__方法的描述符属于覆盖型描述符,虽然描述符是类属性,但是实现了__set__方法的话,会覆盖对实例属性的赋值操作。特性也是覆盖型描述符,见Python动态属性和特性(二),如果没有提供设值函数,property类中的fset方法就会抛出AttributeError异常,指明那个属性时只读的

下面我们来看下覆盖型描述符的行为:

>>> obj = Managed()  # <1>
>>> obj.over # <2>
-> Overriding.__get__(<Overriding object>, <Managed object>, <class Managed>)
>>> Managed.over # <3>
-> Overriding.__get__(<Overriding object>, None, <class Managed>)
>>> obj.over = 8 # <4>
-> Overriding.__set__(<Overriding object>, <Managed object>, 8)
>>> obj.over # <5>
-> Overriding.__get__(<Overriding object>, <Managed object>, <class Managed>)
>>> obj.__dict__["over"] = 9 # <6>
>>> vars(obj) # <7>
{'over': 9}
>>> obj.over # <8>
-> Overriding.__get__(<Overriding object>, <Managed object>, <class Managed>)

  

  1. 创建托管实例Managed对象
  2. obj.over触发描述符的__get__方法,__get__方法第一个参数是描述符实例,第二个参数的值是托管实例obj,第三个参数是托管类对象
  3. Managed.over 触发描述符的__get__方法,第二个参数(instance)的值是 None,第一个和第三个同上
  4. 为obj.over赋值,触发描述符的__set__方法,最后一个参数的值是8
  5. 读取 obj.over,仍会触发描述符的__get__方法
  6. 跳过描述符,直接通过obj.__dict__属性设值
  7. 确认值在obj.__dict__属性中,在over键名下
  8. 然而,即使是名为over的实例属性,Managed.over描述符仍会覆盖读取 obj.over 这个操作

没有__get__方法的覆盖型描述符

>>> obj.over_no_get  # <1>
<descriptorkinds.OverridingNoGet object at 0x0000001742369780>
>>> Managed.over_no_get # <2>
<descriptorkinds.OverridingNoGet object at 0x0000001742369780>
>>> obj.over_no_get = 8 # <3>
-> OverridingNoGet.__set__(<OverridingNoGet object>, <Managed object>, 8)
>>> obj.over_no_get # <4>
<descriptorkinds.OverridingNoGet object at 0x0000001742369780>
>>> obj.__dict__['over_no_get'] = 6 # <5>
>>> obj.over_no_get # <6>
6
>>> obj.over_no_get = 7 # <7>
-> OverridingNoGet.__set__(<OverridingNoGet object>, <Managed object>, 7)
>>> obj.over_no_get # <8>
6

    

  1. 描述符实例没有__get__方法,所以obj.over_no_get从类中获取描述符实例
  2. 直接从托管类中读取over_no_get属性,也就是描述符实例
  3. 为obj.over_no_get赋值hui会触发描述符类的__set__方法
  4. 因为__set__方法没有修改属性,所以在此读取obj.over_no_get获取的仍然是托管类中的描述符实例
  5. 通过实例的__dict__属性设置名为over_no_get的实例属性
  6. 现在,over_no_get实例属性会覆盖描述符实例
  7. 为obj.over_no_get实例属性赋值,仍然会经过__set__方法
  8. 读取时,只要有同名的实例属性,描述符实例就会被覆盖

非覆盖型描述符:没有实现__set__方法的描述符称为是非覆盖型描述符,如果设置了同名的实例属性,描述符会被覆盖,致使描述符无法处理那个实例的那个属性

>>> obj.non_over  # <1>
-> NonOverriding.__get__(<NonOverriding object>, <Managed object>, <class Managed>)
>>> obj.non_over = 6 # <2>
>>> obj.non_over # <3>
6
>>> Managed.non_over # <4>
-> NonOverriding.__get__(<NonOverriding object>, None, <class Managed>)
>>> del obj.non_over # <5>
>>> obj.non_over # <6>
-> NonOverriding.__get__(<NonOverriding object>, <Managed object>, <class Managed>)

  

  1. obj.non_over触发描述符的__get__方法,第二个参数的值是obj
  2. Managed.non_over是非覆盖型描述符,因此没有干涉赋值操作的__set__方法
  3. obj有个名为non_over的实例属性,把Managed类的同名描述符属性遮盖掉
  4. Managed.non_over描述符依然存在,会通过类截获这次访问
  5. 删除non_over实例属性
  6. 读取obj.non_over时,会触发类中描述符的__get__方法

覆盖类中的描述符:不管描述符是不是覆盖型的,为类属性赋值都能覆盖描述符

  

>>> obj = Managed()  # <1>
>>> Managed.over = 1 # <2>
>>> Managed.over_no_get = 2
>>> Managed.non_over = 3
>>> obj.over, obj.over_no_get, obj.non_over # <3>
(1, 2, 3)

  

  1. 新建一个Managed实例
  2. 覆盖类中的描述符属性
  3. 通过实例访问描述符属性,新的值覆盖描述符

方法是描述符:在类中定义的函数属于绑定方法,用户定义的函数都有__get__方法,所以依附到类上时,就相当于描述符,方法是非覆盖型描述符

>>> obj = Managed()
>>> obj.spam # <1>
<bound method Managed.spam of <descriptorkinds.Managed object at 0x00000017423284A8>>
>>> Managed.spam # <2>
<function Managed.spam at 0x0000001742322AE8>
>>> obj.spam = 7 # <3>
>>> obj.spam
7

  

  1. obj.spam获取的是绑定方法对象
  2. Managed.spam获取的是函数
  3. 如果为obj.spam赋值,会遮盖类属性,导致无法通过obj实例访问spam方法

函数没有__set__方法,因此是非覆盖型描述符,从上面的例子来看,obj.spam和Managed.spam获取的是不同对象。与描述符一样,通过托管类访问时,函数__get__方法会返回自身的引用,但是通过实例访问时,函数的__get__方法返回的是绑定方法对象:一种可调用的对象,里面包装着函数,并把托管实例(如obj)绑定给函数的第一个参数(即self),这与functools.partial函数的行为一致

为了了解这种机制,让我们看下面一个例子:

import collections

class Text(collections.UserString):

    def __repr__(self):
return 'Text({!r})'.format(self.data) def reverse(self):
return self[::-1]

  

测试Text类:

>>> word = Text("forward")
>>> word # <1>
Text('forward')
>>> word.reverse() # <2>
Text('drawrof')
>>> Text.reverse(Text('backward')) # <3>
Text('drawkcab')
>>> type(Text.reverse), type(word.reverse) # <4>
(<class 'function'>, <class 'method'>)
>>> list(map(Text.reverse, ['repaid', (10, 20, 30), Text('stressed')])) # <5>
['diaper', (30, 20, 10), Text('desserts')]
>>> func = Text.reverse.__get__(word) # <6>
>>> func() # <7>
Text('drawrof')
>>> Text.reverse.__get__(None, Text) # <8>
<function Text.reverse at 0x000000266209E598>
>>> Text.reverse
<function Text.reverse at 0x000000266209E598>
>>> word.reverse # <9>
<bound method Text.reverse of Text('forward')>
>>> Text.reverse.__get__(word)
<bound method Text.reverse of Text('forward')>
>>> word.reverse.__self__ # <10>
Text('forward')
>>> word.reverse.__func__ is Text.reverse # <11>
True

      

  1. Text实例的repr方法返回一个类似Text构造方法调用的字符串,可用于创建相同的实例
  2. reverse方法返回反向拼写的单词
  3. 在类上调用方法并传入一个实例相当于调用实例的函数
  4. 从类获取方法和从实例获取方法的类型是不同的,一个是function,一个是method
  5. Text.reverse相当于函数,甚至可以处理Text实例之外的其他对象
  6. 函数都是非覆盖型描述符。在函数上调用__get__方法时传入实例,得到的是绑定到那个实例上的方法
  7. 我们执行第六个步骤得到的func对象,与在实例上调用函数效果一样
  8. 调用函数的__get__方法时,如果instance 参数的值是None,那么得到的是函数本身
  9. word.reverse表达式其实会调用Text.reverse.__get__(word),返回对应的绑定方法
  10. 绑定方法对象有个__self__属性,其值是调用这个方法的实例引用
  11. 绑定方法的__func__属性是依附在托管类上那个原始函数的引用

绑定方法对象还有个__call__方法,用于处理真正的调用过程,这个方法调用__func__属性引用的原始函数,把函数的第一个参数设置为绑定方法的__self__属性,这就是形参self的隐式绑定方式

描述符用法建议:

  • 使用特性以保持简单:内置的property类创建的其实是覆盖型描述符,__set__方法和__get__方法都实现了,即便不定义设值方法也是如此。特性的__set__方法默认抛出AttributeError: can't set attribute异常,因此创建只读属性最简单的方式是使用特性
  • 只读描述符必须有__set__方法:如果使用描述符类实现只读属性,__get__和__set__两个方法都必须定义,否则实例的同名属性会遮盖住描述符,只读属性的__set__方法只需要抛出AttributeError异常,并提供合适的错误信息
  • 用于验证的描述符可以只有__set__方法:对仅用于验证的描述符来说,__set__方法应该检查value参数获得的值,如果有效,使用描述符实例的名称为键,直接在实例的__dict__ 属性中设置。这样,从实例中读取同名属性的速度很快,因为不用经过 __get__方法处理
  • 仅有__get__方法的描述符可以实现高效缓存:如果只编写了__get__方法,那么创建的是非覆盖型描述符。这种描述符可用于执行某些耗费资源的计算,然后为实例设置同名属性,缓存结果。 同名实例属性会遮盖描述符,因此后续访问会直接从实例的__dict__ 属性中获取值,而不会再触发描述符的__get__方法
  • 非特殊的方法可以被实例属性遮盖:由于函数和方法只实现了__get__方法,它们不会处理同名实例属性的赋值操作。因此,像 my_obj.the_method = 7这样简单赋值之后, 后续通过该实例访问the_method得到的是数字7——但是不影响类或其他实例。然而,特殊方法不受这个问题的影响。解释器只会在类中寻找特殊的方法,也就是说,repr(x)执行的其实是x.__class__.__repr__(x),因此x的__repr__属性对repr(x)方法调用没有影响。出于同样的原因,实例的_getattr__属性不会破坏常规的属性访问规则

Python属性描述符(二)的更多相关文章

  1. Python 属性描述符和属性的查找过程

    属性描述符可以用来控制给属性赋值的时候的一些行为 import numbers class IntField: def __get__(self, instance, owner): return s ...

  2. python 属性描述符

    import numbers class IntField: # 一个类只要实现了这个魔法函数,那么它就是属性描述符 #数据描述符 def __get__(self, instance, owner) ...

  3. Python属性描述符

    实现了__get__.set.__delete__中任意一个方法的类,称之为属性描述符. 属性描述符可以控制属性操作时的一些行为. 只要具有__get__方法的类就是描述符类. 如果一个类中具有__g ...

  4. Python属性描述符(一)

    描述符是对多个属性运用相同存取逻辑的一种方式,,是实现了特性协议的类,这个协议包括了__get__.__set__和__delete__方法.property类实现了完整的描述符协议.通常,可以只实现 ...

  5. Python:高级主题之(属性取值和赋值过程、属性描述符、装饰器)

    Python:高级主题之(属性取值和赋值过程.属性描述符.装饰器) 背景 学习了Javascript才知道原来属性的取值和赋值操作访问的“位置”可能不同.还有词法作用域这个东西,这也是我学习任何一门语 ...

  6. python之属性描述符与属性查找规则

    描述符 import numbers class IntgerField: def __get__(self, isinstance, owner): print('获取age') return se ...

  7. JS属性描述符之Object.defineProperty()定义对象属性特性

    一.Object.defineProperty的作用 用来给对象新增属性,和修改对象中的属性. 二.JS对象中的描述符 js对象中两种属性描述符:数据描述符和存取描述符(访问描述符). 注意事项: 1 ...

  8. python数据描述符

    Python的描述符是接触到Python核心编程中一个比较难以理解的内容,自己在学习的过程中也遇到过很多的疑惑,通过google和阅读源码,现将自己的理解和心得记录下来,也为正在为了该问题苦恼的朋友提 ...

  9. 深入理解javascript对象系列第三篇——神秘的属性描述符

    × 目录 [1]类型 [2]方法 [3]详述[4]状态 前面的话 对于操作系统中的文件,我们可以驾轻就熟将其设置为只读.隐藏.系统文件或普通文件.于对象来说,属性描述符提供类似的功能,用来描述对象的值 ...

随机推荐

  1. I/O————流

    流的关系图 缓冲流分为字节和字符缓冲流(图中是经常用的搭配,PrintWrite与BufferedWrite都继承java.io.Write) 字节缓冲流为: BufferedInputStream— ...

  2. 【转】说说Runnable与Callable

    说说Runnable与Callable   Callable接口:   Runnable接口: 相同点: 两者都是接口:(废话) 两者都可用来编写多线程程序: 两者都需要调用Thread.start( ...

  3. IO(File、递归)

      第1章 File 1.1 IO概述 回想之前写过的程序,数据都是在内存中,一旦程序运行结束,这些数据都没有了,等下次再想使用这些数据,可是已经没有了.那怎么办呢?能不能把运算完的数据都保存下来,下 ...

  4. 洛谷P1435 回文字串(dp)

    题意 题目链接 回文词是一种对称的字符串.任意给定一个字符串,通过插入若干字符,都可以变成回文词.此题的任务是,求出将给定字符串变成回文词所需要插入的最少字符数. 比如 “Ab3bd”插入2个字符后可 ...

  5. PHP 获取JSON json_decode返回NULL解决办法

    在用json_decode对JSON格式的字符串进行解码时竟然为空,页面空白啊,整半天检查这里检查那里,问同事都没用. 今天必应搜索了下,问题解决了,原来是有BOM头输出,大虾的解决办法如下: 1). ...

  6. hadoop完全分布式模式搭建和hive安装

    简介 Hadoop是用来处理大数据集合的分布式存储计算基础架构.可以使用一种简单的编程模式,通过多台计算机构成的集群,分布式处理大数据集.hadoop作为底层,其生态环境很丰富. hadoop基础包括 ...

  7. ae(ArcEngine) java swing开发入门系列(2):ae的类型转换和Proxy类说明

    做过C#版ae的都知道,操作同一个“对象”,用他的不同功能要转换到相应的接口,但java版有时不能直接做类型转换 例如下图在C#是可以的 但在java不行,这样转会报错,看IFeatureClass的 ...

  8. escape,encodeURI,encodeURIComponent 之间的区别和使用

    escape(目前已经被淘汰)是对字符串(string)进行编码(而另外两种是对URL),不会对下列字符编码 ASCII字母  数字  @*/+ 最关键的是,当你需要对URL编码时,请忘记这个方法,这 ...

  9. ceisum_加载倾斜摄影模型

    osgb转换为3Dtiles格式(使用工具转换) 然后加载到cesium中(加载代码见下,可以控制模型高度) var offset = function(height,tileset) { conso ...

  10. 菜鸟的数据库实战-4-数据阅读器SqlDataReader

    老铁们大家好啊,我是菜鸟思奎,今天我学习的是数据库和前端的连接用到的字符串,如果有什么纰漏希望大家在评论区指正.阿里嘎多. 我的环境是Visual Studio 2008 + Microsoft SQ ...