Декораторы или утверждения в установщиках для проверки типа свойства? - PullRequest
0 голосов
/ 20 июня 2019

В проекте 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) выполнит эту работу. Увы, я с треском проваливаюсь из-за этого, так как все примеры, которые я нашел (например, это или это ), не нацелены на то, что я хочу.

Первый вопрос, таким образом: Лучше ли использовать декоратор для проверки типов свойств? Если да, то следующий вопрос: что не так с декоратором ниже (я хочу иметь возможность написать @accepts(int)

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

1 Ответ

1 голос
/ 20 июня 2019

Закуска

* 1003 вызываемых объектов * Это, вероятно, выходит за рамки ваших потребностей, поскольку, похоже, вы имеете дело с вводом данных от конечного пользователя, но я подумал, что это может быть полезно для других. Вызываемые функции включают в себя функции, определенные с помощью def, встроенные функции / методы, такие как open(), lambda выражения, вызываемые классы и многое другое. Очевидно, что если вы хотите разрешить только определенный тип (ы) вызываемых элементов, вы все равно можете использовать isinstance() с types.FunctionType, types.BuiltinFunctionType, types.LambdaType и т. Д. Но если это не так В этом случае лучшее решение, которое мне известно, демонстрирует свойство MyDecoratedClass.z, использующее isinstance() с collections.abc.Callable. Он не идеален и будет возвращать ложные срабатывания в исключительных случаях (например, если класс определяет функцию __call__, которая фактически не делает класс вызываемым). Насколько мне известно, встроенная функция callable(obj) является единственной надежной функцией проверки. Свойство MyClass.z use демонстрирует эту функцию, но вам придется написать другую / модифицировать существующую функцию декоратора в MyDecoratedClass, чтобы поддерживать использование функций проверки, отличных от isinstance(). Итерации (и последовательности и наборы)

Предполагается, что свойство y в предоставленном вами коде ограничено кортежами, наборами и списками, поэтому вам может пригодиться следующее.

Вместо проверки, являются ли аргументы отдельных типов, вы можете рассмотреть возможность использования Iterable, Sequence и Set из модуль collections.abc. Пожалуйста, будьте осторожны, так как эти типы далеко менее ограничительны, чем просто передача (кортеж, набор, список), как у вас. abc.Iterable (как и другие) почти идеально работает с isinstance(), хотя иногда он также возвращает ложные срабатывания (например, класс определяет функцию __iter__, но на самом деле не возвращает итератор - кто ранил вы?). Единственный надежный способ определить, является ли аргумент итеративным, - это вызвать встроенный iter(obj) и позволить ему поднять TypeError, если он не повторяется, что может сработать в вашем случае. Я не знаю каких-либо встроенных альтернатив abc.Sequence и abc.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!

Примечание об изменении: мой предыдущий ответ был совершенно неверным. Я предлагал использовать подсказки типа , забывая, что они на самом деле не гарантированы. Они строго инструмент разработки / IDE. Они все еще безумно полезны, хотя; Я рекомендую изучить их использование.

...