setter中的修饰符或断言来检查属性类型?

时间:2019-06-20 16:37:52

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

在python项目中,我的课程有几个我需要为特定类型的属性。该类的用户必须具有设置属性的能力。

做到这一点的最佳方法是什么?我想到两种解决方案: 1.在每个设置器功能中都有测试例程。 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)都没有针对我想要的内容。

因此,第一个问题是:使用装饰器检查属性类型是否更好?如果是,则下一个问题是:下面的装饰器有什么问题(我希望能够写成{ {1}}?

@accepts(int)

1 个答案:

答案 0 :(得分:1)

开胃菜

Callables

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

可调用对象包括用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,而不是检查参数是否为单个类型。不过,请谨慎行事,因为这些类型的限制远小于简单地传递(元组,集合,列表)的限制。 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工具。但是他们仍然提供了疯狂的帮助。我建议您研究使用它们。