python oop inheritance docstring template-method-pattern

python documentation generator



Heredar métodos ''docstrings en Python (5)

Tengo una jerarquía OO con docstrings que requieren tanto mantenimiento como el código en sí. P.ej,

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

Ahora, el problema es que AfricanSwallow.airspeed no hereda el docstring del método de la superclase. Sé que puedo guardar la docstring usando el patrón de método de plantilla, es decir,

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

e implementando _ask_arthur en cada subclase. Sin embargo, me preguntaba si existe otra forma de heredar docstrings, ¿quizás algún decorador que aún no había descubierto?


Escriba una función en un estilo de decorador de clases para hacer la copia por usted. En Python2.5, puede aplicarlo directamente después de crear la clase. En versiones posteriores, puede aplicar con la notación @decorator .

Aquí hay un primer corte sobre cómo hacerlo:

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__

En las versiones más nuevas de Python, la última parte es aún más simple y hermosa:

@fix_docs class Dog(Animal): def walk(self): pass

Esta es una técnica Pythonic que coincide exactamente con el diseño de las herramientas existentes en la biblioteca estándar. Por ejemplo, el decorador de clases functools.total_ordering agrega métodos de comparación ricos faltantes a las clases. Y para otro ejemplo, el decorador functools.wraps copia los metadatos de una función a otra.


Esta es una variación de la metaclass DocStringInheritor de Paul McGuire .

  1. Hereda la docstring de un miembro padre si la docstring del miembro del niño está vacía.
  2. Hereda una docstring de clase primaria si la docstring de clase secundaria está vacía.
  3. Puede heredar la docstring de cualquier clase en cualquiera de las MRO de las clases base, al igual que la herencia de atributos regulares.
  4. A diferencia de un decorador de clases, la metaclase se hereda, por lo que solo necesita establecer la metaclase una vez en alguna clase base de nivel superior, y la herencia de docstring se producirá en toda su jerarquía de 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 __name__ == ''__main__'': sys.argv.insert(1, ''--verbose'') unittest.main(argv=sys.argv)


La siguiente adaptación también maneja propiedades y clases mixin. También me encontré con una situación en la que tuve que usar func.__func__ (para " func.__func__ de instancias"), pero no estoy completamente seguro de por qué las otras soluciones no abordaron ese problema.

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


Para su información, la gente acaba de tropezar con este tema: a partir de Python 3.5, inspect.getdoc recupera automáticamente docstrings de la jerarquía de herencia.

Por lo tanto, las respuestas anteriores son útiles para Python 2, o si desea ser más creativo con la fusión de las cadenas de documentos de padres y niños.

También he creado algunas herramientas livianas para la herencia de docstring . Estos admiten algunos buenos estilos predeterminados de docstring (numpy, google, reST) listos para usar. También puede usar fácilmente su propio estilo de docstring


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