描述符的使用

Usage of descriptors

第一次尝试使用描述符。

目标:0 到 100(含)之间的任何整数都将是此描述符的合法值。非整数和大于 100 或小于 0 的数字应导致抛出异常,指示类型 and/or 值错误。

我的代码:

class Percentage():
    def __init__(self, initial_value=None):
        self.pct_min = 0
        self.pct_max = 100
        self.value = initial_value

    def __get__(self, obj, initial_value):
        return self.value

    def __set__(self, obj, initial_value):
        if self.pct_min <= int(self.value) <= self.pct_max:
            return self.value
        elif self.value < self.pct_min:
            raise ValueError(print("Value to low"))
        else:
            raise ValueError(print("Value to high"))


class Foo(object):
    participation = Percentage()



f1 = Foo()
f1.participation = 30

f2 = Foo()
f2.participation = 70

print(f1.participation)  # should print 30
print(f2.participation)  # should print 70

对于这一行:if self.pct_min <= int(self.value) <= self.pct_max:

我收到这个错误:

TypeError: int() argument must be a string, a bytes-like object or a number, not 'NoneType'

我通过 PythonTutor 运行 它似乎我没有传递整数,但我不明白我失败的地方。

我也以此为指导:https://www.blog.pythonlibrary.org/2016/06/10/python-201-what-are-descriptors/

这不是描述符本身的问题。问题是,在 __set__ 中,您检查现有的 self.value,而不是检查您获得的值。由于它被初始化为None,你总是会在第一次赋值时失败。

描述符本身也存在(不相关的)问题。 getter 和 setter 应该来自 obj.__dict__,而不是来自描述符对象本身的 read/write。否则,所有实例将共享一个公共值

这真的不是描述符的问题!您只是将描述符上的初始 self.value 属性设置为 None:

def __init__(self, initial_value=None):
    # ...
    self.value = initial_value  # this is None by default!

然后您尝试将设置为 Noneself.value 属性值转换为整数:

# self.value starts out as None; int(None) fails.
if self.pct_min <= int(self.value) <= self.pct_max:

您可能想将此应用到 __set__initial_value 参数!

您的描述符实现确实还有几个问题:

  • __set__ setter method 不应该 return 任何东西。它应该为绑定对象设置新值,仅此而已。 Python 忽略 return 由 __set__ 编辑的任何内容。
  • 您正在将数据存储在描述符实例本身上。您的 class 只有描述符对象的 一个副本 ,因此当您在 Foo() 的不同实例之间使用描述符时,您会看到完全相同的数据。您希望将数据存储在描述符绑定到的对象上,因此在 __set__ 方法中,在 obj 上,或者至少在一个可以让您将值与 objobj 是描述符绑定到的 Foo() 的特定实例。

  • __get__ getter method 不采用 'initial value',它采用您要绑定的对象,以及该对象的类型 (class) .在 class 上访问时,objNone 并且只设置了类型。

  • 不要混用 print() 和创建异常实例。 ValueError(print(...)) 没有多大意义,print() 写入控制台然后 returns None。只需使用 ValueError(...).

我做出以下假设:

  • 您想在 设置 无效值时引发异常。
  • 您不想在获取值时进行任何验证。
  • 当绑定对象上没有值时,应使用描述符上设置的初始值。
  • 值本身可以直接存储在实例上。

那么下面的代码就可以工作了:

class Percentage:
    def __init__(self, initial_value=None):
        self.pct_min = 0
        self.pct_max = 100
        self.initial_value = initial_value

    def __get__(self, obj, owner):
        if obj is None:
            # accessed on the class, there is no value. Return the
            # descriptor itself instead.
            return self

        # get the value from the bound object, or, if it is missing, 
        # the initial_value attribute
        return getattr(obj, '_percentage_value', self.initial_value)

    def __set__(self, obj, new_value):
        # validation. Make sure it is an integer and in range
        new_value = int(new_value)
        if new_value < self.pct_min:
            raise ValueError("Value to low")
        if new_value > self.pct_max:
            raise ValueError("Value to high")

        # validation succeeded, set it on the instance
        obj._percentage_value = new_value

演示:

>>> class Foo(object):
...     participation = Percentage()
...
>>> f1 = Foo()
>>> f1.participation is None
True
>>> f1.participation = 110
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 24, in __set__
ValueError: Value to high
>>> f1.participation = -10
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 22, in __set__
ValueError: Value to low
>>> f1.participation = 75
>>> f1.participation
75
>>> vars(f1)
{'_percentage_value': 75}
>>> Foo.participation  # this calls Participation().__get__(None, Foo)
<__main__.Percentage object at 0x105e3a240>
>>> Foo.participation.__get__(f1, Foo)  # essentially what f1.participation does
75

请注意,上面使用实例上的特定属性名称来设置值。如果您在同一个 class 上有多个描述符实例,那将是一个问题!

如果您想避免这种情况,您的选择是:

  • 使用某种映射存储,在键中使用其他一些唯一标识符来跟踪什么描述符存储了绑定对象上的什么值。
  • 将数据存储在描述符实例上,以实例的唯一标识符为键。
  • 将属性名称存储在存储此描述符的 class 上,在描述符实例本身上,并将您的实例属性名称基于此。您可以通过实现 descriptor.__set_name__() hook method 来做到这一点,当创建使用您的描述符的 class 时,Python 会调用它。此挂钩需要 Python 3.6 或更新版本。对于 Foo class 示例,将通过 Foo'participation'.
  • 调用

请注意,您应该 使用属性名称(participation 在您的 Foo 示例中),至少不要直接使用。使用 obj.participation 将触发描述符对象本身,因此 Percentage.__get__()Percencage.__set__()。对于数据描述符(实现 __set____delete__ 方法的描述符),您可以直接使用 vars(obj)obj.__dict__ 来存储属性。