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中条件选择和循环语句使用方法介绍
Mar 13 Python
基于Python实现的扫雷游戏实例代码
Aug 01 Python
Python中unittest用法实例
Sep 25 Python
python将文本转换成图片输出的方法
Apr 28 Python
详解Golang 与python中的字符串反转
Jul 21 Python
关于Python中空格字符串处理的技巧总结
Aug 10 Python
浅谈numpy数组中冒号和负号的含义
Apr 18 Python
解决Pycharm出现的部分快捷键无效问题
Oct 22 Python
python读写csv文件的方法
Aug 13 Python
在python中做正态性检验示例
Dec 09 Python
Python3标准库之dbm UNIX键-值数据库问题
Mar 24 Python
Python图像识别+KNN求解数独的实现
Nov 13 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
php入门学习知识点一 PHP与MYSql连接与查询
2011/07/14 PHP
很让人受教的 提高php代码质量36计
2012/09/05 PHP
php常用的安全过滤函数集锦
2014/10/09 PHP
PHP pthreads v3下worker和pool的使用方法示例
2020/02/21 PHP
JavaScript 在线压缩和格式化收藏
2009/01/16 Javascript
某页码显示的helper 少量调整,另附js版
2010/09/12 Javascript
js setTimeout 参数传递使用介绍
2013/08/13 Javascript
javascript学习笔记(八)正则表达式
2014/10/08 Javascript
input输入框鼠标焦点提示信息
2015/03/17 Javascript
浅析AngularJs HTTP响应拦截器
2015/12/28 Javascript
BootStrap实现树形目录组件代码详解
2016/06/21 Javascript
Bootstrap Validator 表单验证
2016/07/25 Javascript
JS数字千分位格式化实现方法总结
2016/12/16 Javascript
快速解决Vue项目在IE浏览器中显示空白的问题
2018/09/04 Javascript
Typescript 中的 interface 和 type 到底有什么区别详解
2019/06/18 Javascript
vue 实现LED数字时钟效果(开箱即用)
2019/12/08 Javascript
详解JavaScript作用域 闭包
2020/07/29 Javascript
Python原始字符串(raw strings)用法实例
2014/10/13 Python
Python文件操作基本流程代码实例
2017/12/11 Python
Python 实现取矩阵的部分列,保存为一个新的矩阵方法
2018/11/14 Python
搭建python django虚拟环境完整步骤详解
2019/07/08 Python
用Python将Excel数据导入到SQL Server的例子
2019/08/24 Python
Python通过socketserver处理多个链接
2020/03/18 Python
Python如何爬取qq音乐歌词到本地
2020/06/01 Python
linux面试题参考答案(10)
2013/11/04 面试题
社区包粽子活动方案
2014/01/21 职场文书
中学生班主任评语
2014/01/30 职场文书
餐厅经理岗位职责和岗位目标
2014/02/13 职场文书
酒店采购员岗位职责
2014/03/14 职场文书
幼儿园的门卫岗位职责
2014/04/10 职场文书
服务整改报告
2014/11/06 职场文书
2015商场元旦促销活动策划方案
2014/12/09 职场文书
酒店圣诞节活动总结
2015/05/06 职场文书
公司食堂管理制度
2015/08/05 职场文书
oracle通过存储过程上传list保存功能
2021/05/12 Oracle
Golang Elasticsearches 批量修改查询及发送MQ
2022/04/19 Golang