我正在编写一个用于与数据集交互的包,并且代码看起来像这样
from abc import ABC, ABCMeta, abstractmethod
from functools import cache
from pathlib import Path
from warnings import warn
class DatasetMetaClass(ABCMeta):
r"""Meta Class for Datasets"""
@property
@cache
def metaclass_property(cls):
r"""Compute an expensive property (for example: dataset statistics)."""
warn("Caching metaclass property...")
return "result"
# def __dir__(cls):
# return list(super().__dir__()) + ['metaclass_property']
class DatasetBaseClass(metaclass=DatasetMetaClass):
r"""Base Class for datasets that all datasets must subclass"""
@classmethod
@property
@cache
def baseclass_property(cls):
r"""Compute an expensive property (for example: dataset statistics)."""
warn("Caching baseclass property...")
return "result"
class DatasetExampleClass(DatasetBaseClass, metaclass=DatasetMetaClass):
r"""Some Dataset Example."""
现在的问题是,在
make html
期间,sphinx实际上执行了baseclass_property
,这是一个非常昂贵的操作。 (除其他事项外:检查数据集是否本地存在,如果不存在,则下载它,预处理它,计算数据集统计数据,修剪草坪并倒垃圾。)
我注意到,如果我将其设为 MetaClass 属性,则不会发生这种情况,因为 meta-class 属性不会出现在类
__dir__
call 中,这可能是也可能不是一个错误。通过取消注释这两行来手动将其添加到 __dir__
会使 sphinx 也处理元类属性。
问题:
@properties
通常处理得很好,似乎无意中它会因为 @classmethod@property
而中断。# noqa
、# type: ignore
、# pylint disable=
等的注释或通过某种 @nodoc
装饰器来禁用 sphinx 的功能。一切都按其应有的方式运行,Sphinx 中没有“bug”,ABC 机器中也没有“bug”,语言中更不用说。
Sphinx 使用语言自省功能来检索类的成员,然后内省方法。当你组合 @classmethod 和 @property 时会发生什么,除了它实际上起作用之外,当这样创建的类成员被 Sphynx 访问时,就像它在搜索文档字符串时必须做的那样,代码被触发并运行。
如果属性和类方法实际上不能组合使用,那其实并不奇怪,因为
property
和 classmethod
装饰器都使用描述符协议来创建一个新对象,并为其实现的功能提供适当的方法。 (更新:property
和classmethod
的交互实际上足够复杂,从Python 3.12开始不再支持这种模式)
我认为不太令人惊讶的事情是在“类方法属性缓存”函数中放置一些显式保护,以便在 sphinx 处理文件时不运行。由于 sphinx 本身没有此功能,因此您可以使用环境变量来实现此功能,例如
GENERATING_DOCS
。 (这不存在,它可以是任何名称),然后在你的方法中设置一个守卫,例如:
...
def baseclass_property(self):
if os.environ.get("GENERATING_DOCS", False):
return
然后您可以在运行脚本之前手动设置此变量,或者将其设置在 Sphinx 的
conf.py
文件本身中。
如果你有几个这样的方法,并且不想在所有这些方法中编写保护代码,你可以做一个装饰器,同时,只需使用相同的装饰器一次应用其他 3 个装饰器:
from functools import cache, wraps
import os
def cachedclassproperty(func):
@wraps(func)
def wrapper(*args, **kwargs):
if os.environ.get("GENERATING_DOCS", False):
return
return func(*args, **kwargs)
return classmethod(property(cache(wrapper)))
现在,至于使用元类上的属性:我建议不要这样做。元类适用于您确实需要自定义类创建过程的情况,并且元类上的
property
几乎偶然也可以用作类属性。正如您所调查的,在这种情况下发生的所有事情都是该属性将从类中隐藏,因此不会受到 Sphinx 内省的影响 - 但即使您将元类用于其他目的,如果您只是按照我的建议添加一个防护,甚至可能not阻止sphinx正确记录类属性(如果它有文档字符串)。如果你向 Sphinx 隐藏它,它显然不会被记录下来。