【问题标题】:Python 3 - Method docstring inheritance without breaking decorators or violating DRYPython 3 - 方法文档字符串继承而不破坏装饰器或违反 DRY
【发布时间】:2013-06-27 21:46:45
【问题描述】:

这个问题似乎经常出现在 StackOverflow 和其他地方,但我无法在任何地方找到完全令人满意的解决方案。

似乎有两种常见的解决方案。第一个(来自例如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 的解决方案感兴趣。

【问题讨论】:

    标签: python python-3.x inheritance docstring


    【解决方案1】:

    改用类装饰器:

    @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'
    

    【讨论】:

    • 本地命名空间默认无权访问类工厂参数,true。但是元类不能用来改变吗?
    • 对于 Python 2,'isfunction' 似乎需要是 'ismethod'。
    • @spookylukey:是的,在 Python 2 中,类的成员是未绑定的方法。
    【解决方案2】:

    从 Python 3.5 开始,inspect.getdoc 在继承树中搜索文档字符串。因此,如果您将子级的文档字符串留空,它将从父级检索它。这避免了代码重复的需要,像 sphinx 这样的自动代码生成器会做正确的事情。

    $ cat mwe.py
    import inspect
    
    class A:
        def foo(self):
            """Fool!"""
            return 42
    
    class B(A):
        def foo(self):
            return super().foo()
    
    print(A.foo.__doc__, B.foo.__doc__, A().foo.__doc__, B().foo.__doc__,
          inspect.getdoc(A.foo), inspect.getdoc(B.foo),
          inspect.getdoc(A().foo), inspect.getdoc(B().foo))
    $ python mwe.py
    Fool! None Fool! None Fool! Fool! Fool! Fool!
    

    【讨论】:

      【解决方案3】:

      我认为元类的__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),则它不起作用。

      【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-12-27
      • 2010-12-19
      • 1970-01-01
      • 1970-01-01
      • 2011-03-01
      • 2019-08-02
      • 2019-11-27
      相关资源
      最近更新 更多