Python的 enum 模組原始碼分析
成員名不允許重複
這部分我的第一個想法是去控制 __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 模組相對獨立,且程式碼量不多,對於想知道元類程式設計可以閱讀一下,教科書式教學,還有單例模式等,值得一讀。