Python 3 - 方法docstring继承,不破坏装饰器或违反DRY

时间:2013-06-30 17:51:16

标签: python python-3.x inheritance docstring

这个问题似乎在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__

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

第二种方法使用元类或类装饰器(参见Inheriting methods' docstrings in PythonInherit a parent class docstring as __doc__ attributehttp://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__

但是,使用这种方法,docstring只在类创建后设置,因此装饰器无法访问,因此以下内容不起作用:

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

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

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

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

3 个答案:

答案 0 :(得分:2)

改为使用类装饰器:

@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'

这会在定义整个类之后设置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'

答案 1 :(得分: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),它就不起作用。

答案 2 :(得分:1)

从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!