新风格的类引发的属性访问问题

标签:Python

Python里有个内建的property函数,可以把函数封装成属性,这是一个非常不错的功能。不过假如我希望这个函数只被调用一次,以后访问属性时就直接访问上次的结果,而不需要重新调用一次函数,那就有些麻烦了。

最容易想到的就是手动创建一个属性,然后返回这个属性。然而这仍然会导致一次函数调用,这在Python里仍然会产生一点点的开销。

另一种方法就是直接将返回值设为该属性,避免后续访问仍然调用函数,这在传统类(classic class)中是可以实现的:
class Classic:

  @property
  def attr(self):
    print 'in property'
    self.attr = 'hello'
    return 'hello'

o = Classic()

print o.attr
print o.attr
结果是:
in property
hello
hello
可以看到attr方法只被调用了一次,之后的访问就不再调用函数了。

然而将它换成新风格的类(new-style class),就会出现这个异常:AttributeError: can't set attribute。
原因很简单,因为传统类的实例在设置属性时是直接修改实例的__dict__属性(这可以看成是property的bug,从这层意义上来说,property的set不应该用于传统类),因此self.attr = 'hello'会被隐式转变为self.__dict__['attr'] = 'hello',这不会引起任何冲突。
而新风格的类在设置属性时,如果是被property封装过的,那么会调用property的第2个参数来设置。可我在设置property时只传递了1个attr参数,因此第2个参数为None,于是这个属性就变成只读的,而不能被重设。

为了重设这个值,我必须给property传递第2个参数,于是变成了这样:
class NewStyle(object):

  def getAttr(self):
    print 'in get'
    self.attr = 'hello'
    return 'hello'

  def setAttr(self, value):
    print 'in set'
    self.attr = value

  attr = property(getAttr, setAttr)

o = NewStyle()

print o.attr
print o.attr
可是运行时却又拿到一个错误:RuntimeError: maximum recursion depth exceeded。
原因就是setAttr中的self.attr = value会被隐式转换成setAttr(self, value),也就是不断地递归调用自身了。

于是仿照传统类,直接写成self.__dict__['attr'] = value,结果为:
in get
in set
hello
in get
in set
hello
可以看到,访问仍然被当成函数调用了,根本没有访问__dict__。原因就是当新风格类的一个descriptor属性同时实现了__get__和__set__(或__delete__)后,就成为了一个data descriptor,它的优先级就高于__dict__了。
而__set__这个方法被property改写了(即使为None),因此Python不会使用默认的策略去寻找__dict__,而是必须调用__get__。
一个简单的优化如下:
def getAttr(self):
  print 'in get'
  try:
    return self.__dict__['attr']
  except:
    self.attr = 'hello'
    return 'hello'
但这仍避免不了getAttr的调用。

最终我也没找到好的办法,只能自己用descriptor来实现:
class Attr(object):

  def __get__(self, obj, objtype):
    print 'in get'
    obj.attr = 'hello'
    return 'hello'


class NewStyle(object):

  attr = Attr()

o = NewStyle()

print o.attr
print o.attr
这个复杂的玩意终于实现了我想要的结果,性能也的确比一般的实现快6倍:
in get
hello
hello
原理就是只实现__get__,而不实现__set__和__delete__时,因此是一个non-data descriptor,优先级低于__dict__。
不过要注意的是,这种方法实际上设置的是类属性,在处理私有数据时显得有些危险。但Python本身就是很危险的语言,可以说约定的作用要大于编译器的限制。

最后,如果要设置多个属性的话,总会觉得不便,所以我从《How-To Guide for Descriptors》里抄了个property的实现:
class Property(object):

  def __init__(self, fget):
    self.fget = fget

  def __get__(self, obj, objtype=None):
    if obj is None:
      return self
    if self.fget is None:
      raise AttributeError, 'unreadable attribute'
    return self.fget(obj)

class NewStyle(object):

  @Property
  def attr(self):
    print 'in property'
    self.attr = 'hello'
    return 'hello'

o = NewStyle()

print o.attr
print o.attr
print NewStyle.attr.__get__(o)

0条评论 你不来一发么↓

    想说点什么呢?