Как ввести аннотатные переопределенные методы в подкласс?

Скажем, у меня уже есть метод с аннотациями типа:

class Shape: def area(self) -> float: raise NotImplementedError 

Затем я буду подклассифицировать несколько раз:

 class Circle: def area(self) -> float: return math.pi * self.radius ** 2 class Rectangle: def area(self) -> float: return self.height * self.width 

Как вы можете видеть, я много дублирую -> float . Скажем, у меня есть 10 различных фигур, с несколькими методами, такими как, некоторые из которых содержат параметры тоже. Есть ли способ просто «скопировать» аннотацию из родительского класса, аналогичную функции functools.wraps() с docstrings?

Это может сработать, хотя я обязательно пропущу крайние случаи, например, дополнительные аргументы:

 from functools import partial, update_wrapper def annotate_from(f): return partial(update_wrapper, wrapped=f, assigned=('__annotations__',), updated=()) 

который назначит атрибут __annotations__ функции «обертки» из f.__annotations__ (имейте в виду, что это не копия).

Согласно документам, функция update_wrapper по умолчанию для уже включает __annotations__ , но я могу понять, почему вы не хотите, чтобы все остальные атрибуты были назначены из завернутого .

С помощью этого вы можете определить Circle и Rectangle как

 class Circle: @annotate_from(Shape.area) def area(self): return math.pi * self.radius ** 2 class Rectangle: @annotate_from(Shape.area) def area(self): return self.height * self.width 

и результат

 In [82]: Circle.area.__annotations__ Out[82]: {'return': builtins.float} In [86]: Rectangle.area.__annotations__ Out[86]: {'return': builtins.float} 

В качестве побочного эффекта ваши методы будут иметь атрибут __wrapped__ , который в этом случае Shape.area на Shape.area .


Менее стандартным (если вы можете назвать вышеупомянутое использование стандарта update_wrapper ) способ выполнения обработки переопределенных методов может быть достигнут с помощью декоратора класса:

 from inspect import getmembers, isfunction, signature def override(f): """ Mark method overrides. """ f.__override__ = True return f def _is_method_override(m): return isfunction(m) and getattr(m, '__override__', False) def annotate_overrides(cls): """ Copy annotations of overridden methods. """ bases = cls.mro()[1:] for name, method in getmembers(cls, _is_method_override): for base in bases: if hasattr(base, name): break else: raise RuntimeError( 'method {!r} not found in bases of {!r}'.format( name, cls)) base_method = getattr(base, name) method.__annotations__ = base_method.__annotations__.copy() return cls 

а потом:

 @annotate_overrides class Rectangle(Shape): @override def area(self): return self.height * self.width 

Опять же, это не будет обрабатывать переопределяющие методы с дополнительными аргументами.

Вы можете использовать декоратор класса для обновления аннотаций методов подкласса. В вашем декораторе вам нужно пройти через определение класса, а затем обновить только те методы, которые присутствуют в вашем суперклассе. Конечно, для доступа к суперклассу вам нужно использовать его __mro__ который является только кортежем класса, подкласса, до object . Здесь нас интересует второй элемент в этом наборе, который имеет индекс 1 таким образом, __mro__[1] или используя cls.mro()[1] . Наконец, и не в последнюю очередь, ваш декоратор должен вернуть класс.

 def wraps_annotations(cls): mro = cls.mro()[1] vars_mro = vars(mro) for name, value in vars(cls).items(): if callable(value) and name in vars_mro: value.__annotations__.update(vars(mro).get(name).__annotations__) return cls 

Демо-версия:

 >>> class Shape: ... def area(self) -> float: ... raise NotImplementedError ... >>> import math >>> >>> @wraps_annotations ... class Circle(Shape): ... def area(self): ... return math.pi * self.radius ** 2 ... >>> c = Circle() >>> c.area.__annotations__ {'return': <class 'float'>} >>> @wraps_annotations ... class Rectangle(Shape): ... def area(self): ... return self.height * self.width ... >>> r = Rectangle() >>> r.area.__annotations__ {'return': <class 'float'>}