Подтвердить что ты не робот

Принудительно/убедитесь, что атрибуты класса python имеют определенный тип

Как ограничить переменную-член класса конкретным типом в Python?


Более длинная версия:

У меня есть класс, который имеет несколько переменных-членов, которые установлены снаружи класса. Из-за того, как они используются, они должны быть определенных типов, либо int, либо списка. Если бы это был С++, я бы просто сделал их частными и выполнял проверку типов в функции "set". Учитывая, что это невозможно, существует ли способ ограничить тип переменных, чтобы ошибка/исключение возникали во время выполнения, если им присваивается значение неправильного типа? Или мне нужно проверить их тип внутри каждой функции, которая их использует?

Спасибо.

4b9b3361

Ответ 1

Вы можете использовать свойство, как и другие ответы - поэтому, если вы хотите ограничить один атрибут, скажем, "bar", и ограничить его целым числом, вы можете написать код следующим образом:

class Foo(object):
    def _get_bar(self):
        return self.__bar
    def _set_bar(self, value):
        if not isinstance(value, int):
            raise TypeError("bar must be set to an integer")
        self.__bar = value
    bar = property(_get_bar, _set_bar)

И это работает:

>>> f = Foo()
>>> f.bar = 3
>>> f.bar
3
>>> f.bar = "three"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in _set_bar
TypeError: bar must be set to an integer
>>> 

(Существует также новый способ написания свойств, используя "свойство", встроенное в качестве декоратора, в метод getter, но я предпочитаю старый способ, как я выразился выше).

Конечно, если у вас есть много атрибутов на ваших классах и вы хотите защитить их все таким образом, он начинает получать подробные сведения. Не о чем беспокоиться - возможности самоанализа Python позволяют создать декоратор класса, который мог бы автоматизировать это с минимальным количеством строк.

def getter_setter_gen(name, type_):
    def getter(self):
        return getattr(self, "__" + name)
    def setter(self, value):
        if not isinstance(value, type_):
            raise TypeError("%s attribute must be set to an instance of %s" % (name, type_))
        setattr(self, "__" + name, value)
    return property(getter, setter)

def auto_attr_check(cls):
    new_dct = {}
    for key, value in cls.__dict__.items():
        if isinstance(value, type):
            value = getter_setter_gen(key, value)
        new_dct[key] = value
    # Creates a new class, using the modified dictionary as the class dict:
    return type(cls)(cls.__name__, cls.__bases__, new_dct)

И вы просто используете auto_attr_check в качестве декоратора класса и объявляете атрибуты, которые вы хотите, чтобы тело класса было равно типам, которые необходимо также ограничить атрибутами:

...     
... @auto_attr_check
... class Foo(object):
...     bar = int
...     baz = str
...     bam = float
... 
>>> f = Foo()
>>> f.bar = 5; f.baz = "hello"; f.bam = 5.0
>>> f.bar = "hello"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in setter
TypeError: bar attribute must be set to an instance of <type 'int'>
>>> f.baz = 5
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in setter
TypeError: baz attribute must be set to an instance of <type 'str'>
>>> f.bam = 3 + 2j
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in setter
TypeError: bam attribute must be set to an instance of <type 'float'>
>>> 

Ответ 2

В общем, это не очень хорошая идея по причинам, которые @yak упомянул в своем комментарии. Вы в основном запрещаете пользователю предоставлять действительные аргументы, которые имеют правильные атрибуты/поведение, но не находятся в дереве наследования, в котором вы жестко запрограммированы.

Отказ от ответственности, есть несколько вариантов того, что вы пытаетесь сделать. Основная проблема заключается в том, что в Python нет личных атрибутов. Поэтому, если у вас есть просто старая ссылка на объект, скажем, self._a, вы не можете гарантировать, что пользователь не установит его напрямую, даже если вы предоставили установщик, который выполняет проверку типа для него. Варианты ниже демонстрируют, как действительно обеспечить проверку типов.

Переопределить __setattr__

Этот метод будет удобен только для (очень) небольшого количества атрибутов, с которыми вы это делаете. Метод __setattr__ вызывается, когда вы используете точечную запись для назначения обычного атрибута. Например,

class A:
    def __init__(self, a0):
        self.a = a0

Если мы теперь сделаем A().a = 32, это вызовет A().__setattr__('a', 32) под капотом. На самом деле self.a = a0 в __init__ использует self.__setattr__. Вы можете использовать это для принудительной проверки типа:

 class A:
    def __init__(self, a0):
        self.a = a0
    def __setattr__(self, name, value):
        if name == 'a' and not isinstance(value, int):
            raise TypeError('A.a must be an int')
        super().__setattr__(name, value)

Недостаток этого метода заключается в том, что у вас должно быть отдельное if name ==... для каждого типа, который вы хотите проверить (или if name in... чтобы проверить несколько имен для данного типа). Преимущество состоит в том, что это самый простой способ сделать практически невозможным для пользователя обойти проверку типа.

Сделать недвижимость

Свойства - это объекты, которые заменяют ваш обычный атрибут объектом дескриптора (обычно с использованием декоратора). Дескрипторы могут иметь __get__ и __set__ которые настраивают доступ к базовому атрибуту. Это все равно, что взять соответствующую ветвь if в __setattr__ и поместить ее в метод, который будет запускаться только для этого атрибута. Вот пример:

class A:
    def __init__(self, a0):
        self.a = a0
    @property
    def a(self):
        return self._a
    @a.setter
    def a(self, value):
        if not isinstance(value, int):
            raise TypeError('A.a must be an int')
        self._a = value

Несколько иной способ сделать то же самое можно найти в ответе @jsbueno.

Хотя использование свойства таким способом изящно и в основном решает проблему, оно представляет собой пару проблем. Во-первых, у вас есть "приватный" атрибут _a который пользователь может изменять напрямую, минуя проверку типа. Это почти та же проблема, что и при использовании простого метода получения и установки, за исключением того, что теперь a доступен как "правильный" атрибут, который перенаправляет к установщику за кулисами, уменьшая вероятность того, что пользователь будет связываться с _a. Вторая проблема заключается в том, что у вас есть лишний метод получения, чтобы свойство работало как чтение-запись. Эти вопросы являются предметом этого вопроса.

Создать True Setter-Only Descriptor

Это решение, пожалуй, самое надежное в целом. Это предлагается в принятом ответе на вопрос, упомянутый выше. По сути, вместо использования свойства, которое имеет кучу изысков и удобств, от которых вы не можете избавиться, создайте свой собственный дескриптор (и декоратор) и используйте его для любых атрибутов, которые требуют проверки типа:

class SetterProperty:
    def __init__(self, func, doc=None):
        self.func = func
        self.__doc__ = doc if doc is not None else func.__doc__
    def __set__(self, obj, value):
        return self.func(obj, value)

class A:
    def __init__(self, a0):
        self.a = a0
    @SetterProperty
    def a(self, value):
        if not isinstance(value, int):
            raise TypeError('A.a must be an int')
        self.__dict__['a'] = value

__dict__ фактическое значение непосредственно в __dict__ экземпляра, чтобы избежать его повторного использования в течение неопределенного времени. Это позволяет получить значение атрибута без указания явного метода получения. Поскольку дескриптор a не имеет метода __get__, поиск будет продолжаться до тех пор, пока он не найдет атрибут в __dict__. Это гарантирует, что все наборы проходят через дескриптор/установщик, в то время как получает прямой доступ к значению атрибута.

Если у вас есть большое количество атрибутов, которые требуют такой проверки, вы можете переместить строку self.__dict__['a'] = value в метод дескриптора __set__:

class ValidatedSetterProperty:
    def __init__(self, func, name=None, doc=None):
        self.func = func
        self.__name__ = name if name is not None else func.__name__
        self.__doc__ = doc if doc is not None else func.__doc__
    def __set__(self, obj, value):
        ret = self.func(obj, value)
        obj.__dict__[self.__name__] = value

class A:
    def __init__(self, a0):
        self.a = a0
    @ValidatedSetterProperty
    def a(self, value):
        if not isinstance(value, int):
            raise TypeError('A.a must be an int')

Обновить

Python3.6 делает это для вас практически из коробки: https://docs.python.org/3.6/whatsnew/3.6.html#pep-487-descriptor-protocol-enhancements

TL; DR

Для очень небольшого числа атрибутов, требующих проверки типа, переопределите __setattr__ напрямую. Для большего количества атрибутов используйте дескриптор только для установки, как показано выше. Использование свойств непосредственно для такого рода приложений создает больше проблем, чем решает.

Ответ 3

Примечание 1: @Blckknght спасибо за ваш честный комментарий. Я пропустил проблему рекурсии в моем слишком простом наборе тестов.

Примечание 2: я написал этот ответ, когда я был в самом начале изучения Python. Прямо сейчас я бы предпочел использовать дескрипторы Python, см., Например, link1, link2.

Благодаря предыдущим постам и некоторым размышлениям, я считаю, что нашел гораздо более удобный способ ограничить атрибут класса определенным типом.

Прежде всего, мы создаем функцию, которая универсально проверяет тип:

def ensure_type(value, types):
    if isinstance(value, types):
        return value
    else:
        raise TypeError('Value {value} is {value_type}, but should be {types}!'.format(
            value=value, value_type=type(value), types=types))

Затем мы просто используем и применяем его в наших классах через сеттер. Я думаю, что это относительно просто и следует DRY, особенно после того, как вы экспортируете его в отдельный модуль, чтобы прокормить весь ваш проект. Смотрите пример ниже:

class Product:
    def __init__(self, name, quantity):
        self.name = name
        self.quantity = quantity

    @property
    def name(self):
        return self.__dict__['name']

    @name.setter
    def name(self, value):
        self.__dict__['name'] = ensure_type(value, str)

    @property
    def quantity(self):
        return self.quantity

    @quantity.setter
    def quantity(self, value):
        self.__dict__['quantity'] = ensure_type(value, int)

Тесты дают разумные результаты. Смотрите сначала тесты:

if __name__ == '__main__':
    from traceback import format_exc

    try:
        p1 = Product(667, 5)
    except TypeError as err:
        print(format_exc(1))

    try:
        p2 = Product('Knight who say...', '5')
    except TypeError as err:
        print(format_exc(1))

    p1 = Product('SPAM', 2)
    p2 = Product('...and Love', 7)
    print('Objects p1 and p2 created successfully!')

    try:
        p1.name = -191581
    except TypeError as err:
        print(format_exc(1))

    try:
        p2.quantity = 'EGGS'
    except TypeError as err:
        print(format_exc(1))

И результат испытаний:

Traceback (most recent call last):
  File "/Users/BadPhoenix/Desktop/Coding/Coders-Lab/Week-2/WAR_PYT_S_05_OOP/2_Praca_domowa/day-1/stackoverflow.py", line 35, in <module>
    p1 = Product(667, 5)
TypeError: Value 667 is <class 'int'>, but should be <class 'str'>!

Traceback (most recent call last):
  File "/Users/BadPhoenix/Desktop/Coding/Coders-Lab/Week-2/WAR_PYT_S_05_OOP/2_Praca_domowa/day-1/stackoverflow.py", line 40, in <module>
    p2 = Product('Knights who say...', '5')
TypeError: Value 5 is <class 'str'>, but should be <class 'int'>!

Objects p1 and p2 created successfully!

Traceback (most recent call last):
  File "/Users/BadPhoenix/Desktop/Coding/Coders-Lab/Week-2/WAR_PYT_S_05_OOP/2_Praca_domowa/day-1/stackoverflow.py", line 49, in <module>
    p1.name = -191581
TypeError: Value -191581 is <class 'int'>, but should be <class 'str'>!

Traceback (most recent call last):
  File "/Users/BadPhoenix/Desktop/Coding/Coders-Lab/Week-2/WAR_PYT_S_05_OOP/2_Praca_domowa/day-1/stackoverflow.py", line 54, in <module>
    p2.quantity = 'EGGS'
TypeError: Value EGGS is <class 'str'>, but should be <class 'int'>!

Ответ 4

Начиная с Python 3.5, вы можете использовать подсказки типов, чтобы указать, что атрибут класса должен быть определенного типа. Затем вы можете включить что-то вроде MyPy в процесс непрерывной интеграции, чтобы проверить, соблюдаются ли все типовые контракты.

Например, для следующего скрипта Python:

class Foo:
    x: int
    y: int

foo = Foo()
foo.x = "hello"

MyPy выдаст следующую ошибку:

6: error: Incompatible types in assignment (expression has type "str", variable has type "int")

Если вы хотите, чтобы типы принудительно применялись во время выполнения, вы можете использовать пакет принудительного применения. Из README:

>>> import enforce
>>>
>>> @enforce.runtime_validation
... def foo(text: str) -> None:
...     print(text)
>>>
>>> foo('Hello World')
Hello World
>>>
>>> foo(5)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/william/.local/lib/python3.5/site-packages/enforce/decorators.py", line 106, in universal
    _args, _kwargs = enforcer.validate_inputs(parameters)
  File "/home/william/.local/lib/python3.5/site-packages/enforce/enforcers.py", line 69, in validate_inputs
    raise RuntimeTypeError(exception_text)
enforce.exceptions.RuntimeTypeError: 
  The following runtime type errors were encountered:
       Argument 'text' was not of type <class 'str'>. Actual type was <class 'int'>.

Ответ 6

Вы можете сделать это точно так же, как говорите, что сказали, что сделаете это на С++; выполнить присвоение им, пройти через метод сеттера и установить метод setter типа. Концепции "частного состояния" и "публичных интерфейсов" в Python выполняются с документацией и условными обозначениями, и практически невозможно заставить использовать ваш сеттер вместо прямого назначения переменной. Но если вы дадите имена атрибутов, начинающиеся с подчеркивания, и документируйте сеттеры как способ использования вашего класса, это должно сделать это (не используйте __names с двумя символами подчеркивания, это почти всегда больше проблем, чем это стоит, re на самом деле в ситуации, для которой они предназначены, что является связыванием имен атрибутов в иерархии наследования). Только особенно тупые разработчики избегают простого способа использования класса так, как он документирован, чтобы работать, чтобы выяснить, что такое внутренние имена и использовать их напрямую; или разработчики, которые разочарованы вашим классом, ведут себя необычно (для Python) и не позволяют им использовать собственный список-подобный класс вместо списка.

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


Лично я считаю, что попытки обеспечить безопасность типов в Python довольно бесполезны. Не потому, что я считаю, что статическая проверка типов всегда уступает, но поскольку даже если вы могли бы добавить требования типа к своим переменным Python, которые работали в 100% случаев, они просто не будут эффективны в обеспечении уверенности в том, что ваша программа свободна от типа потому что они будут только увеличивать исключения во время выполнения.

Подумайте об этом; когда ваша статически скомпилированная программа успешно компилируется без ошибок, вы знаете, что она полностью свободна от всех ошибок, которые может обнаружить компилятор (в случае таких языков, как Haskell или Mercury, что довольно хорошая гарантия, хотя все еще не завершена; случай языков, таких как С++ или Java... meh).

Но в Python ошибка типа будет замечена только в том случае, если она когда-либо выполняется. Это означает, что даже если вы можете получить полный статический тип принудительного применения повсюду в своей программе, вам нужно регулярно выполнять тестовые пакеты с 100% -ным охватом кода, чтобы на самом деле знать, что ваша программа свободна от ошибок типа. Но если вы регулярно проводили тесты с полным охватом, вы бы знали, если у вас были какие-либо ошибки типа, даже не пытаясь принудительно применять типы! Так что польза просто не кажется мне такой. Вы отбрасываете силу Python (гибкость), не получая больше, чем пустяк в одной из своих слабых сторон (статическое обнаружение ошибок).

Ответ 7

Я знаю, что это обсуждение было решено, но гораздо более простым решением является использование ниже представленного модуля структуры Python. Это потребовало бы, чтобы вы создали контейнер для своих данных, прежде чем назначать ему значение, но он очень эффективен при сохранении типа данных static. https://pypi.python.org/pypi/structures