python ≥3.9 中的只读类属性,支持 `help()`

Posted

技术标签:

【中文标题】python ≥3.9 中的只读类属性,支持 `help()`【英文标题】:Read-only class properties in python ≥3.9 with `help()` support 【发布时间】:2021-11-24 07:23:04 【问题描述】:

从 python 3.9 开始,支持堆叠 @property@classmethod 装饰器。但是,我很难创建一个显示在helpReadonly properties 部分下的类属性。 [1],[2],[3],[4],[5] 中提出的解决方案没有解决我的问题。考虑:

from time import sleep
from abc import ABC, ABCMeta, abstractmethod

def compute(obj, s):
    print(f"Computing s of obj ...", end="")
    sleep(3)
    print("DONE!")
    return "Phew, that was a lot of work!"


class MyMetaClass(ABCMeta):
    @property
    def expensive_metaclass_property(cls):
        """This may take a while to compute!""" 
        return compute(cls, "metaclass property")

    
class MyBaseClass(ABC, metaclass=MyMetaClass):
    @classmethod
    @property
    def expensive_class_property(cls):
        """This may take a while to compute!"""
        return compute(cls, "class property")
    
    @property
    def expensive_instance_property(self):
        """This may take a while to compute!"""   
        return compute(self, "instance property")

class MyClass(MyBaseClass):
    """Some subclass of MyBaseClass"""
    
help(MyClass)

问题是调用help(MyBaseClass) 会多次执行expensive_class_property。这导致过去的文档出现问题,例如 sphinx 最终也会执行属性代码。

使用元类可以避免这个问题,但缺点是expensive_metaclass_property 既不会出现在dir(MyClass) 中,也不会出现在help(MyClass) 中,也不会出现在MyClass 的文档中。如何获得在 help(MyClass)Readonly properties 部分下显示的类属性?

调用help(MyClass)时会发生以下情况:

Computing class property of <class '__main__.MyClass'> ...DONE!
Computing class property of <class '__main__.MyClass'> ...DONE!
Computing class property of <class '__main__.MyClass'> ...DONE!
Computing class property of <class '__main__.MyBaseClass'> ...DONE!
Computing class property of <class '__main__.MyClass'> ...DONE!

Help on class MyClass in module __main__:

class MyClass(MyBaseClass)
 |  Some subclass of MyBaseClass
 |  
 |  Method resolution order:
 |      MyClass
 |      MyBaseClass
 |      abc.ABC
 |      builtins.object
 |  
 |  Data and other attributes defined here:
 |  
 |  __abstractmethods__ = frozenset()
 |  
 |  ----------------------------------------------------------------------
 |  Class methods inherited from MyBaseClass:
 |  
 |  expensive_class_property = 'Phew, that was a lot of work!'
 |  ----------------------------------------------------------------------
 |  Readonly properties inherited from MyBaseClass:
 |  
 |  expensive_instance_property
 |      This may take a while to compute!
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors inherited from MyBaseClass:
 |  
 |  __dict__
 |      dictionary for instance variables (if defined)
 |  
 |  __weakref__
 |      list of weak references to the object (if defined)

【问题讨论】:

是否可以缓存计算结果?还是必须在每次调用时重新计算? @wim 我稍后已经将它与缓存装饰器结合使用。但是,这意味着当它应该执行 0 次时,它仍然至少执行一次,就像 expensive_instance_property 一样。 好的,所以主要问题实际上并不是多次调用,而是自动文档调用了属性代码? 来自 OP 的错误报告:bugs.python.org/issue45356 【参考方案1】:

使用调用堆栈,您可以检测该属性是否被 pydoc 调用并在这种情况下短路:

import inspect

class A:
    @classmethod
    @property
    def expensive_class_property(cls):
        """This may take a while to compute!"""
        fnames = [f.filename for f in inspect.getouterframes(inspect.currentframe())]
        if any(fname.endswith("pydoc.py") for fname in fnames):
            return "Text returned for autodoc"
        print("computing class property")
        return "Phew, that was a lot of work!"

此帮助将如下所示:

Help on class A in module __main__:

class A(builtins.object)
 |  Class methods defined here:
 |  
 |  expensive_class_property = 'Text returned for autodoc'
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |  
 |  __dict__
 |      dictionary for instance variables (if defined)
 |  
 |  __weakref__
 |      list of weak references to the object (if defined)

但正常访问会如预期:

>>> A.expensive_class_property
computing class property
'Phew, that was a lot of work!'

它不会阻止help(A) 多次调用描述符,但如果不直接修改pydoc.py,您可能无法阻止这种情况。只要短路很快,那么您可能无论如何都不需要防止它。

这种方法应该很容易扩展以检测 Sphinx 的调用。如果您不喜欢将短路代码直接添加到函数体中,则可以将此想法分解为装饰器。

【讨论】:

以上是关于python ≥3.9 中的只读类属性,支持 `help()`的主要内容,如果未能解决你的问题,请参考以下文章

python代码如何判断windows文件是不是为只读?

在 Python 中将类实例属性设为只读

如何在 Python 中创建只读类属性? [复制]

Objective-C:(私有/公共属性)为外部类调用设置只读属性,为自调用设置只读属性

Python、__slots__、继承和类变量 ==> 属性是只读的 bug

Python面向对象编程第19篇 只读属性