首頁 > 後端開發 > Python教學 > Python中enum模組原始碼的詳細分析(程式碼範例)

Python中enum模組原始碼的詳細分析(程式碼範例)

不言
發布: 2018-12-11 10:33:08
轉載
2419 人瀏覽過

本篇文章帶給大家的內容是關於Python中enum模組原始碼的詳細分析(程式碼範例),有一定的參考價值,有需要的朋友可以參考一下,希望對你有幫助。

上一篇 《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.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 就是這樣的,不用額外做什麼就有的「特性」了。

#

以上是Python中enum模組原始碼的詳細分析(程式碼範例)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:segmentfault.com
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
作者最新文章
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板