Python中的 enum 模塊源碼詳析
起步
上一篇 《Python 的枚舉類型》 文末說有機(jī)會(huì)的話可以看看它的源碼。那就來讀一讀,看看枚舉的幾個(gè)重要的特性是如何實(shí)現(xiàn)的。
要想閱讀這部分,需要對(duì)元類編程有所了解。
成員名不允許重復(fù)
這部分我的第一個(gè)想法是去控制 __dict__ 中的 key 。但這樣的方式并不好,__dict__ 范圍大,它包含該類的所有屬性和方法。而不單單是枚舉的命名空間。我在源碼中發(fā)現(xiàn) enum 使用另一個(gè)方法。通過 __prepare__ 魔術(shù)方法可以返回一個(gè)類字典實(shí)例,在該實(shí)例 使用 __prepare__ 魔術(shù)方法自定義命名空間,在該空間內(nèi)限定成員名不允許重復(fù)。
# 自己實(shí)現(xiàn) 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 模塊的具體實(shí)現(xiàn):
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 創(chuàng)建了 _member_names 列表來存儲(chǔ)成員名,這是因?yàn)椴皇撬械拿臻g內(nèi)的成員都是枚舉的成員。比如 __str__, __new__ 等魔術(shù)方法就不是了,所以這邊的 __setitem__ 需要做一些過濾:
def __setitem__(self, key, value): if _is_sunder(key): # 下劃線開頭和結(jié)尾的,如 _order__ raise ValueError('_names_ are reserved for future Enum use') elif _is_dunder(key): # 雙下劃線結(jié)尾的, 如 __new__ if key == '__order__': key = '_order_' elif key in self._member_names: # 重復(fù)定義的 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)
模塊考慮的會(huì)更全面。
每個(gè)成員都有名稱屬性和值屬性
上述的代碼中,Color.red 取得的值是 1。而 eumu 模塊中,定義的枚舉類中,每個(gè)成員都是有名稱和屬性值的;并且細(xì)心的話還會(huì)發(fā)現(xiàn) Color.red 是 Color 的實(shí)例。這樣的情況是如何來實(shí)現(xiàn)的呢。
還是用元類來完成,在元類的 __new__ 中實(shí)現(xiàn),具體的思路是,先創(chuàng)建目標(biāo)類,然后為每個(gè)成員都創(chuàng)建一樣的類,再通過 setattr 的方式將后續(xù)的類作為屬性添加到目標(biāo)類中,偽代碼如下:
def __new__(metacls, cls, bases, classdict): __new__ = cls.__new__ # 創(chuàng)建枚舉類 enum_class = super().__new__() # 每個(gè)成員都是cls的示例,通過setattr注入到目標(biāo)類中 for name, value in cls.members.items(): member = super().__new__() member.name = name member.value = value setattr(enum_class, name, member) return enum_class
來看下一個(gè)可運(yùn)行的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__ # 創(chuàng)建枚舉類 enum_class = super().__new__(metacls, cls, bases, classdict) # 創(chuàng)建成員 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 模塊在讓每個(gè)成員都有名稱和值的屬性的實(shí)現(xiàn)思路是一樣的(代碼我就不貼了)。EnumMeta.__new__ 是該模塊的重點(diǎn),幾乎所有枚舉的特性都在這個(gè)函數(shù)實(shí)現(xiàn)。
當(dāng)成員值相同時(shí),第二個(gè)成員是第一個(gè)成員的別名
從這節(jié)開始就不再使用自己實(shí)現(xiàn)的類的說明了,而是通過拆解 enum 模塊的代碼來說明其實(shí)現(xiàn)了,從模塊的使用特性中可以知道,如果成員值相同,后者會(huì)是前者的一個(gè)別名:
from enum import Enum class Color(Enum): red = 1 _red = 1 print(Color.red is Color._red) # True
從這可以知道,red和_red是同一對(duì)象。這又要怎么實(shí)現(xiàn)呢?
元類會(huì)為枚舉類創(chuàng)建 _member_map_ 屬性來存儲(chǔ)成員名與成員的映射關(guān)系,如果發(fā)現(xiàn)創(chuàng)建的成員的值已經(jīng)在映射關(guān)系中了,就會(huì)用映射表中的對(duì)象來取代:
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 ...
從代碼上來看,即使是成員值相同,還是會(huì)先為他們都創(chuàng)建對(duì)象,不過后創(chuàng)建的很快就會(huì)被垃圾回收掉了(我認(rèn)為這邊是有優(yōu)化空間的)。通過與 _member_map_ 映射表做對(duì)比,用以創(chuàng)建該成員值的成員取代后續(xù),但兩者成員名都會(huì)在 _member_map_ 中,如例子中的 red 和 _red 都在該字典,但他們指向的是同一個(gè)對(duì)象。
屬性 _member_names_ 只會(huì)記錄第一個(gè),這將會(huì)與枚舉的迭代有關(guān)。
可以通過成員值來獲取成員
print(Color['red']) # Color.red 通過成員名來獲取成員 print(Color(1)) # Color.red 通過成員值來獲取成員
枚舉類中的成員都是單例模式,元類創(chuàng)建的枚舉類中還維護(hù)了值到成員的映射關(guān)系 _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__))
迭代的方式遍歷成員
枚舉類支持迭代的方式遍歷成員,按定義的順序,如果有值重復(fù)的成員,只獲取重復(fù)的第一個(gè)成員。對(duì)于重復(fù)的成員值只獲取第一個(gè)成員,正好屬性 _member_names_ 只會(huì)記錄第一個(gè):
class Enum(metaclass=EnumMeta): def __iter__(cls): return (cls._member_map_[name] for name in cls._member_names_)
總結(jié)
enum 模塊的核心特性的實(shí)現(xiàn)思路就是這樣,幾乎都是通過元類黑魔法來實(shí)現(xiàn)的。對(duì)于成員之間不能做比較大小但可以做等值比較。這反而不需要講,這其實(shí)繼承自 object 就是這樣的,不用額外做什么就有的“特性”了。
總之,enum 模塊相對(duì)獨(dú)立,且代碼量不多,對(duì)于想知道元類編程可以閱讀一下,教科書式教學(xué),還有單例模式等,值得一讀。
好了,以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
相關(guān)文章
Python3 常用數(shù)據(jù)標(biāo)準(zhǔn)化方法詳解
這篇文章主要介紹了Python3 常用數(shù)據(jù)標(biāo)準(zhǔn)化方法詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2021-03-03Python pandas如何向excel添加數(shù)據(jù)
這篇文章主要介紹了Python pandas如何向excel添加數(shù)據(jù),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-05-05python實(shí)現(xiàn)根據(jù)主機(jī)名字獲得所有ip地址的方法
這篇文章主要介紹了python實(shí)現(xiàn)根據(jù)主機(jī)名字獲得所有ip地址的方法,涉及Python解析IP地址的相關(guān)技巧,需要的朋友可以參考下2015-06-06Python列表刪除重復(fù)元素與圖像相似度判斷及刪除實(shí)例代碼
這篇文章主要給大家介紹了關(guān)于Python列表刪除重復(fù)元素與圖像相似度判斷及刪除的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-05-05