Python descriptor(描述符)的实现

(编辑:jimmy 日期: 2024/12/31 浏览:2)

问题

问题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改变值;

"htmlcode">

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已经给出了解决方法,就是描述符…

描述符的定义和介绍

"htmlcode">

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__()

"htmlcode">

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

调用描述符

"htmlcode">

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

当实例修改了描述符以后,会影响到其他实例,有没有一种方法可以让实例之间互不影响呢"color: #ff0000">数据字典法

"htmlcode">

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__方法,未进行类型检查,还是非法值啊...哈哈- -

小结

"color: #ff0000">描述符的应用…

"htmlcode">

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__()方法感觉就像一个监督人员,监视属性的一举一动。

"htmlcode">

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"_blank" href="http://www.geekfan.net/7862/">http://www.geekfan.net/7862/
5.http://pyzh.readthedocs.io/en/latest/Descriptor-HOW-TO-Guide.html
6.//www.jb51.net/article/62987.htm
7.//www.jb51.net/article/97741.htm
8.http://www.tuicool.com/articles/yYJbqun

一句话新闻

一文看懂荣耀MagicBook Pro 16
荣耀猎人回归!七大亮点看懂不只是轻薄本,更是游戏本的MagicBook Pro 16.
人们对于笔记本电脑有一个固有印象:要么轻薄但性能一般,要么性能强劲但笨重臃肿。然而,今年荣耀新推出的MagicBook Pro 16刷新了人们的认知——发布会上,荣耀宣布猎人游戏本正式回归,称其继承了荣耀 HUNTER 基因,并自信地为其打出“轻薄本,更是游戏本”的口号。
众所周知,寻求轻薄本的用户普遍更看重便携性、外观造型、静谧性和打字办公等用机体验,而寻求游戏本的用户则普遍更看重硬件配置、性能释放等硬核指标。把两个看似难以相干的产品融合到一起,我们不禁对它产生了强烈的好奇:作为代表荣耀猎人游戏本的跨界新物种,它究竟做了哪些平衡以兼顾不同人群的各类需求呢?