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学习笔记:字典的使用示例详解
Jun 13 Python
Python实现批量更换指定目录下文件扩展名的方法
Sep 19 Python
python3将视频流保存为本地视频文件
Jun 20 Python
Python参数解析模块sys、getopt、argparse使用与对比分析
Apr 02 Python
Python实现堡垒机模式下远程命令执行操作示例
May 09 Python
django admin组件使用方法详解
Jul 19 Python
python requests证书问题解决
Sep 05 Python
Python selenium使用autoIT上传附件过程详解
May 26 Python
pandas创建DataFrame的7种方法小结
Jun 14 Python
python和php哪个容易学
Jun 19 Python
python des,aes,rsa加解密的实现
Jan 16 Python
如何用用Python将地址标记在地图上
Feb 07 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
三级下拉菜单的js实现代码
2011/05/23 Javascript
DB.ASP 用Javascript写ASP很灵活很好用很easy
2011/07/31 Javascript
JavaScript中如何通过arguments对象实现对象的重载
2014/05/12 Javascript
自写的jQuery异步加载数据添加事件
2014/05/15 Javascript
推荐25个超炫的jQuery网格插件
2014/11/28 Javascript
node.js中的fs.lstat方法使用说明
2014/12/16 Javascript
Javascript通过overflow控制列表闭合与展开的方法
2015/05/15 Javascript
Jquery easyui 实现动态树
2015/11/17 Javascript
详解JavaScript的AngularJS框架中的表达式与指令
2016/03/05 Javascript
JS复制对应id的内容到粘贴板(Ctrl+C效果)
2017/01/23 Javascript
vue组件初学_弹射小球(实例讲解)
2017/09/06 Javascript
表格展示利器 Bootstrap Table实例代码
2017/09/06 Javascript
微信小程序tabbar底部导航
2018/11/05 Javascript
微信小程序实现省市区三级地址选择
2020/06/21 Javascript
layUI使用layer.open,在content打开数据表格,获取值并返回的方法
2019/09/26 Javascript
vue 解除鼠标的监听事件的方法
2019/11/13 Javascript
原生javascript制作的拼图游戏实现方法详解
2020/02/23 Javascript
Python中实现的RC4算法
2015/02/14 Python
python 开发的三种运行模式详细介绍
2017/01/18 Python
老生常谈Python基础之字符编码
2017/06/14 Python
Python中property函数用法实例分析
2018/06/04 Python
Python 函数绘图及函数图像微分与积分
2019/11/20 Python
python中关于数据类型的学习笔记
2020/07/19 Python
Python如何输出百分比
2020/07/31 Python
解决margin 外边距合并问题
2019/07/03 HTML / CSS
澳大利亚电子产品购物网站:Dick Smith
2017/02/02 全球购物
Rakuten Kobo台湾:电子书、eReaders和Reading应用程式
2017/11/24 全球购物
标准自荐信范文
2014/01/29 职场文书
学生顶撞老师的检讨书
2014/09/17 职场文书
2014年创卫工作总结
2014/11/24 职场文书
2015年法制宣传月活动总结
2015/03/26 职场文书
MySQL 四种连接和多表查询详解
2021/07/16 MySQL
【海涛解说】暗牧也疯狂,牛蛙成配角
2022/04/01 DOTA
【海涛dota】偶遇拉娜娅 质量局德鲁伊第一视角解说
2022/04/01 DOTA
Windows Server 2019 配置远程控制以及管理方法
2022/04/28 Servers
Python 操作pdf pdfplumber读取PDF写入Exce
2022/08/14 Python