Метод подкласса 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/

Другие вопросы по тегам