如何记录从元类继承的方法?

Ale*_*ood 6 python docstring metaclass class-method python-3.x

考虑以下元类/类定义:

class Meta(type):
    """A python metaclass."""
    def greet_user(cls):
        """Print a friendly greeting identifying the class's name."""
        print(f"Hello, I'm the class '{cls.__name__}'!")

    
class UsesMeta(metaclass=Meta):
    """A class that uses `Meta` as its metaclass."""
Run Code Online (Sandbox Code Playgroud)

我们知道,在元类中定义一个方法意味着它被类继承,并且可以被类使用。这意味着交互式控制台中的以下代码可以正常工作:

class Meta(type):
    """A python metaclass."""
    def greet_user(cls):
        """Print a friendly greeting identifying the class's name."""
        print(f"Hello, I'm the class '{cls.__name__}'!")

    
class UsesMeta(metaclass=Meta):
    """A class that uses `Meta` as its metaclass."""
Run Code Online (Sandbox Code Playgroud)

然而,这种方法的一个主要缺点是我们可能包含在方法定义中的任何文档都丢失了。如果我们help(UsesMeta)在交互式控制台中键入,我们会看到没有对方法的引用greet_user,更不用说我们在方法定义中放入的文档字符串了:

>>> UsesMeta.greet_user()
Hello, I'm the class 'UsesMeta'!
Run Code Online (Sandbox Code Playgroud)

现在当然,__doc__类的属性是 writable,因此一种解决方案是重写元类/类定义,如下所示:

from pydoc import render_doc
from functools import cache

def get_documentation(func_or_cls):
    """Get the output printed by the `help` function as a string"""
    return '\n'.join(render_doc(func_or_cls).splitlines()[2:])


class Meta(type):
    """A python metaclass."""

    @classmethod
    @cache
    def _docs(metacls) -> str:
        """Get the documentation for all public methods and properties defined in the metaclass."""

        divider = '\n\n----------------------------------------------\n\n'
        metacls_name = metacls.__name__
        metacls_dict = metacls.__dict__

        methods_header = (
            f'Classmethods inherited from metaclass `{metacls_name}`'
            f'\n\n'
        )

        method_docstrings = '\n\n'.join(
            get_documentation(method)
            for method_name, method in metacls_dict.items()
            if not (method_name.startswith('_') or isinstance(method, property))
        )

        properties_header = (
            f'Classmethod properties inherited from metaclass `{metacls_name}`'
            f'\n\n'
        )

        properties_docstrings = '\n\n'.join(
            f'{property_name}\n{get_documentation(prop)}'
            for property_name, prop in metacls_dict.items()
            if isinstance(prop, property) and not property_name.startswith('_')
        )

        return ''.join((
            divider,
            methods_header,
            method_docstrings,
            divider,
            properties_header,
            properties_docstrings,
            divider
        ))


    def __new__(metacls, cls_name, cls_bases, cls_dict):
        """Make a new class, but tweak `.__doc__` so it includes information about the metaclass's methods."""

        new = super().__new__(metacls, cls_name, cls_bases, cls_dict)
        metacls_docs = metacls._docs()

        if new.__doc__ is None:
            new.__doc__ = metacls_docs
        else:
            new.__doc__ += metacls_docs

        return new

    def greet_user(cls):
        """Print a friendly greeting identifying the class's name."""
        print(f"Hello, I'm the class '{cls.__name__}'!")

    
class UsesMeta(metaclass=Meta):
    """A class that uses `Meta` as its metaclass."""
Run Code Online (Sandbox Code Playgroud)

这“解决”了问题;如果我们现在help(UsesMeta)在交互式控制台中输入,那么继承自的方法Meta现在已完整记录:

Help on class UsesMeta in module __main__:
class UsesMeta(builtins.object)
 |  A class that uses `Meta` as its metaclass.
 |  
 |  Data descriptors defined here:
 |  
 |  __dict__
 |      dictionary for instance variables (if defined)
 |  
 |  __weakref__
 |      list of weak references to the object (if defined)
Run Code Online (Sandbox Code Playgroud)

然而,要实现这个目标需要大量的代码。有没有更好的办法?

标准库是如何做到的?

我也很好奇标准库中某些类的管理方式。如果我们有这样的Enum定义:

from enum import Enum

class FooEnum(Enum):
    BAR = 1
Run Code Online (Sandbox Code Playgroud)

然后,help(FooEnum)在交互式控制台中输入包含以下代码段:

from pydoc import render_doc
from functools import cache

def get_documentation(func_or_cls):
    """Get the output printed by the `help` function as a string"""
    return '\n'.join(render_doc(func_or_cls).splitlines()[2:])


class Meta(type):
    """A python metaclass."""

    @classmethod
    @cache
    def _docs(metacls) -> str:
        """Get the documentation for all public methods and properties defined in the metaclass."""

        divider = '\n\n----------------------------------------------\n\n'
        metacls_name = metacls.__name__
        metacls_dict = metacls.__dict__

        methods_header = (
            f'Classmethods inherited from metaclass `{metacls_name}`'
            f'\n\n'
        )

        method_docstrings = '\n\n'.join(
            get_documentation(method)
            for method_name, method in metacls_dict.items()
            if not (method_name.startswith('_') or isinstance(method, property))
        )

        properties_header = (
            f'Classmethod properties inherited from metaclass `{metacls_name}`'
            f'\n\n'
        )

        properties_docstrings = '\n\n'.join(
            f'{property_name}\n{get_documentation(prop)}'
            for property_name, prop in metacls_dict.items()
            if isinstance(prop, property) and not property_name.startswith('_')
        )

        return ''.join((
            divider,
            methods_header,
            method_docstrings,
            divider,
            properties_header,
            properties_docstrings,
            divider
        ))


    def __new__(metacls, cls_name, cls_bases, cls_dict):
        """Make a new class, but tweak `.__doc__` so it includes information about the metaclass's methods."""

        new = super().__new__(metacls, cls_name, cls_bases, cls_dict)
        metacls_docs = metacls._docs()

        if new.__doc__ is None:
            new.__doc__ = metacls_docs
        else:
            new.__doc__ += metacls_docs

        return new

    def greet_user(cls):
        """Print a friendly greeting identifying the class's name."""
        print(f"Hello, I'm the class '{cls.__name__}'!")

    
class UsesMeta(metaclass=Meta):
    """A class that uses `Meta` as its metaclass."""
Run Code Online (Sandbox Code Playgroud)

enum模块究竟是如何实现这一点的?

我在这里使用元类的原因,而不是仅仅classmethod在类定义的主体中定义s

您可能在元类中编写的某些方法(例如__iter____getitem____len__不能编写classmethods,但如果您在元类中定义它们,则可能会产生极具表现力的代码。该enum模块就是一个很好的例子

ekh*_*oro 5

help()函数依赖于dir(),目前并不总是给出一致的结果。这就是您的方法在生成的交互式文档中丢失的原因。在这个主题上有一个开放的 python 问题,它更详细地解释了这个问题:请参阅错误 40098(尤其是第一个要点)。

同时,解决方法是__dir__在元类中定义自定义:

class Meta(type):
    """A python metaclass."""
    def greet_user(cls):
        """Print a friendly greeting identifying the class's name."""
        print(f"Hello, I'm the class '{cls.__name__}'!")

    def __dir__(cls):
        return super().__dir__() + [k for k in type(cls).__dict__ if not k.startswith('_')]

class UsesMeta(metaclass=Meta):
    """A class that uses `Meta` as its metaclass."""
Run Code Online (Sandbox Code Playgroud)

它产生:

Help on class UsesMeta in module __main__:

class UsesMeta(builtins.object)
 |  A class that uses `Meta` as its metaclass.
 |
 |  Methods inherited from Meta:
 |
 |  greet_user() from __main__.Meta
 |      Print a friendly greeting identifying the class's name.
Run Code Online (Sandbox Code Playgroud)

这基本上就是enum所做的 - 尽管它的实现显然比我的要复杂一些!(该模块是用python编写的,因此有关更多详细信息,只需在源代码中搜索“__dir__”即可)。