Почему переменные значения в Python перечисляет один и тот же объект?

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

Если я определяю значения Enum как разные списки, члены по-прежнему ведут себя аналогично тому, когда значения Enum являются типичными неизменяемыми типами, такими как str или int, хотя я могу изменить значения члены на месте, чтобы значения двух членов Enum были одинаковыми:

>>> class Color(enum.Enum):
        black = [1,2]
        blue = [1,2,3]  

>>> Color.blue is Color.black
False
>>> Color.black == Color.blue
False
>>> Color.black.value.append(3)
>>> Color.black
<Color.black: [1, 2, 3]>
>>> Color.blue
<Color.blue: [1, 2, 3]>
>>> Color.blue == Color.black
False
>>> Color.black.value == Color.blue.value
True

Однако, если я определяю значения как идентичные списки, каждое значение элемента похоже будет тем же самым объектом, и, таким образом, любая мутация одного значения элемента влияет на всех членов:

>>> class Color(enum.Enum):
        black = [1,2,3]
        blue = [1,2,3]

>>> Color.blue is Color.black
True
>>> Color.black == Color.blue
True
>>> Color.black.value.append(4)
>>> Color.black
<Color.black: [1, 2, 3, 4]>
>>> Color.blue
<Color.black: [1, 2, 3, 4]>
>>> Color.blue == Color.black
True

Почему Enum ведет себя так? Является ли это предполагаемым поведением или это ошибка?

ПРИМЕЧАНИЕ: Я не планирую фактически использовать Enums таким образом, я просто экспериментировал с использованием нестандартных значений для членов Enum

Ответы

Ответ 1

Из docs:

Учитывая два члена A и B с тем же значением (и A, определенным вначале), B является псевдонимом A. Поиск по значению значений A и B будет возвращать A. Поиск по имени B также будет return A:

>>> class Shape(Enum):
...     square = 2
...     diamond = 1
...     circle = 3
...     alias_for_square = 2
...
>>> Shape.square
<Shape.square: 2>
>>> Shape.alias_for_square
<Shape.square: 2>
>>> Shape(2)
<Shape.square: 2>

Это работает по принципу равенства, даже если значения изменяемы. Поскольку вы определили равные значения для black и blue, сначала black, blue является псевдонимом для black.

Ответ 2

Чтобы дополнить @user2357112 ответ, загляните в EnumMeta, метакласс для всех классов Enum; он получает быстрый взгляд на каждое определение класса, которое имеет свой тип и получает изменения, чтобы изменить его.

В частности, позаботится переназначить членов с тем же значением в своем методе __new__ через простое назначение:

    # If another member with the same value was already defined, the
    # new member becomes an alias to the existing one.
    for name, canonical_member in enum_class._member_map_.items():
        if canonical_member._value_ == enum_member._value_:
            enum_member = canonical_member
            break

Я не хотел проверять документы и вместо этого смотрел исходный код. Занятие: Всегда проверяйте документы, и если ExplanationNotFound поднят; проверьте источник: -)

Ответ 4

Из документации Python для Enums:

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

Это означает, что blue является псевдонимом для black. Когда один из них меняется, другой должен также.

Однако вы можете заставить Python сделать каждое значение перечисления уникальным, используя декоратор enum.unique. Также из документов:

>>> from enum import Enum, unique
>>> @unique
 ... class Mistake(Enum):
 ...     one = 1
 ...     two = 2
 ...     three = 3
 ...     four = 3
 ...
 Traceback (most recent call last):
 ...
 ValueError: duplicate values found in <enum 'Mistake'>: four -> three