【问题标题】:Overload __init__() for a subclass of Enum为 Enum 的子类重载 __init__()
【发布时间】:2019-07-29 19:17:23
【问题描述】:

我正在尝试重载枚举子类的__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

【问题讨论】:

    标签: python python-3.x inheritance enums python-3.6


    【解决方案1】:

    你必须重载_missing_ 钩子。 WeekDay 的所有实例都是在第一次定义类时创建的; WeekDay(date(...)) 是索引操作而不是创建操作,__new__ 最初是在寻找绑定到整数 0 到 6 的预先存在的值。如果失败,它会调用 _missing_,您可以在其中转换 @987654327 @object 变成这样一个整数。

    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
    

    【讨论】:

    • _missing_ 也可以在第三方包aenum 中使用(可以清楚地追溯到 Python 2.7)。有关与Enum 混在一起的危险的有趣读物,请阅读my question/answer here。注意:我是Enumenum34aenum的作者。
    【解决方案2】:

    有一个更好的答案,但我还是发布了这个,因为它可能有助于理解这个问题。

    文档给出了这个提示:

    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
    

    【讨论】:

    • _new 定义为EnumMeta 的子类的__call__ 方法可能会更简洁一些,并使用该元类而不是继承自Enum 的元类。这避免了猴子修补Weekday,但我不确定是否有任何意外的副作用。
    • 我会将我的想法添加到答案的末尾;欢迎 cmets。
    猜你喜欢
    • 2015-03-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-06-20
    • 1970-01-01
    • 2022-10-13
    • 2017-12-13
    • 1970-01-01
    相关资源
    最近更新 更多