资讯专栏INFORMATION COLUMN

Python 的 enum 模块源码分析

muddyway / 578人阅读

摘要:起步上一篇的枚举类型文末说有机会的话可以看看它的源码。但这样的方式并不好,范围大,它包含该类的所有属性和方法。而不单单是枚举的命名空间。每个成员都有名称属性和值属性上述的代码中,取得的值是。

起步

上一篇 《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.redColor 的示例。这样的情况是如何来实现的呢。

还是用元类来完成,在元类的 __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.ucloud.cn/yun/42781.html

相关文章

  • Python 枚举类型

    摘要:起步的原生类型中并不包含枚举类型。枚举类型可以看作是一种标签或是一系列常量的集合,通常用于表示某些特定的有限集合,例如星期月份状态等。简单的示例枚举成员有值默认可重复,枚举成员具有友好的字符串表示枚举类型不可实例化,不可更改。 起步 Python 的原生类型中并不包含枚举类型。为了提供更好的解决方案,Python 通过 PEP 435 在 3.4 版本中添加了 enum 标准库。 枚举...

    wall2flower 评论0 收藏0
  • PyTips 0x17-Python枚举类型

    摘要:中的枚举类型枚举类型可以看作是一种标签或是一系列常量的集合,通常用于表示某些特定的有限集合,例如星期月份状态等。 Python 中的枚举类型 枚举类型可以看作是一种标签或是一系列常量的集合,通常用于表示某些特定的有限集合,例如星期、月份、状态等。Python 的原生类型(Built-in types)里并没有专门的枚举类型,但是我们可以通过很多方法来实现它,例如字典、类等: WEEKD...

    yedf 评论0 收藏0
  • 别再说Python没有枚举类型了,好好看看

    摘要:枚举类型可以看作是一种标签或是一系列常量的集合,通常用于表示某些特定的有限集合,例如星期月份状态等。 showImg(https://segmentfault.com/img/remote/1460000018679998?w=740&h=728); 枚举类型可以看作是一种标签或是一系列常量的集合,通常用于表示某些特定的有限集合,例如星期、月份、状态等。 Python 的原生类型(Bu...

    zsy888 评论0 收藏0
  • Python入门细节

    摘要:入门细节相除后的类型双斜杠是整除,出来的类型是。序列,集合和字典属于组,是的基本数据类型。变量名区分大小写。尽量避免循环引入。变量函数等引入内指定的变量函数等。中如果没有出现模块名也是绝对导入。顶级包与入口文件的位置 python入门细节 相除后的类型 type(2/2) float type(2//2) int 双斜杠是整除,出来的类型是int。单斜杠的出来的是float类型。 进制...

    microcosm1994 评论0 收藏0
  • python模块enum_上

    摘要:模块定义了种枚举类装饰器助手在中加入创建枚举注意点枚举值可以是任何类型,如果值不重要可以使用自动选择。使用装饰器可以对枚举值进行唯一约束枚举专用的类装饰器。序列化一般要求序列化的枚举要定义在模块顶层,因为反序列化要求枚举能够从模块导入。 enum模块定义了: 4种枚举类:Enum, IntEnum, Flag, IntFlag 装饰器:unique() 助手:auto Flag, ...

    kelvinlee 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<