8

这个问题似乎经常出现在 StackOverflow 和其他地方,但我无法在任何地方找到完全令人满意的解决方案。

似乎有两种常见的解决方案。第一个(来自例如http://article.gmane.org/gmane.comp.python.general/630549)使用函数装饰器:

class SuperClass:
    def my_method(self):
        '''Has a docstring'''
        pass

class MyClass(SuperClass):
    @copy_docstring_from(SuperClass)
    def my_method(self):
        pass

assert SuperClass.my_method.__doc__ == MyClass.my_method._doc__

这可能是最直接的方法,但它需要至少重复一次父类名称,如果在直接祖先中找不到文档字符串,也会变得更加复杂。

第二种方法使用元类或类装饰器(参见在 Python中继承方法的文档字符串,将父类文档字符串继承为 __doc__ 属性, http://mail.python.org/pipermail/python-list/2011-June/606043 。 html ) 并且看起来像这样:

class MyClass1(SuperClass, metaclass=MagicHappeningHere):
    def method(self):
        pass

# or 

@frobnicate_docstrings
class MyClass2(SuperClass):
    def method(self):
        pass

assert SuperClass.my_method.__doc__ == MyClass1.my_method._doc__
assert SuperClass.my_method.__doc__ == MyClass2.my_method._doc__

但是,使用这种方法,文档字符串仅在类创建之后设置,因此装饰器无法访问,因此以下内容将不起作用:

def log_docstring(fn):
    print('docstring for %s is %s' % (fn.__name__, fn.__doc__)
    return fn

class MyClass(SuperClass, metaclass=MagicHappeningHere):
# or
#@frobnicate_docstrings
#class MyClass2(SuperClass): 
    @log_docstring
    def method(self):
        pass

在 Python 类继承中的 Inherit docstrings 中讨论了第三个有趣的想法。在这里,函数装饰器实际上包装了方法并将其转换为方法描述符,而不仅仅是更新其文档字符串。然而,这似乎是使用大锤来破解一个坚果,因为它把方法变成了一个方法描述符(这可能也有性能影响,虽然我没有检查),并且也不使文档字符串可用于任何其他装饰器(和在上面的例子中实际上会使它们崩溃,因为方法描述符没有__name__属性)。

是否有一种解决方案可以避免上述所有缺点,即不需要我重复自己并使用装饰器立即分配文档字符串?

我对 Python 3 的解决方案感兴趣。

4

3 回答 3

4

改用类装饰器:

@inherit_docstrings
class MyClass(SuperClass):
    def method(self):
        pass

其中inherit_docstrings()定义为:

from inspect import getmembers, isfunction

def inherit_docstrings(cls):
    for name, func in getmembers(cls, isfunction):
        if func.__doc__: continue
        for parent in cls.__mro__[1:]:
            if hasattr(parent, name):
                func.__doc__ = getattr(parent, name).__doc__
    return cls

演示:

>>> class SuperClass:
...     def method(self):
...         '''Has a docstring'''
...         pass
... 
>>> @inherit_docstrings
... class MyClass(SuperClass):
...     def method(self):
...         pass
... 
>>> MyClass.method.__doc__
'Has a docstring'

这会在定义整个类设置文档字符串,而无需先创建实例。

如果您需要方法装饰器可用的文档字符串,不幸的是,您完全被复制父类的装饰器所困扰。

这样做的原因是,在定义类主体时,您无法自省超类将是什么。类定义期间的本地命名空间无权访问传递给类工厂的参数。

可以使用元类将基类添加到本地命名空间,然后使用装饰器再次将其拉出,但在我看来,这会变得丑陋、快速:

import sys

class InheritDocstringMeta(type):
    _key = '__InheritDocstringMeta_bases'

    def __prepare__(name, bases, **kw):
        return {InheritDocstringMeta._key: bases}

    def __call__(self, name, bases, namespace, **kw):
        namespace.pop(self._key, None)

def inherit_docstring(func):
    bases = sys._getframe(1).f_locals.get(InheritDocstringMeta._key, ())
    for base in bases:
        for parent in base.mro():
            if hasattr(parent, func.__name__):
                func.__doc__ = getattr(parent, func.__name__).__doc__
    return func

演示用法:

>>> class MyClass(SuperClass, metaclass=InheritDocstringMeta):
...     @inherit_docstring
...     def method(self):
...         pass
... 
>>> MyClass.method.__doc__
'Has a docstring'
于 2013-06-30T17:59:54.490 回答
4

从 Python 3.5 开始,inspect.getdoc在继承树中搜索文档字符串。因此,如果您将子级的文档字符串留空,它将从父级检索它。这避免了代码重复的需要,像 sphinx 这样的自动代码生成器会做正确的事情。

$ cat mwe.py
import inspect

class A:
    def foo(self):
        """Fool!"""
        return 42

class B(A):
    def foo(self):
        return super().foo()

print(A.foo.__doc__, B.foo.__doc__, A().foo.__doc__, B().foo.__doc__,
      inspect.getdoc(A.foo), inspect.getdoc(B.foo),
      inspect.getdoc(A().foo), inspect.getdoc(B().foo))
$ python mwe.py
Fool! None Fool! None Fool! Fool! Fool! Fool!
于 2018-12-19T20:29:03.403 回答
1

我认为__prepare__可以通过注入一个知道类层次结构的装饰器来使用元类的方法:

def log_docstring(fn):
    print('docstring for %r is %r' % (fn, fn.__doc__))
    return fn

class InheritableDocstrings(type):
    def __prepare__(name, bases):
        classdict = dict()

        # Construct temporary dummy class to figure out MRO
        mro = type('K', bases, {}).__mro__[1:]
        assert mro[-1] == object
        mro = mro[:-1]

        def inherit_docstring(fn):
            if fn.__doc__ is not None:
                raise RuntimeError('Function already has docstring')

            # Search for docstring in superclass
            for cls in mro:
                super_fn = getattr(cls, fn.__name__, None)
                if super_fn is None:
                    continue
                fn.__doc__ = super_fn.__doc__
                break
            else:
                raise RuntimeError("Can't inherit docstring for %s: method does not "
                                   "exist in superclass" % fn.__name__)

            return fn

        classdict['inherit_docstring'] = inherit_docstring
        return classdict

class Animal():
    def move_to(self, dest):
        '''Move to *dest*'''
        pass

class Bird(Animal, metaclass=InheritableDocstrings):
    @log_docstring
    @inherit_docstring
    def move_to(self, dest):
        self._fly_to(dest)

assert Animal.move_to.__doc__ == Bird.move_to.__doc__

印刷:

docstring for <function Bird.move_to at 0x7f6286b9a200> is 'Move to *dest*'

当然,这种方法还有一些其他问题: - 一些分析工具(例如 pyflakes)会抱怨使用(显然)未定义的inherit_docstring名称 - 如果父类已经有不同的元类(例如 ),它就不起作用ABCMeta

于 2013-06-30T18:34:04.303 回答