在Python中继承方法的文档字符串

60 投票
6 回答
11429 浏览
提问于 2025-04-17 06:08

我有一个面向对象的层次结构,里面的文档字符串维护起来和代码一样麻烦。例如:

class Swallow(object):
    def airspeed(self):
        """Returns the airspeed (unladen)"""
        raise NotImplementedError

class AfricanSwallow(Swallow):
    def airspeed(self):
        # whatever

现在,问题是 AfricanSwallow.airspeed 这个方法没有继承父类的方法的文档字符串。我知道我可以使用模板方法模式来保留文档字符串,也就是:

class Swallow(object):
    def airspeed(self):
        """Returns the airspeed (unladen)"""
        return self._ask_arthur()

然后在每个子类中实现 _ask_arthur。不过,我在想有没有其他方法可以让文档字符串被继承,或者说有没有什么装饰器是我还没发现的?

6 个回答

19

给刚接触这个话题的人一个小提示:从Python 3.5开始,inspect.getdoc这个功能可以自动从继承关系中获取文档字符串。

所以,上面的回答对于Python 2来说很有用,或者如果你想更灵活地合并父类和子类的文档字符串。

我还创建了一些轻量级的文档字符串继承工具。这些工具默认支持一些不错的文档字符串样式(比如numpy、google、reST),而且你也可以轻松使用自己的文档字符串样式。

24

这是对Paul McGuire的DocStringInheritor元类的一种变体。

  1. 如果子成员的文档字符串是空的,它会继承父成员的文档字符串。
  2. 如果子类的文档字符串是空的,它会继承父类的文档字符串。
  3. 它可以从任何基类的MRO(方法解析顺序)中的任何类继承文档字符串,就像普通的属性继承一样。
  4. 与类装饰器不同,元类是可以被继承的,所以你只需在某个顶层基类中设置一次元类,文档字符串的继承就会在整个面向对象的层次结构中发生。

import unittest
import sys

class DocStringInheritor(type):
    """
    A variation on
    http://groups.google.com/group/comp.lang.python/msg/26f7b4fcb4d66c95
    by Paul McGuire
    """
    def __new__(meta, name, bases, clsdict):
        if not('__doc__' in clsdict and clsdict['__doc__']):
            for mro_cls in (mro_cls for base in bases for mro_cls in base.mro()):
                doc=mro_cls.__doc__
                if doc:
                    clsdict['__doc__']=doc
                    break
        for attr, attribute in clsdict.items():
            if not attribute.__doc__:
                for mro_cls in (mro_cls for base in bases for mro_cls in base.mro()
                                if hasattr(mro_cls, attr)):
                    doc=getattr(getattr(mro_cls,attr),'__doc__')
                    if doc:
                        if isinstance(attribute, property):
                            clsdict[attr] = property(attribute.fget, attribute.fset, 
                                                     attribute.fdel, doc)
                        else:
                            attribute.__doc__ = doc
                        break
        return type.__new__(meta, name, bases, clsdict)



class Test(unittest.TestCase):

    def test_null(self):
        class Foo(object):

            def frobnicate(self): pass

        class Bar(Foo, metaclass=DocStringInheritor):
            pass

        self.assertEqual(Bar.__doc__, object.__doc__)
        self.assertEqual(Bar().__doc__, object.__doc__)
        self.assertEqual(Bar.frobnicate.__doc__, None)

    def test_inherit_from_parent(self):
        class Foo(object):
            'Foo'

            def frobnicate(self):
                'Frobnicate this gonk.'
        class Bar(Foo, metaclass=DocStringInheritor):
            pass
        self.assertEqual(Foo.__doc__, 'Foo')
        self.assertEqual(Foo().__doc__, 'Foo')
        self.assertEqual(Bar.__doc__, 'Foo')
        self.assertEqual(Bar().__doc__, 'Foo')
        self.assertEqual(Bar.frobnicate.__doc__, 'Frobnicate this gonk.')

    def test_inherit_from_mro(self):
        class Foo(object):
            'Foo'

            def frobnicate(self):
                'Frobnicate this gonk.'
        class Bar(Foo):
            pass

        class Baz(Bar, metaclass=DocStringInheritor):
            pass

        self.assertEqual(Baz.__doc__, 'Foo')
        self.assertEqual(Baz().__doc__, 'Foo')
        self.assertEqual(Baz.frobnicate.__doc__, 'Frobnicate this gonk.')

    def test_inherit_metaclass_(self):
        class Foo(object):
            'Foo'

            def frobnicate(self):
                'Frobnicate this gonk.'
        class Bar(Foo, metaclass=DocStringInheritor):
            pass

        class Baz(Bar):
            pass
        self.assertEqual(Baz.__doc__, 'Foo')
        self.assertEqual(Baz().__doc__, 'Foo')
        self.assertEqual(Baz.frobnicate.__doc__, 'Frobnicate this gonk.')

    def test_property(self):
        class Foo(object):
            @property
            def frobnicate(self): 
                'Frobnicate this gonk.'
        class Bar(Foo, metaclass=DocStringInheritor):
            @property
            def frobnicate(self): pass

        self.assertEqual(Bar.frobnicate.__doc__, 'Frobnicate this gonk.')


if __name__ == '__main__':
    sys.argv.insert(1, '--verbose')
    unittest.main(argv=sys.argv)
23

写一个函数,用类装饰器的方式来帮你复制东西。在Python2.5中,你可以在类创建后直接使用它。在后来的版本中,你可以用@decorator的写法来使用。

下面是一个初步的实现方式:

import types

def fix_docs(cls):
    for name, func in vars(cls).items():
        if isinstance(func, types.FunctionType) and not func.__doc__:
            print func, 'needs doc'
            for parent in cls.__bases__:
                parfunc = getattr(parent, name, None)
                if parfunc and getattr(parfunc, '__doc__', None):
                    func.__doc__ = parfunc.__doc__
                    break
    return cls


class Animal(object):
    def walk(self):
        'Walk like a duck'

class Dog(Animal):
    def walk(self):
        pass

Dog = fix_docs(Dog)
print Dog.walk.__doc__

在更新的Python版本中,最后的部分变得更加简单和优雅:

@fix_docs
class Dog(Animal):
    def walk(self):
        pass

这是一种符合Python风格的技巧,正好与标准库中现有工具的设计相匹配。例如,functools.total_ordering这个类装饰器可以为类添加缺失的丰富比较方法。再比如,functools.wraps这个装饰器可以把一个函数的元数据复制到另一个函数上。

撰写回答