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

Posted

技术标签:

【中文标题】如何记录从元类继承的方法?【英文标题】:How can I document methods inherited from a metaclass? 【发布时间】:2021-10-23 05:45:22 【问题描述】:

考虑以下元类/类定义:

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

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

>>> 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)

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

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\nget_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)

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

标准库是如何做到的?

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

from enum import Enum

class FooEnum(Enum):
    BAR = 1

然后,在交互式控制台中输入help(FooEnum) 包括这个sn-p:

 |  ----------------------------------------------------------------------
 |  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 模块究竟是如何实现这一点的?

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

您可能会在元类中编写一些方法,例如 __iter____getitem____len__can't be written 和 classmethods,但如果您在元类中定义它们,可能会产生极具表现力的代码. enum 模块是这个的 excellent example。

【问题讨论】:

【参考方案1】:

help() 函数依赖于 dir(),它目前并不总是给出一致的结果。这就是您的方法在生成的交互式文档中丢失的原因。关于这个主题有一个开放的 python 问题,它更详细地解释了这个问题:参见bugs 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."""

产生:

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 所做的——尽管它的实现显然比我的要复杂一点! (该模块是用python编写的,所以更多详细信息,只需在source code中搜索“__dir__”即可。

【讨论】:

一个近乎完美的答案——唯一的问题是通过以这种方式覆盖__dir__help 的输出会丢失有关UsesMeta 中定义的方法的任何信息——它现在 only 有关于在元类中实现的方法的信息!解决方案是将其作为__dir__ 定义:return super().__dir__() + [k for k in type(cls).__dict__ if not k.startswith('_')] 好的 - 我说它并不复杂!我将根据您的建议更新我的示例。 这不是批评——只是对我如何根据我的用例进行调整提供反馈!非常感谢您提供有用且内容丰富的答案。【参考方案2】:

我还没有查看 stdlib 的其余部分,但 EnumMeta 通过覆盖 __dir__ 方法(即在 EnumMeta 类中指定它)实现了这一点:

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

【讨论】:

完美运行——非常感谢!我不知道help 依赖于__dir__ 的实现。

以上是关于如何记录从元类继承的方法?的主要内容,如果未能解决你的问题,请参考以下文章

从元类访问构造函数的参数

从元类设置实例变量

元类冲突、多重继承、实例为父

避免使用元类继承生成的类属性

涉及 Enum 的多重继承元类冲突

为啥元类不能访问由元类定义的类的子类继承的属性?