Python中的 enum 模块源码详析
作者:栖迟于一丘 发布时间:2021-11-19 04:11:10
起步
上一篇 《Python 的枚举类型》 文末说有机会的话可以看看它的源码。那就来读一读,看看枚举的几个重要的特性是如何实现的。
要想阅读这部分,需要对元类编程有所了解。
成员名不允许重复
这部分我的第一个想法是去控制 __dict__ 中的 key 。但这样的方式并不好,__dict__ 范围大,它包含该类的所有属性和方法。而不单单是枚举的命名空间。我在源码中发现 enum 使用另一个方法。通过 __prepare__ 魔术方法可以返回一个类字典实例,在该实例 使用 __prepare__ 魔术方法自定义命名空间,在该空间内限定成员名不允许重复。
# 自己实现
class _Dict(dict):
def __setitem__(self, key, value):
if key in self:
raise TypeError('Attempted to reuse key: %r' % key)
super().__setitem__(key, value)
class MyMeta(type):
@classmethod
def __prepare__(metacls, name, bases):
d = _Dict()
return d
class Enum(metaclass=MyMeta):
pass
class Color(Enum):
red = 1
red = 1 # TypeError: Attempted to reuse key: 'red'
再看看 Enum 模块的具体实现:
class _EnumDict(dict):
def __init__(self):
super().__init__()
self._member_names = []
...
def __setitem__(self, key, value):
...
elif key in self._member_names:
# descriptor overwriting an enum?
raise TypeError('Attempted to reuse key: %r' % key)
...
self._member_names.append(key)
super().__setitem__(key, value)
class EnumMeta(type):
@classmethod
def __prepare__(metacls, cls, bases):
enum_dict = _EnumDict()
...
return enum_dict
class Enum(metaclass=EnumMeta):
...
模块中的 _EnumDict 创建了 _member_names 列表来存储成员名,这是因为不是所有的命名空间内的成员都是枚举的成员。比如 __str__, __new__ 等魔术方法就不是了,所以这边的 __setitem__ 需要做一些过滤:
def __setitem__(self, key, value):
if _is_sunder(key): # 下划线开头和结尾的,如 _order__
raise ValueError('_names_ are reserved for future Enum use')
elif _is_dunder(key): # 双下划线结尾的, 如 __new__
if key == '__order__':
key = '_order_'
elif key in self._member_names: # 重复定义的 key
raise TypeError('Attempted to reuse key: %r' % key)
elif not _is_descriptor(value): # value得不是描述符
self._member_names.append(key)
self._last_values.append(value)
super().__setitem__(key, value)
模块考虑的会更全面。
每个成员都有名称属性和值属性
上述的代码中,Color.red 取得的值是 1。而 eumu 模块中,定义的枚举类中,每个成员都是有名称和属性值的;并且细心的话还会发现 Color.red 是 Color 的实例。这样的情况是如何来实现的呢。
还是用元类来完成,在元类的 __new__ 中实现,具体的思路是,先创建目标类,然后为每个成员都创建一样的类,再通过 setattr 的方式将后续的类作为属性添加到目标类中,伪代码如下:
def __new__(metacls, cls, bases, classdict):
__new__ = cls.__new__
# 创建枚举类
enum_class = super().__new__()
# 每个成员都是cls的示例,通过setattr注入到目标类中
for name, value in cls.members.items():
member = super().__new__()
member.name = name
member.value = value
setattr(enum_class, name, member)
return enum_class
来看下一个可运行的demo:
class _Dict(dict):
def __init__(self):
super().__init__()
self._member_names = []
def __setitem__(self, key, value):
if key in self:
raise TypeError('Attempted to reuse key: %r' % key)
if not key.startswith("_"):
self._member_names.append(key)
super().__setitem__(key, value)
class MyMeta(type):
@classmethod
def __prepare__(metacls, name, bases):
d = _Dict()
return d
def __new__(metacls, cls, bases, classdict):
__new__ = bases[0].__new__ if bases else object.__new__
# 创建枚举类
enum_class = super().__new__(metacls, cls, bases, classdict)
# 创建成员
for member_name in classdict._member_names:
value = classdict[member_name]
enum_member = __new__(enum_class)
enum_member.name = member_name
enum_member.value = value
setattr(enum_class, member_name, enum_member)
return enum_class
class MyEnum(metaclass=MyMeta):
pass
class Color(MyEnum):
red = 1
blue = 2
def __str__(self):
return "%s.%s" % (self.__class__.__name__, self.name)
print(Color.red) # Color.red
print(Color.red.name) # red
print(Color.red.value) # 1
enum 模块在让每个成员都有名称和值的属性的实现思路是一样的(代码我就不贴了)。EnumMeta.__new__ 是该模块的重点,几乎所有枚举的特性都在这个函数实现。
当成员值相同时,第二个成员是第一个成员的别名
从这节开始就不再使用自己实现的类的说明了,而是通过拆解 enum 模块的代码来说明其实现了,从模块的使用特性中可以知道,如果成员值相同,后者会是前者的一个别名:
from enum import Enum
class Color(Enum):
red = 1
_red = 1
print(Color.red is Color._red) # True
从这可以知道,red和_red是同一对象。这又要怎么实现呢?
元类会为枚举类创建 _member_map_ 属性来存储成员名与成员的映射关系,如果发现创建的成员的值已经在映射关系中了,就会用映射表中的对象来取代:
class EnumMeta(type):
def __new__(metacls, cls, bases, classdict):
...
# create our new Enum type
enum_class = super().__new__(metacls, cls, bases, classdict)
enum_class._member_names_ = [] # names in definition order
enum_class._member_map_ = OrderedDict() # name->value map
for member_name in classdict._member_names:
enum_member = __new__(enum_class)
# 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
else:
# Aliases don't appear in member names (only in __members__).
enum_class._member_names_.append(member_name) # 新成员,添加到_member_names_中
enum_class._member_map_[member_name] = enum_member
...
从代码上来看,即使是成员值相同,还是会先为他们都创建对象,不过后创建的很快就会被垃圾回收掉了(我认为这边是有优化空间的)。通过与 _member_map_ 映射表做对比,用以创建该成员值的成员取代后续,但两者成员名都会在 _member_map_ 中,如例子中的 red 和 _red 都在该字典,但他们指向的是同一个对象。
属性 _member_names_ 只会记录第一个,这将会与枚举的迭代有关。
可以通过成员值来获取成员
print(Color['red']) # Color.red 通过成员名来获取成员
print(Color(1)) # Color.red 通过成员值来获取成员
枚举类中的成员都是单例模式,元类创建的枚举类中还维护了值到成员的映射关系 _value2member_map_ :
class EnumMeta(type):
def __new__(metacls, cls, bases, classdict):
...
# create our new Enum type
enum_class = super().__new__(metacls, cls, bases, classdict)
enum_class._value2member_map_ = {}
for member_name in classdict._member_names:
value = enum_members[member_name]
enum_member = __new__(enum_class)
enum_class._value2member_map_[value] = enum_member
...
然后在 Enum 的 __new__ 返回该单例即可:
class Enum(metaclass=EnumMeta):
def __new__(cls, value):
if type(value) is cls:
return value
# 尝试从 _value2member_map_ 获取
try:
if value in cls._value2member_map_:
return cls._value2member_map_[value]
except TypeError:
# 从 _member_map_ 映射获取
for member in cls._member_map_.values():
if member._value_ == value:
return member
raise ValueError("%r is not a valid %s" % (value, cls.__name__))
迭代的方式遍历成员
枚举类支持迭代的方式遍历成员,按定义的顺序,如果有值重复的成员,只获取重复的第一个成员。对于重复的成员值只获取第一个成员,正好属性 _member_names_ 只会记录第一个:
class Enum(metaclass=EnumMeta):
def __iter__(cls):
return (cls._member_map_[name] for name in cls._member_names_)
总结
enum 模块的核心特性的实现思路就是这样,几乎都是通过元类黑魔法来实现的。对于成员之间不能做比较大小但可以做等值比较。这反而不需要讲,这其实继承自 object 就是这样的,不用额外做什么就有的“特性”了。
总之,enum 模块相对独立,且代码量不多,对于想知道元类编程可以阅读一下,教科书式教学,还有单例模式等,值得一读。
好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对脚本之家的支持。
来源:https://www.hongweipeng.com/index.php/archives/1686/


猜你喜欢
- 前言pytest是一款强大的python自动化测试工具,可以胜任各种类型或者级别的软件测试工作。pytest提供了丰富的功能,包括asser
- 什么是事务?事务是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。数据库事务通常包含了一个序列的对数据库的读/写操作
- 最近发现数据库服务器压力很大,CPU经常达到100%。查看进程,发现有大量的sp_cursorclose;1进程信息。网上查了下,出现sp_
- 问题描述时间在我们日常的代码编写中会是经常出现的筛选或排序条件,尤其是一些特殊时间节点的时间显得尤为突出,例如昨天,当前日期,当前月份,当前
- 本文要点:爬虫的基本流程requests模块的使用保存csv可视化分析展示环境介绍python 3.8pycharm 2021专业版 激活码
- Python数据库编程之pymysql学习之前务必安装MySQL并已启动相关服务。一、pymsql的安装在python3的环境中直接使用以下
- 魔法方法凡是在类内部定义,以“__开头__结尾”的方法都称之为魔法方法,又称“类的内置方法”, 这些方法会在某些条件成立时触发。经常用到的双
- 创建main.py文件并粘贴下面代码点击右键运行Debug 'main'后,下方的Debug窗口会出现ImportError
- 前言今天跟大家介绍一个开源项目:id-maker,主要功能是用来在分布式环境下生成唯一 ID。上周停更了一周,也是用来开发和测试这个项目的相
- 一、python3对文本和二进制数据做了区分。文本是Unicode编码,str类型,用于显示。二进制类型是bytes类型,用于存
- django是 * 页,一般来说需要实时的生成访问的页面,展示给访问者,这样,内容可以随时变化,也就说请求到达视图函数之后,然后进行模板渲染
- python的ImageTk.PhotoImage大坑如果大家遇到这样的报错:Exception in Tkinter callbackTr
- 于是写测试程序。。。不行 下载最新的ODBC。。。还是不行 通过sql plus查询。。。咦?竟然也查不到。。。 于是,折腾。。。折腾。。。
- 翻译说明:这是Solid State Group网站上的一篇很友好的文章,解决了我在设计中遇到的很多问题,故在此我翻译其文,并对原作者表示非
- 本文实例讲述了Python反转序列的方法。分享给大家供大家参考,具体如下:序列是python中最基本的数据结构,序列中每个元素都有一个跟位置
- 有如下的代码:class p1:def __init__(self,a,b):print("init in p1")se
- 概要贝叶斯网络是一种基于概率的图模型,可用于建立变量之间的条件概率关系。在拼写检查器中,贝叶斯网络可以通过建立一个隐含状态、错误观察值和正确
- 如下所示:import numpy as npimport pandas as pd################# 准备数据 #####
- 实例如下所示:import osos.chdir("G:\Python1\Lib\site-packages\pytesser&q
- 这是我在做的一个游戏的半成品,整理了一下发出来.原理:通过更新变换矩阵来记录转动(函数remx()).利用矩阵计算出转动后的正方体顶点坐标,