Python中的实例属性作为描述符

29

针对这个问题:

为什么描述符不能是实例属性?

已经被 回答

描述符对象需要存在于类中,而不是实例中

因为这是 __getattribute__ 的实现方式。

这里有一个简单的例子。考虑一个描述符:

class Prop(object):

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return obj._value * obj._multiplier

    def __set__(self, obj, value):
        if obj is None:
            return self
        obj._value = value

class Obj(object):

    val = Prop()

    def __init__(self):
        self._value = 1
        self._multiplier = 0
考虑每个对象都有多个属性的情况:我需要使用唯一的名称来标识值和乘数(就像这里)。每个实例描述符对象可以用来在描述符本身中存储_multiplier(和_value),从而简化一些操作。
要实现每个实例描述符属性,您需要执行以下操作之一:
  1. 创建每个实例类 请参见此处
  2. 覆盖__getattribute__请参见此处
我知道以前已经提出了类似的问题,但我没有找到真正的解释:
  1. 为什么Python被设计成这样?
  2. 存储描述符所需但是每个实例的信息的建议方法是什么?

只是供参考,这里在文档中明确提到。 - Suyog Shimpi
3个回答

18

今年早些时候,这个问题在Python-list上提出。我只是引用Ian G. Kelly的回答

The behavior is by design. First, keeping object behavior in the class definition simplifies the implementation and also makes instance checks more meaningful. To borrow your Register example, if the "M" descriptor is defined by some instances rather than by the class, then knowing that the object "reg" is an instance of Register does not tell me anything about whether "reg.M" is a valid attribute or an error. As a result, I'll need to guard virtually every access of "reg.M" with a try-except construct just in case "reg" is the wrong kind of register.

Second, the separation of class from instance also helps you keep object behavior separate from object data. Consider the following class:

class ObjectHolder(object):
    def __init__(self, obj):
        self.obj = obj

Don't worry about what this class might be useful for. Just know that it's meant to hold and provide unrestricted access to arbitrary Python objects:

>>> holder = ObjectHolder(42)
>>> print(holder.obj) 42
>>> holder.obj = range(5)
>>> print(holder.obj) [0, 1, 2, 3, 4]

Since the class is meant to hold arbitrary objects, it's even valid that somebody might want to store a descriptor object there:

>>> holder.obj = property(lambda x: x.foo)
>>> print(holder.obj) <property object at 0x02415AE0>

Now suppose that Python invoked the descriptor protocol for descriptors stored in instance attributes:

>>> holder = ObjectHolder(None)
>>> holder.obj = property(lambda x: x.foo)
>>> print(holder.obj)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'ObjectHolder' object has no attribute 'foo'

In this case, the ObjectHolder would fail to simply hold the property object as data. The mere act of assigning the property object, a descriptor, to an instance attribute would change the behavior of the ObjectHolder. Instead of treating "holder.obj" as a simple data attribute, it would start invoking the descriptor protocol on accesses to "holder.obj" and ultimately redirect them to the non-existent and meaningless "holder.foo" attribute, which is certainly not what the author of the class intended.

如果您想支持描述符的多个实例,只需使该描述符的构造函数接受一个名称参数(前缀),并使用该名称为添加的属性添加前缀。您甚至可以在类实例中创建一个命名空间对象(字典),以保存所有新属性实例。


14

许多高级功能只有在类上定义而不是实例上才能工作;例如,所有特殊方法都是如此。除了使代码评估更加高效之外,这还清晰地区分了实例和类型之间的差别,否则它们会倾向于混淆(因为所有类型都是对象)。

我不确定这是否被推荐,但是您可以在实例中存储从描述符实例到属性值的映射:

class Prop(object):
     def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return obj._value * obj._multiplier[self]

    def __set__(self, obj, value):
        if obj is None:
            return self
        obj._value = value

class Obj(object):
    val = Prop()

    def __init__(self):
        self._value = 1
        self._multiplier = {Obj.val: 0}

相比于另外两个建议的选项,这种方法具有明显的优势:

  1. 每个实例都创建一个类破坏了对象导向并增加了内存使用量;
  2. 重写__getattribute__是低效的(因为所有属性访问必须通过被重写的特殊方法),并且非常脆弱。

作为一种替代方法,您可以使用代理属性:

class PerInstancePropertyProxy(object):
    def __init__(self, prop):
        self.prop = prop
    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__[self.prop].__get__(instance, owner)
    def __set__(self, instance, value):
        instance.__dict__[self.prop].__set__(instance, value)
class Prop(object):
    def __init__(self, value, multiplier):
        self.value = value
        self.multiplier = multiplier
    def __get__(self, instance, owner):
        if instance is None:
            return self
        return self.value * self.multiplier
    def __set__(self, instance, value):
        self.value = value
class Obj(object):
    val = PerInstancePropertyProxy('val')
    def __init__(self):
        self.__dict__['val'] = Prop(1.0, 10.0)
    def prop(self, attr_name):
        return self.__dict__[attr_name]

你能详细解释一下 打破面向对象编程 的意思吗?其次,我认为这种方法存在的问题是如何提供一个简单的API来改变乘数。用户将不得不执行类似于 obj._multiplier[Obj.val] = 10 的操作。这可以包装在一个函数中 def change_multiplier(self, attr_name, new_value),但如果 Prop 有多个属性,则无法很好地扩展。可以使用类似于 def prop(self, attr_name): return self.__dict__[attr_name] 的方法来执行类似于 obj.prop('val').multiplier = 10 的操作。 - Hernan
@Hernan,通常认为实例具有相同的类型;如果违反这一点,各种事情都会破裂。至于更改乘数,也许可以使用代理属性?- 请参见上面的编辑。 - ecatmur
所有实例不会是相同类型的,但您可以创建子类,这样isinstance仍然可以工作。关于代理,我写了类似于这样的东西,但我不确定这是否是一个好主意。基本上,obj.prop('val')返回一个代理对象,该对象知道objval。当您执行obj.prop('val').multiplier = 10时,它会将值写入obj._multiplier[val] = 10。我只是不确定它的可维护性如何。 - Hernan

2
在Python 3.6中,这可以很容易地完成。也许这不是预期的方式,但是如果它能运行,那就没问题了,对吧?;) Python 3.6增加了__set_name__方法:(链接)

object.__set_name__(self, owner, name)

在创建拥有者类owner的同时调用。描述符已分配给name。

版本3.6中新增。

使用此名称将内部值存储在实例的字典中似乎很有效。
>>> class Prop:
...     def __set_name__(self, owner, name):
...         self.name = name
...     def __get__(self, instance, owner):
...         print('get')
...         return instance.__dict__.setdefault(self.name, None)
...     def __set__(self, instance, value):
...         print('set')
...         instance.__dict__[self.name] = value
... 
>>> class A:
...     prop = Prop()
... 
>>> a = A()
>>> a.prop = 'spam'
set
>>> a.prop
get
'spam'

请注意,这不是完整的描述符实现。当然,如果您决定使用它,那么使用时存在风险,需要自行承担责任。

1
问题:这不是一个完整的描述符实现 是什么意思?据我所知,它是一个完全适用的描述符示例。 - Sebastian Nielsen
instance.__dict__ - 对于具有 __slots__ 的类呢? - rysson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接