【问题标题】:Decorators or assertions in setters to check property type?设置器中的装饰器或断言来检查属性类型?
【发布时间】:2019-11-03 12:55:33
【问题描述】:

在一个 python 项目中,我的类有几个属性,我需要它们是特定类型的。类的用户必须具有设置属性的能力。

最好的方法是什么?我想到了两个解决方案: 1.在每个setter函数中都有测试例程。 2. 对属性使用装饰器

我当前的解决方案是 1,但由于代码重复,我对此并不满意。它看起来像这样:

class MyClass(object):
    @property
    def x(self):
        return self._x

    @x.setter
    def x(self, val):
        if not isinstance(self, int):
            raise Exception("Value must be of type int")
        self._x = val

    @property
    def y(self):
        return self._y

    @x.setter
    def y(self, val):
        if not isinstance(self, (tuple, set, list)):
            raise Exception("Value must be of type tuple or set or list")
        self._y = val

根据我对装饰器的了解,在def x(self) 处理这项工作之前应该可以有一个装饰器。唉,我在这方面惨败,因为我发现的所有示例(如 thisthis)都不是针对我想要的。

第一个问题是这样的:使用装饰器检查属性类型更好吗?如果是,下一个问题是:下面的装饰器有什么问题(我希望能够写@ 987654325@?

def accepts(types):
    """Decorator to check types of property."""
    def outer_wrapper(func):
        def check_accepts(prop):
            getter = prop.fget
            if not isinstance(self[0], types):
                msg = "Wrong type."
                raise ValueError(msg)
            return self
        return check_accepts
    return outer_wrapper

【问题讨论】:

    标签: python-3.x properties python-decorators


    【解决方案1】:

    开胃菜

    可调用对象

    这可能超出了您的需求,因为听起来您正在处理最终用户的输入,但我认为这可能对其他人有所帮助。

    可调用对象包括使用def 定义的函数、内置函数/方法(如open()lambda 表达式)、可调用类等等。显然,如果您只想允许某种类型的可调用对象,您仍然可以使用 isinstance()types.FunctionType, types.BuiltinFunctionType, types.LambdaType, etc. 但如果不是这种情况,我所知道的最佳解决方案是由MyDecoratedClass.z 属性使用 isinstance()collections.abc.Callable。它并不完美,并且在特殊情况下会返回误报(例如,如果一个类定义了一个 __call__ 函数,该函数实际上并不使该类可调用)。据我所知,内置的callable(obj) 是唯一万无一失的检查功能。 MyClass.z use 属性演示了此功能,但您必须编写另一个/修改MyDecoratedClass 中现有的装饰器功能,以支持使用isinstance() 以外的检查功能。

    可迭代对象(以及序列和集合)

    您提供的代码中的y 属性应该仅限于元组、集合和列表,因此以下内容可能对您有所帮助。

    您可能需要考虑使用collections.abc 模块中的IterableSequenceSet,而不是检查参数是否属于单个类型。 谨慎使用,因为这些类型的限制比简单地传递 (tuple, set, list) 的限制要少。 abc.Iterable(以及其他人)与isinstance() 几乎完美地工作,尽管它有时也会返回误报(例如,一个类定义了一个__iter__ 函数但实际上并没有返回一个迭代器——谁伤害了你?)。确定参数是否可迭代的唯一万无一失的方法是调用内置的iter(obj),如果它不可迭代,则让它引发TypeError,这可能适用于您的情况。我不知道abc.Sequenceabc.Set 的任何内置替代品,但如果有帮助的话,从Python 3 开始,几乎每个序列/集合对象也是可迭代的。 MyClass.y2 属性实现iter() 作为演示,但是MyDecoratedClass 中的装饰器函数(当前)不支持isinstance() 以外的函数;因此,MyDecoratedClass.y2 使用 abc.Iterable

    为了完整起见,以下是它们之间差异的快速比较:

    >>> from collections.abc import Iterable, Sequence, Set
    >>> def test(x):
    ...     print((isinstance(x, Iterable),
    ...              isinstance(x, Sequence),
    ...              isinstance(x, Set)))
    ... 
    >>> test(123)          # int
    False, False, False
    >>> test("1, 2, 3")    # str
    True, True, False
    >>> test([1, 2, 3])    # list
    (True, True, False)
    >>> test(range(3))     # range
    (True, True, False)
    >>> test((1, 2, 3))    # tuple
    (True, True, False)
    >>> test({1, 2, 3})    # set
    (True, False, True)
    >>> import numpy as np
    >>> test(numpy.arange(3))    # numpy.ndarray
    (True, False, False)
    >>> test(zip([1, 2, 3],[4, 5, 6]))    # zip
    (True, False, False)
    >>> test({1: 4, 2: 5, 3: 6})          # dict
    (True, False, False)
    >>> test({1: 4, 2: 5, 3: 6}.keys())      # dict_keys
    (True, False, True)
    >>> test({1: 4, 2: 5, 3: 6}.values())    # dict_values
    (True, False, False)
    >>> test({1: 4, 2: 5, 3: 6}.items())     # dict_items
    (True, False, True)
    

    其他限制

    我能想到的几乎所有其他参数类型限制都必须使用hasattr(),我不打算在这里讨论。

    主菜

    这是真正回答您问题的部分。 assert 绝对是最简单的解决方案,但也有其局限性。

    class MyClass:
        @property
        def x(self):
            return self._x
        @x.setter
        def x(self, val):
            assert isinstance(val, int) # raises AssertionError if val is not of type 'int'
            self._x = val
    
        @property
        def y(self):
            return self._y
        @y.setter
        def y(self, val):
            assert isinstance(val, (list, set, tuple)) # raises AssertionError if val is not of type 'list', 'set', or 'tuple'
            self._y = val
    
        @property
        def y2(self):
            return self._y2
        @y2.setter
        def y2(self, val):
            iter(val)       # raises TypeError if val is not iterable
            self._y2 = val
    
        @property
        def z(self):
            return self._z
        @z.setter
        def z(self, val):
            assert callable(val) # raises AssertionError if val is not callable
            self._z = val
    
        def multi_arg_example_fn(self, a, b, c, d, e, f, g):
            assert isinstance(a, int)
            assert isinstance(b, int)
            # let's say 'c' is unrestricted
            assert isinstance(d, int)
            assert isinstance(e, int)
            assert isinstance(f, int)
            assert isinstance(g, int)
            this._a = a
            this._b = b
            this._c = c
            this._d = d
            this._e = e
            this._f = f
            this._g = g
            return a + b * d - e // f + g
    

    总体上很干净,除了我最后放入的多参数函数之外,这表明断言可能会变得乏味。但是,我认为这里最大的缺点是缺少Exception 消息/变量。如果最终用户看到一个 AssertionError,它没有任何消息,因此基本上是无用的。如果您编写的中间代码可以排除这些错误,那么该代码将没有变量/数据能够向用户解释出了什么问题。进入装饰器功能...

    from collections.abc import Callable, Iterable
    
    class MyDecoratedClass:
        def isinstance_decorator(*classinfo_args, **classinfo_kwargs):
            '''
            Usage:
                Always remember that each classinfo can be a type OR tuple of types.
    
                If the decorated function takes, for example, two positional arguments...
                  * You only need to provide positional arguments up to the last positional argument that you want to restrict the type of. Take a look:
                 1. Restrict the type of only the first argument with '@isinstance_decorator(<classinfo_of_arg_1>)'
                     * Notice that a second positional argument is not required
                     * Although if you'd like to be explicit for clarity (in exchange for a small amount of efficiency), use '@isinstance_decorator(<classinfo_of_arg_1>, object)'
                         * Every object in Python must be of type 'object', so restricting the argument to type 'object' is equivalent to no restriction whatsoever
                 2. Restrict the types of both arguments with '@isinstance_decorator(<classinfo_of_arg_1>, <classinfo_of_arg_2>)'
                 3. Restrict the type of only the second argument with '@isinstance_decorator(object, <classinfo_of_arg_2>)'
                     * Every object in Python must be of type 'object', so restricting the argument to type 'object' is equivalent to no restriction whatsoever
    
                Keyword arguments are simpler: @isinstance_decorator(<a_keyword> = <classinfo_of_the_kwarg>, <another_keyword> = <classinfo_of_the_other_kwarg>, ...etc)
                  * Remember that you only need to include the kwargs that you actually want to restrict the type of (no using 'object' as a keyword argument!)
                  * Using kwargs is probably more efficient than using example 3 above; I would avoid having to use 'object' as a positional argument as much as possible
    
            Programming-Related Errors:
                Raises IndexError if given more positional arguments than decorated function
                Raises KeyError if given keyword argument that decorated function isn't expecting
                Raises TypeError if given argument that is not of type 'type'
                  * Raised by 'isinstance()' when fed improper 2nd argument, like 'isinstance(foo, 123)'
                  * Virtually all UN-instantiated objects are of type 'type'
                    Examples:
                        example_instance = ExampleClass(*args)
                         # Neither 'example_instance' nor 'ExampleClass(*args)' is of type 'type', but 'ExampleClass' itself is
                        example_int = 100
                         # Neither 'example_int' nor '100' are of type 'type', but 'int' itself is
                        def example_fn: pass
                         # 'example_fn' is not of type 'type'.
                        print(type(example_fn).__name__)    # function
                        print(type(isinstance).__name__)    # builtin_function_or_method
                         # As you can see, there are also several types of callable objects
                         # If needed, you can retrieve most function/method/etc. types from the built-in 'types' module
    
            Functional/Intended Errors:
                Raises TypeError if a decorated function argument is not an instance of the type(s) specified by the corresponding decorator argument
            '''
            def isinstance_decorator_wrapper(old_fn):
                def new_fn(self, *args, **kwargs):
                    for i in range(len(classinfo_args)):
                        classinfo = classinfo_args[i]
                        arg = args[i]
                        if not isinstance(arg, classinfo):
                            raise TypeError("%s() argument %s takes argument of type%s' but argument of type '%s' was given" % 
                                            (old_fn.__name__, i,
                                             "s '" + "', '".join([x.__name__ for x in classinfo]) if isinstance(classinfo, tuple) else " '" + classinfo.__name__,
                                             type(arg).__name__))
                    for k, classinfo in classinfo_kwargs.items():
                        kwarg = kwargs[k]
                        if not isinstance(kwarg, classinfo):
                            raise TypeError("%s() keyword argument '%s' takes argument of type%s' but argument of type '%s' was given" % 
                                            (old_fn.__name__, k, 
                                             "s '" + "', '".join([x.__name__ for x in classinfo]) if isinstance(classinfo, tuple) else " '" + classinfo.__name__,
                                             type(kwarg).__name__))
                    return old_fn(self, *args, **kwargs)
                return new_fn
            return isinstance_decorator_wrapper
    
        @property
        def x(self):
            return self._x
        @x.setter
        @isinstance_decorator(int)
        def x(self, val):
            self._x = val
    
        @property
        def y(self):
            return self._y
        @y.setter
        @isinstance_decorator((list, set, tuple))
        def y(self, val):
            self._y = val
    
        @property
        def y2(self):
            return self._y2
        @y2.setter
        @isinstance_decorator(Iterable)
        def y2(self, val):
            self._y2 = val
    
        @property
        def z(self):
            return self._z
        @z.setter
        @isinstance_decorator(Callable)
        def z(self, val):
            self._z = val
    
        @isinstance_decorator(int, int, e = int, f = int, g = int, d = (int, float, str))
        def multi_arg_example_fn(self, a, b, c, d, e, f, g):
            # Identical to assertions in MyClass.multi_arg_example_fn
            self._a = a
            self._b = b
            self._c = c
            self._d = d
            return a + b * e - f // g
    

    显然,multi_example_fn 是这个装饰器真正闪耀的地方。断言造成的混乱已减少到一行。让我们看一些示例错误消息:

    >>> test = MyClass()
    >>> dtest = MyDecoratedClass()
    >>> test.x = 10
    >>> dtest.x = 10
    >>> print(test.x == dtest.x)
    True
    >>> test.x = 'Hello'
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<string>", line 7, in x
    AssertionError
    >>> dtest.x = 'Hello'
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<string>", line 100, in new_fn
    TypeError: x() argument 0 takes argument of type 'int' but argument of type 'str' was given
    >>> test.y = 1
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<string>", line 15, in y
    AssertionError
    >>> test.y2 = 1
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<string>", line 23, in y2
    TypeError: 'int' object is not iterable
    >>> dtest.y = 1
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<string>", line 100, in new_fn
    TypeError: y() argument 0 takes argument of types 'list', 'set', 'tuple' but argument of type 'int' was given
    >>> dtest.y2 = 1
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<string>", line 100, in new_fn
    TypeError: y2() argument 0 takes argument of type 'Iterable' but argument of type 'int' was given
    >>> test.z = open
    >>> dtest.z = open
    >>> test.z = None
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<string>", line 31, in z
    AssertionError
    >>> dtest.z = None
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<string>", line 100, in new_fn
    TypeError: z() argument 0 takes argument of type 'Callable' but argument of type 'NoneType' was given
    

    在我看来要好得多。一切看起来都不错,除了...

    >>> test.multi_arg_example_fn(9,4,[1,2],'hi', g=2,e=1,f=4)
    11
    >>> dtest.multi_arg_example_fn(9,4,[1,2],'hi', g=2,e=1,f=4)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<string>", line 102, in new_fn
    KeyError: 'd'
    >>> print('I forgot that you have to merge args and kwargs in order for the decorator to work properly with both but I dont have time to fix it right now. Absolutely safe for properties for the time being though!')
    I forgot that you have to merge args and kwargs in order for the decorator to work properly with both but I dont have time to fix it right now. Absolutely safe for properties for the time being though!
    

    编辑通知:我之前的回答完全不正确。我建议使用type hints,忘记了它们实际上并没有以任何方式得到保证。它们严格来说是一个开发/IDE 工具。不过,它们仍然非常有用;我建议考虑使用它们。

    【讨论】:

    • @MERose 只是想让你知道我重写了我的答案并在这一次进行了相当多的细节。干杯!
    • 这真是太棒了!我要尽快实施!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-08-21
    • 2020-06-20
    • 2015-10-23
    • 2013-03-25
    • 2021-10-23
    • 2014-07-09
    • 2018-03-01
    相关资源
    最近更新 更多