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'}