📜 ⬆️ ⬇️

Python Tips, Tricks, and Hacks (Part 3)

This part of the article discusses the tricks for choosing one of two values ​​based on a logical condition, passing and receiving an arbitrary number of function arguments, as well as a common source of errors - the fact that the default values ​​of the function arguments are calculated only once.

4. Choice of values


4.1. The right way

Starting with version 2.5, Python supports the syntax "value_if_true if test else value_if_false". Thus, you can choose one of two values ​​without resorting to strange syntax and detailed explanations:
test = True # test = False result = 'Test is True' if test else 'Test is False' # result = 'Test is True' 

Alas, it is still a bit ugly. You can also use several such constructions in one line:
 test1 = False test2 = True result = 'Test1 is True' if test1 else 'Test1 is False, test2 is True' if test2 else 'Test1 and Test2 are both False' 

First, the first if / else is executed, and if test1 = false, the second if / else is executed. You can do more complicated things, especially if you use brackets.

This method is quite new, and I have mixed feelings for it. This is a correct, clear construction, I like it ... but it is still ugly, especially when using several nested constructions. Of course, the syntax of all tricks for choosing values ​​is ugly. I have a weakness for the method described below with and / or, now I find it intuitive, now I understand how it works. In addition, it is no less effective than the "correct" method.

Although inline if / else is a new, more correct way, you should still familiarize yourself with the following points. Even if you plan to use Python 2.5, you will find these methods in the old code. Of course, if you need backward compatibility, it’s really better to review them.
')
4.2. Trick and / or

"And" and "or" in Python are complex creations. Applying and to multiple expressions does not just return True or False. It returns the first false expression, or the last expression, if all of them are true. The result is expected: if all expressions are correct, the last one is returned, which is true; if one of them is false, it is returned and converted to False when the Boolean value is checked.

Similarly, the or operation returns the first true value, or the last, if none of them is true.

This does not help you if you just check the logical value of the expression. But you can use and and or for other purposes. My favorite way is to select a value in a style similar to the ternary C operator “test? value_if_true: value_if_false ":
 test = True # test = False result = test and 'Test is True' or 'Test is False' #  result = 'Test is True' 

How it works? If test = true, the and operator skips it and returns the second (last) of the values ​​given to it: 'Test is True' or 'Test is False' . Next, or will return the first true expression, i.e. 'Test is True'.

If test = false, and returns test, test or 'Test is False' will remain. Because test = false, or it will skip it and return the second expression, 'Test is False'.

Caution, be careful with the average ("if_true"). If it turns out to be false, the expression with or will always skip it and return the last value (“if_false”), regardless of the value of test.

After using this method, the correct method (p. 4.1) seems to me less intuitive. If you do not need backward compatibility, try both ways and see which one you like best. If you can not decide, use the right one.

Of course, if you need compatibility with previous versions of Python, the “right” way will not work. In this case, and / or is the best choice in most situations.

4.3. True / False as indexes

Another way to choose from two values ​​is to use True and False as list indices, taking into account the fact that False == 0 and True == 1:
 test = True # test = False result = ['Test is False','Test is True'][test] #  result = 'Test is True' 

This method is more honest, and value_if_true does not have to be true. However, it has a significant drawback: both elements of the list are calculated before the check. For strings and other simple elements, this is not a problem. But if each of them requires large calculations or I / O operations, the calculation of both expressions is unacceptable. Therefore, I prefer the usual construction or and / or.

Also note that this method only works when you are sure that test is a boolean value and not some object. Otherwise, you will have to write bool (test) instead of test so that it works correctly.

5. Functions


5.1. Default values ​​for arguments are evaluated only once.

We start this section with a warning. This problem confused many programmers many times, including me, even after I figured out the problem. It is easy to make a mistake using the default values:
 def function(item, stuff = []): stuff.append(item) print stuff function(1) #  '[1]' function(2) #  '[1,2]' !!! 

The default values ​​for the arguments are calculated only once, at the time of the function definition. Python simply assigns this value to the desired variable each time the function is called. However, it does not check if this value has changed. Therefore, if you change it, the change will be valid for the next function calls. In the previous example, when we added a value to the stuff list, we changed its default value forever. When we call the function again, expecting the default value, we get the change.

Solution: do not use mutable objects as default values. You can leave it as it is if you don’t change it, but this is a bad idea. Here's how to write the previous example:
 def function(item, stuff = None): if stuff is None: stuff = [] stuff.append(item) print stuff function(1) #  '[1]' function(2) #  '[2]',    

None is immutable (in any case, we are not trying to change it), so we protected ourselves from a sudden change in the default value.

On the other hand, an intelligent programmer may turn this into a trick for using static variables, as in C.

5.1.1. We force default values ​​to be calculated every time.

If you do not want to add unnecessary confusion to the function code, you can force the interpreter to re-evaluate the argument values ​​before each call. The following decorator does this:
 from copy import deepcopy def resetDefaults(f): defaults = f.func_defaults def resetter(*args, **kwds): f.func_defaults = deepcopy(defaults) return f(*args, **kwds) resetter.__name__ = f.__name__ return resetter 

Simply apply this decorator to the function to get the expected results:
 @resetDefaults #     def function(item, stuff = []): stuff.append(item) print stuff function(1) #  '[1]' function(2) #  '[2]',    

5.2. Variable number of arguments

Python allows you to use an arbitrary number of arguments in functions. First, the required arguments (if any) are determined, then you need to specify a variable with an asterisk. Python assigns it the value of a list of other (not named) arguments:
 def do_something(a, b, c, *args): print a, b, c, args do_something(1,2,3,4,5,6,7,8,9) #  '1, 2, 3, (4, 5, 6, 7, 8, 9)' 

Why do you need it? For example, a function should take several elements and do the same thing with them (for example, add). You can force the user to transfer a list of functions: sum_all ([1,2,3]). And you can allow to pass an arbitrary number of arguments, then you get a cleaner code: sum_all (1,2,3).

A function can also have a variable number of named arguments. After defining all other arguments, specify the variable with a "**" at the beginning. Python assigns to this variable a dictionary of the received named arguments, besides the required ones:
 def do_something_else(a, b, c, *args, **kwargs): print a, b, c, args, kwargs do_something_else(1,2,3,4,5,6,7,8,9, timeout=1.5) #  '1, 2, 3, (4, 5, 6, 7, 8, 9), {"timeout": 1.5}' 

Why do so? I believe the most common reason is that a function is a wrapper around another function (or functions), and unused named arguments can be transferred to another function (see § 5.3).

5.2.1. Refinement
The use of named arguments and an arbitrary number of ordinary arguments after them, apparently, is impossible, because named arguments must be defined before the "*" parameter. For example, imagine the function:
 def do_something(a, b, c, actually_print = True, *args): if actually_print: print a, b, c, args 

We have a problem: it will not be possible to pass actually_print as a named argument, if you need to pass several unnamed ones. Both of the following options will cause an error:
 do_something(1, 2, 3, 4, 5, actually_print = True) # actually_print    4 (, ?),   # ,  TypeError ('got multiple values for keyword argument') do_something(1, 2, 3, actually_print = True, 4, 5, 6) #      .  SyntaxError.    actually_print    —     : do_something(1, 2, 3, True, 4, 5, 6) # : '1, 2, 3, (4, 5, 6)' 

The only way to set actually_print in this situation is to pass it as a normal argument:
 do_something(1, 2, 3, True, 4, 5, 6) # : '1, 2, 3, (4, 5, 6)' 

5.3. Passing a list or dictionary as several arguments

Since you can get the passed arguments in the form of a list or a dictionary, it is not surprising that you can also pass arguments to a function from a list or a dictionary. The syntax is exactly the same as in the previous paragraph, you need to put an asterisk in front of the list:
 args = [5,2] pow(*args) #  pow(5,2), . . 25 

And for the dictionary (which is used more often) you need to put two asterisks:
 def do_something(actually_do_something=True, print_a_bunch_of_numbers=False): if actually_do_something: print 'Something has been done' # if print_a_bunch_of_numbers: print range(10) kwargs = {'actually_do_something': True, 'print_a_bunch_of_numbers': True} do_something(**kwargs) #  'Something has been done',  '[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]' 

Historical note: in Python prior to version 2.3, the built-in function apply (function, arg_list, keyword_arg_dict) was used for these purposes.

Full article in PDF

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


All Articles