Получить класс-владелец функции Python от декоратора

У меня есть декоратор в PY. Это метод, который принимает функцию в качестве параметра. Я хочу создать структуру каталогов на основе переданной функции. Я использую имя модуля для родительского каталога, но хотел бы использовать имя класса для подкаталога. Я не могу понять, как получить имя класса, которому принадлежит объект fn.

Мой декоратор:

def specialTest(fn):
    filename = fn.__name__
    directory = fn.__module__
    subdirectory = fn.__class__.__name__ #WHERE DO I GET THIS

person Corey Coogan    schedule 06.10.2011    source источник
comment
укажите в декораторе специальный аргумент, говорящий is_member_func, если это правда, получите первый аргумент, который является self.   -  person bhathiya-perera    schedule 11.03.2015


Ответы (3)


Если fn является instancemethod, то вы можете использовать fn.im_class.

>>> class Foo(object):
...     def bar(self):
...         pass
...
>>> Foo.bar.im_class
__main__.Foo

Обратите внимание, что это не будет работать из декоратора, потому что функция преобразуется в метод экземпляра только после определения класса (т. е. если @specialTest использовался для декорирования bar, это не сработает; если это вообще возможно, сделать это в этот момент нужно будет путем проверки стека вызовов или чего-то столь же неудачного).

person David Wolever    schedule 06.10.2011
comment
Вот чего я боялся. Спасибо. - person Corey Coogan; 07.10.2011
comment
По какой-то причине это не регистрировалось, пока я не вернулся в код. Это возможно, мне просто нужно переместить свой код, взаимодействующий с FN, внутрь обернутой функции, где FN — это экземпляр. - person Corey Coogan; 07.10.2011
comment
Это невозможно во время определения, однако вы можете перечислить декорированные функции после создания класса: для этого декоратор должен поместить в класс некоторый атрибут маркера. - person kolypto; 19.01.2014
comment
@kolypto вы имеете в виду атрибут маркера в обернутой функции ... поскольку проблема пока заключается в том, что декоратор не может получить доступ к классу? - person Anentropic; 25.04.2014
comment
@ Anentropic, верно: некоторый код должен выполнять постобработку методов созданного класса, и чтобы определить, какие методы были украшены, нам нужно как-то их пометить. Например, с пользовательским атрибутом - person kolypto; 25.04.2014

В Python 2 вы можете использовать атрибут im_class для объекта метода. В Python 3 это будет __self__.__class__ (или type(method.__self__)).

person Cat Plus Plus    schedule 06.10.2011

Получение имени класса

Если вам нужно только имя класса (а не сам класс), оно доступно как часть функции (частично) полный атрибут имени (__qualname__).

import os.path

def decorator(fn):
    filename = fn.__name__
    directory = fn.__module__
    subdirectory = fn.__qualname__.removesuffix('.' + fn.__name__).replace('.', os.path.sep)
    return fn

class A(object):
    @decorator
    def method(self):
        pass
    
    class B(object):
        @decorator
        def method(self):
            pass

Если класс метода является внутренним классом, qualname будет включать внешние классы. Приведенный выше код обрабатывает это, заменяя все разделители точек локальным разделителем пути.

Ничто в классе, кроме его имени, недоступно при вызове декоратора, поскольку сам класс еще не определен.

Получение класса при вызове метода

Если необходим сам класс и доступ может быть отложен до тех пор, пока не будет (сначала) вызван декорированный метод, декоратор может обернуть функцию, как обычно, а затем обертка может получить доступ к экземпляру и классу. Оболочка также может удалить себя и удалить метод, если он должен быть вызван только один раз.

import types

def once(fn):
    def wrapper(self, *args, **kwargs):
        # do something with the class
        subdirectory = type(self).__name__
        ...
        # undecorate the method (i.e. remove the wrapper)
        setattr(self, fn.__name__, types.MethodType(fn, self))
        
        # invoke the method
        return fn(self, *args, **kwargs)
    return wrapper

class A(object):
    @once
    def method(self):
        pass

a = A()
a.method()
a.method()

Обратите внимание, что это будет работать только в том случае, если метод вызывается.

Получение класса после определения класса

Если вам нужно получить информацию о классе, даже если декорированный метод не вызывается, вы можете сохранить ссылку на декоратор в оболочке (метод #3), затем просмотрите методы всех классов (после определения интересующих классов) на наличие тех, которые относятся к декоратору:

def decorator(fn):
    def wrapper(self, *args, **kwargs):
        return fn(self, *args, **kwargs)
    wrapper.__decorator__ = decorator
    wrapper.__name__ = 'decorator + ' + fn.__name__
    wrapper.__qualname__ = 'decorator + ' + fn.__qualname__
    return wrapper

def methodsDecoratedBy(cls, decorator):
    for method in cls.__dict__.values():
        if     hasattr(method, '__decorator__') \
           and method.__decorator__ == decorator:
            yield method

#...
import sys, inspect

def allMethodsDecoratedBy(decorator)
    for name, cls in inspect.getmembers(sys.modules, lambda x: inspect.isclass(x)):
        for method in methodsDecoratedBy(cls, decorator):
            yield method

По сути, это делает декоратор аннотацией в общем смысле программирования (а не смыслом функции аннотации в Python, которые предназначены только для аргументов функции и возвращаемого значения). Одна проблема заключается в том, что декоратор должен быть применен последним, иначе атрибут класса будет хранить не соответствующую оболочку, а другую, внешнюю оболочку. Частично это можно решить, сохранив (и позже проверив) все декораторы в обертке:

def decorator(fn):
    def wrapper(self, *args, **kwargs):
        return fn(self, *args, **kwargs)
    wrapper.__decorator__ = decorator
    if not hasattr(fn, '__decorators__'):
        if hasattr(fn, '__decorator__'):
            fn.__decorators__ = [fn.__decorator__]
        else:
            fn.__decorators__ = []
    wrapper.__decorators__ = [decorator] + fn.__decorators__
    wrapper.__name__ = 'decorator(' + fn.__name__ + ')'
    wrapper.__qualname__ = 'decorator(' + fn.__qualname__ + ')'
    return wrapper

def methodsDecoratedBy(cls, decorator):
    for method in cls.__dict__.values():
        if hasattr(method, '__decorators__') and decorator in method.__decorators__:
            yield method

Кроме того, любые декораторы, которых вы не контролируете, можно заставить сотрудничать, украсив их так, чтобы они сохраняли себя на своих обертках, как это делает decorator:

def bind(*values, **kwvalues):
    def wrap(fn):
        def wrapper(self, *args, **kwargs):
            nonlocal kwvalues
            kwvalues = kwvalues.copy()
            kwvalues.update(kwargs)
            return fn(self, *values, *args, **kwvalues)
        wrapper.__qualname__ = 'bind.wrapper'
        return wrapper
    wrap.__qualname__ = 'bind.wrap'
    return wrap

def registering_decorator(decorator):
    def wrap(fn):
        decorated = decorator(fn)
        decorated.__decorator__ = decorator
        if not hasattr(fn, '__decorators__'):
            if hasattr(fn, '__decorator__'):
                fn.__decorators__ = [fn.__decorator__]
            else:
                fn.__decorators__ = []
        if not hasattr(decorated, '__decorators__'):
            decorated.__decorators__ = fn.__decorators__.copy()
        decorated.__decorators__.insert(0, decorator)
        decorated.__name__ = 'reg_' + decorator.__name__ + '(' + fn.__name__ + ')'
        decorated.__qualname__ = decorator.__qualname__ + '(' + fn.__qualname__ + ')'
        return decorated
    wrap.__qualname__ = 'registering_decorator.wrap'
    return wrap

class A(object):
    @decorator
    def decorated(self):
        pass
    
    @bind(1)
    def add(self, a, b):
        return a + b
    
    @registering_decorator(bind(1))
    @decorator
    def args(self, *args):
        return args
    
    @decorator
    @registering_decorator(bind(a=1))
    def kwargs(self, **kwargs):
        return kwargs

A.args.__decorators__
A.kwargs.__decorators__
assert not hasattr(A.add, '__decorators__')
a = A()
a.add(2)
# 3

Другая проблема заключается в том, что сканирование всех классов неэффективно. Вы можете сделать это более эффективным, используя дополнительный декоратор класса, чтобы зарегистрировать все классы для проверки декоратора метода. Однако этот подход ненадежен; если вы забудете украсить класс, он не будет записан в реестре.

class ClassRegistry(object):
    def __init__(self):
        self.registry = {}
    
    def __call__(self, cls):
        self.registry[cls] = cls
        cls.__decorator__ = self
        return cls
    
    def getRegisteredClasses(self):
        return self.registry.values()

class DecoratedClassRegistry(ClassRegistry):
    def __init__(self, decorator):
        self.decorator = decorator
        super().__init__()
    
    def isDecorated(self, method):
        return (    hasattr(method, '__decorators__') \
                and self.decorator in method.__decorators__) \
            or (    hasattr(method, '__decorator__') \
                and method.__decorator__ == self.decorator)
    
    def getDecoratedMethodsOf(self, cls):
        if cls in self.registry:
            for method in cls.__dict__.values():
                if self.isDecorated(method):
                    yield method
    
    def getAllDecoratedMethods(self):
        for cls in self.getRegisteredClasses():
            for method in self.getDecoratedMethodsOf(cls):
                yield method

Использование:

decoratedRegistry = DecoratedClassRegistry(decorator)

@decoratedRegistry
class A(object):
    @decoratedRegistry
    class B(object):
        @decorator
        def decorated(self):
            pass
        
        def func(self):
            pass
    
    @decorator
    def decorated(self):
        pass
    
    @bind(1)
    def add(self, a, b):
        return a + b
    
    @registering_decorator(bind(1))
    @decorator
    def args(self, *args):
        return args
    
    @decorator
    @registering_decorator(bind(a=1))
    def kwargs(self, **kwargs):
        return kwargs

decoratedRegistry.getRegisteredClasses()
list(decoratedRegistry.getDecoratedMethodsOf(A.B))
list(decoratedRegistry.getDecoratedMethodsOf(A))
list(decoratedRegistry.getAllDecoratedMethods())

Мониторинг нескольких декораторов и применение нескольких реестров декораторов, оставленных в качестве упражнений.

person outis    schedule 17.06.2021