Python中的 enum 模块源码详析


Posted in Python onJanuary 09, 2019

起步

上一篇 《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 模块相对独立,且代码量不多,对于想知道元类编程可以阅读一下,教科书式教学,还有单例模式等,值得一读。

好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对三水点靠木的支持。

Python 相关文章推荐
python 中的列表解析和生成表达式
Mar 10 Python
深入理解Django中内置的用户认证
Oct 06 Python
利用nohup来开启python文件的方法
Jan 14 Python
python 阶乘累加和的实例
Feb 01 Python
Python使用lambda表达式对字典排序操作示例
Jul 25 Python
PyQt 图解Qt Designer工具的使用方法
Aug 06 Python
python各层级目录下import方法代码实例
Jan 20 Python
python实现爱奇艺登陆密码RSA加密的方法示例详解
May 27 Python
学习Python爬虫的几点建议
Aug 05 Python
Python利用matplotlib绘制散点图的新手教程
Nov 05 Python
python实现无边框进度条的实例代码
Dec 30 Python
Python读取pdf表格写入excel的方法
Jan 22 Python
python linecache 处理固定格式文本数据的方法
Jan 08 #Python
Python 调用PIL库失败的解决方法
Jan 08 #Python
解决pyinstaller打包pyqt5的问题
Jan 08 #Python
Python Numpy库安装与基本操作示例
Jan 08 #Python
用Python和WordCloud绘制词云的实现方法(内附让字体清晰的秘笈)
Jan 08 #Python
Python离线安装PIL 模块的方法
Jan 08 #Python
Python数据预处理之数据规范化(归一化)示例
Jan 08 #Python
You might like
自己动手,丰衣足食 - 短波框形天线制作
2021/03/01 无线电
php gd等比例缩放压缩图片函数
2016/06/12 PHP
PHP使用FFmpeg获取视频播放总时长与码率等信息
2016/09/13 PHP
PHP实现的常规正则验证helper公共类完整实例
2017/04/27 PHP
php 可变函数使用小结
2018/06/12 PHP
qTip2 精致的基于jQuery提示信息插件
2012/02/17 Javascript
jQuery获取动态生成的元素示例
2014/06/15 Javascript
jquery中EasyUI使用技巧小结
2015/02/10 Javascript
javascript动态生成树形菜单的方法
2015/11/14 Javascript
JavaScript中Object.prototype.toString方法的原理
2016/02/24 Javascript
JavaScript中创建对象的模式汇总
2016/04/19 Javascript
vue制作加载更多功能的正确打开方式
2016/10/12 Javascript
javascript中apply/call和bind的使用
2017/02/15 Javascript
ie下js不执行的几种可能
2017/02/28 Javascript
jQuery.form.js的使用详解
2017/06/14 jQuery
详解ionic本地相册、拍照、裁剪、上传(单图完全版)
2017/10/10 Javascript
Node.js使用cookie保持登录的方法
2018/05/11 Javascript
vscode中vue-cli项目es-lint的配置方法
2018/07/30 Javascript
JavaScript数据结构与算法之检索算法示例【二分查找法、计算重复次数】
2019/02/22 Javascript
解决mui框架中switch开关通过js控制开或者关状态时小圆点不动的问题
2019/09/03 Javascript
Python实现提取文章摘要的方法
2015/04/21 Python
玩转python selenium鼠标键盘操作(ActionChains)
2020/04/12 Python
基于Django URL传参 FORM表单传数据 get post的用法实例
2018/05/28 Python
python和shell监控linux服务器的详细代码
2018/06/22 Python
opencv python 2D直方图的示例代码
2018/07/20 Python
PyQt5组件读取参数的实例
2019/06/25 Python
python读写csv文件方法详细总结
2019/07/05 Python
tensorflow实现在函数中用tf.Print输出中间值
2020/01/21 Python
百度软件工程师职位
2013/02/14 面试题
假面舞会策划方案
2014/05/29 职场文书
政府班子四风问题整改措施思想汇报
2014/10/08 职场文书
公安四风对照检查材料思想汇报
2014/10/11 职场文书
幼儿园教师教学反思
2016/03/02 职场文书
python 实现体质指数BMI计算
2021/05/26 Python
Rhit高效可视化Nginx日志查看工具
2021/11/01 Servers
Win10服务全部禁用了怎么启动?Win10服务全部禁用解决方法
2022/09/23 数码科技