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

How can I document methods inherited from a metaclass?

考虑以下 metaclass/class 定义:

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."""

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

>>> UsesMeta.greet_user()
Hello, I'm the class 'UsesMeta'!

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

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)

现在当然是 class is writable__doc__ 属性,所以一种解决方案是像这样重写 metaclass/class 定义:

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."""

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

Help on class UsesMeta in module __main__:
class UsesMeta(builtins.object)
 |  A class that uses `Meta` as its metaclass.
 |  
 |  ----------------------------------------------
 |  
 |  Classmethods inherited from metaclass `Meta`
 |  
 |  greet_user(cls)
 |      Print a friendly greeting identifying the class's name.
 |  
 |  ----------------------------------------------
 |  
 |  Classmethod properties inherited from metaclass `Meta`
 |  
 |  
 |  
 |  ----------------------------------------------
 |  
 |  Data descriptors defined here:
 |  
 |  __dict__
 |      dictionary for instance variables (if defined)
 |  
 |  __weakref__
 |      list of weak references to the object (if defined)

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

标准库是怎么做到的?

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

from enum import Enum

class FooEnum(Enum):
    BAR = 1

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

 |  ----------------------------------------------------------------------
 |  Readonly properties inherited from enum.EnumMeta:
 |  
 |  __members__
 |      Returns a mapping of member name->value.
 |      
 |      This mapping lists all enum members, including aliases. Note that this
 |      is a read-only view of the internal mapping.

enum 模块究竟是如何做到这一点的?

我在这里使用 metaclasses 的原因,而不是仅仅在 class 定义 [=62] 的正文中定义 classmethods =]

一些你可能会写在metaclass中的方法,比如__iter____getitem__或者__len__ as classmethods, but can lead to extremely expressive code if you define them in a metaclass. The enum module is an excellent example这样的。

我没有查看 stdlib 的其余部分,但是 EnumMeta 通过覆盖 __dir__ 方法(即在 EnumMeta class ):

class EnumMeta(type):
    .
    .
    .
    def __dir__(self):
        return (
                ['__class__', '__doc__', '__members__', '__module__']
                + self._member_names_
                )

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

与此同时,解决方法是在元class:

中定义自定义 __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."""

产生:

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.

本质上就是enum does - although its implementation is obviously a little more sophisticated than mine! (The module is written in python, so for more details, just search for "__dir__" in the source code).