I'm writing a logger decorator that can be applied to, among others, classmethods (and theoretically any kind of function or method). My problem is that their parametrization changes according to whether a function is
An example code that works but all but elegant and can be tricked by parametrization:
def some_decorator(func):
def wrapper(*argsWrap, **kwargsWrap):
if isinstance(func, classmethod):
if isinstance(argsWrap[0], SomeClass):
# Called by some_instance.some_method as the first parameter is SomeClass
print(argsWrap[1])
return func.__func__(*argsWrap, **kwargsWrap)
else:
# Called by SomeClass.some_method
print(argsWrap[0])
return func.__func__(SomeClass, *argsWrap, **kwargsWrap)
else:
return func(*argsWrap, **kwargsWrap)
return wrapper
class SomeClass:
@some_decorator
@classmethod
def some_method(cls, some_var: str):
print(some_var)
if __name__ == '__main__':
SomeClass.some_method('Test')
some_instance = SomeClass()
some_instance.some_method('Test2')
Question is simple: is it possible to make a cleaner and safer decision regarding just the parametrization?
Note that for a general purpose decorator (used by others) one cannot say that this must be put below the @classmethod, not above etc.
Also note that this solution is a "bad" workaround in a sense that one can conveniently imagine functions that use more instances of the same class (for example, a typical overloaded operator, like __eq__
works like this).
Also note that the real problem, as I narrowed it, is that a classmethod called upon a class or called upon an instance of a class passes different parametrization (the first parameter being the first arg for a class-bound function and the class reference for an instance-bound function).
I use class decorator, __call__
and __get__
to do the trick.
Here it is:
class LoggerDecorator:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs)
def __get__(self, instance, owner):
if not instance:
return self.func.__get__(owner, owner)
return self.func.__get__(instance, owner)
The key idea behind this implementation is to use the __get__
and __call__
methods within a class decorator to handle different types of function call.
The problem we are facing here is that the decorator is used as a generic way to wrap various functions, but the parameters it receives vary depending on the type of function being called (e.g., classmethod(cls, )
, staticmethod()
, instance method(self, )
). As the author of the topic mentioned, parametrization(e.g. using the type of function's first parameter) as the determining condition for the wrapper function is undoubtedly risky.
Here, instead of using a function decorator, we use a class decorator. The condition for handling the decorator is shifted from the 'parametrization' of a wrapper helper function to the __get__
descriptor, which determines whether the function is being used by an instantiated instance.
__get__
: The __get__
method is called to get the attribute of the owner class (class attribute access) or of an instance of that class (instance attribute access).
Let’s take a "classmethod" as an example and compare the differences in the get method when it's called 'directly' (normal usage) versus when it's called by an instantiated instance:
class LoggerDecorator:
def __init__(self, func):
self.func = func
def __get__(self, instance, owner):
print(f'self: {self}')
print(f'instance: {instance}')
print(f'owner: {owner}')
return self.func.__get__(owner, owner)
class SomeClass:
@LoggerDecorator
@classmethod
def class_method(cls, some_var):
print(f"print some_var: {some_var}")
# Call Directly
SomeClass.class_method("Call Directly")
# Called by instance.
s = SomeClass()
s.class_method("Called by instance")
# OUTPUT
--- Called by directly ---
self: <__main__.LoggerDecorator object at 0x7f36aad05590>
instance: None
owner: <class '__main__.SomeClass'>
print some_var: Call Directly
--- Called by instance ---
self: <__main__.LoggerDecorator object at 0x7f36aad05590>
instance: <__main__.SomeClass object at 0x7f36aad06150>
owner: <class '__main__.SomeClass'>
print some_var: Called by instance
We can observe that if called directly, the instance will be None
. And the result is the same for a static method (an instance is needed to call it). Therefore, we can use whether the instance in __get__
is None
as a determining factor.
__call__
: The __call__
method is responsible for handling the decoration of "standalone functions" or any function that does not belong to a class. When we decorate a function like this:
@LoggerDecorator
def standalone_function():
return
which is equal to
LoggerDecorator(standalone_function)
and this triggers the __call__
method.
class LoggerDecorator:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs)
def __get__(self, instance, owner):
print(f'self: {self}')
print(f'instance: {instance}')
print(f'owner: {owner}')
if not instance:
return self.func.__get__(owner, owner)
return self.func.__get__(instance, owner)
class SomeClass:
@LoggerDecorator
@classmethod
def class_method(cls, some_var: str, *args, **kwargs):
print(f"Class method: {some_var}")
print(f"Class method args: {args}")
print(f"Class method kwargs: {kwargs}")
@LoggerDecorator
def instance_method(self, some_var: str, *args, **kwargs):
print(f"Instance method: {some_var}")
print(f"Instance method args: {args}")
print(f"Instance method kwargs: {kwargs}")
@LoggerDecorator
@staticmethod
def static_method(some_var: str, *args, **kwargs):
print(f"Static method: {some_var}")
print(f"Static method args: {args}")
print(f"Static method kwargs: {kwargs}")
@LoggerDecorator
def standalone_function(some_var: str, *args, **kwargs):
print(f"Standalone function: {some_var}")
print(f"Standalone function args: {args}")
print(f"Standalone function kwargs: {kwargs}")
if __name__ == '__main__':
### Class Method ###
# Call Directly
print("### Class Method ###")
print("--- Called by directly ---")
SomeClass.class_method("Test1", "class_directly", a1="class_a", b1="class_b")
# Called by instance.
print("")
print("--- Called by instance ---")
s = SomeClass()
s.class_method("Test2", "class_by_instance", a1="class_a", b1="class_b")
### Static Method ###
# Call Directly
print("")
print("### Static Method ###")
print("--- Called by directly ---")
SomeClass.static_method("Test3", "static_directly", a1="static_a", b1="static_b")
# Called by instance.
print("")
print("--- Called by instance ---")
s.static_method("Test4", "static_by_instance", a1="static_a", b1="static_b")
### Instance Method ###
# Call only by instance
print("")
print("### Instance Method ###")
s.instance_method("Test5", "instance", a1="instance_a", b1="instance_b")
### Standalone Function
print("")
print("### Standalone Function ###")
standalone_function("Test6", "standalone", a1="standalone_a", b1="standalone_b")
### Class Method ###
--- Called by directly ---
self: <__main__.LoggerDecorator object at 0x7f858c34e390>
instance: None
owner: <class '__main__.SomeClass'>
Class method: Test1
Class method args: ('class_directly',)
Class method kwargs: {'a1': 'class_a', 'b1': 'class_b'}
--- Called by instance ---
self: <__main__.LoggerDecorator object at 0x7f858c34e390>
instance: <__main__.SomeClass object at 0x7f858c34f410>
owner: <class '__main__.SomeClass'>
Class method: Test2
Class method args: ('class_by_instance',)
Class method kwargs: {'a1': 'class_a', 'b1': 'class_b'}
### Static Method ###
--- Called by directly ---
self: <__main__.LoggerDecorator object at 0x7f858c34f0d0>
instance: None
owner: <class '__main__.SomeClass'>
Static method: Test3
Static method args: ('static_directly',)
Static method kwargs: {'a1': 'static_a', 'b1': 'static_b'}
--- Called by instance ---
self: <__main__.LoggerDecorator object at 0x7f858c34f0d0>
instance: <__main__.SomeClass object at 0x7f858c34f410>
owner: <class '__main__.SomeClass'>
Static method: Test4
Static method args: ('static_by_instance',)
Static method kwargs: {'a1': 'static_a', 'b1': 'static_b'}
### Instance Method ###
self: <__main__.LoggerDecorator object at 0x7f858c34e510>
instance: <__main__.SomeClass object at 0x7f858c34f410>
owner: <class '__main__.SomeClass'>
Instance method: Test5
Instance method args: ('instance',)
Instance method kwargs: {'a1': 'instance_a', 'b1': 'instance_b'}
### Standalone Function ###
Standalone function: Test6
Standalone function args: ('standalone',)
Standalone function kwargs: {'a1': 'standalone_a', 'b1': 'standalone_b'}