⬆️ ⬇️

Understand the decorators in Python'e, step by step. Step 2



And again, good day to all readers!

Thank you for your interest in the first part of the translation, I hope the second will not disappoint you either.




So, in the first part of this article we made a basic acquaintance with the decorators, the principles of their work, and even wrote our own hand.

However, all the decorators that we had previously considered did not have one very important functional - passing the arguments of the function being decorated.

Well, correct this misunderstanding!





Passing ("forwarding") arguments to the function being decorated



No black magic, all we need is to actually pass the arguments on!

def a_decorator_passing_arguments(function_to_decorate): def a_wrapper_accepting_arguments(arg1, arg2): #    print ",   :", arg1, arg2 function_to_decorate(arg1, arg2) return a_wrapper_accepting_arguments # ,    ,   , #    "",         #      @a_decorator_passing_arguments def print_full_name(first_name, last_name): print " ", first_name, last_name print_full_name("", "") # : # ,   :   #     # * 


* - Approx. translator: Peter Venkman - the name of one of the Ghostbusters, the main character of the cult film of the same name .

')

Decorating methods



One of the important facts to be understood is that the functions and methods in Python are almost the same, except that the methods always expect the object to be a reference to the object ( self ) as the first parameter. This means that we can create decorators for methods as well as for functions, just remembering about self .

 def method_friendly_decorator(method_to_decorate): def wrapper(self, lie): lie = lie - 3 # ,  -     :-) return method_to_decorate(self, lie) return wrapper class Lucy(object): def __init__(self): self.age = 32 @method_friendly_decorator def sayYourAge(self, lie): print " %s,     ?" % (self.age + lie) l = Lucy() l.sayYourAge(-3) # :  26,     ? 


Of course, if we create the most general decorator and we want it to be applied to any function or method, then you should use the fact that * args unpacks the list of args , and ** kwargs unpacks the kwargs dictionary:

 def a_decorator_passing_arbitrary_arguments(function_to_decorate): #  ""    def a_wrapper_accepting_arbitrary_arguments(*args, **kwargs): print "   -?:" print args print kwargs #    *args  **kwargs #        ,    : # http://www.saltycrane.com/blog/2008/01/how-to-use-args-and-kwargs-in-python/ function_to_decorate(*args, **kwargs) return a_wrapper_accepting_arbitrary_arguments @a_decorator_passing_arbitrary_arguments def function_with_no_argument(): print "Python is cool, no argument here." #   ,   :) function_with_no_argument() # : #    -?: # () # {} # Python is cool, no argument here. @a_decorator_passing_arbitrary_arguments def function_with_arguments(a, b, c): print a, b, c function_with_arguments(1,2,3) # : #    -?: # (1, 2, 3) # {} # 1 2 3 @a_decorator_passing_arbitrary_arguments def function_with_named_arguments(a, b, c, platypus=" ?"): print "  %s, %s  %s ? %s" %\ (a, b, c, platypus) function_with_named_arguments("", "", "", platypus="!") # : #    -?: # ('', '', '') # {'platypus': '!'} #   ,    ? ! class Mary(object): def __init__(self): self.age = 31 @a_decorator_passing_arbitrary_arguments def sayYourAge(self, lie=-3): #        print " %s,     ?" % (self.age + lie) m = Mary() m.sayYourAge() # : #    -?: # (<__main__ .Mary object at 0xb7d303ac>,) # {} #  28,     ? 




Calling a decorator with various arguments



Great, we figured it out. What do you say now about trying to call decorators with different arguments?



This is not as simple as it seems, since the decorator must take a function as an argument, and we cannot just pass it on to something else.

So, before showing you the solution, I would like to refresh what we already know:

 #  -    def my_decorator(func): print "  " def wrapper(): print " - ,  " func() return wrapper #  ,    ,   "@"-: def lazy_function(): print "zzzzzzzz" decorated_function = my_decorator(lazy_function) # :    #    "  ",     ,   : #  .   @my_decorator def lazy_function(): print "zzzzzzzz" # :    


As we can see, these are two similar actions. When we write
 @my_decorator 
- we simply say to the interpreter "to call a function called my_decorator ". This is an important point, because this name can either lead us directly to the decorator ... and no!

Let's do something scary :)

 def decorator_maker(): print "  !     : "+\ "      ." def my_decorator(func): print " - !     :    ." def wrapped(): print (" -    . " "         . " "     .") return func() print "   ." return wrapped print "  ." return my_decorator #    .        new_decorator = decorator_maker() # : #   !     :       . #   . #    def decorated_function(): print " -  ." decorated_function = new_decorator(decorated_function) # : #  - !     :    . #    . #    : decorated_function() # : #  -    .          . #      . #  -  . 


Long Long Rewrite this code without using intermediate variables:

 def decorated_function(): print " -  ." decorated_function = decorator_maker()(decorated_function) # : #   !     :       . #   . #  - !     :    . #    . # : decorated_function() # : #  -    .          . #      . #  -  . 


And now again, even shorter:

 @decorator_maker() def decorated_function(): print "I am the decorated function." # : #   !     :       . #   . #  - !     :    . #    . #  : decorated_function() # : #  -    .          . #      . #  -  . 


Did you notice that we called the function after the "@" sign? :)



Let's return, finally, to the arguments of the decorators, because if we use the function to create decorators on the fly, we can pass any arguments to it, right?

 def decorator_maker_with_arguments(decorator_arg1, decorator_arg2): print "  !     :", decorator_arg1, decorator_arg2 def my_decorator(func): print " - .         :", decorator_arg1, decorator_arg2 #       ! def wrapped(function_arg1, function_arg2) : print (" -    .\n" "      : \n" "\t-  : {0} {1}\n" "\t-  : {2} {3}\n" "      " .format(decorator_arg1, decorator_arg2, function_arg1, function_arg2)) return func(function_arg1, function_arg2) return wrapped return my_decorator @decorator_maker_with_arguments("", "") def decorated_function_with_arguments(function_arg1, function_arg2): print (" -         : {0}" " {1}".format(function_arg1, function_arg2)) decorated_function_with_arguments("", "") # : #   !     :   #  - .         :   #  -    . #       : # -  :   # -  :   #        #  -         :   


* - Approx. translator: in this example, the author mentions the names of the main characters of the popular series “The Big Bang Theory” .

Here it is, the desired decorator, which can pass arbitrary arguments.

Of course, any variables can be arguments:

 c1 = "" c2 = "" @decorator_maker_with_arguments("", c1) def decorated_function_with_arguments(function_arg1, function_arg2): print (" -         : {0}" " {1}".format(function_arg1, function_arg2)) decorated_function_with_arguments(c2, "") # : #   !     :   #  - .         :   #  -    . #       : # -  :   # -  :   #        #  -         :   


Thus, we can pass any arguments to the decorator as a normal function. We can use and unpack via * args and ** kwargs if necessary.

But you must always keep in mind that the decorator is called exactly once . Exactly at the moment when Python imports your script. After that, we can no longer change the arguments with which.

When we write " import x " all functions from x are decorated immediately , and we can no longer change anything.



A little practice: write decorator decorator



If you read up to this point and still in the ranks - here is a bonus from me.

This small trick will allow you to turn any ordinary decorator into a decorator who takes arguments.

Initially, to get the decorator to take arguments, we created it using another function.

We wrapped our decorator.

Do we have something to wrap the function?

Exactly, decorators!



Let's have a little fun and write a decorator for decorators:

 def decorator_with_args(decorator_to_enhance): """        .     ,    .    .        ,       ,    ,  ,    . """ #     ,      : def decorator_maker(*args, **kwargs): #    ,      # ,    ,   "" def decorator_wrapper(func): #   ,     , ,    #   ( ). #    ,        # decorator(func, *args, **kwargs) # ,     return decorator_to_enhance(func, *args, **kwargs) return decorator_wrapper return decorator_maker 


It can be used like this:

 #   ,         :-) #   ,      "decorator(func, *args, **kwargs)" @decorator_with_args def decorated_decorator(func, *args, **kwargs): def wrapper(function_arg1, function_arg2): print "  ...:", args, kwargs return func(function_arg1, function_arg2) return wrapper #       ,   : @decorated_decorator(42, 404, 1024) def decorated_function(function_arg1, function_arg2): print "", function_arg1, function_arg2 decorated_function(" ", " ") # : #   ...: (42, 404, 1024) {} #      # ! 


I think I know how you feel right now.

The last time you experienced this feeling was listening to how they say to you: “To understand recursion, you must first understand recursion.”

But now you are glad that you have dealt with this?;)



Recommendations for working with decorators





The last problem was partially solved in Python 2.5, adding a functools module to the standard library, which includes functools.wraps , which copies all the information about the function being wrapped (its name, from which district it is, its docstrings, etc.) to the wrapper function .

The fun fact is that functools.wraps is in itself a decorator.

 #   ,     __name__ . def foo(): print "foo" print foo.__name__ # : foo # ,     : def bar(func): def wrapper(): print "bar" return func() return wrapper @bar def foo(): print "foo" print foo.__name__ # : wrapper # "functools"      import functools def bar(func): #  "wrapper"  "func" #   : @functools.wraps(func) def wrapper(): print "bar" return func() return wrapper @bar def foo(): print "foo" print foo.__name__ # : foo 


How can decorators be used?



And finally, I would like to answer the question that I often hear: why do we need decorators? How can they be used?

Decorators can be used to extend the functionality of functions from third-party libraries (the code of which we cannot change), or to simplify debugging (we don’t want to change the code that has not yet been settled).

It is also useful to use decorators to extend various functions with the same code, without rewriting it again each time, for example:

 def benchmark(func): """ ,  ,     . """ import time def wrapper(*args, **kwargs): t = time.clock() res = func(*args, **kwargs) print func.__name__, time.clock() - t return res return wrapper def logging(func): """ ,   . (,    ,      !) """ def wrapper(*args, **kwargs): res = func(*args, **kwargs) print func.__name__, args, kwargs return res return wrapper def counter(func): """ ,       . """ def wrapper(*args, **kwargs): wrapper.count += 1 res = func(*args, **kwargs) print "{0}  : {1}x".format(func.__name__, wrapper.count) return res wrapper.count = 0 return wrapper @benchmark @logging @counter def reverse_string(string): return str(reversed(string)) print reverse_string("     ") print reverse_string("A man, a plan, a canoe, pasta, heros, rajahs, a coloratura, maps, snipe, percale, macaroni, a gag, a banana bag, a tan, a tag, a banana bag again (or a camel), a crepe, pins, Spam, a rut, a Rolo, cash, a jar, sore hats, a peon, a canal: Panama!") # : # reverse_string ('     ',) {} # wrapper 0.0 # reverse_string  : 1x #       # reverse_string ('A man, a plan, a canoe, pasta, heros, rajahs, a coloratura, maps, snipe, percale, macaroni, a gag, a banana bag, a tan, a tag, a banana bag again (or a camel), a crepe, pins, Spam, a rut, a Rolo, cash, a jar, sore hats, a peon, a canal: Panama!',) {} # wrapper 0.0 # reverse_string  : 2x # !amanaP :lanac a ,noep a ,stah eros ,raj a ,hsac ,oloR a ,tur a ,mapS ,snip ,eperc a ,)lemac a ro( niaga gab ananab a ,gat a ,nat a ,gab ananab a ,gag a ,inoracam ,elacrep ,epins ,spam ,arutaroloc a ,shajar ,soreh ,atsap ,eonac a ,nalp a ,nam A 


Thus, decorators can be applied to any function by expanding its functionality and without rewriting a single line of code!

 import httplib @benchmark @logging @counter def get_random_futurama_quote(): conn = httplib.HTTPConnection("slashdot.org:80") conn.request("HEAD", "/index.html") for key, value in conn.getresponse().getheaders(): if key.startswith("xb") or key.startswith("xf"): return value return ", ...  !" print get_random_futurama_quote() print get_random_futurama_quote() #outputs: #get_random_futurama_quote () {} #wrapper 0.02 #get_random_futurama_quote  : 1x #The laws of science be a harsh mistress. #get_random_futurama_quote () {} #wrapper 0.01 #get_random_futurama_quote  : 2x #Curse you, merciful Poseidon! 


Python includes such decorators as property, staticmethod , etc.

In Django, decorators are used to control caching, control access rights and define address handlers. In Twisted, to create fake asynchronous inline calls.

Decorators open the widest scope for experiments! And I hope that this article will help you in its development!

Thanks for attention!



Content:



Note translator:

I would be grateful for any comments on the translation and design. I hope the second part of the article will seem to you more obscure and useful than the first.

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



All Articles