Как добавить подмножества членов в перечисление Python? - PullRequest
3 голосов
/ 21 октября 2019

Скажем, у меня есть Python Enum, как это:

from enum import Enum

class FRUIT(Enum):
    APPLE = 1
    BANANA = 2
    LEMON = 3
    ORANGE = 4

, и я хочу "поднастроить" их полезными способами, например, сказать:

if fruit in FRUIT.CITRUS:
    make_juice()

где-нибудьЯ определил: CITRUS = {LEMON, ORANGE}.

Я хотел бы сохранить подмножества в качестве атрибутов основного Enum, потому что он поддерживает использование подмножества в контексте.

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

@classmethod
def CITRUS(cls):
    return {cls.LEMON, cls.ORANGE}

Есть ли способ добавить атрибут класса после того, как метакласс Enum выполнил свою работу, не нарушая работу?

Ответы [ 3 ]

4 голосов
/ 21 октября 2019

Если вам не нужны конкретные значения для значений Enum, скорее всего, вы можете получить всю необходимую функциональность, используя перечисления enum.IntFlag вместо простого Enum.

Просто объявите свой класс ENum какIntFlag, и вы можете использовать &, | и, возможно, другие побитовые операторы, чтобы иметь нужное вам поведение:

In [1]: import enum                                                                                                                  

In [2]: class Fruit(enum.IntFlag): 
   ...:     APPLE = 1 
   ...:     BANANA = 2 
   ...:     LEMON = 4 
   ...:     ORANGE = 8 
In [4]: CITRUS = Fruit.LEMON | Fruit.ORANGE                                                                                          


In [6]: for fruit in Fruit: 
   ...:     if fruit & CITRUS: 
   ...:         print(f"making_juice({fruit.name})") 
   ...:          
   ...:   

Это не позволяет взаимодействовать непосредственно с "CITRUS", итребуется шаблон фильтра, как я использовал выше.

Однако, совсем недавно, несколько недель назад, я хотел именно эту функцию и мог реализовать ее как метод __iter__, выполняющий этот фильтр прямо в классе Enum. :

    def __iter__(self):
        for element in self.__class__:
            if self & element:
                yield element

Если мы просто включим это в перечисленное выше перечисление:

In [8]: class Fruit(enum.IntFlag): 
   ...:     APPLE = 1 
   ...:     BANANA = 2 
   ...:     LEMON = 4 
   ...:     ORANGE = 8 
   ...:      
   ...:     def __iter__(self): 
   ...:         for element in self.__class__: 
   ...:             if self & element: 
   ...:                 yield element 
   ...:                                                                                                                              

In [9]: CITRUS = Fruit.LEMON | Fruit.ORANGE                                                                                          

In [10]: for fruit in CITRUS: 
    ...:     print (fruit.name) 
    ...:                                                                                                                             
LEMON
ORANGE

__iter__ не конфликтует с итерацией самого класса Fruit, так как при этом используется__iter__ метод в метаклассе EnumMeta и, как может показаться, он будет корректно вызываться подмножествами ORed перечислений. Что означает, что если вам нужно, вам просто нужно написать правильные методы __len__ и __contains__, чтобы иметь все функции, которые вы ожидаете от подмножеств.

Я использую этот код в личном проекте, и он работает как шарм:

https://github.com/jsbueno/terminedia/blob/9714d6890b8336678cd10e0c6275f56392e409ed/terminedia/values.py#L51

(хотя, прямо сейчас "unicode_effects" объявлен нижеперечисление есть обычный набор, теперь, когда вы упомянули об этом, я думаю, я просто напишу __contains__ и использую это вместо набора.)

4 голосов
/ 21 октября 2019

Поскольку CITRUS предназначен не для самого плода, а для его типа, имеет смысл создать отдельный подкласс Enum с типами фруктов в качестве членов:

class FRUIT_TYPE(Enum):
    CITRUS = {FRUIT.LEMON, FRUIT.ORANGE}

, так что вы можете сделать что-то вроде:

fruit = FRUIT.LEMON
if fruit in FRUIT_TYPE.CITRUS.value:
     make_juice()

Однако использование FRUIT_TYPE.CITRUS.value для проверки членства выглядит громоздким. Чтобы разрешить проверку членства для FRUIT_TYPE.CITRUS, вы также можете сделать FRUIT_TYPE подклассом set:

class FRUIT_TYPE(set, Enum):
    CITRUS = {FRUIT.LEMON, FRUIT.ORANGE}

, чтобы вместо этого можно было сделать следующее:

fruit = FRUIT.LEMON
if fruit in FRUIT_TYPE.CITRUS:
     make_juice()
0 голосов
/ 24 октября 2019

ОБНОВЛЕНИЕ: Еще одна вещь, которую я хотел бы рассмотреть, что, мне кажется, мне нравится больше, в целом, это определение членства в подмножестве как атрибута каждого члена Enum, например:

fruit = FRUIT.ORANGE  # ---or whatever, probably in far away code---
...
if fruit.is_citrus:
    make_juice()

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

class FRUIT(Enum):
    APPLE = 1
    BANANA = 2
    LEMON = 3
    ORANGE = 4

    @property
    def is_citrus(self):
        return self in frozenset((FRUIT.LEMON, FRUIT.ORANGE))


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

from enum import Enum

class FRUIT(Enum):
    APPLE = 1
    BANANA = 2
    LEMON = 3
    ORANGE = 4

FRUIT.CITRUS_TYPES = frozenset((FRUIT.LEMON, FRUIT.ORANGE))

Это прекрасно работает и (на удивление для меня) не нарушает ни одно из других действий Enum:

# ---CITRUS_TYPES subset has desired behavior---
>>> FRUIT.LEMON in FRUIT.CITRUS_TYPES
True
>>> FRUIT.APPLE in FRUIT.CITRUS_TYPES
False
>>> "foobar" in FRUIT.CITRUS_TYPES
False

# ---CITRUS_TYPES has not become a member of FRUIT enum---
>>> tuple(FRUIT)
(FRUIT.APPLE: 1>, <FRUIT.BANANA: 2>, <FRUIT.LEMON: 3>, <FRUIT.ORANGE: 4>)
>>> FRUIT.APPLE in FRUIT
True
>>> FRUIT.CITRUS_TYPES in FRUIT
DeprecationWarning: using non-Enums in containment checks will raise TypeError in Python 3.8
False

# ---CITRUS_TYPES not reported by dir(FRUIT)---
>>> dir(FRUIT)
['APPLE', 'BANANA', 'LEMON', 'ORANGE', '__class__', '__doc__', '__members__', '__module__']

# ---But it does appear on FRUIT.__dict__---
FRUIT.__dict__ == {
    '_generate_next_value_': <function Enum._generate_next_value_ at 0x1010e9268>, 
    '__module__': '__main__',
    '__doc__': 'An enumeration.',
    '_member_names_': ['APPLE', 'BANANA', 'LEMON', 'ORANGE'],
    '_member_map_': OrderedDict([
        ('APPLE', <FRUIT.APPLE: 1>),
        ('BANANA', <FRUIT.BANANA: 2>),
        ('LEMON', <FRUIT.LEMON: 3>),
        ('ORANGE', <FRUIT.ORANGE: 4>)
    ]),
    '_member_type_': <class 'object'>,
    '_value2member_map_': {
        1: <FRUIT.APPLE: 1>,
        2: <FRUIT.BANANA: 2>,
        3: <FRUIT.LEMON: 3>,
        4: <FRUIT.ORANGE: 4>,
    },
    'APPLE': <FRUIT.APPLE: 1>,
    'BANANA': <FRUIT.BANANA: 2>,
    'LEMON': <FRUIT.LEMON: 3>,
    'ORANGE': <FRUIT.ORANGE: 4>,
    '__new__': <function Enum.__new__ at 0x1010e91e0>,
    'CITRUS_TYPES': frozenset({<FRUIT.LEMON: 3>, <FRUIT.ORANGE: 4>})
}

Таким образом, он, похоже, хранит CITRUS_TYPES в классе, но скрывает его от dir() по любой причине.

У него есть уязвимость , хотя, вчто добавленный атрибут является изменяемым, как и любой другой атрибут класса;если какая-то часть кода клиента присваивается FRUIT.CITRUS_TYPES, FRUIT не будет жаловаться, и это, конечно, сломает вещи. Это поведение отличается от поведения Enum члена, который вызывает AttributeError при попытке присвоения.

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

@ blhsing поднимает интересный вопрос о том, имеет ли смысл такой атрибут в FRUIT. Я понимаю его точку зрения и, возможно, еще приму его точку зрения, но мое текущее мнение состоит в том, что для меня лучше всего локализовать характеристики, связанные с фруктами, для одного импортированного имени. Можно было бы считать FRUIT строгим набором видов фруктов и подмножеств фруктов, таким образом, как отдельный набор. Я считаю эту строгость непригодной для моих текущих целей и предпочитаю думать о FRUIT как о совокупности связанных значений констант, включая как члены, так и подмножества. YMMV конечно. Как я уже сказал, я еще могу принять его точку зрения.

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