前言
为什么看 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
元类的魔法函数的执行顺序
__prepare__()
:这是一个元类在创建类时最先调用的方法,返回一个字典对象,又叫做类字典(classdict),用于存放新创建的类的属性和方法。我们定义的类的属性和方法会通过字典对象的__setitem__()
记录到自己内部__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 标准库枚举模块的过程记录。如果你也想了解面向对象编程,看看枚举模块的实现是一个不错的想法。