2013-06-30 18 views
5

Questa domanda sembra venire regolarmente sia su StackOverflow che altrove, tuttavia non sono stato in grado di trovare una soluzione completamente soddisfacente.Python 3 - Metodo di ereditarietà di docstring senza rompere decoratori o violazione di DRY

Sembrano esserci due tipi di soluzioni comuni. La prima (ad esempio da http://article.gmane.org/gmane.comp.python.general/630549) utilizza una funzione decoratore:

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__ 

Questo è probabilmente l'approccio più semplice, ma richiede di ripetere il nome della classe genitore, almeno una volta, e anche diventa molto più complicato se il docstring non può essere trovato nell'antenato diretto.

Il secondo approccio utilizza un metaclasse o classe di decoratore (cfr Inheriting methods' docstrings in Python, Inherit a parent class docstring as __doc__ attribute, http://mail.python.org/pipermail/python-list/2011-June/606043.html) e si presenta così:

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__ 

Tuttavia, con questo approccio la docstring viene impostato solo dopo la creazione della classe e quindi non accessibile a decoratori, quindi il seguente non funzionerà:

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 

Una terza idea interessante è stata discussa in Inherit docstrings in Python class inheritance. In questo caso, il decoratore di funzioni avvolge effettivamente il metodo e lo trasforma in un descrittore di metodo piuttosto che semplicemente aggiornando la sua docstring. Tuttavia, sembra che usare il martello per rompere un dado perché trasforma il metodo in un descrittore di metodo (che può avere anche implicazioni sulle prestazioni, sebbene io non abbia controllato), e inoltre non rende disponibile la docstring a nessun altro decoratore (e nell'esempio sopra li farà effettivamente crash perché il descrittore del metodo non ha attributo __name__).

Esiste una soluzione che eviti tutti gli inconvenienti sopra citati, ovvero non richiede che io ripeta me stesso e assegni immediatamente la docstring utilizzando un decoratore?

Sono interessato a una soluzione per Python 3.

risposta

1

penso che il metaclasse __prepare__ metodo può essere utilizzato per questo iniettando un decoratore che conosce la gerarchia di classe:

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__ 

Stampe:

docstring for <function Bird.move_to at 0x7f6286b9a200> is 'Move to *dest*' 

Naturalmente, questo approccio ha alcune altre questioni: - Alcuni strumenti di analisi (per esempio pyflakes) si lamenterà dell'uso del nome (apparentemente) indefinito inherit_docstring - Non funziona se la classe padre ha già un metaclass diverso (ad esempio ABCMeta).

+0

Ho implementato un po 'più intelligente di questo e alcuni esempi su http://code.activestate.com/recipes/578587-inherit-method-docstrings-without-breaking-decorat/ – Nikratio

2

Utilizzare un decoratore di classe, invece:

@inherit_docstrings 
class MyClass(SuperClass): 
    def method(self): 
     pass 

dove inherit_docstrings() è definito come:

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 

Demo:

>>> class SuperClass: 
...  def method(self): 
...   '''Has a docstring''' 
...   pass 
... 
>>> @inherit_docstrings 
... class MyClass(SuperClass): 
...  def method(self): 
...   pass 
... 
>>> MyClass.method.__doc__ 
'Has a docstring' 

Imposta la doc. dopo il definendo l'intera classe, senza dover prima creare un'istanza.

Se è necessaria la docstring per i decoratori di metodi, sfortunatamente si è completamente bloccati con il decoratore che duplica la classe padre.

La ragione di ciò è che non è possibile approfondire quale sarà la superclasse durante la definizione del corpo della classe. Lo spazio dei nomi locale durante la definizione della classe non ha accesso agli argomenti passati alla classe factory.

È potrebbe utilizzare un metaclasse per aggiungere le classi base allo spazio dei nomi locali, quindi utilizzare un decoratore per tirare quelli fuori di nuovo, ma a mio parere, che diventa brutto, veloce: l'utilizzo

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 

Demo :

>>> class MyClass(SuperClass, metaclass=InheritDocstringMeta): 
...  @inherit_docstring 
...  def method(self): 
...   pass 
... 
>>> MyClass.method.__doc__ 
'Has a docstring' 
+0

Il namespace locale non ha accesso agli argomenti factory class per impostazione predefinita, true. Ma non potrebbe essere usato un metaclass per cambiarlo? – Nikratio

+0

Per Python 2, 'isfunction' deve essere 'ismethod' sembra. – spookylukey

+0

@spookylukey: sì, in Python 2, i membri di una classe sono metodi non associati. –

Problemi correlati