Python Tips, Tricks, and Hacks (Part 3)

Original author: David
  • Transfer
In this part of the article, we consider tricks for choosing one of two values ​​based on a logical condition, transmitting and receiving an arbitrary number of function arguments, as well as a common source of errors - the fact that default values ​​of function arguments are calculated only once.

4. Selection 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 = 'TestisTrue' if test else'TestisFalse'
# result = 'TestisTrue'

Alas, this is still a little ugly. You can also use several such constructions in one line:
test1 = False
test2 = True
result = 'Test1isTrue' if test1 else'Test1isFalse, test2 isTrue' if test2 else'Test1and 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 complex things, especially if you use parentheses.

This method is very new, and I have mixed feelings for it. This is a correct, understandable design, I like it ... but it is still ugly, especially when using several nested constructions. Of course, the syntax of all the 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. Moreover, it is no less effective than the “correct” method.

Although inline if / else is a new, more correct way, you should still read the following points. Even if you plan to use Python 2.5, you will come across these methods in the old code. Of course, if you need backward compatibility, it would really be better to look at them.

4.2. Trick and / or

"And" and "or" in Python are complex creatures. Applying and to multiple expressions doesn't just return True or False. It returns the first false expression, or the last of the expressions if all of them are true. The result is expected: if all the expressions are true, the last is returned, which is true; if one of them is false, it returns and converts to False when checking the boolean value.

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

This will not help you if you just check the boolean 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 language operator “test? value_if_true: value_if_false ":
test = True
# test = False
result = test and'TestisTrue' or'TestisFalse'
# теперь result = 'TestisTrue'

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  . Since test = false, or it will skip and return the second expression, 'Test is False'.

Caution, be careful with the average value ("if_true"). If it turns out to be false, an 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 (section 4.1) seems to me less intuitive. If you don’t need backward compatibility, try both methods and see which one you like best. If you can not decide, use the correct 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 indices

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 checking. For strings and other simple elements this is not a problem. But if each of them requires large calculations or input-output operations, the calculation of both expressions is unacceptable. Therefore, I prefer the usual construct or and / or.

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

5. Functions

5.1. The default values ​​for the arguments are calculated only once.

We begin this section with a warning. This problem has 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 = []):
    print stuff
# выводит '[1]'function(2)
# выводит '[1,2]' !!!

The default values ​​for the arguments are calculated only once, at the time the function is defined. 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, waiting for the default value, we get changed.

Solution: do not use mutable objects as default values. You can leave it as it is if you do not change them, but this is a bad idea. Here's how to write the previous example:
def function(item, stuff = None):
    if stuff is None:
        stuff = []
    print stuff
# выводит '[1]'function(2)
# выводит '[2]', как и ожидалось

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

A smart programmer, on the other hand, might turn this into a trick for using static variables, as in C.

5.1.1. Making default values ​​computed every time

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

Just apply this decorator to the function to get the expected results:
@resetDefaults # так мы применяем декоратор
def function(item, stuff = []):
    print stuff
# выводит '[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 are determined (if any), then you need to specify a variable with an asterisk. Python will assign it the value of a list of the remaining (unnamed) arguments:
defdo_something(a, b, c, *args):print a, b, c, args
# выводит '1, 2, 3, (4, 5, 6, 7, 8, 9)'

Why is this needed? For example, a function should take several elements and do the same thing with them (for example, add). You can force the user to pass a list function: sum_all ([1,2,3]). And you can let it 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 "**" at the beginning. Python will assign this variable a dictionary of the named arguments received, except for the required ones:
defdo_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 this? I believe the most common reason is that a function is a wrapper for another function (or functions), and unused named arguments can be passed to another function (see section 5.3).

5.2.1. Clarification
Using named arguments and an arbitrary number of regular arguments after them is apparently impossible, because named arguments must be specified before the "*" parameter. For example, imagine a function:
defdo_something(a, b, c, actually_print = True, *args):if actually_print:
        print a, b, c, args

We have a problem: we cannot pass actually_print as a named argument if we need to pass several unnamed ones. Both of the following 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 regular argument:
do_something(1, 2, 3, True, 4, 5, 6)
# результат: '1, 2, 3, (4, 5, 6)'

5.3. Passing a list or dictionary as multiple arguments

Since you can get the passed arguments in the form of a list or a dictionary, it is not surprising that you can 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(5,2), т. е. 25

And for the dictionary (which is used more often), you need to put two asterisks:
defdo_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}
# печатает 'Something has been done', затем '[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]'

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

Full article in PDF

Also popular now: