Принудительно / обеспечить атрибуты класса Python определенного типа - PullRequest
36 голосов
/ 16 февраля 2012

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


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

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

Спасибо.

Ответы [ 7 ]

39 голосов
/ 17 февраля 2012

Вы можете использовать свойство, как и в других ответах - Итак, если вы хотите ограничить один атрибут, скажем, «бар», и ограничив его целым числом, вы можете написать код так:

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'>
>>> 
11 голосов
/ 16 декабря 2016

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

Создание дескриптора только для истинного установщика

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

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__ экземпляра, чтобы избежать его повторного использования в течение неопределенного времени.Это позволяет получить значение атрибута без указания явного метода получения.Поскольку дескриптор a не имеет метода __get__, поиск будет продолжаться до тех пор, пока он не найдет атрибут в __dict__.Это гарантирует, что все наборы проходят через дескриптор / установщик, в то время как get разрешают прямой доступ к значению атрибута.

Если у вас есть большое количество атрибутов, которые требуют такой проверки, вы можете переместить строку 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__ напрямую. Для большего количества атрибутов используйте дескриптор только для установки, как показано выше. Использование свойств непосредственно для такого рода приложений создает больше проблем, чем решает.

4 голосов
/ 07 ноября 2017

Начиная с Python 3.5, вы можете использовать type-hints , чтобы указать, что атрибут класса должен быть определенного типа. Затем вы можете включить что-то вроде 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'>.
2 голосов
/ 12 сентября 2017

Примечание 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'>!
1 голос
/ 30 августа 2017

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

1 голос
/ 17 февраля 2012

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

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


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

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

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

1 голос
/ 16 февраля 2012

Вы можете использовать тот же тип property, что и в C ++. Вы получите помощь по недвижимости от http://adam.gomaa.us/blog/2008/aug/11/the-python-property-builtin/.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...