26

对于这个问题:

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

有人回答说:

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

因为这就是__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

考虑每个 obj 有多个 Prop 的情况:我需要使用唯一的名称来标识值和乘数(就像这里。拥有每个实例描述符对象将允许将_multiplier(和_value) 存储在描述符本身中,从而简化一些事情。

要实现每个实例描述符属性,您需要:

  1. 创建每个实例类见这里
  2. 覆盖__getattribute__ 看这里

我知道之前也有人提出过类似的问题,但我还没有找到真正的解释:

  1. 为什么 Python 是这样设计的?
  2. 存储描述符需要但每个实例的信息的建议方法是什么?
4

3 回答 3

16

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

行为是设计使然。首先,在类定义中保留对象行为简化了实现,也使实例检查更有意义。借用您的 Register 示例,如果“M”描述符是由某些实例而不是由类定义的,那么知道对象“reg”是 Register 的一个实例并不能告诉我任何关于“reg.M”是否是有效属性或错误。因此,我需要使用 try-except 构造来保护几乎所有对“reg.M”的访问,以防“reg”是错误类型的寄存器。

其次,类与实例的分离还有助于将对象行为与对象数据分开。考虑以下类:

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

不要担心这个类可能对什么有用。只需知道它旨在保存并提供对任意 Python 对象的无限制访问:

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

由于该类旨在保存任意对象,因此有人可能希望在其中存储描述符对象甚至是有效的:

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

现在假设 Python 为存储在实例属性中的描述符调用了描述符协议:

>>> 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'

在这种情况下,ObjectHolder 将无法简单地将属性对象保存为数据。仅将属性对象(描述符)分配给实例属性的行为就会改变ObjectHolder 的行为。它不会将“holder.obj”视为一个简单的数据属性,而是会在访问“holder.obj”时开始调用描述符协议,并最终将它们重定向到不存在且无意义的“holder.foo”属性,这当然是不是课程作者的意图。

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

于 2012-09-28T18:09:51.107 回答
13

大量高级功能仅在定义在类而不是实例上时才有效;例如,所有特殊方法。除了使代码评估更有效之外,这还明确了实例和类型之间的分离,否则它们往往会崩溃(因为当然所有类型都是对象)。

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

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]
于 2012-09-28T18:19:58.900 回答
1

在 Python 3.6 中,这可以很容易地完成。也许它不是预期的,但是嘿,如果它有效,对吧?;)

Python 3.6 添加了__set_name__方法

object.__set_name__(self, 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'

请注意,这不是一个完整的描述符实现,当然,如果您决定使用它,风险自负。

于 2018-05-31T16:10:40.100 回答