Python - Metaclass decorator - Как использовать @classmethod - PullRequest
0 голосов
/ 17 марта 2019

У меня есть следующий метакласс Python, который добавляет декоратор deco_with_args к каждому классу:

def deco_with_args(baz):
    def decorator(func):
        ...
        return func
    return decorator

class Foo(type):
    def __prepare__(name, bases):    
        return {'deco_with_args': deco_with_args}

Это позволяет мне использовать декоратор так:

class Bar(metaclass=Foo):
    @deco_with_args('baz')
    def some_function(self):
        ...

Как мне заставить декоратор deco_with_args вести себя как @classmethod, чтобы я мог получить доступ к классу Bar (или к любому другому классу) из функции decorator?

Я попытался использовать @classmethod для функции deco_with_args, но безуспешно.

Ответы [ 3 ]

1 голос
/ 17 марта 2019

@classmethod не делает ничего полезного для вашего декоратора, потому что он не вызывается через класс или экземпляр.classmethod является дескриптором , и дескрипторы действуют только при доступе к атрибуту .Другими словами, это помогло бы, только если декоратор был вызван как @Bar.deco_with_args('baz').

Следующая проблема состоит в том, что класс еще не существует во время выполнения декоратора.Python выполняет весь код в теле функции до создания класса.Поэтому невозможно получить доступ к классу в deco_with_args или decorator.

1 голос
/ 17 марта 2019

В вашем вопросе есть две интерпретации - если вам нужно, чтобы cls был доступен, когда вызывается функция с именем decorator в вашем примере (т.е. вам нужно, чтобы ваши декорированные методы стали методами класса), достаточно того, чтопреобразован в метод класса:

def deco_with_args(baz):
    def decorator(func):
        ...
        return classmethod(func)
    return decorator

Второй - если вам нужно, чтобы cls был доступен при вызове самого deco_with_args при создании самой декорированной функции при создании класса.Ответ, который указан как принятый сейчас, перечисляет простую проблему с этим: Класс еще не существует, когда тело класса запускается , так что нет никакого способа, которым в конце синтаксического анализа тела классау вас могут быть методы, которые знали бы о самом классе.

Однако, в отличие от этого ответа, он подразумевает, что это не настоящая сделка.Все, что вам нужно сделать, - это запустить код декоратора (код, который требует cls) лениво, в конце процесса создания класса.У вас уже есть настройка метакласса, так что сделать это почти тривиально, просто добавив еще один вызываемый слой вокруг кода декоратора:

def deco_with_args(baz):
    def outter_decorator(func):
        def decorator(cls):
            # Code that needs cls at class creation time goes here
            ...

            return func
        return decorator
    outter_decorator._deco_with_args = True
    return outter_decorator

class Foo(type):
    def __prepare__(name, bases):    
        return {'deco_with_args': deco_with_args}

    def __init__(cls, cls_name, bases, namespace, **kwds):
        for name, method in cls.__dict__.items():
            if getattr(method, '_deco_with_args', False):
                cls.__dict__[name] = method(cls)

        super().__init__(cls_name, bases, namespace, **kwds)

Это будет выполнено, конечно же, после завершения выполнения тела класса, но перед любым другим оператором Python после запуска class.Если ваш декоратор будет влиять на другие элементы, которые выполняются внутри самого тела класса, все, что вам нужно сделать, это обернуть их вокруг, чтобы гарантировать выполнение отложенного выполнения.

0 голосов
/ 17 марта 2019

Вы можете использовать протокол дескриптора для захвата ваших вызовов метода и добавления класса в качестве параметра на лету:

def another_classmethod(baz):

  class decorator:
    def __init__(self, func):
      self.func = func
    def __get__(self, instance, owner):
      def new_call(*args, **kwargs):
        print(baz, self.func(owner, *args, **kwargs))
      return new_call

  return decorator


class Bar():
    @another_classmethod('baz')
    def some_function(cls):
        return f"test {cls.__name__}"

Bar.some_function()

Это печатает:

baz test Bar

Основная «хитрость» заключается в том, что протокол при вызове Bar.some_function() заключается в том, чтобы сначала вызвать __get__, а затем __call__ в функции, возвращаемой __get__.

Обратите внимание, что __get__ также вызывается, когда вы просто делаете Bar.some_function, это то, что используется в декораторах, таких как @property.

Одно небольшое замечание: при использовании classmethod вы не должны называть свой первый параметр self, поскольку это сбивает с толку (это заставит людей думать, что первый параметр является экземпляром, а не объектом / типом класса).

...