2013-06-30 17 views
5

這個問題似乎定期出現在StackOverflow和其他地方,但我無法在任何地方找到完全令人滿意的解決方案。Python 3 - 不破壞裝飾器或違反DRY的方法docstring繼承

似乎有兩種常見的解決方案。第一個(例如從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__ 

這可能是最直接的方法,但它需要重複父類的名字至少一次,也成爲如果文檔字符串複雜得多在直接的祖先中找不到。

第二種方法使用元類或類裝飾(參見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了討論。這裏,函數裝飾器實際上包裝了方法並將其轉換爲方法描述符,而不僅僅是更新其文檔字符串。然而,這看起來像使用大錘來破解一個堅果,因爲它將方法轉變爲一個方法描述符(雖然我沒有檢查,但也可能會有性能影響),也不會使任何其他裝飾器可用的文檔字符串(和在上面的例子中實際上會使它們崩潰,因爲方法描述符沒有__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-without-breaking-decorat/上放了一個稍微更巧妙的實現方法 – Nikratio

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' 

這會在定義整個類之後設置文檔字符串,而不必先創建實例。

如果您需要可用於方法裝飾器的文檔字符串,那麼不幸的是,您完全被複制父類的裝飾器卡住。

其原因是您無法在定義類體時反思超類的內容。類定義期間的本地名稱空間無法訪問傳遞給類工廠的參數。

可能使用元類的基類添加到本地命名空間,然後使用一個裝飾再次拉那些了,但在我看來,那得難看,快捷:

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

默認情況下,本地命名空間無權訪問類工廠參數,爲true。但是不能用元類來改變它嗎? – Nikratio

+0

對於Python 2,'isfunction'似乎需要'ismethod'。 – spookylukey

+0

@spookylukey:是的,在Python 2中,類的成員是未綁定的方法。 –