前言

为什么看 Python 枚举模块实现?Python 是面向对象(OOP)的语言,标准库中的枚举模块的实现涉及很多 OOP 的概念,比如:mixin、鸭子类型、元类、魔法函数等。阅读枚举模块的源码是了解面向对象编程一个比较有效的方式。本文基于 CPython 的 3.11 分支,对应的 Python 版本是 3.11.4 版本。

会包含如下内容:

  • 枚举类
  • 鸭子类型
  • 魔法函数
  • 协议
  • 装饰器
  • 描述符类
  • 元类

概念

枚举类

枚举类的成员是枚举类的特殊实例。也就是说枚举类的属性名不变,值变成了枚举类的一个特殊实例。在枚举类中,原先定义的属性名和属性值被存储到该特殊实例的 name 属性和 value 属性内。

from enum import Enum

class Role(Enum):
    ADMIN = 'admin'
    DEVELOPER = 'developer'
    GUEST = 'guest'

print(type(Role.ADMIN))
# 输出: <enum 'Role'>
# 原本的属性名被更新为一个 Role 的实例

print(Role.ADMIN.name) # 'ADMIN'
print(Role.ADMIN.value) # 'admin'
# 原先属性的名和值被保存到特殊实例的 name 属性和 value 属性内

枚举类的用法

在 Python Web 开发中,用户权限的管理和判断会通枚举类来实现。

不同的权限对应不同的枚举类实例,比如上面的例子中的:GUEST,DEVELOPER,ADMIN

在使用 ORM(比如:SQLAlchemy)定义用户表时,用户角色一列会被设为枚举类相关的数据库类型。写数据库的时候,ORM 会将枚举实例的值存到数据库。即:

from sqlalchemy import Column, Integer, String, Enum
from sqlalchemy.ext.declarative import declarative_base
from enum import Enum as PyEnum

class Role(PyEnum):
    ADMIN = 'admin'
    DEVELOPER = 'developer'
    GUEST = 'guest'

Base = declarative_base()

class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    name = Column(String)
    role = Column(Enum(Role))

查数据库时,ORM 会返回值对应的枚举实例,比如:

# 假设数据库查询到的用户的 role 是: 'admin',保存到 value 变量内
value = 'admin'
# Role(value) 实际上是对枚举类 `__call__()` 方法的调用
# 如果传入的枚举值有对应的枚举实例,返回该实例
current_user.role = Role(value)

从而实现面向对象的权限判断逻辑。即:

if current_user.role is Role.admin:
    # 业务逻辑

鸭子类型/魔法函数/协议

鸭子类型:如果它走起来像鸭子,叫起来像鸭子,那么它就是鸭子。

魔法函数:形如 __iter__ __next__ __getitem__ __call__ __lt__ __set_name__ 的内部函数。

协议:协议跟魔法函数关联,一个类如果实现了一个或多个协议规定的魔法函数,即为实现协议。常见有:可迭代协议,可调用协议,上下文管理协议等。

三者的关系:

  • 一个协议关联一个或多个魔法函数
  • 在定义类时如果实现了协议中的一个或多个魔法函数,即实现了该协议
  • 实现了相关的协议(像鸭子),就能在特定的函数或者语法内使用(是鸭子)比如:
from collections import defaultdict

class AnimalCount(object):
    def __init__(self, **kwargs):
        self._dict = defaultdict(int)
        for key, value in kwargs.items():
            self._dict[key] = value

    def __getitem__(self, key):
        return self._dict[key]

    def __call__(self, name, new_value):
        self._dict[name] = new_value


# 创建类的实例
my_pets = AnimalCount(cat=1, dog=2)

# 因为实现了 __getitem__() 所以可以像字典一样使用实例
print(my_pets['cat'])
print(my_pets['dog'])
print(my_pets['bear'])

# 因为实现了 __call__() 所以可以像函数一样调用实例
my_pets('dog', 3)
print(my_pets['dog'])

装饰器

什么是装饰器?

用来增强已有函数功能的函数或者类。

什么是装饰器类?

一个实现了 __call__() 魔法函数的类,类的实例用来取代被修饰函数,并基于 __call__() 实现类似函数的调用功能。比如:

from functools import wraps
class MyDecorator:
    def __init__(self, arg1, arg2):
        # 这里的arg1和arg2是装饰器参数
        self.arg1 = arg1
        self.arg2 = arg2

    def __call__(self, f):
        # 我们实际上是通过这个 wrapped 函数替换 my_function 函数
        # wrapped 没有 my_function 内部的一些属性的,比如: __doc__, __name__
        # wraps 装饰器可以让 wrapped 具备 my_function 相同的一些内部属性
        @wraps(f)
        # 这里的f是被装饰的函数
        def wrapped(*args, **kwargs):
            print(f"Decorator arguments: {self.arg1}, {self.arg2}")
            f(*args, **kwargs)
        return wrapped

# 现在我们使用装饰器,并传入参数
@MyDecorator("hello", "world")
def my_function(a, b):
    print(f"Function arguments: {a}, {b}")

my_function("foo", "bar")

原理是:装饰器类创建实例后,调用 __call__() 返回一个函数用于替换原先的 my_function

什么是装饰器函数?

from functools import wraps

def repeat(num_times):
    def decorator_repeat(func):
        @wraps(func)
        def wrapper_repeat(*args, **kwargs):
            for _ in range(num_times):
                result = func(*args, **kwargs)
            return result
        # 返回一个调用被装饰函数 num_times 次的新函数
        return wrapper_repeat
    # 返回一个传入了参数的装饰器,装饰器的参数是被装饰函数
    return decorator_repeat

@repeat(3)
def greet(name):
    print(f"Hello {name}")

greet("World")

原理类似,传入参数返回带参数的装饰器,然后传入被装饰函数返回新函数,新函数对原先的函数的功能做了增强,然后备用。

描述符类

最常见的描述符类实现是 property 装饰器,property 是用 C 实现的,类似的 Python 实现见: DynamicClassAttribute

class property(object):

    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        if doc is None and fget is not None and hasattr(fget, "__doc__"):
            doc = fget.__doc__
        self.__get = fget
        self.__set = fset
        self.__del = fdel
        try:
            self.__doc__ = doc
        except AttributeError:  # read-only or dict-less class
            pass

    def __get__(self, inst, type=None):
        if inst is None:
            return self
        if self.__get is None:
            raise AttributeError, "property has no getter"
        return self.__get(inst)

    def __set__(self, inst, value):
        if self.__set is None:
            raise AttributeError, "property has no setter"
        return self.__set(inst, value)

    def __delete__(self, inst):
        if self.__del is None:
            raise AttributeError, "property has no deleter"
        return self.__del(inst)

在 Python 中,描述符是实现了描述符协议的类。描述符协议包括 __get__()__set__()__delete__() 这三个特殊方法。在 Python 3.6,新加了一个方法:__set_name__,这也是枚举模块会使用到的魔法函数(后面会展开说)。描述符类可以将一个属性的创建过程(读、写、删除),分拆成多个函数来处理,常见的用法是:

  • 在读取、记录一个值前,可以对这个值添加验证的逻辑
  • 定义只读属性方法,比如 property 装饰器的常见用法
class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def radius(self):
        return self._radius

    @property
    def diameter(self):
        return self._radius * 2

c = Circle(5)
print(c.radius)  # 输出: 5
print(c.diameter)  # 输出: 10

元类

元类就是创建类的类。通过定义一个元类,可以控制相关的类创建的过程

class EnumType(type):
    def __prepare__(metacls, cls, bases, **kwds):
        # 将会返回类字典

    def __new__(metacls, cls, bases, classdict, *, boundary=None, _simple=False, **kwds):
        # 实际创建类的逻辑

# 通过指定 metaclass 参数,从而基于该元类创建类
class Enum(metaclass=EnumType):
    pass

元类的魔法函数的执行顺序

  1. __prepare__():这是一个元类在创建类时最先调用的方法,返回一个字典对象,又叫做类字典(classdict),用于存放新创建的类的属性和方法。我们定义的类的属性和方法会通过字典对象的 __setitem__() 记录到自己内部
  2. __new__() 实际创建类实例的地方

枚举类实例的创建原理

由于枚举类的属性是枚举类的实例,因此,在元类的 __new__(),实际上发生的事是:以属性名作为实例名创建一个枚举类实例,这个实例不保留类中定义的属性(因为这些类属性将全部转换成特殊实例),类属性的属性名和属性值将添加到该实例的 name 属性和 value 属性内,即:

from enum import Enum

class Role(Enum):
    ADMIN = 'admin'
    DEVELOPER = 'developer'
    GUEST = 'guest'

print(type(Role.ADMIN)) # 输出:<enum 'Role'>
print(Role.ADMIN.name) # 输出:ADMIN
print(Role.ADMIN.value) # 输出:admin
print(Role.DEVELOPER.value) # 输出:developer
print(Role.GUEST.value) # 输出:guest

因此创建的过程是:首先通过 __prepare__() 准备并返回类字典。然后通过 __new__() 基于类字典,创建类。在调用父类 type__new__() 时,类字典内如果有描述符类的实例就将基于鸭子类型的原则,调用该实例的 __set_name__() 方法。

下面是描述符类的定义,完整代码见:_proto_member 描述符类

class _proto_member:
    def __init__(self, value):
        self.value = value

    def __set_name__(self, enum_class, member_name):
        # 省略内容

下面是将描述符类的实例添加到类字典的操作:

for name in member_names:
   value = classdict[name]
   classdict[name] = _proto_member(value)

枚举模块就是利用这个特性,将类属性的值更新为枚举类实例。

因此,最终将属性的值更新为枚举类实例的操作是发生在描述符类的 __set_name__() 魔法函数内的,见代码:setattr

总结

如果说运维行业要求的是视野的广(了解和使用各类工具、开源项目、解决方案),那么开发则是对特定方向的深(或者理解为细致,对各种可能性的充分考虑和实现)。

这是我阅读 Python 标准库枚举模块的过程记录。如果你也想了解面向对象编程,看看枚举模块的实现是一个不错的想法。