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

时间:2011-11-11 21:26:08

标签: python oop inheritance docstring template-method-pattern

我有一个带有文档字符串的OO层次结构,它需要与代码本身一样多的维护。如,

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

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

现在,问题是AfricanSwallow.airspeed没有继承超类方法的docstring。我知道我可以使用模板方法模式保留文档字符串,即

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

并在每个子类中实现_ask_arthur。但是,我想知道是否有另一种方法可以继承文档字符串,也许还有一些我尚未发现的装饰器?

5 个答案:

答案 0 :(得分:23)

这是Paul McGuire's DocStringInheritor metaclass的变体。

  1. 如果子成员的话,它会继承父成员的docstring docstring是空的。
  2. 如果子类docstring是,则它继承父类docstring 空。
  3. 它可以继承任何类中的docstring 任何基类的MRO,就像常规属性继承一样。
  4. 与类装饰器不同,元类是继承的,因此您只需要在某个顶级基类中设置一次元类,并在整个OOP层次结构中进行docstring继承。

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

答案 1 :(得分:22)

在类装饰器样式中编写一个函数来为您进行复制。在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

这是一种Pythonic技术,与标准库中现有工具的设计完全匹配。例如,functools.total_ordering类装饰器向类添加缺少的丰富比较方法。另一个例子是,functools.wraps装饰器将元数据从一个函数复制到另一个函数。

答案 2 :(得分:12)

F.Y.I刚才有人关注这个话题:从Python 3.5开始,inspect.getdoc会自动从继承层次结构中检索文档字符串。

上面的回答因此对Python 2很有用,或者如果你想在合并父母和孩子的文档字符串方面更有创意。

我还创建了一些lightweight tools for docstring inheritance。这些支持一些不错的默认文档字符串样式(numpy,google,reST)开箱即用。您也可以轻松使用自己的文档字符串样式

答案 3 :(得分:4)

以下改编还处理属性和mixin类。我也遇到了一种情况,我必须使用func.__func__(对于" instancemethod" s),但我不完全确定为什么其他解决方案没有解决这个问题

def inherit_docs(cls):
    for name in dir(cls):
        func = getattr(cls, name)
        if func.__doc__: 
            continue
        for parent in cls.mro()[1:]:
            if not hasattr(parent, name):
                continue
            doc = getattr(parent, name).__doc__
            if not doc: 
                continue
            try:
                # __doc__'s of properties are read-only.
                # The work-around below wraps the property into a new property.
                if isinstance(func, property):
                    # We don't want to introduce new properties, therefore check
                    # if cls owns it or search where it's coming from.
                    # With that approach (using dir(cls) instead of var(cls))
                    # we also handle the mix-in class case.
                    wrapped = property(func.fget, func.fset, func.fdel, doc)
                    clss = filter(lambda c: name in vars(c).keys() and not getattr(c, name).__doc__, cls.mro())
                    setattr(clss[0], name, wrapped)
                else:
                    try:
                        func = func.__func__ # for instancemethod's
                    except:
                        pass
                    func.__doc__ = doc
            except: # some __doc__'s are not writable
                pass
            break
    return cls

答案 4 :(得分:0)

def fix_docs(cls):
    """ copies docstrings of derived attributes (methods, properties, attrs) from parent classes."""
    public_undocumented_members = {name: func for name, func in vars(cls).items()
                                   if not name.startswith('_') and not func.__doc__}

    for name, func in public_undocumented_members.iteritems():
        for parent in cls.mro()[1:]:
            parfunc = getattr(parent, name, None)
            if parfunc and getattr(parfunc, '__doc__', None):
                if isinstance(func, property):
                    # copy property, since its doc attribute is read-only
                    new_prop = property(fget=func.fget, fset=func.fset,
                                        fdel=func.fdel, doc=parfunc.__doc__)
                    cls.func = new_prop
                else:
                    func.__doc__ = parfunc.__doc__
                break
    return cls