SymPy 实用装饰器模块全解析:threaded、deprecated 与 memoize_property 的源码级指南
【免费下载链接】sympyA computer algebra system written in pure Python项目地址: https://gitcode.com/GitHub_Trending/sy/sympy
导读
sympy.utilities.decorator是 SymPy(一个用纯 Python 编写的计算机代数系统)中承载通用实用装饰器的核心工具模块,为整个代码库提供了表达式逐元素映射(threading)、API 标记、废弃管理、属性缓存与文档测试依赖声明等基础设施。本文基于仓库中 sympy/utilities/decorator.py 的完整源码与其配套测试 sympy/utilities/tests/test_decorator.py,逐一定义每个装饰器的设计意图、调用约定与实现原理,并结合partfrac.py、assumptions/ask.py、fp_groups.py等真实调用场景,帮助你在编写 SymPy 扩展或自己的符号计算库时,正确选择并灵活运用这些装饰器。
模块概览与文档来源
SymPy 官方文档中关于该模块的说明位于 doc/src/modules/utilities/decorator.rst,它通过 Sphinx 的automodule指令直接挂载sympy.utilities.decorator的成员文档,并排除deprecated成员(因其单独以autodecorator形式展示)。这意味着该模块的"文档正文"实质就是源码中的 docstring 与 doctest,本文即以此为主体展开。
模块顶层导出了以下 API:
| 名称 | 类型 | 作用 |
|---|---|---|
threaded | 装饰器 | 把函数应用到复合对象的各个元素(含Add) |
xthreaded | 装饰器 | 把函数应用到复合对象的各个元素(排除Add) |
threaded_factory | 工厂函数 | threaded/xthreaded的底层实现 |
no_attrs_in_subclass | 描述符类 | 阻止子类"继承"基类的某些属性 |
doctest_depends_on | 装饰器 | 为 doctest 声明运行依赖(可执行文件、模块等) |
public | 装饰器 | 在定义处把对象名追加进所在模块的__all__ |
memoize_property | 装饰器 | 属性首次求值后缓存结果 |
deprecated | 装饰器 | 标记整函数或整类为废弃 |
此外,模块为保持向后兼容还转导出sympy.external.mpmath.conserve_mpmath_dps(见源码 decorator.py),并依赖 sympy/utilities/exceptions.py 中的sympy_deprecation_warning。
threaded 与 xthreaded:对复合表达式做逐元素映射
设计目标与签名约定
threaded的目的是"统一地让一个函数能够作用于复合对象的所有元素",这些复合对象包括矩阵、列表、元组、集合以及其他可迭代容器,或普通表达式本身。其用法约定如下:
@threaded def function(expr, *args, **kwargs): ...被装饰的函数签名必须是function(expr, *args, **kwargs),其中第一个位置参数expr是待展开的目标对象。两个变体的差异仅在于是否对Add类表达式展开:
threaded:允许对Add的元素逐项应用(源码 docstring 明确说明"allows threading over elements of Add class");xthreaded:禁止对Add展开,此时整个Add表达式作为整体传给函数("disallows threading over elements of Add class")。
底层实现:threaded_factory
两者都由工厂函数threaded_factory(func, use_add)生成包装器(decorator.py),threaded传入use_add=True,xthreaded传入use_add=False。包装器的分派逻辑依次为:
- 矩阵:若
expr是MatrixBase实例,则调用expr.applyfunc(lambda f: func(f, *args, **kwargs)),即对每个矩阵元素应用函数; - 一般可迭代容器:若
expr可迭代(通过sympy.utilities.iterables.iterable判断),尝试expr.__class__([func(f, ...) for f in expr])重建同类型容器(列表、元组、集合均可保持类型);若抛TypeError(例如某些不可重建的容器),则原样返回expr; - 符号表达式:先
sympify(expr)规范化;若use_add为真且expr.is_Add,对每一项func(f, ...)后以expr.__class__重组;若expr.is_Relational(如Eq),对左右两侧分别应用函数后重建关系式;否则直接func(expr, ...)。
由于包装器使用functools.wraps,被装饰函数的__name__、__doc__与自定义属性都会保留(测试test_wraps验证了这一点,见 test_decorator.py)。
行为验证
仓库测试 test_decorator.py 给出了精确的预期行为:
@threaded def function(expr, *args): return 2*expr + sum(args) function(Matrix([[x, y], [1, x]]), 1, 2) # -> Matrix([[2*x + 3, 2*y + 3], [5, 2*x + 3]]) function(Eq(x, y), 1, 2) # -> Eq(2*x + 3, 2*y + 3) function([x, y], 1, 2) # -> [2*x + 3, 2*y + 3] function((x, y), 1, 2) # -> (2*x + 3, 2*y + 3) function({x, y}, 1, 2) # -> {2*x + 3, 2*y + 3} @threaded def function(expr, n): return expr**n function(x + y, 2) # -> x**2 + y**2 (Add 被逐项展开) function(x, 2) # -> x**2 @xthreaded def function(expr, n): return expr**n function(x + y, 2) # -> (x + y)**2 (Add 保持整体)仓库内的真实用法
一个典型的真实调用位于 sympy/polys/partfrac.py:@xthreaded被用于apart的部分分式分解函数,使得用户传入矩阵或集合时函数能自动逐元素作用,而传入多项式加法表达式时则整体处理。这体现了该装饰器在 SymPy 公共 API 中的核心价值:让数学函数天然具备对复合对象的批量能力,而无需为每种容器单独写分发代码。
public:在定义处自动登记all
public装饰器的职责是把被装饰函数或类的名字追加到其所在模块的全局__all__列表中(decorator.py),从而免去手动维护__all__的重复劳动,也让"对象是公开 API"这一信息在定义处即可见。
其实现要点:
- 对函数:通过
obj.__globals__获取模块命名空间; - 对类:通过
sys.modules[obj.__module__].__dict__获取; - 对其他对象:抛出
TypeError; - 若模块尚无
__all__则创建,否则追加。
源码 docstring 中的 doctest 直观展示了效果:
>>> from sympy.utilities.decorator import public >>> __all__ # NameError: name '__all__' is not defined >>> @public ... def some_function(): ... pass >>> __all__ ['some_function']重要注意事项:在多重装饰器叠加时,@public必须放在最外层(最先应用),因为它依赖指向对象全局命名空间的指针;若先应用其他装饰器,@public可能修改错误的命名空间。这一约束是官方文档明确强调的。
仓库中的典型用法如 sympy/combinatorics/fp_groups.py、sympy/combinatorics/free_groups.py、sympy/concrete/guess.py 以及 sympy/polys/appellseqs.py,都以@public标记对外暴露的函数。
memoize_property:带缓存的属性装饰器
memoize_property是一个属性装饰器,首次求值后把结果缓存在同名带下划线前缀的属性上(decorator.py):
attrname = '_' + propfunc.__name__ sentinel = object() @wraps(propfunc) def accessor(self): val = getattr(self, attrname, sentinel) if val is sentinel: val = propfunc(self) setattr(self, attrname, val) return val return property(accessor)实现通过一个哨兵对象sentinel区分"从未计算"与"缓存值为空"两种情况,确保任何返回值(包括None、False等假值)都能被正确缓存;缓存只挂在实例上,不污染类。测试 test_decorator.py 验证了两次访问返回同一个对象(obj1 is obj2)。
仓库中大规模使用该装饰器的典型位置是 sympy/assumptions/ask.py,其中AskHandler相关类用大量@memoize_property缓存一次计算得到的谓词处理器表——这些属性求值成本较高,而结果在对象生命周期内不变,非常适合缓存。
doctest_depends_on:声明 doctest 的运行依赖
doctest_depends_on为被装饰对象(函数或类)的 docstring 测试声明所需满足的依赖,缺失依赖时自动跳过对应 doctest(decorator.py)。可选参数:
| 参数 | 类型 | 含义 |
|---|---|---|
exe | 可执行文件列表 | doctest 运行所需的外部可执行程序 |
modules | 模块列表 | doctest 运行所需导入的 Python 模块 |
disable_viewers | 查看器列表 | 需要为sympy.printing.preview.preview禁用的查看器 |
python_version | 元组 | 所需的最低 Python 版本,如(3, 0) |
ground_types | 列表 | 所需的 ground types(与多项式域相关) |
实现上,它把依赖信息存入fn._doctest_depends_on,并把跳过判定函数挂到fn.__doctest_skip__;判定逻辑惰性导入sympy.testing.runtests中的SymPyDocTests._check_dependencies,依赖不满足时抛DependencyError从而返回"跳过"。特别地,当装饰对象是类时,依赖元数据用no_attrs_in_subclass包裹,防止被子类"继承"(见下节)。
真实用例:sympy/categories/diagram_drawing.py 中@doctest_depends_on(exe=('latex', 'dvipng'), modules=('pyglet',)),表明该对象的 doctest 需要 LaTeX、dvipng 可执行文件以及 pyglet 模块,缺失时测试框架自动跳过。
no_attrs_in_subclass:阻止属性被子类继承
no_attrs_in_subclass是一个描述符类,用于阻止子类继承基类的某些属性(decorator.py)。用法:
>>> class A(object): ... x = 'test' >>> A.x = no_attrs_in_subclass(A, A.x) >>> class B(A): ... pass >>> hasattr(A, 'x') # True >>> hasattr(B, 'x') # False其__get__逻辑是:只有当owner == self.cls(即访问者正是当初绑定该描述符的类本身)时才返回原属性值,否则抛AttributeError。这样doctest_depends_on装饰类时,依赖元数据不会"泄漏"给子类——每个类都必须显式声明自己的 doctest 依赖,这正是 decorator.py 中用它包裹元数据的原因。
deprecated:标记整函数或整类为废弃
用法与参数
deprecated装饰器用于标记整个函数或类为废弃;若只是部分功能废弃,则应直接在函数体内调用sympy_deprecation_warning()。官方明确说明"该装饰器与在函数开头调用warns_deprecated_sympy()在功能上没有区别,只是便利性封装"。
@deprecated( "The simplify_this(expr) function is deprecated. Use simplify(expr) instead.", deprecated_since_version="1.1", active_deprecations_target='simplify-this-deprecation', ) def simplify_this(expr): ...参数与sympy_deprecation_warning一致:
message:废弃说明,最终会拼入警告全文;deprecated_since_version:自哪个 SymPy 版本起废弃(字符串);active_deprecations_target:指向 doc/src/explanation/active-deprecations.md 中对应条目的锚点名;stacklevel(默认 3):控制警告堆栈层级,指向用户代码调用处。
实现原理:函数与类的不同包装路径
源码 decorator.py 根据被装饰对象是否为类(通过hasattr(wrapped, '__mro__')判断)走两条路径:
- 函数:用
functools.wraps生成包装器,每次调用先触发sympy_deprecation_warning(message, **decorator_kwargs, stacklevel=stacklevel),再调用原函数;并把原函数挂到wrapper._sympy_deprecated_func便于内省; - 类:动态创建
wrapper(wrapped)子类,保留__doc__、__module__、__name__;若原类自定义了__new__则在__new__中触发警告,否则在__init__中触发。这一设计保证了无论实例化路径如何(含__new__返回非本类对象的场景)警告都会发出——测试test_deprecated用__new__返回arg与__new__/__init__混用的类验证了这一点(test_decorator.py)。
配套的警告基础设施
deprecated依赖 sympy/utilities/exceptions.py 中的SymPyDeprecationWarning(DeprecationWarning的子类)。它有两个关键设计:
- 通过
warnings.simplefilter("once", SymPyDeprecationWarning)(exceptions.py)让废弃警告默认可见(普通DeprecationWarning在非交互式场景默认被隐藏); - 警告全文会自动附上"自 SymPy 版本 X 起废弃,将在未来版本移除"与指向 active-deprecations 文档的链接(exceptions.py)。
测试中通过sympy.testing.pytest.warns_deprecated_sympy上下文断言警告确实触发(test_decorator.py)。
仓库内的真实用例
@deprecated在仓库中广泛用于逐步淘汰旧 API,例如 sympy/ntheory/factor_.py 与 sympy/ntheory/generate.py 中对旧版数论函数的标记。若要了解 SymPy 的废弃策略(何时可弃用、如何走完弃用周期、如何移除),可阅读 doc/src/contributing/deprecations.md。
使用建议与注意事项汇总
- 优先
xthreaded还是threaded?若你的函数对Add表达式整体才有数学意义(如(x + y)**2不应展开为x**2 + y**2),必须用xthreaded;只有希望函数严格逐项作用于和式时(如apart之外的逐项变换)才用threaded。参见 partfrac.py 的取舍。 @public的摆放位置:与其他装饰器组合时务必置于最外层,否则可能污染错误模块的__all__。memoize_property的适用前提:只对"首次求值成本高且结果不可变"的属性使用;缓存值存放在实例的_属性名上,注意与手动定义的_前缀属性避免命名冲突。- 废弃策略:只废弃整个函数/类时用
@deprecated,部分废弃时直接在函数体内调用sympy_deprecation_warning();active_deprecations_target必须对应 doc/src/explanation/active-deprecations.md 中真实存在的锚点。 - doctest 依赖声明:当 docstring 示例依赖外部程序或第三方库时,务必用
@doctest_depends_on声明,否则 CI 与本地测试会因环境差异误报失败。
深入阅读
- 模块完整实现:sympy/utilities/decorator.py
- 单元测试与行为契约:sympy/utilities/tests/test_decorator.py
- 废弃警告基础设施:sympy/utilities/exceptions.py
- 废弃策略文档:doc/src/contributing/deprecations.md
- 现行废弃条目清单:doc/src/explanation/active-deprecations.md
- 官方 API 文档入口:doc/src/modules/utilities/decorator.rst
【免费下载链接】sympyA computer algebra system written in pure Python项目地址: https://gitcode.com/GitHub_Trending/sy/sympy
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考