Python descriptor(描述符)的实现


Posted in Python onNovember 15, 2020

问题

问题1

Python是一种动态语言,不支持类型检查。当需要对一个对象执行类型检查时,可能会采用下面的方式:

class Foo(object):
 def __init__(self,a):
 if isinstance(a,int):
  self.__a = a
 else:
  raise TypeError("Must be an int")

 def set_a(self,val):
 if isinstance(val,int):
  self.__a = val
 else:
  raise TypeError("Must be an int")

 def get_a(self):
 return self.__a

上述是一种类型检查的方法,但是如果需要类型检查的参数非常多的话就会变得非常繁琐,重复代码太多,Python这么简洁,优雅,肯定有更好的解决方法。另外上述方法也有缺陷。

f = Foo(1)
print f.get_a() #1
print f._Foo__a #1,还是能访问到= =
f._Foo__a = 'test'
print f.get_a() #test,还是改变了__a的值,而且不是int型
print f._Foo__a #test

问题2

在一个对象中,创建一个只读属性。

问题3

class Foo(object):
 a = 1
f = Foo()
print f.a #1,实例属性中没有a属性,所以到Foo.__dict__中查找
print Foo.a #1
print f.__dict__ #{}
f.a = 2  #增加一个名为a的实例属性
print f.a #2,搜索属性时先在实例字典中查找,然后再去类的字典中查找,在实例的__dict__中找到了..
print Foo.a #1
print f.__dict__ #{'a': 2}

如果不想给实例f增加实例属性,而是想对类属性操作怎么办呢。解决方案:

1) 使用class.attr改变值;

Foo.a = 2就不会给实例增加实例属性了。

2) 自定义属性访问,描述符;

class descriptor(object):
 def __init__(self,val):
 self.val = val
 def __get__(self,obj,type = None):
 print 'get',
 return self.val
 def __set__(self,obj,val):
 print 'set',val
 self.val = val
 def __delete__(self,obj):
 raise AttributeError("Can't delete attribute")
class Foo(object):
 a = descriptor(0)

f = Foo()
print f.a #get 0
print Foo.a #get 0
print f.__dict__ #{}
f.a = 2  #set 2,并没有增加实例属性
print f.a #get 2
print Foo.a #get 2
print f.__dict__ #{}

问题总结

上述三个问题均与属性访问有关,如果能够自定义属性访问,上述问题就能解决啦= =。其实问题3已经给出了解决方法,就是描述符…

描述符的定义和介绍

​ 描述符(Descriptor)是Python中非常重要的一部分,它广泛应用于Python的内核。

​ 一般来说,描述符就是一个带有绑定方法的对象,只不过照比其他对象多了几个特殊的描述符方法,即 __get__(),__set__(),__delete__()对描述符对象的属性访问主要通过描述符方法。

定义:一个对象如果定义了__get__(),__set__(),__delete__()方法中的任何一个,它就可以被称为描述符。

​ 对属性的访问默认是从对象的字典中获取(get),设置(set)和删除(delete)属性。

假设有实例a,a有属性x,获取a.x的值。

一般来说,a.x会按以下顺序查找属性,查找链: a.__dict__['x']type(a).__dict__['x']根据mro顺序在type(a)的父类中查找(不包括元类)

​ 但是,如果被查找的属性是一个描述符,并且为类属性,那么就会覆盖其默认行为,转而去调用描述符方法。注意,描述符仅适用于新式类和新式对象。

class descriptor(object):
 def __init__(self,val):
  self.val = val

 def __get__(self, obj, objtype):
  print 'get',
  return self.val

 def __set__(self, obj, val):
  print 'set'
  self.val = val

class Foo(object):
 x = descriptor(0)
 y = 0

f = Foo()
'''描述符覆盖默认的访问行为'''
print f.x   #get 0,调用的是描述符的__get__函数
print f.__dict__ #{}
f.x = 1    #set,调用的是描述符的__set__函数
print f.x   #get 1
print f.__dict__ #{},即使赋值也没有增加实例属性...,是不是覆盖了默认行为- -
f.y = 2    
print f.__dict__ #{'y': 2},因为没有与y同名的描述符对象...

描述符十分强大。properties,methods, static methods,class methods, and super()都是基于描述符实现的。从Python2.2开始,描述符就在新式类中出现了。描述符是一个灵活的工具,使程序开发更加便利。感觉描述符很吊啊…

Descriptor Protocol(描述符协议)

descriptor.__get__(self, obj, type=None) --> value

descriptor.__set__(self, obj, value) --> None

descriptor.__delete__(self, obj) --> None

上述三个方法就是描述符方法,如果一个对象定义了描述符方法中的任何一个,那么这个对象就会成为描述符。

假设有一个对象t,t.a是一个定义了三个描述符方法的描述符,并且a是类属性,调用情况如下:

print t.a → a.__get__(t, type(t))

t.a = v → a.__set__(t, v)

del t.a → a.__delete__(t)

注意:当描述符作为类属性时,才会自动调用描述符方法,描述符作为实例属性时,不会自动调用描述符方法。

数据和非数据描述符

data descriptor(数据描述符):定义了__get__()__set__()方法的对象;

non-data descriptor(非数据描述符):定义了__get__()的对象;非数据描述符典型应用是class methods。

数据和非数据描述符的区别

如果一个实例的字典(__dict__)中有一个和数据描述符对象相同名字的实例属性,则优先访问数据描述符;

class descriptor(object):
 def __init__(self,val = 0):
  self.val = val

 def __get__(self, obj, objtype):
  print '__get__',
  return self.val

 def __set__(self, obj, val):
  self.val = val

class Foo(object):
 a = descriptor()#数据描述符
f = Foo()
f.a = 1    #不会增加实例属性,会调用descriptor.__set__方法,数据描述符的优先级比实例属性高
print f.a   #__get__ 1,调用的是descriptor.__get__方法
print f.__dict__ #{}

如果一个实例中存在和数据描述符名字相同的实例属性,利用下述方法就可以访问实例属性…

f.__dict__['a'] = 2   #增加一个与数据描述符同名的实例属性
print f.__dict__   #{'a': 2}
print f.a     #__get__ 1,正常使用,调用的仍是描述符的__get__方法
print f.__dict__['a']  #2,这个时候就是实例属性了...

如果一个实例的字典中有一个和非数据描述符对象相同名字的实例属性,则优先访问实例属性;

class descriptor(object):
 def __init__(self,val = 0):
  self.val = val

 def __get__(self, obj, objtype):
  print '__get__',
  return self.val

class Foo(object):
 a = descriptor()#非数据描述符,没有__set__
f = Foo()
print f.a   #__get__ 0,调用descriptor.__get__方法
f.a = 1    #增加实例属性,因为实例属性的优先级比非数据描述符高
print f.a   #1,不会调用descriptor.__get__方法,实例属性的优先级比非数据描述符高
print f.__dict___ #{'a': 1},增加了实例属性a

数据和非数据描述符测试code

class data_descriptor(object):#数据描述符
 def __init__(self,val):
  self.val = val

 def __get__(self, obj,type = None):
  print 'data get'
  return self.val

 def __set__(self, obj, value):
  if not isinstance(value,int):#赋值时可以类型检查啊
   raise ValueError('value must be int')
  self.val = value

class non_data_descriptor(object):#非数据描述符
 def __init__(self,val):
  self.val = val

 def __get__(self,obj,type = None):
  print 'non data get'
  return self.val

class Foo(object):
 data = data_descriptor(0)
 non_data = non_data_descriptor(1)

f = Foo()
print f.__dict__ #{}
print f.data  #data get,0
print f.non_data #non data get,1
f.data = 2   #数据描述符优先级较高,不会创建实例属性,而是调用描述符的__set__方法
f.non_data = 3  #增加了与非数据描述符同名的实例属性

print f.__dict__ #{'non_data': 3}
print f.data  #data get,2 调用数据描述符的__get__()方法
print f.non_data #3,非数据描述符优先级比实例属性低
print Foo.non_data #non data get 1,利用类属性查找还是可以访问非数据描述符的,非数据描述符值未改变

属性访问,__getattribute__()

​ 在Python中,__getattribute__()就是属性解析机制,当调用一个属性时,不管是成员还是方法,都会触发 __getattribute__()来调用属性。

属性解析机制按照优先级链搜索属性。在优先级链中,类字典中的数据描述符的优先级高于实例变量,实例属性的优先级高于非数据描述符,如果定义了__getattr()__,优先级链会为__getattr()__分配最低优先级。可以通过自定义__getattribute__方法来重写优先级链。

优先级链 : 数据描述符 > 实例属性 > (非数据描述符,非描述符的类属性) > __getattr()__

上述所说的描述符均要为类属性,当描述符作为实例属性出现时,不会自动调用描述符方法。

class Foo(object):
 def __init__(self):
  self.x = 1
  self.y = 2

 def __getattribute__(self,keys = None):#这样优先级链,描述符什么的就都没用了。。。。
  return 'test'

f = Foo()
print f.x,f.y,f.z #test,test,test,优先级链什么的都没用啦= =

__getattribute__()的Python实现大致如下,

def __getattribute__(self, key):
 "Emulate type_getattro() in Objects/typeobject.c"
 v = object.__getattribute__(self, key)
 if hasattr(v, '__get__'):#如果v定义了__get__方法的话,优先调用v.__get__ 
  return v.__get__(None, self)
 return v

调用描述符

​ 描述符调用的细节取决于obj是一个对象还是一个类。不管是哪种,描述符只对新式对象和新式类起作用。继承了 object 的类是新式类。

​ 描述符也是一个对象,可以通过方法名直接调用描述符方法,如描述符d,d.__get__(object)。另外,在访问描述符时会自动调用相应的描述符方法(只有为类属性时才会自动调用)。描述符的自动调用机制基于__getattribute__()__getattribute__()确保了descriptor的机制,所以,如果重写了__getattribute__, 就可以消除descriptor机制。

​ 对于对象来说,obj.__getattribute__()会将b.x转化为 type(b).__dict__['x'].__get__(b,type(b)) ,按照下述顺序搜索属性:

类属性中的数据描述符 > 实例变量 > 类属性的非数据描述符 > __getattr__()

​ 对于类来说,class.__getattribute__()会将B.x转化为B.__dict__['x'].__get__(None,B)

记住以下几点:

1.描述符的调用基于__getattribute__();
2.重写__getattribute__()会阻止描述符的正常调用;
3.object.__getattribute__()和type.__getattribute__()会调用不同的__get__();
4.数据描述符一直覆盖实例属性,即同时存在同名的数据描述符和实例属性,优先调用数据描述符
5.非数据描述符一直被实例属性覆盖,即同时存在同名的非数据描述符和实例属性,优先调用实例属性;

​ 描述符的机制在 object, type, 和 super__getattribute__()方法中实现。由 object 派生出的类自动继承这个机制,或者它们有个有类似机制的元类。如果想让描述符失效的话,可以重写 __getattribute__()

class descriptor(object):
 def __init__(self, initval=None, name='var'):
  self.val = initval
  self.name = name

 def __get__(self, obj, objtype):
  print 'get', self.name,
  return self.val

 def __set__(self, obj, val):
  print 'set', self.name,val
  self.val = val

class Foo(object):
 x = descriptor(10, 'var "x"')
 y = 5

m = Foo()
'''访问m.x的三种方法'''
print m.x       #get var "x",10
print type(m).__dict__['x'].__get__(m,type(m)) #m.x会转化为这种形式,等价于m.x
print m.__getattribute__('x')  #等价于m.x,因为x定义了__get__方法,调用x的__get__方法,上面已经给出了__getattribute__的实现原理
'''设置m.x的值'''
m.x = 20        #set var "x" 20
type(m).__dict__['x'].__set__(m,20) #m.x = 20会转化为此种形式,等价于m.x = 20

print m.x       #get var "x",20
print m.y       #5
#print type(m).__dict__['y'].__get__(m,type(m)) #error,AttributeError: 'int' object has no attribute '__get__'
print m.__getattribute__('y')  #5,等价于m.y

描述符的陷阱

描述符应放在类层次上,即作为类属性(class level)

说了N多次了,要谨记…

class Foo(object):
 y = descriptor(0)
 def __init__(self):
  self.x = descriptor(1)  #实例属性的描述符是不会自动调用对应的描述符方法的...
b = Foo()
print "X is %s, Y is %s" % (b.x, b.y)#X is <__main__.descriptor object at 0x10432c250>, Y is 0
print "Y is %s"%b.y.__get__(b)  #需要自己调用__get__方法,解释器不会自己调用的

从上述代码可知,

  1. 访问类层次上的描述符会自动调用相应的描述符方法;
  2. 访问实例层次上的描述符只会返回描述符对象自身,并不会调用相应的描述符方法;

(实例属性的描述符不会自动调用描述符方法,这么做肯定是有原因的吧…有知道的大神求指导…)

**描述符是所有实例共享的,让不同实例保存的值互不影响

class descriptor(object):
 def __init__(self, default):
  self.value = default

 def __get__(self, instance, owner):
  return self.value

 def __set__(self, instance, value):
  self.value = value

class Foo(object):
 bar = descriptor(5)

bar是类属性,所有Foo的实例共享bar。

f = Foo()
g = Foo()
print "f.bar is %s g.bar is %s" % (f.bar, g.bar) #f.bar is 5 g.bar is 5
f.bar = 10          #调用__set__函数
print "f.bar is %s g.bar is %s" % (f.bar, g.bar) #f.bar is 10 g.bar is 10

当实例修改了描述符以后,会影响到其他实例,有没有一种方法可以让实例之间互不影响呢?

数据字典法

​ 在descriptor中使用数据字典,由__get____set__的第一个参数来确定是哪个实例,使用实例作为字典的key,为每一个实例单独保存一份数据,修改代码如下…

from weakref import WeakKeyDictionary
class descriptor(object):
 def __init__(self, default):
  self.default = default
  self.data = WeakKeyDictionary()

 def __get__(self, instance, owner):# instance = x,owner = type(x)
  # we get here when someone calls x.d, and d is a descriptor instance
  return self.data.get(instance, self.default)

 def __set__(self, instance, value):
  # we get here when someone calls x.d = val, and d is a descriptor instance
  self.data[instance] = value

class Foo(object):
 bar = descriptor(5)

f = Foo()
g = Foo()
print "f.bar is %s g.bar is %s" % (f.bar, g.bar) #f.bar is 5 g.bar is 5
print "Setting f.bar to 10"
f.bar = 10
print "f.bar is %s\ng.bar is %s" % (f.bar, g.bar) ##f.bar is 10 g.bar is 5

上述方法虽然可行,但是存在缺陷。

descriptor使用了一个字典来保存不同实例的数据。一般来说是不会出现问题,但是如果实例为不可哈希对象,如list,上述方法就会出现问题,因为不可哈希对象不能作为键值。

标签法

说白了就是给实例增加一个与描述符同名的实例属性,利用该实例属性来保存该实例描述符的值,描述符相当于一个中间操作,描述符的__get__()返回实例属性,__set__也是对实例属性操作。

此方法的实现原理: 数据描述符的访问优先级比实例属性高…

还是见图和代码吧,代码最直观…

Python descriptor(描述符)的实现

class descriptor(object):
 def __init__(self, label):#label为给实例增加的实例属性名
  self.label = label
 def __get__(self, instance, owner):
  #dict.get(k[,d]) -> D[k] if k in D, else d. d defaults to None.
  return instance.__dict__.get(self.label) #获取与描述符同名的实例属性的值 

 def __set__(self, instance, value):
  #注意这里,要这么写,不能写instance.x = val这种形式,这样会形成自身的循环调用
  instance.__dict__[self.label] = value  #修改与描述符同名的实例属性的值 

class Foo(list):
 x = descriptor('x') #注意这个初始化值为要给实例增加的实例属性名,要和描述符对象同名。
 y = descriptor('y')

f1 = Foo()
f2 = Foo()
print f1.__dict__ #{}
print f1.x,f2.x,f1.y,f2.y#None None None None,此时尚未增加实例属性,需要调用__set__方法建立一个与描述符同名的实例属性
#print Foo.__dict__
f1.x = 1
f1.y = 2
f2.x = 3
f2.y = 4   
print f1.__dict__ #{'y': 2, 'x': 1} #增加了的实例属性
print f1.x,f1.y,f2.x,f2.y #1 2 3 4

因为只有调用了__set__函数才会建立一个与描述符同名的实例属性,所以可以在__init__()函数中对描述符赋值。

class Foo(list):
 x = descriptor('x')
 y = descriptor('y')
 def __init__(self):
  self.x = 1 #调用的是描述符的__set__方法,与描述符同名的实例属性增加完毕....
  self.y = 2
f = Foo()
print f.x,f.y

注意事项:

给描述符添加标签时,初始化值要和描述符的变量名相同,比如name = descriptor(‘name'),因为这个初始化值是给实例增加的实例属性名,必须要和描述符对象同名。

下面为错误示范,初始化值和描述符不同名的情况。

class descriptor(object):
 def __init__(self, label):
  self.label = label
 def __get__(self, instance, owner):
  return instance.__dict__.get(self.label) 

 def __set__(self, instance, value):
  if not isinstance(value,int):
   raise ValueError('must be int')
  instance.__dict__[self.label] = value 

class Foo(object):
 x = descriptor('y') #应该改为Descriptor('x'),与实例同名
 def __init__(self,val = 0):
  self.x = val

f = Foo()
print f.x #0
f.y = 'a' #绕过了描述符的__set__方法...未进行类型检查,此时x为非法值啊,是不是很坑...
print f.x #a

潜在坑…正常使用时也会带来坑。

class Foo(object):
 x = descriptor('x')
 def __init__(self,val = 0):
  self.x = val

f = Foo()
f.x = 'a'#ValueError: must be int

好像没毛病啊…接着往下看。

f.__dict__['x'] = 'a'
print f.x #a,还是绕过了__set__方法,未进行类型检查,还是非法值啊...哈哈- -

小结

​ 查了很多资料,标签法用的较多,但是标签法也有一定的缺陷,目前没有找到更好的方法解决上述问题,如有更好的方法,求指导,谢谢…

描述符的应用…

​ 描述符也是类,可以为其增加方法。比如增加回调方法,描述符是一个用来回调的很好的手段。比如想要一个类的某个状态发生变化就立刻通知我们,可以自定义回调函数用来响应类中的状态变化。如以下代码,

from weakref import WeakKeyDictionary
class CallbackProperty(object):
  def __init__(self, default=None):
    self.data = WeakKeyDictionary()
    self.default = default
    self.callbacks = WeakKeyDictionary()

  def __get__(self, instance, owner):
    if instance is None:
      return self    
    return self.data.get(instance, self.default)

  def __set__(self, instance, value):#每次改变值的时候都会调用low_balance_warning函数
    for callback in self.callbacks.get(instance, []):
      # alert callback function of new value
      callback(value)
    self.data[instance] = value

  def add_callback(self, instance, callback):
    """Add a new function to call everytime the descriptor within instance updates"""
    if instance not in self.callbacks:
      self.callbacks[instance] = [] #实例->[方法,]
    self.callbacks[instance].append(callback)

class BankAccount(object):
  balance = CallbackProperty(0)

def low_balance_warning(value):
  if value < 100:
    print "You are now poor"
  else:
    print 'You are now rich!!!'

def check(value):
  print 'You have %s money, Good Luck!!!'%value

ba = BankAccount()
BankAccount.balance.add_callback(ba, low_balance_warning)
BankAccount.balance.add_callback(ba, check)

ba.balance = 5000          # You are now rich!!! You have 5000 money, Good Luck!!!
print "Balance is %s" % ba.balance  # Balance is 5000
ba.balance = 99           # You are now poor  You have 99 money, Good Luck!!!
print "Balance is %s" % ba.balance  # Balance is 99

有木有感觉很厉害…__set__()方法感觉就像一个监督人员,监视属性的一举一动。

​ 描述符还有其他用处,如格式检查,类型检查,设置只读变量等。设置一个只读变量的话,只要不让变量再赋值就好了,即调用__set__()函数时触发异常即可,这也是问题2的答案。

class descriptor(object):
  def __init__(self,val):
    self.val = val

  def __get__(self, obj,type = None):
    return self.val

  def __set__(self, obj, value):
    raise Exception('read only')

class Foo(object):
  d = descriptor(1)

d = Foo()
print d.d #1
d.d = 2  #触发异常,read only

参考网址

1.https://docs.python.org/2/howto/descriptor.html#definition-and-introduction
2.https://hg.python.org/cpython/file/2.7/Objects/object.c
3.http://svn.python.org/view/python/trunk/Objects/classobject.c?view=markup
4.http://www.geekfan.net/7862/
5.http://pyzh.readthedocs.io/en/latest/Descriptor-HOW-TO-Guide.html
6.//3water.com/article/62987.htm
7.//3water.com/article/97741.htm
8.http://www.tuicool.com/articles/yYJbqun

到此这篇关于Python descriptor(描述符)的实现的文章就介绍到这了,更多相关Python descriptor内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Python 相关文章推荐
python模拟登陆阿里妈妈生成商品推广链接
Apr 03 Python
在Python中marshal对象序列化的相关知识
Jul 01 Python
python使用fork实现守护进程的方法
Nov 16 Python
python中数据爬虫requests库使用方法详解
Feb 11 Python
python+opencv实现摄像头调用的方法
Jun 22 Python
python 读取修改pcap包的例子
Jul 23 Python
Pandas0.25来了千万别错过这10大好用的新功能
Aug 07 Python
解决Tensorboard 不显示计算图graph的问题
Feb 15 Python
Python下划线5种含义代码实例解析
Jul 10 Python
django 模型字段设置默认值代码
Jul 15 Python
python 利用opencv实现图像网络传输
Nov 12 Python
教你使用pyinstaller打包Python教程
May 27 Python
基于OpenCV的网络实时视频流传输的实现
Nov 15 #Python
彻底解决Python包下载慢问题
Nov 15 #Python
Python eval函数原理及用法解析
Nov 14 #Python
Django怎么在admin后台注册数据库表
Nov 14 #Python
通过实例解析python and和or使用方法
Nov 14 #Python
Python高并发和多线程有什么关系
Nov 14 #Python
Django跨域请求原理及实现代码
Nov 14 #Python
You might like
apache+mysql+php+ssl服务器之完全安装攻略
2006/09/05 PHP
PHP中数字检测is_numeric与ctype_digit的区别介绍
2012/10/04 PHP
Smarty中调用FCKeditor的方法
2014/10/27 PHP
thinkPHP实现将excel导入到数据库中的方法
2016/04/22 PHP
Jquery Ajax学习实例5 向WebService发出请求,返回泛型集合数据的异步调用
2010/03/17 Javascript
JQuery为textarea添加maxlength属性的代码
2010/04/07 Javascript
Javascript动态绑定事件的简单实现代码
2010/12/25 Javascript
JavaScript函数定义的常见注意事项小结
2014/09/16 Javascript
整理Javascript基础语法学习笔记
2015/11/29 Javascript
picLazyLoad 实现图片延时加载(包含背景图片)
2016/07/21 Javascript
用JS中split方法实现彩色文字背景效果实例
2016/08/24 Javascript
HTML5 canvas 9绘制图片实例详解
2016/09/06 Javascript
mui开发中获取单选按钮、复选框的值(实例讲解)
2017/07/24 Javascript
vue下拉菜单组件(含搜索)的实现代码
2018/11/25 Javascript
学习python类方法与对象方法
2016/03/15 Python
Python读写/追加excel文件Demo分享
2018/05/03 Python
Python 统计字数的思路详解
2018/05/08 Python
对python中字典keys,values,items的使用详解
2019/02/03 Python
Pandas中Series和DataFrame的索引实现
2019/06/27 Python
Pycharm简单使用教程(入门小结)
2019/07/04 Python
Python PyInstaller库基本使用方法分析
2019/12/12 Python
python中wx模块的具体使用方法
2020/05/15 Python
如何基于python实现年会抽奖工具
2020/10/20 Python
python多线程和多进程关系详解
2020/12/14 Python
Python中使用Selenium环境安装的方法步骤
2021/02/22 Python
毕业生文员求职信
2013/11/03 职场文书
金融专业大学生职业生涯规划范文
2014/01/16 职场文书
深入开展党的群众路线教育实践活动方案
2014/02/04 职场文书
全国优秀教师事迹材料
2014/08/26 职场文书
2014小学二年级班主任工作总结
2014/12/05 职场文书
个人先进材料范文
2014/12/30 职场文书
鼋头渚导游词
2015/02/05 职场文书
党员读书活动心得体会
2016/01/14 职场文书
使用canvas实现雪花飘动效果的示例代码
2021/03/30 HTML / CSS
Go语言实现Base64、Base58编码与解码
2021/07/26 Golang
python运算符之与用户交互
2022/04/13 Python