2013-06-30 6 views
5

この質問は、StackOverflowやその他の場所で定期的に出てくるようですが、どこでも完全に満足のいく解決策を見つけることはできませんでした。Python 3 - デコレータを破損することなく、またはDRYに違反することなくdocstring継承する方法

一般的な解決策には2つのタイプがあるようです。 (例えばhttp://article.gmane.org/gmane.comp.python.general/630549から)最初の1は、関数デコレータを使用しています。

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__ 

これはおそらく最も簡単な方法ですが、それは少なくとも一度は親クラス名を繰り返す必要があり、また、ドキュメンテーション文字列なら、もっと多くの複雑になります直接祖先には見つからない。

第二のアプローチは、メタクラスやクラスのデコレータ(参照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__ 

しかし、この方法でドキュメント文字列が唯一のクラスの作成後に設定され、したがって、されていませんデコレータにアクセス可能な、次のように動作しませんので:

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で議論されてきました。ここでは、関数デコレータは実際にはそのメソッドをラップして、単にそのdo​​cstringを更新するのではなく、メソッド記述子に変換します。しかし、これはsledgehammerを使ってナットをクラックさせるようなものですが、メソッドをメソッド記述子に変換します(これはパフォーマンス上の影響もありますが、チェックしませんでした)。また、docstringを他のデコレータで使用できるようにしません上記の例では、メソッド記述子に__name__属性がないため、実際にはクラッシュします。

上記の欠点をすべて回避するソリューションがありますか?自分自身を繰り返す必要がなく、デコレータを使用してドキュメントストリングをすぐに割り当てる必要はありませんか?

私は、Python 3

答えて

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)を持っている場合は動作しません。

+0

私はこれと少しの巧妙な実装をhttp://code.activestate.com/recipes/578587-inherit-method-docstrings-with-breaking-decorat/に掲載しました。 – Nikratio

2

のためのソリューションに興味があるではなく、クラスのデコレータを使用します。

inherit_docstrings()は次のように定義されて
@inherit_docstrings 
class MyClass(SuperClass): 
    def method(self): 
     pass 

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 を設定します。

メソッドデコレータで使用できる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' 
+0

ローカルネームスペースは、デフォルトではクラスファクトリ引数にアクセスできません。しかし、メタクラスを使ってそれを変えることはできませんでしたか? – Nikratio

+0

Python 2の場合、 'isfunction'は 'ismethod'にする必要があります。 – spookylukey

+0

@spookylukey:はい、Python 2では、クラスのメンバーはバインドされていないメソッドです。 –

関連する問題