枚举类方法带有默认枚举值时失败。

3

我知道如果你有一个使用枚举类名作为类型提示的类方法,那么在Python 3.6及以下版本中有一个技巧可使其正常工作。

不要写成...

class Release(Enum):
   ...
   @classmethod
   def get(cls, release: Release):
      ...

您需要像这样使用字符串值...
class Release(Enum):
   ...
   @classmethod
   def get(cls, release: "Release"):
      ...

我相信在 Python 3.7 及以上版本中,有一种更Pythonic的方法来避免使用引号的“hack”。原因是类在所有方法和变量都完成之前不存在。由于类尚不存在,我无法使用类名,必须使用带引号的字符串作为hack。 然而,我正在尝试更进一步并使用默认值,但这并不起作用。是否存在一种Pythonic方法适用于 Python 3.6 而不是hack? 另外,在 Python 3.7 及以上版本中是否已经修复了这个问题?

代码

from enum import Enum

class Release(Enum):
    Canary = (1, [])
    Beta = (2, [1])
    RC = (3, [2, 1])
    Stable = (4, [3, 2, 1])

    def __new__(cls, value, cascade):
        obj = object.__new__(cls)
        obj._value_ = value
        obj.current = ["Release" * value] # This would technically be a list of all releasese in this enum. This is just to emulate different values
        obj.cascade = cascade
        return obj

    @classmethod
    def get_all_releases(cls, release: "Release" = Canary):  # Default Value = Release.Canary
        return release.current


print(Release.get_all_releases(Release.Canary))
print(Release.get_all_releases(Release.Beta))
print(Release.get_all_releases(Release.RC))
print(Release.get_all_releases(Release.Stable))

# Error. Even with default value
# print(Release.get_all_releases())

使用此代码,我收到以下错误消息。
AttributeError: 'tuple' object has no attribute 'current'

这是因为它返回的是Canary元组,而不是实际值。


“current”和“cascade”的区别是什么? “current”实际上如何设置? - Ethan Furman
3个回答

1
参考了@ufoxDan的答案,但尝试使它更加自然而不是绕弯子。
基本上,我开始通过检查return之前的type(release),注意到我得到了结果...
<enum 'Release'>
<enum 'Release'>
<enum 'Release'>
<enum 'Release'>
<class 'tuple'>

我注意到如果类型是Release,那么我可以执行代码,但是如果它是任何其他类型,比如Canary类型未创建时的None,那么我可以假设它是要求Canary。所以我做了以下操作...
@classmethod
def get_all_releases(cls, release: "Release" = None):
   if type(release) is Release:
       return release.current
   return Release.Canary.current

# Now these all work
print(Release.get_all_releases())
print(Release.get_all_releases(Release.Canary))
print(Release.get_all_releases(Release.Stable))

这似乎是实现结果的最pythonic的方式。在阅读代码时,这也似乎是最好的方式,而且不需要重复代码。任何人都应该能够实现类似的东西。


我喜欢它。只需注意,如果您需要更改默认值,则需要在每个使用此默认值的函数中进行重构(尽管对于一个函数来说并不算太糟糕)。因此,您也可以在枚举定义中设置 default = Canary,然后执行 return Release.default.current。这不是一个主要问题,但如果是一个大型项目,它可以简化未来的开发 :) - ufoxDan
1
@ufoxDan 我明白你的意思。我的代码目前循环遍历其他地方的所有枚举,我认为我必须更改那段代码来处理新的 default 枚举。如果我需要更新类,我会研究一下这个问题。 - Christopher Rucinski

1

虽然这只是一种变通方法,但对我来说似乎很有效:

@classmethod
def get_all_releases(cls, release: "Release" = Canary):  # Default Value = Release.Canary
    if release == (Release.Canary.value,):
        return Release.Canary.current
    return release.current

无论您为Canary分配什么值,它都可以工作。因此,只要这是您的默认设置,我相信它将起作用。
为了更加通用,这样你只需要调整类定义中的默认值而不是每个函数,可以按照以下方式进行:
class Release(Enum):
    Canary = 6,
    Beta = 2,
    RC = 3,
    Stable = 4
    default = Canary

    ...

    @classmethod
    def get_all_releases(cls, release: "Release" = default):
        if release == (Release.Canary.value,):
            return Release.Canary.current
        return release.current

据您所知,没有任何内置的方法可以做到这一点,对吗? - Christopher Rucinski
我不知道有没有更简单的方法,也没有找到任何比这更简单的东西,但这不是我以前尝试过的东西,所以我不能百分之百确定。 - ufoxDan
我更新了答案,展示了一种更通用的设置和使用默认值的方法。这里还有其他一些答案 https://dev59.com/AlcP5IYBdhLWcg3wFmve 但如果这种方法适用于您,它至少不需要太多更改。 - ufoxDan
你在 Stable 上缺少了一个逗号。 - Ethan Furman

1

在你的Release Enum中,有几件事情可以让生活更轻松,第一种技术在这里展示:

    def __new__(cls, value, cascade):
        obj = object.__new__(cls)
        obj._value_ = value
        obj.current = ["Release" * value]      # not sure what this should actually be

        # if always the previous versions (don't need cascade defined)
        obj.cascade = sorted(list(cls), reverse=True)

        # if some already defined subset (need cascade defined)
        obj.cascade = [cls._value2member_map_(c) for c in cascade]

        return obj

第二种技术有两种方式 -- 默认情况下始终使用第一个Enum成员。
    @classmethod
    def get_all_releases(cls):
        return list(cls[0]).current

或者,如果默认值可以是任何成员,则类似于this answer的东西应该可以工作:
class add_default:
    """
    add DEFAULT psuedo-member to enumeration; use first member if none specified
    (default should be name of member)
    """
    def __init__(self, default=''):
        self._default = default
    def __call__(self, enumeration):
        if self._default:
            member = enumeration[self._default]
        else:
            member = enumeration[enumeration._member_names_[0]]
        enumeration._member_map_['DEFAULT'] = member
        return enumeration

您的最终Enum将如下所示(假设cascade是所有先前成员,并使用装饰器方法):
@add_default('Canary')
class Release(Enum):
    Canary = 1
    Beta = 2
    RC = 3
    Stable = 4
    def __new__(cls, value):
        obj = object.__new__(cls)
        obj._value_ = value
        obj.current = ["Release" * value]      # not sure what this should actually be or how it's calculated
        obj.cascade = list(cls)[::-1]
        return obj
    @classmethod
    def get_all_releases(cls, release: "Release" = None):
        if release is None:
            release = cls.DEFAULT
        return release.current

并在使用中:

>>> Release.DEFAULT
<Release.Canary: 1>

>>> Release.get_all_releases()
['Release']

>>> Release.get_all_releases(Release.RC)
['ReleaseReleaseRelease']

原始回答

你的代码出问题了,问题出在这里:

class Release(Enum):
    Canary = 1,

通过添加那个额外的逗号,你已经使得Canary的值为(1, )。去掉那个逗号就可以消除tuple异常。

感谢您解释值为(1,)的原因。我直到现在才完全理解;然而,我的真实代码确实使用了第二个元素为列表的元组。我试图制作一个最小可行的示例。 - Christopher Rucinski
问题是它给出了一个AttributeError,而不是一个tuple - Christopher Rucinski
@ChristopherRucinski:已更新答案以匹配您的更新问题。 :) - Ethan Furman
@ChristopherRucinski:啊,我不该在先运行代码之前就发布它!现在它可以工作了。:/ - Ethan Furman
这里有很多好的信息需要解析。在这个问题中,我只是试图找出一种使用默认Enum值的Pythonic方法,在classmethod中留下了许多重要的细节,你必须假设。例如,current实际上是一个带有键为latest和每个版本(如1.01.11.21.32.02.12.23.0等)的字典,而值则是一些数据,比如下载链接。此外,虽然cascade通常是所有先前版本,但有一个我没有包括的奇怪情况,它不遵循那个逻辑。 - Christopher Rucinski

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接