Python中的 enum 模块源码详析

起步

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

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

(0)

相关推荐

  • Python简单实现enum功能的方法

    本文实例讲述了Python简单实现enum功能的方法.分享给大家供大家参考,具体如下: class Enumerate(object): def __init__(self,names): for number,name in enumerate(names.split()): setattr(self, name, number) codes = Enumerate('FOO BAR BAZ') print codes.FOO codes.FOO = 10 print codes.FOO 更多

  • 浅谈Python 的枚举 Enum

    枚举是常用的功能,看看Python的枚举. from enum import Enum Month = Enum('Month', ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec')) 枚举的定义 首先,定义枚举要导入enum模块. 枚举定义用class关键字,继承Enum类. 注意: 定义枚举时,成员名称不允许重复 默认情况下,不同的成员值允许相同.但是两个相同值的成员,第二个

  • python模拟enum枚举类型的方法小结

    本文实例总结了python模拟enum枚举类型的方法.分享给大家供大家参考.具体分析如下: python中没有enum枚举类型,可能python认为这玩意压根就没用,下面列举了三种方法模拟enum枚举类型 方法1. 使用自定义类 class Numbers(object): ONE = 1 TWO = 2 THREE = 3 assert Numbers.ONE == 1 assert Numbers.TWO == 2 assert Numbers.THREE == 3 方法2:使用函数动态创建

  • Python中模拟enum枚举类型的5种方法分享

    以下几种方法来模拟enum:(感觉方法一简单实用) 复制代码 代码如下: # way1 class Directions:     up = 0     down = 1     left = 2     right =3     print Directions.down # way2 dirUp, dirDown, dirLeft, dirRight = range(4) print dirDown # way3 import collections dircoll=collections.

  • Python中的 enum 模块源码详析

    起步 上一篇 <Python 的枚举类型> 文末说有机会的话可以看看它的源码.那就来读一读,看看枚举的几个重要的特性是如何实现的. 要想阅读这部分,需要对元类编程有所了解. 成员名不允许重复 这部分我的第一个想法是去控制 __dict__ 中的 key .但这样的方式并不好,__dict__ 范围大,它包含该类的所有属性和方法.而不单单是枚举的命名空间.我在源码中发现 enum 使用另一个方法.通过 __prepare__ 魔术方法可以返回一个类字典实例,在该实例 使用 __prepare__

  • Python中的heapq模块源码详析

    起步 这是一个相当实用的内置模块,但是很多人竟然不知道他的存在--笔者也是今天偶然看到的,哎--尽管如此,还是改变不了这个模块好用的事实 heapq 模块实现了适用于Python列表的最小堆排序算法. 堆是一个树状的数据结构,其中的子节点都与父母排序顺序关系.因为堆排序中的树是满二叉树,因此可以用列表来表示树的结构,使得元素 N 的子元素位于 2N + 1 和 2N + 2 的位置(对于从零开始的索引). 本文内容将分为三个部分,第一个部分简单介绍 heapq 模块的使用:第二部分回顾堆排序算法

  • YOLOv5中SPP/SPPF结构源码详析(内含注释分析)

    目录 一.SPP的应用的背景 二.SPP结构分析 三.SPPF结构分析 四.YOLOv5中SPP/SPPF结构源码解析(内含注释分析) 总结 一.SPP的应用的背景 在卷积神经网络中我们经常看到固定输入的设计,但是如果我们输入的不能是固定尺寸的该怎么办呢? 通常来说,我们有以下几种方法: (1)对输入进行resize操作,让他们统统变成你设计的层的输入规格那样.但是这样过于暴力直接,可能会丢失很多信息或者多出很多不该有的信息(图片变形等),影响最终的结果. (2)替换网络中的全连接层,对最后的卷

  • python如何使用contextvars模块源码分析

    目录 前记 更新说明 1.有无上下文传变量的区别 2.如何使用contextvars模块 3.如何优雅的使用contextvars 4.contextvars的原理 4.1 ContextMeta,ContextVarMeta和TokenMeta 4.2 Token 4.3 全局唯一context 4.4contextvar自己封装的Context 4.5 ContextVar 5.contextvars asyncio 5.1在asyncio中获取context 5.2 对上下文的操作 5.2

  • Java8中AbstractExecutorService与FutureTask源码详解

    目录 前言 一.AbstractExecutorService 1.定义 2.submit 3.invokeAll 4.invokeAny 二.FutureTask 1.定义 2.构造方法 3.get 4.run/ runAndReset 5. cancel 三.ExecutorCompletionService 1.定义 2.submit 3.take/ poll 总结 前言 本篇博客重点讲解ThreadPoolExecutor的三个基础设施类AbstractExecutorService.F

  • Python 中的 Counter 模块及使用详解(搞定重复计数)

    文章目录 参考描述Counter 模块Counter() 类Counter() 对象字典有序性KeyError魔术方法 \_\_missing\_\_ update() 方法 Counter 对象的常用方法most_common()elements()total()subtract() Counter 对象间的运算加法运算减法运算并集运算交集运算单目运算 Counter 对象间的比较>== 参考 项目 描述 Python 标准库 DougHellmann 著 / 刘炽 等 译 搜索引擎 Bing

  • 关于Redis网络模型的源码详析

    前言 Redis的网络模型是基于I/O多路复用程序来实现的.源码中包含四种多路复用函数库epoll.select.evport.kqueue.在程序编译时会根据系统自动选择这四种库其中之一.下面以epoll为例,来分析Redis的I/O模块的源码. epoll系统调用方法 Redis网络事件处理模块的代码都是围绕epoll那三个系统方法来写的.先把这三个方法弄清楚,后面就不难了. epfd = epoll_create(1024); 创建epoll实例 参数:表示该 epoll 实例最多可监听的

  • SPRING BOOT启动命令参数及源码详析

    前言 使用过Spring Boot,我们都知道通过java -jar可以快速启动Spring Boot项目.同时,也可以通过在执行jar -jar时传递参数来进行配置.本文带大家系统的了解一下Spring Boot命令行参数相关的功能及相关源码分析. 命令行参数使用 启动Spring Boot项目时,我们可以通过如下方式传递参数: java -jar xxx.jar --server.port=8081 默认情况下Spring Boot使用8080端口,通过上述参数将其修改为8081端口,而且通

  • Java1.8中StringJoiner的使用及源码详析

    前言 StringJoiner是Java里1.8新增的类,主要是帮助我们把一个列表拼接字符串, 或许有一部分人没有接触过. 所以本文将从使用例子入手, 分析StringJoiner的源码. 基本好的同学, 其实只要把这段例子自己运行一下, 自己看看源码就可以了.因为我觉得这个类挺简单的. 没必要看我下面的废话.... public class StringJoinerTest { public static void main(String[] args) { StringJoiner join

  • SpringBoot拦截器以及源码详析

    目录 1.拦截器是什么 2.自定义拦截器 2.1 编写拦截器 2.2 注册和配置拦截器 3.拦截器原理 3.1 找到可以处理请求的handler以及handler的所有拦截器 3.2 执行拦截器的preHandle方法 3.3 执行目标方法 3.4 执行拦截器的postHandle方法 3.5 执行拦截器的afterCompletion方法 3.6 异常处理 4.总结 1.拦截器是什么 java里的拦截器(Interceptor)是动态拦截Action调用的对象,它提供了一种机制可以使开发者在一

随机推荐