Перечисление Python предотвращает неправильное присвоение атрибута - PullRequest
0 голосов
/ 20 января 2019

Когда я использую функциональный API для создания перечисления, я получаю обратно объект перечисления, который допускает произвольное присвоение (т. Е. У него есть __dict __):

e = enum.Enum('Things',[('foo',1),('bar',2)])
e.baz = 3

Элемент не отображается в списке:

list(e)
[<foo.foo: 1>, <foo.bar: 2>]

Но на него все еще можно ссылаться:

if thing == e.baz: ...

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

Есть ли способ динамически создать перечисление, которое ведет себя больше как объект __slots__, который не позволяет произвольным атрибутамбыть назначенным?

Ответы [ 2 ]

0 голосов
/ 23 января 2019

Чтобы сделать перечисляющий класс полностью доступным только для чтения, все, что требуется, - это метакласс, который использует __setattr__ hook , который предотвращает все присвоения атрибутов.Поскольку метакласс присоединяется к классу после создания , нет проблем с назначением правильных перечисляемых значений.

Как и ответ Этана, я использую класс EnumMeta какоснова для пользовательского метакласса:

from enum import EnumMeta, Enum

class FrozenEnumMeta(EnumMeta):
    "Enum metaclass that freezes an enum entirely"
    def __new__(mcls, name, bases, classdict):
        classdict['__frozenenummeta_creating_class__'] = True
        enum = super().__new__(mcls, name, bases, classdict)
        del enum.__frozenenummeta_creating_class__
        return enum

    def __call__(cls, value, names=None, *, module=None, **kwargs):
        if names is None:  # simple value lookup
            return cls.__new__(cls, value)
        enum = Enum._create_(value, names, module=module, **kwargs)
        enum.__class__ = type(cls)
        return enum

    def __setattr__(cls, name, value):
        members = cls.__dict__.get('_member_map_', {})
        if hasattr(cls, '__frozenenummeta_creating_class__') or name in members:
            return super().__setattr__(name, value)
        if hasattr(cls, name):
            msg = "{!r} object attribute {!r} is read-only"
        else:
            msg = "{!r} object has no attribute {!r}"
        raise AttributeError(msg.format(cls.__name__, name))

    def __delattr__(cls, name):
        members = cls.__dict__.get('_member_map_', {})
        if hasattr(cls, '__frozenenummeta_creating_class__') or name in members:
            return super().__delattr__(name)
        if hasattr(cls, name):
            msg = "{!r} object attribute {!r} is read-only"
        else:
            msg = "{!r} object has no attribute {!r}"
        raise AttributeError(msg.format(cls.__name__, name))

class FrozenEnum(Enum, metaclass=FrozenEnumMeta):
    pass

В приведенном выше описании различаются атрибуты, которые уже доступны, и новые атрибуты для упрощения диагностики.Он также блокирует атрибут delete , что, вероятно, так же важно!

Он также предоставляет метакласс и базовый класс FrozenEnum для перечислений;используйте это вместо Enum.

Чтобы заморозить образец Color перечисление:

>>> class Color(FrozenEnum):
...     red = 1
...     green = 2
...     blue = 3
...
>>> list(Color)
[<Color.red: 1>, <Color.green: 2>, <Color.blue: 3>]
>>> Color.foo = 'bar'
Traceback (most recent call last):
    # ...
AttributeError: 'Color' object has no attribute 'foo'
>>> Color.red = 42
Traceback (most recent call last):
    # ...
Cannot reassign members.
>>> del Color.red
Traceback (most recent call last):
    # ...
AttributeError: Color: cannot delete Enum member.

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

Если ваше enum использует свойства, которые изменяют атрибуты в классе enum, вы должны либо внести их в белый список,или разрешить указывать имена, начинающиеся с одного подчеркивания;в __setattr__ определите, какие имена было бы допустимо установить и использовать super().__setattr__(name, value) для этих исключений, точно так же, как код теперь различает построение класса и последующие изменения с помощью атрибута флага.

Вышеприведенный класс может бытьиспользуется так же, как Enum(), чтобы программно создать перечисление:

e = FrozenEnum('Things', [('foo',1), ('bar',2)]))

Демо:

>>> e = FrozenEnum('Things', [('foo',1), ('bar',2)])
>>> e
<enum 'Things'>
>>> e.foo = 'bar'
Traceback (most recent call last):
    # ...
AttributeError: Cannot reassign members.
0 голосов
/ 20 января 2019

Не обязательно просто, но возможно.Нам нужно создать новый EnumMeta тип 1 , создать Enum как обычно, затем переназначить тип после того, как Enum создан:

from enum import Enum, EnumMeta

class FrozenEnum(EnumMeta):
    "prevent creation of new attributes"
    def __getattr__(self, name):
        if name not in self._member_map_:
            raise AttributeError('%s %r has no attribute %r'
                % (self.__class__.__name__, self.__name__, name))
        return super().__getattr__(name)

    def __setattr__(self, name, value):
        if name in self.__dict__ or name in self._member_map_:
            return super().__setattr__(name, value)
        raise AttributeError('%s %r has no attribute %r'
                % (self.__class__.__name__, self.__name__, name))

class Color(Enum):
    red = 1
    green = 2
    blue = 3

Color.__class__ = FrozenEnum

и используется:

>>> type(Color)
<class 'FrozenEnum'>

>>> list(Color)
[<Color.red: 1>, <Color.green: 2>, <Color.blue: 3>]

>>> Color.blue
<Color.blue: 3>

>>> Color.baz = 3
Traceback (most recent call last):
  ...
AttributeError: FrozenEnum 'Color' has no attribute 'baz'

>>> Color.baz
Traceback (most recent call last):
  ...
AttributeError: 'FrozenEnum' object has no attribute 'baz'

Попытка переназначения члена по-прежнему приводит к более дружественной ошибке:

>>> Color.blue = 9
Traceback (most recent call last):
  ...
AttributeError: Cannot reassign members.

Чтобы немного переназначить класс, мы можем написать декоратор для инкапсуляции процесса:

def freeze(enum_class):
    enum_class.__class__ = FrozenEnum
    return enum_class

и используется:

@freeze
class Color(Enum):
    red = 1
    green = 2
    blue = 3

Обратите внимание, что все еще можно перезаписать обычные атрибуты, такие как функции:

@freeze
class Color(Enum):
    red = 1
    green = 2
    blue = 3
    def huh(self):
        print("Huh, I am %s!" % self.name)

и используемые:

>>> Color.huh
<function Color.huh at 0x7f7d54ae96a8>

>>> Color.blue.huh()
Huh, I am blue!

>>> Color.huh = 3
>>> Color.huh
3
>>> Color.blue.huh()
Traceback (most recent call last):
  ...
TypeError: 'int' object is not callable

Даже это можно заблокировать, но я оставлю это (пока) в качестве упражнения для кого-то другого.


1 Этотолько второй случай, который я видел, где требуется подкласс EnumMeta.Для других см. this question.

Раскрытие информации: я являюсь автором Python stdlib Enum, enum34 backport и расширенное перечисление (aenum) .

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