Перегрузка __init __ () для подкласса Enum - PullRequest
4 голосов
/ 07 марта 2019

Я пытаюсь перегрузить метод __init__() подкласса перечисления.Странно, шаблон, который работает с нормальным классом, больше не работает с Enum.

Ниже показан желаемый шаблон, работающий с нормальным классом:

class Integer:
    def __init__(self, a):
        """Accepts only int"""
        assert isinstance(a, int)
        self.a = a

    def __repr__(self):
        return str(self.a)


class RobustInteger(Integer):
    def __init__(self, a):
        """Accepts int or str"""
        if isinstance(a, str):
            super().__init__(int(a))
        else:
            super().__init__(a)


print(Integer(1))
# 1
print(RobustInteger(1))
# 1
print(RobustInteger('1'))
# 1

Тот же шаблон затем разрываетсяесли используется с Enum:

from enum import Enum
from datetime import date


class WeekDay(Enum):
    MONDAY = 0
    TUESDAY = 1
    WEDNESDAY = 2
    THURSDAY = 3
    FRIDAY = 4
    SATURDAY = 5
    SUNDAY = 6

    def __init__(self, value):
        """Accepts int or date"""
        if isinstance(value, date):
            super().__init__(date.weekday())
        else:
            super().__init__(value)


assert WeekDay(0) == WeekDay.MONDAY
assert WeekDay(date(2019, 4, 3)) == WeekDay.MONDAY
# ---------------------------------------------------------------------------
# TypeError                                 Traceback (most recent call last)
# /path/to/my/test/file.py in <module>()
#      27 
#      28 
# ---> 29 class WeekDay(Enum):
#      30     MONDAY = 0
#      31     TUESDAY = 1

# /path/to/my/virtualenv/lib/python3.6/enum.py in __new__(metacls, cls, bases, classdict)
#     208             enum_member._name_ = member_name
#     209             enum_member.__objclass__ = enum_class
# --> 210             enum_member.__init__(*args)
#     211             # If another member with the same value was already defined, the
#     212             # new member becomes an alias to the existing one.

# /path/to/my/test/file.py in __init__(self, value)
#      40             super().__init__(date.weekday())
#      41         else:
# ---> 42             super().__init__(value)
#      43 
#      44 

# TypeError: object.__init__() takes no parameters

Ответы [ 2 ]

4 голосов
/ 07 марта 2019

Вы должны перегрузить крюк _missing_.Все экземпляры WeekDay создаются при первом определении класса;WeekDay(date(...)) - это операция индексирования, а не операция создания, а __new__ первоначально ищет уже существующие значения, связанные с целыми числами от 0 до 6. В противном случае он вызывает _missing_, в который вы можете преобразовать date объект в такое целое число.

class WeekDay(Enum):
    MONDAY = 0
    TUESDAY = 1
    WEDNESDAY = 2
    THURSDAY = 3
    FRIDAY = 4
    SATURDAY = 5
    SUNDAY = 6

    @classmethod
    def _missing_(cls, value):
        if isinstance(value, date):
            return cls(value.weekday())
        return super()._missing_(value)

Несколько примеров:

>>> WeekDay(date(2019,3,7))
<WeekDay.THURSDAY: 3>
>>> assert WeekDay(date(2019, 4, 1)) == WeekDay.MONDAY
>>> assert WeekDay(date(2019, 4, 3)) == WeekDay.MONDAY
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError

(Примечание: _missing_ недоступен до Python 3.6.)


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

# Silently convert an instance of datatime.date to a day-of-week
# integer for lookup.
class WeekDayMeta(EnumMeta):
    def __call__(cls, value, *args, **kwargs):
        if isinstance(value, date):
            value = value.weekday())
        return super().__call__(value, *args, **kwargs)

class WeekDay(Enum, metaclass=WeekDayMeta):
    MONDAY = 0
    TUESDAY = 1
    WEDNESDAY = 2
    THURSDAY = 3
    FRIDAY = 4
    SATURDAY = 5
    SUNDAY = 6
2 голосов
/ 07 марта 2019

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

Документы дают эту подсказку:

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

Так что нам нужно подождать с переопределением __new__, пока класс не будет создан.С некоторыми уродливыми исправлениями это проходит тест:

from enum import Enum
from datetime import date

class WeekDay(Enum):
    MONDAY = 0 
    TUESDAY = 1 
    WEDNESDAY = 2 
    THURSDAY = 3 
    FRIDAY = 4 
    SATURDAY = 5 
    SUNDAY = 6 

wnew = WeekDay.__new__

def _new(cls, value):
    if isinstance(value, date):
        return wnew(cls, value.weekday()) # not date.weekday()
    else:
        return wnew(cls, value)

WeekDay.__new__ = _new

assert WeekDay(0) == WeekDay.MONDAY
assert WeekDay(date(2019, 3, 4)) == WeekDay.MONDAY # not 2019,4,3
...