Может ли Python-декоратор метода экземпляра получить доступ к классу?
Привет, у меня есть что-то вроде следующего. По сути, мне нужно получить доступ к классу метода экземпляра из декоратора, используемого для метода экземпляра в его определении.
def decorator(view):
# do something that requires view's class
print view.im_class
return view
class ModelA(object):
@decorator
def a_method(self):
# do some stuff
pass
Код как есть дает
AttributeError: 'function' object has no attribute 'im_class'
Я нашел похожие вопросы / ответы - Python decorator заставляет функцию забыть, что она принадлежит классу и классу Get в Python decorator - но они полагаются на обходной путь, который захватывает экземпляр во время выполнения путем захвата первого параметра. В моем случае я буду вызывать метод, основываясь на информации, полученной из его класса, поэтому я не могу дождаться поступления вызова.
Спасибо.
15 ответов
Если вы используете Python 2.6 или более позднюю версию, вы можете использовать декоратор класса, возможно, что-то вроде этого (предупреждение: непроверенный код).
def class_decorator(cls):
for name, method in cls.__dict__.iteritems():
if hasattr(method, "use_class"):
# do something with the method and class
print name, cls
return cls
def method_decorator(view):
# mark the method as something that requires view's class
view.use_class = True
return view
@class_decorator
class ModelA(object):
@method_decorator
def a_method(self):
# do some stuff
pass
Декоратор метода помечает метод как интересующий его, добавляя атрибут "use_class" - функции и методы также являются объектами, поэтому вы можете прикрепить к ним дополнительные метаданные.
После того, как класс был создан, декоратор класса затем проходит через все методы и делает все, что нужно с методами, которые были отмечены.
Если вы хотите, чтобы все методы были затронуты, вы можете оставить декоратор метода и просто использовать декоратор класса.
Начиная с Python 3.6 вы можете использовать object.__set_name__
сделать это очень простым способом. Док утверждает, что __set_name__
вызывается во время создания владельца класса- владельца. Вот пример:
class class_decorator:
def __init__(self, fn):
self.fn = fn
def __set_name__(self, owner, name):
# do something with owner, i.e.
print(f"decorating {self.fn} and using {owner}")
self.fn.class_name = owner.__name__
# then replace ourself with the original method
setattr(owner, name, self.fn)
Обратите внимание, что он вызывается во время создания класса:
>>> class A:
... @class_decorator
... def hello(self, x=42):
... return x
...
decorating <function A.hello at 0x7f9bedf66bf8> and using <class '__main__.A'>
>>> A.hello
<function __main__.A.hello(self, x=42)>
>>> A.hello.class_name
'A'
>>> a = A()
>>> a.hello()
42
Если вы хотите узнать больше о том, как создаются классы и, в частности, когда именно __set_name__
называется, вы можете обратиться к документации по "Создание объекта класса".
Как уже отмечали другие, класс не был создан во время вызова декоратора. Однако можно аннотировать объект функции параметрами декоратора, а затем повторно декорировать функцию в метаклассе. __new__
метод. Вам нужно будет получить доступ к функции __dict__
приписывать напрямую, как минимум для меня, func.foo = 1
привело к ошибке AttributeError.
Как предполагает Марк:
- Любой декоратор называется ДО создания класса, поэтому он не известен декоратору.
- Мы можем пометить эти методы и сделать любую необходимую последующую обработку позже.
- У нас есть два варианта постобработки: автоматически в конце определения класса или где-то еще до запуска приложения. Я предпочитаю 1-й вариант, используя базовый класс, но вы также можете следовать 2-му подходу.
Этот код показывает, как это может работать с использованием автоматической постобработки:
def expose(**kw):
"Note that using **kw you can tag the function with any parameters"
def wrap(func):
name = func.func_name
assert not name.startswith('_'), "Only public methods can be exposed"
meta = func.__meta__ = kw
meta['exposed'] = True
return func
return wrap
class Exposable(object):
"Base class to expose instance methods"
_exposable_ = None # Not necessary, just for pylint
class __metaclass__(type):
def __new__(cls, name, bases, state):
methods = state['_exposed_'] = dict()
# inherit bases exposed methods
for base in bases:
methods.update(getattr(base, '_exposed_', {}))
for name, member in state.items():
meta = getattr(member, '__meta__', None)
if meta is not None:
print "Found", name, meta
methods[name] = member
return type.__new__(cls, name, bases, state)
class Foo(Exposable):
@expose(any='parameter will go', inside='__meta__ func attribute')
def foo(self):
pass
class Bar(Exposable):
@expose(hide=True, help='the great bar function')
def bar(self):
pass
class Buzz(Bar):
@expose(hello=False, msg='overriding bar function')
def bar(self):
pass
class Fizz(Foo):
@expose(msg='adding a bar function')
def bar(self):
pass
print('-' * 20)
print("showing exposed methods")
print("Foo: %s" % Foo._exposed_)
print("Bar: %s" % Bar._exposed_)
print("Buzz: %s" % Buzz._exposed_)
print("Fizz: %s" % Fizz._exposed_)
print('-' * 20)
print('examine bar functions')
print("Bar.bar: %s" % Bar.bar.__meta__)
print("Buzz.bar: %s" % Buzz.bar.__meta__)
print("Fizz.bar: %s" % Fizz.bar.__meta__)
Выход дает:
Found foo {'inside': '__meta__ func attribute', 'any': 'parameter will go', 'exposed': True}
Found bar {'hide': True, 'help': 'the great bar function', 'exposed': True}
Found bar {'msg': 'overriding bar function', 'hello': False, 'exposed': True}
Found bar {'msg': 'adding a bar function', 'exposed': True}
--------------------
showing exposed methods
Foo: {'foo': <function foo at 0x7f7da3abb398>}
Bar: {'bar': <function bar at 0x7f7da3abb140>}
Buzz: {'bar': <function bar at 0x7f7da3abb0c8>}
Fizz: {'foo': <function foo at 0x7f7da3abb398>, 'bar': <function bar at 0x7f7da3abb488>}
--------------------
examine bar functions
Bar.bar: {'hide': True, 'help': 'the great bar function', 'exposed': True}
Buzz.bar: {'msg': 'overriding bar function', 'hello': False, 'exposed': True}
Fizz.bar: {'msg': 'adding a bar function', 'exposed': True}
Обратите внимание, что в этом примере:
- Мы можем комментировать любую функцию с любыми произвольными параметрами.
- Каждый класс имеет свои собственные открытые методы.
- Мы также можем наследовать открытые методы.
- методы могут быть переопределены, так как функция экспонирования обновляется.
Надеюсь это поможет
Проблема в том, что когда вызывается декоратор, класс еще не существует. Попробуй это:
def loud_decorator(func):
print("Now decorating %s" % func)
def decorated(*args, **kwargs):
print("Now calling %s with %s,%s" % (func, args, kwargs))
return func(*args, **kwargs)
return decorated
class Foo(object):
class __metaclass__(type):
def __new__(cls, name, bases, dict_):
print("Creating class %s%s with attributes %s" % (name, bases, dict_))
return type.__new__(cls, name, bases, dict_)
@loud_decorator
def hello(self, msg):
print("Hello %s" % msg)
Foo().hello()
Эта программа выведет:
Now decorating <function hello at 0xb74d35dc>
Creating class Foo(<type 'object'>,) with attributes {'__module__': '__main__', '__metaclass__': <class '__main__.__metaclass__'>, 'hello': <function decorated at 0xb74d356c>}
Now calling <function hello at 0xb74d35dc> with (<__main__.Foo object at 0xb74ea1ac>, 'World'),{}
Hello World
Как видите, вам придется придумать другой способ сделать то, что вы хотите.
Как указали муравьи, вы не можете получить ссылку на класс внутри класса. Однако, если вам интересно различать разные классы (не манипулируя действительным объектом типа класса), вы можете передать строку для каждого класса. Вы также можете передать любые другие параметры, которые вам нравятся, с помощью декораторов в стиле класса.
class Decorator(object):
def __init__(self,decoratee_enclosing_class):
self.decoratee_enclosing_class = decoratee_enclosing_class
def __call__(self,original_func):
def new_function(*args,**kwargs):
print 'decorating function in ',self.decoratee_enclosing_class
original_func(*args,**kwargs)
return new_function
class Bar(object):
@Decorator('Bar')
def foo(self):
print 'in foo'
class Baz(object):
@Decorator('Baz')
def foo(self):
print 'in foo'
print 'before instantiating Bar()'
b = Bar()
print 'calling b.foo()'
b.foo()
Печать:
before instantiating Bar()
calling b.foo()
decorating function in Bar
in foo
Что делает flask-classy, так это создает временный кеш, который он хранит в методе, а затем использует что-то еще (тот факт, что Flask зарегистрирует классы, используя register
метод класса), чтобы фактически обернуть метод.
Вы можете повторно использовать этот шаблон, на этот раз, используя метакласс, чтобы вы могли обернуть метод во время импорта.
def route(rule, **options):
"""A decorator that is used to define custom routes for methods in
FlaskView subclasses. The format is exactly the same as Flask's
`@app.route` decorator.
"""
def decorator(f):
# Put the rule cache on the method itself instead of globally
if not hasattr(f, '_rule_cache') or f._rule_cache is None:
f._rule_cache = {f.__name__: [(rule, options)]}
elif not f.__name__ in f._rule_cache:
f._rule_cache[f.__name__] = [(rule, options)]
else:
f._rule_cache[f.__name__].append((rule, options))
return f
return decorator
На самом классе (вы можете сделать то же самое, используя метакласс):
@classmethod
def register(cls, app, route_base=None, subdomain=None, route_prefix=None,
trailing_slash=None):
for name, value in members:
proxy = cls.make_proxy_method(name)
route_name = cls.build_route_name(name)
try:
if hasattr(value, "_rule_cache") and name in value._rule_cache:
for idx, cached_rule in enumerate(value._rule_cache[name]):
# wrap the method here
Источник: https://github.com/apiguy/flask-classy/blob/master/flask_classy.py
Как указывали другие ответы, декоратор - это функциональная вещь, вы не можете получить доступ к классу, к которому принадлежит этот метод, поскольку класс еще не создан. Однако вполне нормально использовать декоратор, чтобы "пометить" функцию, а затем использовать методы метакласса для работы с методом позже, потому что в__new__
stage, класс был создан своим метаклассом.
Вот простой пример:
Мы используем @field
чтобы пометить метод как специальное поле и обработать его в метаклассе.
def field(fn):
"""Mark the method as an extra field"""
fn.is_field = True
return fn
class MetaEndpoint(type):
def __new__(cls, name, bases, attrs):
fields = {}
for k, v in attrs.items():
if inspect.isfunction(v) and getattr(k, "is_field", False):
fields[k] = v
for base in bases:
if hasattr(base, "_fields"):
fields.update(base._fields)
attrs["_fields"] = fields
return type.__new__(cls, name, bases, attrs)
class EndPoint(metaclass=MetaEndpoint):
pass
# Usage
class MyEndPoint(EndPoint):
@field
def foo(self):
return "bar"
e = MyEndPoint()
e._fields # {"foo": ...}
Вот простой пример:
def mod_bar(cls):
# returns modified class
def decorate(fcn):
# returns decorated function
def new_fcn(self):
print self.start_str
print fcn(self)
print self.end_str
return new_fcn
cls.bar = decorate(cls.bar)
return cls
@mod_bar
class Test(object):
def __init__(self):
self.start_str = "starting dec"
self.end_str = "ending dec"
def bar(self):
return "bar"
Выход:
>>> import Test
>>> a = Test()
>>> a.bar()
starting dec
bar
ending dec
Функция не знает, является ли это методом в точке определения, когда выполняется код декоратора. Только когда к нему обращаются через идентификатор класса / экземпляра, он может знать свой класс / экземпляр. Чтобы преодолеть это ограничение, вы можете декорировать по объекту дескриптора, чтобы задержать фактический код декорирования до времени доступа / вызова:
class decorated(object):
def __init__(self, func, type_=None):
self.func = func
self.type = type_
def __get__(self, obj, type_=None):
func = self.func.__get__(obj, type_)
print('accessed %s.%s' % (type_.__name__, func.__name__))
return self.__class__(func, type_)
def __call__(self, *args, **kwargs):
name = '%s.%s' % (self.type.__name__, self.func.__name__)
print('called %s with args=%s kwargs=%s' % (name, args, kwargs))
return self.func(*args, **kwargs)
Это позволяет вам декорировать отдельные (статические | класс) методы:
class Foo(object):
@decorated
def foo(self, a, b):
pass
@decorated
@staticmethod
def bar(a, b):
pass
@decorated
@classmethod
def baz(cls, a, b):
pass
class Bar(Foo):
pass
Теперь вы можете использовать код декоратора для самоанализа...
>>> Foo.foo
accessed Foo.foo
>>> Foo.bar
accessed Foo.bar
>>> Foo.baz
accessed Foo.baz
>>> Bar.foo
accessed Bar.foo
>>> Bar.bar
accessed Bar.bar
>>> Bar.baz
accessed Bar.baz
... и для изменения поведения функции:
>>> Foo().foo(1, 2)
accessed Foo.foo
called Foo.foo with args=(1, 2) kwargs={}
>>> Foo.bar(1, b='bcd')
accessed Foo.bar
called Foo.bar with args=(1,) kwargs={'b': 'bcd'}
>>> Bar.baz(a='abc', b='bcd')
accessed Bar.baz
called Bar.baz with args=() kwargs={'a': 'abc', 'b': 'bcd'}
Это старый вопрос, но наткнулся на венерианца. http://venusian.readthedocs.org/en/latest/
Кажется, у него есть возможность декорировать методы и дать вам доступ к классу и методу при этом. Обратите внимание, что звонит setattr(ob, wrapped.__name__, decorated)
это не типичный способ использования венерианцев и несколько побеждает цель.
В любом случае... приведенный ниже пример завершен и должен работать.
import sys
from functools import wraps
import venusian
def logged(wrapped):
def callback(scanner, name, ob):
@wraps(wrapped)
def decorated(self, *args, **kwargs):
print 'you called method', wrapped.__name__, 'on class', ob.__name__
return wrapped(self, *args, **kwargs)
print 'decorating', '%s.%s' % (ob.__name__, wrapped.__name__)
setattr(ob, wrapped.__name__, decorated)
venusian.attach(wrapped, callback)
return wrapped
class Foo(object):
@logged
def bar(self):
print 'bar'
scanner = venusian.Scanner()
scanner.scan(sys.modules[__name__])
if __name__ == '__main__':
t = Foo()
t.bar()
У вас будет доступ к классу объекта, для которого вызывается метод в украшенном методе, который должен вернуть ваш декоратор. Вот так:
def decorator(method):
# do something that requires view's class
def decorated(self, *args, **kwargs):
print 'My class is %s' % self.__class__
method(self, *args, **kwargs)
return decorated
Используя ваш класс ModelA, вот что это делает:
>>> obj = ModelA()
>>> obj.a_method()
My class is <class '__main__.ModelA'>
Я не думаю, что мы видели здесь решение, позволяющее выполнять инъекцию в экземпляр класса. Однако я только что выпустил проект, который позволяет вам это делать. Например, следующее работает должным образом:
from classy import Routable, get, delete
class UserRoutes(Routable):
"""Inherits from Routable."""
# Note injection here by simply passing values to the constructor. Other injection frameworks also
# supported as there's nothing special about this __init__ method.
def __init__(self, dao: Dao) -> None:
"""Constructor. The Dao is injected here."""
self.__dao = Dao
@get('/user/{name}')
def get_user_by_name(name: str) -> User:
# Use our injected DAO instance.
return self.__dao.get_user_by_name(name)
@delete('/user/{name}')
def delete_user(name: str) -> None:
self.__dao.delete(name)
def main():
args = parse_args()
# Configure the DAO per command line arguments
dao = Dao(args.url, args.user, args.password)
# Simple intuitive injection
user_routes = UserRoutes(dao)
app = FastAPI()
# router member inherited from Routable and configured per the annotations.
app.include_router(user_routes.router)
Вы можете найти его на PyPi и установить через
pip install classy-fastapi
@asterio gonzalez
Я предпочитаю ваш метод, однако его нужно немного изменить, чтобы Python 3 соответствовал новой обработке метакласса (кроме того, в некоторых операторах печати отсутствовали круглые скобки):
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Mon Aug 9 15:27:30 2021
@author: yves
"""
def expose(**kw):
"Note that using **kw you can tag the function with any parameters"
def wrap(func):
name = func.__name__
assert not name.startswith('_'), "Only public methods can be exposed"
meta = func.__meta__ = kw
meta['exposed'] = None
return func
return wrap
class ExposableMetaclass(type):
def __new__(cls, name, bases, state):
methods = state['_exposed_'] = dict()
# inherit bases exposed methods
for base in bases:
methods.update(getattr(base, '_exposed_', {}))
for name, member in state.items():
meta = getattr(member, '__meta__', None)
if meta is not None:
print("Found", name, meta)
methods[name] = member
return type.__new__(cls, name, bases, state)
class Exposable(metaclass=ExposableMetaclass):
"Base class to expose instance methods"
_exposable_ = None # Not necessary, just for pylint
class Foo(Exposable):
@expose(any='parameter will go', inside='__meta__ func attribute')
def foo(self):
pass
class Bar(Exposable):
@expose(hide=True, help='the great bar function')
def bar(self):
pass
class Buzz(Bar):
@expose(hello=False, msg='overriding bar function')
def bar(self):
pass
class Fizz(Foo):
@expose(msg='adding a bar function')
def bar(self):
pass
print('-' * 20)
print("showing exposed methods")
print("Foo: %s" % Foo._exposed_)
print("Bar: %s" % Bar._exposed_)
print("Buzz: %s" % Buzz._exposed_)
print("Fizz: %s" % Fizz._exposed_)
Удовлетворила мои потребности!
Я просто хочу добавить свой пример, поскольку в нем есть все, что я мог придумать для доступа к классу из декорированного метода. Он использует дескриптор, как предлагает @tyrion. Декоратор может принимать аргументы и передавать их дескриптору. Он может иметь дело как с методом в классе, так и с функцией без класса.
import datetime as dt
import functools
def dec(arg1):
class Timed(object):
local_arg = arg1
def __init__(self, f):
functools.update_wrapper(self, f)
self.func = f
def __set_name__(self, owner, name):
# doing something fancy with owner and name
print('owner type', owner.my_type())
print('my arg', self.local_arg)
def __call__(self, *args, **kwargs):
start = dt.datetime.now()
ret = self.func(*args, **kwargs)
time = dt.datetime.now() - start
ret["time"] = time
return ret
def __get__(self, instance, owner):
from functools import partial
return partial(self.__call__, instance)
return Timed
class Test(object):
def __init__(self):
super(Test, self).__init__()
@classmethod
def my_type(cls):
return 'owner'
@dec(arg1='a')
def decorated(self, *args, **kwargs):
print(self)
print(args)
print(kwargs)
return dict()
def call_deco(self):
self.decorated("Hello", world="World")
@dec(arg1='a function')
def another(*args, **kwargs):
print(args)
print(kwargs)
return dict()
if __name__ == "__main__":
t = Test()
ret = t.call_deco()
another('Ni hao', world="shi jie")