Метод подкласса Python для наследования декоратора от метода суперкласса
У меня есть суперкласс, у которого есть метод retrieve(), и каждый из его подклассов реализует свой собственный метод retrieve(). Я хотел бы, чтобы каждый метод retrieve() был оформлен для кэширования возвращаемого значения, когда он получает одинаковые аргументы, без необходимости декорировать метод в каждом подклассе.
Декораторы, похоже, не наследуются. Я мог бы, вероятно, вызвать метод суперкласса, который в свою очередь установил бы кеш, но в настоящее время мой суперкласс вызывает исключение NotImplemented, которое мне нравится.
import json
import operator
from cachetools import cachedmethod, TTLCache
def simple_decorator(func):
def wrapper(*args, **kwargs):
#check cache
print("simple decorator")
func(*args, **kwargs)
#set cache
return wrapper
class AbstractInput(object):
def __init__(self, cacheparams = {'maxsize': 10, 'ttl': 300}):
self.cache = TTLCache(**cacheparams)
super().__init__()
@simple_decorator
def retrieve(self, params):
print("AbstractInput retrieve")
raise NotImplementedError("AbstractInput inheritors must implement retrieve() method")
class JsonInput(AbstractInput):
def retrieve(self, params):
print("JsonInput retrieve")
return json.dumps(params)
class SillyJsonInput(JsonInput):
def retrieve(self, params):
print("SillyJsonInput retrieve")
params["silly"] = True
return json.dumps(params)
Фактические результаты:
>>> ai.retrieve(params)
ai.retrieve(params)
simple decorator
AbstractInput retrieve
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 8, in wrapper
File "<string>", line 22, in retrieve
NotImplementedError: AbstractInput inheritors must implement retrieve() method
>>> ji.retrieve(params)
ji.retrieve(params)
JsonInput retrieve
'{"happy": "go lucky", "angry": "as a wasp"}'
Желаемые результаты:
>>> ai.retrieve(params)
ai.retrieve(params)
simple decorator
AbstractInput retrieve
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 8, in wrapper
File "<string>", line 22, in retrieve
NotImplementedError: AbstractInput inheritors must implement retrieve() method
>>> ji.retrieve(params)
simple decorator
ji.retrieve(params)
JsonInput retrieve
'{"happy": "go lucky", "angry": "as a wasp"}'
2 ответа
Да, использование метакласса для навязывания декоратору определенного метода, как вы указали в своем собственном ответе, является правильным. С некоторыми изменениями это можно сделать так, чтобы декорируемый метод не был фиксированным - например, атрибут, установленный в декорированной функции, можно использовать как "метку", что такой декоратор должен быть принудительно переопределен при переопределении методов.
Кроме того, начиная с Python 3.6, появился новый механизм уровня класса - специальный метод __init_subclass__
, которая имеет конкретную цель уменьшения потребности в метаклассах. Метаклассы могут быть сложными, и если ваша иерархия классов должна объединять более одного метакласса, у вас может возникнуть головная боль.
__init_subclass__
Метод помещается в базовый класс и вызывается один раз при каждом создании дочернего класса. Логика упаковки может быть помещена туда.
По сути, вы можете просто изменить свой декоратор, чтобы поставить отметку, о которой я упоминал выше, и добавить этот класс в иерархию наследования - его можно поместить как класс смешивания в множественном наследовании, так что при необходимости его можно будет использовать для различных деревьев классов:
def simple_decorator(func):
def wrapper(*args, **kwargs):
print("check cache")
rt = func(*args, **kwargs)
print("set cache")
return rt
wrapper.inherit_decorator = simple_decorator
return wrapper
class InheritDecoratorsMixin:
def __init_subclass__(cls, *args, **kwargs):
super().__init_subclass__(*args, **kwargs)
decorator_registry = getattr(cls, "_decorator_registry", {}).copy()
cls._decorator_registry = decorator_registry
# Check for decorated objects in the mixin itself- optional:
for name, obj in __class__.__dict__.items():
if getattr(obj, "inherit_decorator", False) and not name in decorator_registry:
decorator_registry[name] = obj.inherit_decorator
# annotate newly decorated methods in the current subclass:
for name, obj in cls.__dict__.items():
if getattr(obj, "inherit_decorator", False) and not name in decorator_registry:
decorator_registry[name] = obj.inherit_decorator
# finally, decorate all methods anottated in the registry:
for name, decorator in decorator_registry.items():
if name in cls.__dict__ and getattr(getattr(cls, name), "inherit_decorator", None) != decorator:
setattr(cls, name, decorator(cls.__dict__[name]))
Вот и все - у каждого нового подкласса будет свой _decorator_registry
атрибут, в котором указано имя украшенных методов у всех предков, а также какой декоратор применять.
Если декоратор должен использоваться один раз для метода, а не повторяться, когда переопределенный метод выполняет super()
вызовите его предков (не тот случай, когда вы декорируете для кеша, поскольку супер-методы не будут вызываться), что становится сложнее, но может быть сделано.
Однако сделать это непросто - поскольку экземпляры декоратора в суперклассах могут быть другими экземплярами, чем декоратор в подклассе, - один из способов передать информацию о том, что "код декоратора для этого метода уже запущен в этом вызове цепочки" использовать маркер уровня экземпляра - который должен быть локальной переменной потока, если код должен поддерживать параллелизм.
Вся эта проверка приведет к довольно сложному шаблону, который можно будет поместить в то, что может быть простым декоратором - так что мы можем создать "декоратор" для "декораторов", которые мы хотим запустить один раз. В других украшениях украшали childmost
ниже будет работать только на "самый дочерний" класс, но не на соответствующих методах в суперклассах, когда они вызывают super()
import threading
def childmost(decorator_func):
def inheritable_decorator_that_runs_once(func):
decorated_func = decorator_func(func)
name = func.__name__
def wrapper(self, *args, **kw):
if not hasattr(self, f"_running_{name}"):
setattr(self, f"_running_{name}", threading.local())
running_registry = getattr(self, f"_running_{name}")
try:
if not getattr(running_registry, "running", False):
running_registry.running = True
rt = decorated_func(self, *args, **kw)
else:
rt = func(self, *args, **kw)
finally:
running_registry.running = False
return rt
wrapper.inherit_decorator = inheritable_decorator_that_runs_once
return wrapper
return inheritable_decorator_that_runs_once
Пример использования первого списка:
class A: pass
class B(A, InheritDecoratorsMixin):
@simple_decorator
def method(self):
print(__class__, "method called")
class C(B):
def method(self):
print(__class__, "method called")
super().method()
И после вставки list-1 и этих классов A=BC в интерпретатор, результат будет следующим:
In [9]: C().method()
check cache
<class '__main__.C'> method called
check cache
<class '__main__.B'> method called
set cache
set cache
(класс "A" здесь не является обязательным и может быть опущен)
Пример использования второго списка:
# Decorating the same decorator above:
@childmost
def simple_decorator2(func):
def wrapper(*args, **kwargs):
print("check cache")
rt = func(*args, **kwargs)
print("set cache")
return rt
return wrapper
class D: pass
class E(D, InheritDecoratorsMixin):
@simple_decorator2
def method(self):
print(__class__, "method called")
class F(E):
def method(self):
print(__class__, "method called")
super().method()
И результат:
In [19]: F().method()
check cache
<class '__main__.F'> method called
<class '__main__.E'> method called
set cache
Хорошо, кажется, что я могу "декорировать" метод в суперклассе, и подклассы также наследуют это оформление, даже если метод перезаписывается в подклассе, используя метаклассы. В этом случае я украшаю все методы "извлечения" в AbstractInput и его подклассах с помощью simple_decorator, используя метакласс с именем CacheRetrieval.
def simple_decorator(func):
def wrapper(*args, **kwargs):
print("check cache")
rt = func(*args, **kwargs)
print("set cache")
return rt
return wrapper
class CacheRetrieval(type):
def __new__(cls, name, bases, attr):
# Replace each function with
# a print statement of the function name
# followed by running the computation with the provided args and returning the computation result
attr["retrieve"] = simple_decorator(attr["retrieve"])
return super(CacheRetrieval, cls).__new__(cls, name, bases, attr)
class AbstractInput(object, metaclass= CacheRetrieval):
def __init__(self, cacheparams = {'maxsize': 10, 'ttl': 300}):
self.cache = TTLCache(**cacheparams)
super().__init__()
def retrieve(self, params):
print("AbstractInput retrieve")
raise NotImplementedError("DataInput must implement retrieve() method")
class JsonInput(AbstractInput):
def retrieve(self, params):
print("JsonInput retrieve")
return json.dumps(params)
class SillyJsonInput(JsonInput):
def retrieve(self, params):
print("SillyJsonInput retrieve")
params["silly"] = True
return json.dumps(params)
Мне помогла эта страница: https://stackabuse.com/python-metaclasses-and-metaprogramming/