Making a method that is already a class?

I had an interesting problem this morning. I had a base class that looked like this:

# base.py class Base(object): @classmethod def exists(cls, **kwargs): # do some work pass 

And a decorator module that looks like this:

 # caching.py # actual caching decorator def cached(ttl): # complicated def cached_model(ttl=300): def closure(model_class): # ... # eventually: exists_decorator = cached(ttl=ttl) model_class.exists = exists_decorator(model_class.exists)) return model_class return closure 

Here is my subclass model:

 @cached_model(ttl=300) class Model(Base): pass 

The thing is, when I actually call Model.exists, I get complaints about the wrong number of arguments! Checking the arguments in the decorator shows that nothing strange is happening - the arguments are exactly what I expect, and they match the signature of the method. How to add additional decorators to a method already decorated with classmethod ?

Not all models are cached, but the exists () method is present on each model as a class method, so redefining decorators is not an option: cached_model can add a classmethod to exists (), but then what does make () a class method on undisclosed models?

+6
source share
4 answers

The classmethod decorator actually adds the class argument to method calls in certain circumstances, as far as I can tell, in addition to binding the method to the class. The solution was to edit the closure of my class:

 def cached_model(ttl=300): def closure(model_class): # ... # eventually: exists_decorator = cached(ttl=ttl, cache_key=exists_cache_key) model_class.exists = classmethod(exists_decorator(model_class.exists.im_func)) return model_class return closure 

The im_func property seems to get a reference to the original function, which allows me to embrace and decorate the original function with my cache decoder, and then wrap this whole mess in a classmethod call. Synopsis, classmethod scenery cannot be stackable because arguments seem to be introduced.

+1
source

In Python, when a method is declared, in the body of the function it is exactly like a function - as soon as the class is parsed and exists, retrieving the method through ".". the operator converts this function - on the fly - into a method. This conversion adds the first parameter to the method (if it is not a static method) -

So:

 >>> class A(object): ... def b(self): ... pass ... >>> Ab is Ab False 

Make sure that each extraction of attribute โ€œbโ€ in โ€œAโ€ gives a different instance of โ€œmethod object bโ€

 >>> Ab <unbound method Ab> 

The original function "b" can be obtained without any trasnform if you do

 >>> A.__dict__["b"] <function b at 0xe36230> 

The same thing happens for the function decorated with @classmethod , and the value "class" is added to the parameter list when it is extracted from A.

The decorators @classmethod and @staticmethod main function in a different descriptor than the regular instance method. The classmethod object โ€” what a function is when it is wrapped with classmethod โ€” is a handle object that has a __get__ method that returns a function that wraps the base function and adds the cls parameter before everyone else.

Any additional decorator for @classmethod should โ€œknowโ€, it is actually dealing with a handle object, not a function. -

 >>> class A(object): ... @classmethod ... def b(cls): ... print b ... >>> A.__dict__["b"] <classmethod object at 0xd97a28> 

Thus, it is much easier to let the @classmethod decorator be the last to be applied to the method (the first on the stack) so that other decorators work on a simple function (knowing that the cls argument will be inserted as the first).

+5
source

Thanks to jsbueno for getting info on Python. I searched for the answer to this question, based on the case of decorating all the methods of the class . Based on the search for the answer to this question and the answer of jsbueno, I was able to compile something like:

 def for_all_methods(decorator): def decorate(cls): for attr in dir(cls): possible_method = getattr(cls, attr) if not callable(possible_method): continue # staticmethod if not hasattr(possible_method, "__self__"): raw_function = cls.__dict__[attr].__func__ decorated_method = decorator(raw_function) decorated_method = staticmethod(decorated_method) # classmethod elif type(possible_method.__self__) == type: raw_function = cls.__dict__[attr].__func__ decorated_method = decorator(raw_function) decorated_method = classmethod(decorated_method) # instance method elif possible_method.__self__ is None: decorated_method = decorator(possible_method) setattr(cls, attr, decorated_method) return cls return decorate 

There's a bit of redundancy and a few options you could use to cut it a bit.

+2
source

Just a functional example to add to Scott Lobdell a great answer ...

messages.py

 from distutils.cmd import Command import functools import unittest def for_all_methods(decorator): def decorate(cls): for attr in cls.__dict__: possible_method = getattr(cls, attr) if not callable(possible_method): continue # staticmethod if not hasattr(possible_method, "__self__"): raw_function = cls.__dict__[attr].__func__ decorated_method = decorator(raw_function) decorated_method = staticmethod(decorated_method) # classmethod if type(possible_method.__self__) == type: raw_function = cls.__dict__[attr].__func__ decorated_method = decorator(raw_function) decorated_method = classmethod(decorated_method) # instance method elif possible_method.__self__ is None: decorated_method = decorator(possible_method) setattr(cls, attr, decorated_method) return cls return decorate def add_arguments(func): """ The add_arguments decorator simply add the passed in arguments (args and kwargs) the returned error message. """ @functools.wraps(func) def wrapped(self, *args, **kwargs): try: message = func(self, *args, **kwargs) message = ''.join([message, "[ args:'", str(args), "'] ", "[ kwargs:'", str(kwargs), "' ] " ]) return message except Exception as e: err_message = ''.join(["errorhandler.messages.MESSAGE: '", str(func), "(", str(args), str(kwargs), ")' ", "FAILED FOR UNKNOWN REASON. ", " [ ORIGINAL ERROR: ", str(e), " ] " ]) return err_message return wrapped @for_all_methods(add_arguments) class MESSAGE(object): """ log.error(MSG.triggerPhrase(args, kwargs)) """ @classmethod def TEMPLATE(self, *args, **kwargs): message = "This is a template of a pre-digested message." return message 

Using

 from messages import MESSAGE if __name__ == '__main__': result = MESSAGE.TEMPLATE(1,2,test=3) print result 

Exit

 This is a template of a pre-digested message.[ args:'(1, 2)'] [ kwargs:'{'test': 3}' ] 
0
source

Source: https://habr.com/ru/post/906694/


All Articles