Python 3.11+创建无参数__init__的Enum:旧代码失效问题
Python 3.11+ 枚举空元组赋值异常问题
问题背景
我有一段在Python 3.9和3.10中正常运行的枚举代码:
from enum import Enum class Environment(Enum): DEV = () INT = () PROD = () def __init__(self): self._value_ = self.name.lower() def __str__(self): return self.name def get_url(self): if self is Environment.DEV: return 'http://localhost' else: return f'https://{self.value}.my.domain'
在Python 3.9/3.10的REPL中执行结果符合预期:
>>> Environment.DEV.get_url() 'http://localhost' >>> Environment.INT.get_url() 'https://int.my.domain' >>> Environment.PROD.get_url() 'https://prod.my.domain'
这么写是为了避免冗余易出错的显式赋值,比如:
DEV = 'DEV' INT = 'INT' PROD = 'PROD'
但在Python 3.11或3.12中运行时,所有枚举值都指向第一个枚举项,get_url调用全部返回localhost:
>>> Environment.DEV.get_url() 'http://localhost' >>> Environment.INT.get_url() 'http://localhost' >>> Environment.PROD.get_url() 'http://localhost'
>>> Environment.DEV <Environment.DEV: 'dev'> >>> Environment.INT <Environment.DEV: 'dev'> >>> Environment.PROD <Environment.DEV: 'dev'>
请问Python 3.11中是否改变了空元组赋值的规则?如何在3.11+版本中实现相同效果,即无需显式传参给__init__来声明枚举值?
原因分析
Python 3.11对Enum的内部实现做了调整:当枚举成员赋值为相同的不可变值时,会被视为同一个成员的别名。空元组()是不可变对象,所有成员都赋值为(),在3.11+中会被判定为重复值,后续成员会直接指向第一个创建的枚举实例(也就是DEV),这就导致所有成员看起来都是同一个。
而在3.11之前,Enum的成员值校验逻辑没有这么严格,允许通过__init__方法在实例化后修改_value_来覆盖初始值,不会触发别名逻辑。
解决方案
要在3.11+中实现无需显式赋值、自动用小写名称作为枚举值的效果,有几种靠谱的方式:
方式1:使用enum.auto()配合自定义_generate_next_value_
Enum提供了auto()用于自动生成值,同时可以重写_generate_next_value_方法来控制自动生成的规则:
from enum import Enum, auto class Environment(Enum): DEV = auto() INT = auto() PROD = auto() def _generate_next_value_(name, start, count, last_values): # 直接返回成员名称的小写形式作为值 return name.lower() def __str__(self): return self.name def get_url(self): if self is Environment.DEV: return 'http://localhost' else: return f'https://{self.value}.my.domain'
方式2:利用__new__方法初始化
这种方式依然可以用空元组占位,但通过__new__在实例创建时就设置好_value_,避免3.11+的别名逻辑触发:
from enum import Enum class Environment(Enum): DEV = () INT = () PROD = () def __new__(cls): instance = object.__new__(cls) # 获取当前正在创建的成员名称,转为小写作为值 instance._value_ = cls._member_names_[len(cls._member_map_)].lower() return instance def __str__(self): return self.name def get_url(self): if self is Environment.DEV: return 'http://localhost' else: return f'https://{self.value}.my.domain'
验证效果
在Python 3.11+中运行上述方案的代码,会得到和3.9/3.10一致的结果:
>>> Environment.DEV.get_url() 'http://localhost' >>> Environment.INT.get_url() 'https://int.my.domain' >>> Environment.PROD.get_url() 'https://prod.my.domain'
>>> Environment.DEV <Environment.DEV: 'dev'> >>> Environment.INT <Environment.INT: 'int'> >>> Environment.PROD <Environment.PROD: 'prod'>
内容的提问来源于stack exchange,提问作者Aisteru Firë
相关产品推荐
相关产品推荐

