Python中的专业@property修饰符

4

我有一些类,每个类都有许多属性。所有属性的共同之处在于它们应该是数字属性。这似乎是使用Python装饰器的理想场所,但我无法理解正确的实现方式。下面是一个简单的示例:

class Junk(object):
    def __init__(self, var):
        self._var = var

    @property
    def var(self):
        """A numeric variable"""
        return self._var

    @var.setter
    def size(self, value):
        # need to make sure var is an integer
        if not isinstance(value, int):
            raise ValueError("var must be an integer, var = {}".format(value))
        self._var = value

    @var.deleter
    def size(self):
        raise RuntimeError("You can't delete var")

我认为应该可以编写一个装饰器,使得上述内容能够转换成以下形式:

def numeric_property(*args, **kwargs):
    ...

class Junk(object):
    def __init__(self, var):
        self._var = var

    @numeric_property
    def var(self):
        """A numeric variable"""
        return self._var

这样新的numeric_property装饰器就可以在许多类中使用。


“numeric_property”相对于“property”有什么不同之处?如果返回值不是数字,是否会引发异常?严格类型并不符合Python的风格。在我看来,在Python中强制执行它会使您的代码像Java一样不灵活,但速度仍然像Python一样慢。 - zvone
你肯定可以做到,阅读 https://docs.python.org/3/howto/descriptor.html - jonrsharpe
你为什么想让 numeric_property 成为一个装饰器呢?为什么不只是一个返回属性的函数,这样你在类定义中只需要写 var = numeric_property('_var') 就可以了呢?(如果你想的话,你可以让它接受文档字符串作为另一个参数。) - BrenBarn
3个回答

2

@property 只是 Python 的 描述符协议 的特例,因此你可以创建自己的定制版本。对于你的情况:

class NumericProperty:
    """A property that must be numeric.

    Args:
      attr (str): The name of the backing attribute.

    """

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

    def __get__(self, obj, type=None):
        return getattr(obj, self.attr)

    def __set__(self, obj, value):
        if not isinstance(value, int):
            raise ValueError("{} must be an integer, var = {!r}".format(self.attr, value))
        setattr(obj, self.attr, value)

    def __delete__(self, obj):
        raise RuntimeError("You can't delete {}".format(self.attr))

class Junk:

    var = NumericProperty('_var')

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

使用中:

>>> j = Junk('hi')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/jonrsharpe/test.py", line 29, in __init__
    self.var = var
  File "/Users/jonrsharpe/test.py", line 17, in __set__
    raise ValueError("{} must be an integer, var = {!r}".format(self.attr, value))
ValueError: _var must be an integer, var = 'hi'
>>> j = Junk(1)
>>> del j.var
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/jonrsharpe/test.py", line 21, in __delete__
    raise RuntimeError("You can't delete {}".format(self.attr))
RuntimeError: You can't delete _var
>>> j.var = 'hello'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/jonrsharpe/test.py", line 17, in __set__
    raise ValueError("{} must be an integer, var = {!r}".format(self.attr, value))
ValueError: _var must be an integer, var = 'hello'
>>> j.var = 2
>>> j.var
2

你应该检查 numbers.Integral 而不是 int,这样你可以使用任何数字类型。 - Daniel
@Daniel 这一部分取决于 OP,我只是展示如何将他们的逻辑实现为描述符。顺便说一句,我可能也会将其设置为“TypeError”! - jonrsharpe
@Daniel:或者,根据需要,只需调用operator.index将其转换为真正的Python int(在Py2上可能是long),所有numbers.Integral都将支持它,并为您提供可预测的精确类型。 - ShadowRanger

1

选项1:继承property

property是一个描述符。请参见python.org上的描述符指南

因此,可以继承property并重写相关方法。

例如,要在setter中强制使用int:

class numeric_property(property):
    def __set__(self, obj, value):
        assert isinstance(value, int), "numeric_property requires an int"
        super(numeric_property, self).__set__(obj, value)

class A(object):
    @numeric_property
    def x(self):
        return self._x

    @x.setter
    def x(self, value):
        self._x = value

现在您可以强制使用整数:

>>> a = A()
>>> a.x = 'aaa'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in __set__
AssertionError: numeric_property requires an int

选项2:创建更好的描述符

另一方面,实现一个全新的描述符可能会更好,它不继承自属性,这将使您能够一次性定义属性。

拥有这种类型的接口会更好:

class A(object):
    x = numeric_property('_x')

为此,您需要实现一个描述符,该描述符需要获取属性名称:

class numeric_property(object):
    def __init__(self, private_attribute_name, default=0):
        self.private_attribute_name = private_attribute_name
        self.default = default

    def __get__(self, obj, typ):
        if not obj: return self
        return getattr(obj, self.private_attribute_name, self.default)

    def __set__(self, obj, value):
        assert isinstance(value, int), "numeric_property requires an int"
        setattr(obj, self.private_attribute_name, value)

免责声明 :)

我不希望在Python中强制执行严格类型,因为没有它,Python会更加强大。


@jonrsharpe,这就是所谓的“touché”。 - Bharel

0
你可以创建一个函数来完成这个任务。非常简单,不需要创建自定义描述符:
def numprop(name, privname):
    @property
    def _numprop(self):
        return getattr(self, privname)

    @_numprop.setter
    def _numprop(self, value):
        if not isinstance(value, int):
            raise ValueError("{name} must be an integer, {name} = {}".format(value, name=name))
        setattr(self, privname, value)

    @_numprop.deleter
    def _numprop(self):
        raise RuntimeError("You can't delete var")
    return _numprop


class Junk(object):
    def __init__(self, var):
        self._var = var
    var = numprop("var", "_var")

1
“无需创建描述符” - 实际上您是在以一种间接且笨拙的方式创建描述符。 - jonrsharpe
我相信编写一个小函数比自定义描述符更容易、更高效和更清晰。没有必要深入研究描述符协议。很多人都知道属性是什么,但并不是每个人都了解描述符。 - Bharel
1
你有权发表自己的意见,但我不同意它更容易或更清晰(效率也不是一个观点问题)。你所谓的“深入描述符协议”我会重新解释为“知道你在做什么”。更不用说描述符类可以以一种函数返回单个属性无法实现的可重用方式使用。 - jonrsharpe
你可以重新发明轮子,为几乎所有东西创建自定义描述符和元类。如果你知道你在做什么,你也可以重新创建typeobject。但是,对于提问者来说简单的事情并不一定对他来说也是简单的,否则他就不会问这个问题了。关于效率,使用内置函数比使用自定义类通常更快。你还可以教提问者关于__slots__和弱引用的知识。 - Bharel
我让Raymond Hettinger来回答:“学习描述符不仅提供了更多的工具集,还可以深入了解Python的工作原理,并欣赏其设计的优雅之处...描述符简化了底层C代码,并为日常Python程序提供了灵活的一组新工具。”但是,再次强调,你有权发表自己的观点。 - jonrsharpe
@jonrsharpe 我告诉你吧:两种解决方案都可以。我几乎可以确定在这种情况下使用内置函数更有效,因为属性是用许多优化的 C 实现的。我只是认为教 OP 关于描述符虽然是一个好的和周到的想法,但并不需要为了解决他的问题而必须这样做。我自己可能根本不会这样做,因为鸭子类型是 Python 的主要习语,但那是另一个讨论。你的解决方案很棒,是我个人想到的第一个解决方案。我的解决方案不需要教学和打开文档。 - Bharel

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