I don't really need to do this, but was just wondering, is there a way to bind a decorator to all functions within a class generically, rather than explicitly stating it for every function.
I sup开发者_运维知识库pose it then becomes a kind of aspect, rather than a decorator and it does feel a bit odd, but was thinking for something like timing or auth it'd be pretty neat.
The cleanest way to do this, or to do other modifications to a class definition, is to define a metaclass.
Alternatively, just apply your decorator at the end of the class definition using inspect
:
import inspect
class Something:
def foo(self):
pass
for name, fn in inspect.getmembers(Something, inspect.isfunction):
setattr(Something, name, decorator(fn))
In practice of course you'll want to apply your decorator more selectively. As soon as you want to decorate all but one method you'll discover that it is easier and more flexible just to use the decorator syntax in the traditional way.
Everytime you think of changing class definition, you can either use the class decorator or metaclass. e.g. using metaclass
import types
class DecoMeta(type):
def __new__(cls, name, bases, attrs):
for attr_name, attr_value in attrs.iteritems():
if isinstance(attr_value, types.FunctionType):
attrs[attr_name] = cls.deco(attr_value)
return super(DecoMeta, cls).__new__(cls, name, bases, attrs)
@classmethod
def deco(cls, func):
def wrapper(*args, **kwargs):
print "before",func.func_name
result = func(*args, **kwargs)
print "after",func.func_name
return result
return wrapper
class MyKlass(object):
__metaclass__ = DecoMeta
def func1(self):
pass
MyKlass().func1()
Output:
before func1
after func1
Note: it will not decorate staticmethod and classmethod
Following code works for python2.x and 3.x
import inspect
def decorator_for_func(orig_func):
def decorator(*args, **kwargs):
print("Decorating wrapper called for method %s" % orig_func.__name__)
result = orig_func(*args, **kwargs)
return result
return decorator
def decorator_for_class(cls):
for name, method in inspect.getmembers(cls):
if (not inspect.ismethod(method) and not inspect.isfunction(method)) or inspect.isbuiltin(method):
continue
print("Decorating function %s" % name)
setattr(cls, name, decorator_for_func(method))
return cls
@decorator_for_class
class decorated_class:
def method1(self, arg, **kwargs):
print("Method 1 called with arg %s" % arg)
def method2(self, arg):
print("Method 2 called with arg %s" % arg)
d=decorated_class()
d.method1(1, a=10)
d.method2(2)
Update for Python 3:
import types
class DecoMeta(type):
def __new__(cls, name, bases, attrs):
for attr_name, attr_value in attrs.items():
if isinstance(attr_value, types.FunctionType):
attrs[attr_name] = cls.deco(attr_value)
return super().__new__(cls, name, bases, attrs)
@classmethod
def deco(cls, func):
def wrapper(*args, **kwargs):
print("before",func.__name__)
result = func(*args, **kwargs)
print("after",func.__name__)
return result
return wrapper
(and thanks to Duncan for this)
Of course that the metaclasses are the most pythonic way to go when you want to modify the way that python creates the objects. Which can be done by overriding the __new__
method of your class. But there are some points around this problem (specially for python 3.X) that I'd like to mention:
types.FunctionType
doesn't protect the special methods from being decorated, as they are function types. As a more general way you can just decorate the objects which their names are not started with double underscore (__
). One other benefit of this method is that it also covers those objects that exist in namespace and starts with__
but are not function like__qualname__
,__module__
, etc.The
namespace
argument in__new__
's header doesn't contain class attributes within the__init__
. The reason is that the__new__
executes before the__init__
(initializing).It's not necessary to use a
classmethod
as the decorator, as in most of the times you import your decorator from another module.- If your class is contain a global item (out side of the
__init__
) for refusing of being decorated alongside checking if the name is not started with__
you can check the type withtypes.FunctionType
to be sure that you're not decorating a non-function object.
Here is a sample metacalss that you can use:
class TheMeta(type):
def __new__(cls, name, bases, namespace, **kwds):
# if your decorator is a class method of the metaclass use
# `my_decorator = cls.my_decorator` in order to invoke the decorator.
namespace = {k: v if k.startswith('__') else my_decorator(v) for k, v in namespace.items()}
return type.__new__(cls, name, bases, namespace)
Demo:
def my_decorator(func):
def wrapper(self, arg):
# You can also use *args instead of (self, arg) and pass the *args
# to the function in following call.
return "the value {} gets modified!!".format(func(self, arg))
return wrapper
class TheMeta(type):
def __new__(cls, name, bases, namespace, **kwds):
# my_decorator = cls.my_decorator (if the decorator is a classmethod)
namespace = {k: v if k.startswith('__') else my_decorator(v) for k, v in namespace.items()}
return type.__new__(cls, name, bases, namespace)
class MyClass(metaclass=TheMeta):
# a = 10
def __init__(self, *args, **kwargs):
self.item = args[0]
self.value = kwargs['value']
def __getattr__(self, attr):
return "This class hasn't provide the attribute {}.".format(attr)
def myfunction_1(self, arg):
return arg ** 2
def myfunction_2(self, arg):
return arg ** 3
myinstance = MyClass(1, 2, value=100)
print(myinstance.myfunction_1(5))
print(myinstance.myfunction_2(2))
print(myinstance.item)
print(myinstance.p)
Output:
the value 25 gets modified!!
the value 8 gets modified!!
1
This class hasn't provide the attribute p. # special method is not decorated.
For checking the 3rd item from the aforementioned notes you can uncomment the line a = 10
and do print(myinstance.a)
and see the result then change the dictionary comprehension in __new__
as follows then see the result again:
namespace = {k: v if k.startswith('__') and not isinstance(v, types.FunctionType)\
else my_decorator(v) for k, v in namespace.items()}
I will repeat my answer here, for a similar issue
It can be done many different ways. I will show how to make it through meta-class, class decorator and inheritance.
by changing meta class
import functools
class Logger(type):
@staticmethod
def _decorator(fun):
@functools.wraps(fun)
def wrapper(*args, **kwargs):
print(fun.__name__, args, kwargs)
return fun(*args, **kwargs)
return wrapper
def __new__(mcs, name, bases, attrs):
for key in attrs.keys():
if callable(attrs[key]):
# if attrs[key] is callable, then we can easily wrap it with decorator
# and substitute in the future attrs
# only for extra clarity (though it is wider type than function)
fun = attrs[key]
attrs[key] = Logger._decorator(fun)
# and then invoke __new__ in type metaclass
return super().__new__(mcs, name, bases, attrs)
class A(metaclass=Logger):
def __init__(self):
self.some_val = "some_val"
def method_first(self, a, b):
print(a, self.some_val)
def another_method(self, c):
print(c)
@staticmethod
def static_method(d):
print(d)
b = A()
# __init__ (<__main__.A object at 0x7f852a52a2b0>,) {}
b.method_first(5, b="Here should be 5")
# method_first (<__main__.A object at 0x7f852a52a2b0>, 5) {'b': 'Here should be 5'}
# 5 some_val
b.method_first(6, b="Here should be 6")
# method_first (<__main__.A object at 0x7f852a52a2b0>, 6) {'b': 'Here should be 6'}
# 6 some_val
b.another_method(7)
# another_method (<__main__.A object at 0x7f852a52a2b0>, 7) {}
# 7
b.static_method(7)
# 7
Also, will show two approaches how to make it without changing meta information of class (through class decorator and class inheritance). The first approach through class decorator put_decorator_on_all_methods
accepts decorator to wrap all member callable objects of class.
def logger(f):
@functools.wraps(f)
def wrapper(*args, **kwargs):
print(f.__name__, args, kwargs)
return f(*args, **kwargs)
return wrapper
def put_decorator_on_all_methods(decorator, cls=None):
if cls is None:
return lambda cls: put_decorator_on_all_methods(decorator, cls)
class Decoratable(cls):
def __init__(self, *args, **kargs):
super().__init__(*args, **kargs)
def __getattribute__(self, item):
value = object.__getattribute__(self, item)
if callable(value):
return decorator(value)
return value
return Decoratable
@put_decorator_on_all_methods(logger)
class A:
def method(self, a, b):
print(a)
def another_method(self, c):
print(c)
@staticmethod
def static_method(d):
print(d)
b = A()
b.method(5, b="Here should be 5")
# >>> method (5,) {'b': 'Here should be 5'}
# >>> 5
b.method(6, b="Here should be 6")
# >>> method (6,) {'b': 'Here should be 6'}
# >>> 6
b.another_method(7)
# >>> another_method (7,) {}
# >>> 7
b.static_method(8)
# >>> static_method (8,) {}
# >>> 8
And, recently, I've come across on the same problem, but I couldn't put decorator on class or change it in any other way, except I was allowed to add such behavior through inheritance only (I am not sure that this is the best choice if you can change codebase as you wish though).
Here class Logger
forces all callable members of subclasses to write information about their invocations, see code below.
class Logger:
def _decorator(self, f):
@functools.wraps(f)
def wrapper(*args, **kwargs):
print(f.__name__, args, kwargs)
return f(*args, **kwargs)
return wrapper
def __getattribute__(self, item):
value = object.__getattribute__(self, item)
if callable(value):
decorator = object.__getattribute__(self, '_decorator')
return decorator(value)
return value
class A(Logger):
def method(self, a, b):
print(a)
def another_method(self, c):
print(c)
@staticmethod
def static_method(d):
print(d)
b = A()
b.method(5, b="Here should be 5")
# >>> method (5,) {'b': 'Here should be 5'}
# >>> 5
b.method(6, b="Here should be 6")
# >>> method (6,) {'b': 'Here should be 6'}
# >>> 6
b.another_method(7)
# >>> another_method (7,) {}
# >>> 7
b.static_method(7)
# >>> static_method (7,) {}
# >>> 7
Or more abstractly, you can instantiate base class based on some decorator.
def decorator(f):
@functools.wraps(f)
def wrapper(*args, **kwargs):
print(f.__name__, args, kwargs)
return f(*args, **kwargs)
return wrapper
class Decoratable:
def __init__(self, dec):
self._decorator = dec
def __getattribute__(self, item):
value = object.__getattribute__(self, item)
if callable(value):
decorator = object.__getattribute__(self, '_decorator')
return decorator(value)
return value
class A(Decoratable):
def __init__(self, dec):
super().__init__(dec)
def method(self, a, b):
print(a)
def another_method(self, c):
print(c)
@staticmethod
def static_method(d):
print(d)
b = A(decorator)
b.method(5, b="Here should be 5")
# >>> method (5,) {'b': 'Here should be 5'}
# >>> 5
b.method(6, b="Here should be 6")
# >>> method (6,) {'b': 'Here should be 6'}
# >>> 6
b.another_method(7)
# >>> another_method (7,) {}
# >>> 7
b.static_method(7)
# >>> static_method (7,) {}
# >>> 7
There's another slightly similar thing you might want to do in some cases. Sometimes you want to trigger the attachment for something like debugging and not on all the classes but for every method of an object you might want a record of what it's doing.
def start_debugging():
import functools
import datetime
filename = "debug-{date:%Y-%m-%d_%H_%M_%S}.txt".format(date=datetime.datetime.now())
debug_file = open(filename, "a")
debug_file.write("\nDebug.\n")
def debug(func):
@functools.wraps(func)
def wrapper_debug(*args, **kwargs):
args_repr = [repr(a) for a in args] # 1
kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()] # 2
signature = ", ".join(args_repr + kwargs_repr) # 3
debug_file.write(f"Calling {func.__name__}({signature})\n")
value = func(*args, **kwargs)
debug_file.write(f"{func.__name__!r} returned {value!r}\n") # 4
debug_file.flush()
return value
return wrapper_debug
for obj in (self):
for attr in dir(obj):
if attr.startswith('_'):
continue
fn = getattr(obj, attr)
if not isinstance(fn, types.FunctionType) and \
not isinstance(fn, types.MethodType):
continue
setattr(obj, attr, debug(fn))
This function will go through some objects (only self currently) and replace all functions and methods that do not start with _ with a debugging decorator.
The method used for this of just iterating the dir(self) is not addressed above but totally works. And can be called externally from the object and much more arbitrarily.
In Python 3 you could also write a simple function that overwrites/applies a decorator to certain methods like so:
from functools import wraps
from types import MethodType
def logged(func):
@wraps(func)
def wrapper(*args, **kwargs):
res = func(*args, **kwargs)
print("logging:", func.__name__, res)
return res
return wrapper
class Test:
def foo(self):
return 42
...
def aspectize(cls, decorator):
for name, func in cls.__dict__.items():
if not name.startswith("__"):
setattr(cls, name, MethodType(decorator(func), cls)) # MethodType is key
aspectize(Test, logged)
t = Test()
t.foo() # printing "logging: foo 42"; returning 42
I came to this question from:
How to decorate all functions of a class without typing it over and over for each method?
And I want add a one note:
Answers with class decorators or repalcing class methods like this one:
https://stackoverflow.com/a/6307868/11277611
Will not work with staticmethod
.
You will get TypeError, unexpected argument
because your method will get self/cls
as first argument.
Probably:
Decorated class doesn't know about decorators of self methods and can't be distincted even with inspect.ismethod
.
I come to such quickfix:
I'm not checked it closely but it passes my (no so comprehensive) tests.
Using dynamically decorators is already a bad approach, so, it must be okay as temporary solution.
TLD:TD Add try/exception
to use with staticmethod
def log_sent_data(function):
@functools_wraps(function)
def decorator(*args, **kwargs):
# Quickfix
self, *args = args
try: # If method has self/cls/descriptor
result = function(self, *args, **kwargs)
except TypeError:
if args: # If method is static but has positional args
result = function(*args, **kwargs)
else: # If method is static and doesn't has positional args
result = function(**kwargs)
# End of quickfix
return result
return decorator
Combining information from various answers, here's a DecorateMethods metaclass:
class DecorateMethods(type):
""" Decorate all methods of the class with the decorator provided """
def __new__(cls, name, bases, attrs, **kwargs):
try:
decorator = kwargs['decorator']
except KeyError:
raise ValueError('Please provide the "decorator" argument, eg. '
'MyClass(..., metaclass=DecorateMethods, decorator=my_decorator)')
exclude = kwargs.get('exclude', [])
for attr_name, attr_value in attrs.items():
if isinstance(attr_value, types.FunctionType) and \
attr_name not in exclude and \
not attr_name.startswith('__'):
attrs[attr_name] = decorator(attr_value)
return super(DecorateMethods, cls).__new__(cls, name, bases, attrs)
Used as:
class MyClass(metaclass=DecorateMethods, decorator=my_decorator, exclude=["METHOD_TO_BE_EXCLUDED"]):
...
It works nicely with unittests, as opposed to function-based solutions.
Credit to answers in 1, 2 and other answers in this question.
You could override the __getattr__
method. It's not actually attaching a decorator, but it lets you return a decorated method. You'd probably want to do something like this:
class Eggs(object):
def __getattr__(self, attr):
return decorate(getattr(self, `_` + attr))
There's some ugly recursion hiding in there that you'll want to protect against, but that's a start.
精彩评论