Наследование строк документации методов в Python
У меня есть OO-иерархия со строками документации, которые требуют столько же обслуживания, сколько и сам код. Например,
class Swallow(object):
def airspeed(self):
"""Returns the airspeed (unladen)"""
raise NotImplementedError
class AfricanSwallow(Swallow):
def airspeed(self):
# whatever
Теперь проблема в том, что AfricanSwallow.airspeed
не наследует строку документации метода суперкласса. Я знаю, что могу сохранить строку документации, используя шаблонный шаблон, т.е.
class Swallow(object):
def airspeed(self):
"""Returns the airspeed (unladen)"""
return self._ask_arthur()
и внедрение _ask_arthur
в каждом подклассе. Однако мне было интересно, есть ли другой способ наследования строк документации, возможно, какой-нибудь декоратор, который я еще не обнаружил?
6 ответов
Напишите функцию в стиле декоратор класса, чтобы сделать для вас копирование. В Python2.5 вы можете применять его непосредственно после создания класса. В более поздних версиях вы можете подать заявку с помощью нотации @decorator.
Вот первый шаг к тому, как это сделать:
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__
В новых версиях Python последняя часть еще более проста и красива:
@fix_docs
class Dog(Animal):
def walk(self):
pass
Это метод Pythonic, который точно соответствует дизайну существующих инструментов в стандартной библиотеке. Например, декоратор класса functools.total_ordering добавляет недостающие богатые методы сравнения в классы. В другом примере декоратор functools.wraps копирует метаданные из одной функции в другую.
Это вариант метакласса Пола Макгуайра DocStringInheritor.
- Он наследует строку документа родительского члена, если строка документа дочернего элемента пуста.
- Он наследует строку документации родительского класса, если строка документа дочернего класса пуста.
- Он может наследовать строку документации от любого класса в любом из MRO базовых классов, как обычное наследование атрибутов.
- В отличие от декоратора классов, метакласс наследуется, поэтому вам нужно установить метакласс только один раз в некотором базовом классе верхнего уровня, и наследование строк документации будет происходить во всей вашей иерархии ООП.
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)
К сведению людей, которые только сейчас спотыкаются об этой теме: Начиная с Python 3.5, inspect.getdoc автоматически извлекает строки документации из иерархии наследования.
Приведенные выше ответы, таким образом, полезны для Python 2, или если вы хотите проявить больше творчества при объединении строк документов родителей и детей.
Я также создал несколько легких инструментов для наследования документации. Они поддерживают несколько хороших стилей документации по умолчанию (numpy, google, reST) из коробки. Вы также можете легко использовать свой собственный стиль документации
Следующая адаптация также обрабатывает свойства и классы смешивания. Я также столкнулся с ситуацией, когда мне пришлось использовать func.__func__
(для "instancemethod"), но я не совсем уверен, почему другие решения не помогли решить эту проблему.
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
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
Это очень старая тема. Но если кто-то ищет простой способ, вы можете сделать это с помощью
__init_subclass__
который вызывается всякий раз, когда вы наследуете этот класс, если у вас есть доступ к родительскому классу для внесения изменений.
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
parent_method_docstr = {}
for i, v in ParentClass.__dict__.items():
if v and callable(v) and v.__doc__ is not None:
parent_method_docstr[i] = v.__doc__
for i, v in cls.__dict__.items():
if v and callable(v) and v.__doc__ is None and i in parent_method_docstr:
v.__doc__ = parent_method_docstr[i]