Python 进阶:enum 模块源码分析
作者:weapon
來源:https://zhuanlan.zhihu.com/p/52056538
起步
上一篇《Python 的枚舉類型》 (https://zhuanlan.zhihu.com/p/52046237) 文末說有機(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):@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 模塊的具體實(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):@classmethoddef?__prepare__(metacls,?cls,?bases):enum_dict?=?_EnumDict()...return?enum_dictclass?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ù)定義的?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)模塊考慮的會(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?=?namemember.value?=?valuesetattr(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):@classmethoddef?__prepare__(metacls,?name,?bases):d?=?_Dict()return?ddef?__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_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 模塊在讓每個(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?=?1print(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?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...從代碼上來看,即使是成員值相同,還是會(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?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__))迭代的方式遍歷成員
枚舉類支持迭代的方式遍歷成員,按定義的順序,如果有值重復(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é),還有單例模式等,值得一讀。
? ???精 彩 文 章?
seaborn常用的10種數(shù)據(jù)分析圖表
Python一鍵導(dǎo)出微信閱讀記錄和筆記,666!
WebStorm 超好用的10款插件,效率提升了好多!
一文看懂:網(wǎng)址,URL,域名,IP地址,DNS,域名解析
END
來和小伙伴們一起向上生長(zhǎng)呀~~~
掃描下方二維碼,添加小詹微信,可領(lǐng)取千元大禮包并申請(qǐng)加入 Python學(xué)習(xí)交流群,群內(nèi)僅供學(xué)術(shù)交流,日常互動(dòng),如果是想發(fā)推文、廣告、砍價(jià)小程序的敬請(qǐng)繞道!一定記得備注「交流學(xué)習(xí)」,我會(huì)盡快通過好友申請(qǐng)哦!
(添加人數(shù)較多,請(qǐng)耐心等待)
(掃碼回復(fù) 1024? 即可領(lǐng)取IT資料包)
總結(jié)
以上是生活随笔為你收集整理的Python 进阶:enum 模块源码分析的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: seaborn常用的10种数据分析图表
- 下一篇: Python 的 heapq 模块源码分