Python 进阶:enum 模块源码分析
作者:weapon
來源:https://zhuanlan.zhihu.com/p/52056538
起步
上一篇《Python 的枚舉類型》 (https://zhuanlan.zhihu.com/p/52046237) 文末說有機會的話可以看看它的源碼。那就來讀一讀,看看枚舉的幾個重要的特性是如何實現的。
要想閱讀這部分,需要對元類編程有所了解。
成員名不允許重復
這部分我的第一個想法是去控制 __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):@classmethoddef?__prepare__(metacls,?name,?bases):d?=?_Dict()return?dclass?Enum(metaclass=MyMeta):passclass?Color(Enum):red?=?1red?=?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):@classmethoddef?__prepare__(metacls,?cls,?bases):enum_dict?=?_EnumDict()...return?enum_dictclass?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:?#?重復定義的?keyraise?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?=?namemember.value?=?valuesetattr(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):@classmethoddef?__prepare__(metacls,?name,?bases):d?=?_Dict()return?ddef?__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_nameenum_member.value?=?valuesetattr(enum_class,?member_name,?enum_member)return?enum_classclass?MyEnum(metaclass=MyMeta):passclass?Color(MyEnum):red?=?1blue?=?2def?__str__(self):return?"%s.%s"?%?(self.__class__.__name__,?self.name)print(Color.red)????????#?Color.red print(Color.red.name)???#?red print(Color.red.value)??#?1enum 模塊在讓每個成員都有名稱和值的屬性的實現思路是一樣的(代碼我就不貼了)。EnumMeta.__new__ 是該模塊的重點,幾乎所有枚舉的特性都在這個函數實現。
當成員值相同時,第二個成員是第一個成員的別名
從這節開始就不再使用自己實現的類的說明了,而是通過拆解 enum 模塊的代碼來說明其實現了,從模塊的使用特性中可以知道,如果成員值相同,后者會是前者的一個別名:
from?enum?import?Enum class?Color(Enum):red?=?1_red?=?1print(Color.red?is?Color._red)??#?True從這可以知道,red和_red是同一對象。這又要怎么實現呢?
元類會為枚舉類創建 _member_map_ 屬性來存儲成員名與成員的映射關系,如果發現創建的成員的值已經在映射關系中了,就會用映射表中的對象來取代:
class?EnumMeta(type):def?__new__(metacls,?cls,?bases,?classdict):...#?create?our?new?Enum?typeenum_class?=?super().__new__(metacls,?cls,?bases,?classdict)enum_class._member_names_?=?[]???????????????#?names?in?definition?orderenum_class._member_map_?=?OrderedDict()??????#?name->value?mapfor?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?????#?取代breakelse:#?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?typeenum_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?memberraise?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 模塊相對獨立,且代碼量不多,對于想知道元類編程可以閱讀一下,教科書式教學,還有單例模式等,值得一讀。
? ???精 彩 文 章?
seaborn常用的10種數據分析圖表
Python一鍵導出微信閱讀記錄和筆記,666!
WebStorm 超好用的10款插件,效率提升了好多!
一文看懂:網址,URL,域名,IP地址,DNS,域名解析
END
來和小伙伴們一起向上生長呀~~~
掃描下方二維碼,添加小詹微信,可領取千元大禮包并申請加入 Python學習交流群,群內僅供學術交流,日常互動,如果是想發推文、廣告、砍價小程序的敬請繞道!一定記得備注「交流學習」,我會盡快通過好友申請哦!
(添加人數較多,請耐心等待)
(掃碼回復 1024? 即可領取IT資料包)
總結
以上是生活随笔為你收集整理的Python 进阶:enum 模块源码分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: seaborn常用的10种数据分析图表
- 下一篇: 太难得了!大厂数据分析实战项目资料首次公