web-dev-qa-db-ja.com

Python)でメソッドのdocstringを継承する

OO階層があり、コード自体と同じくらい多くのメンテナンスが必要なdocstringがあります。例:

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

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

さて、問題はAfricanSwallow.airspeedはスーパークラスメソッドのdocstringを継承しません。テンプレートメソッドパターンを使用してdocstringを保持できることはわかっています。

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

と実装_ask_arthur各サブクラス。しかし、docstringを継承する別の方法があるのではないかと思っていました。おそらく、まだ発見していないデコレータでしょうか。

52
Fred Foo

コピーを行うために、クラスデコレータスタイルで関数を記述します。 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 デコレータはメタデータをある関数から別の関数にコピーします。

22

これは Paul McGuireのDocStringInheritorメタクラス のバリエーションです。

  1. 子メンバーのdocstringが空の場合、親メンバーのdocstringを継承します。
  2. 子クラスのdocstringが空の場合、親クラスのdocstringを継承します。
  3. 通常の属性継承と同様に、基本クラスのMROの任意のクラスからdocstringを継承できます。
  4. クラスデコレータとは異なり、メタクラスは継承されるため、メタクラスを設定する必要があるのは、最上位の基本クラスで1回だけであり、docstringの継承はOOP階層全体で発生します。

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 __== '__main__':
    sys.argv.insert(1, '--verbose')
    unittest.main(argv=sys.argv)
23
unutbu

F.Y.Iは、このトピックに出くわしたばかりの人向けです。Python 3.5、 inspect.getdoc の時点で、継承階層からdocstringを自動的に取得します。

したがって、上記の応答は、Python 2の場合、または親と子のdocstringをマージしてより創造的になりたい場合に役立ちます。

また、いくつかの docstring継承用の軽量ツール を作成しました。これらは、箱から出してすぐにいくつかの素敵なデフォルトのdocstringスタイル(numpy、google、reST)をサポートします。独自のdocstringスタイルも簡単に使用できます

13
Ryan Soklaski

次の適応は、プロパティとミックスインクラスも処理します。また、func.__func__( "instancemethod"の場合)を使用しなければならない状況に遭遇しましたが、他のソリューションがその問題を引き起こさなかった理由が完全にはわかりません。

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
letmaik
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
0
marscher