1.5. Write Helper Functions Instead of Complex Expressions¶
Python’s pithy syntax makes it easy to write single-line expressions that implement a lot of logic. For example, say that I want to decode the query string from a URL. Here, each query string parameter represents an integer value:
>>> from urllib.parse import parse_qs
>>>
>>> my_values = parse_qs('red=5&blue=0&green=',
>>> keep_blank_values=True)
>>> print(repr(my_values))
{'red': ['5'], 'blue': ['0'], 'green': ['']}
Some query string parameters may have multiple values, some may have single values, some may be present but have blank values, and some may be missing entirely. Using the get method on the result dictionary will return different values in each circumstance:
>>> print('Red: ', my_values.get('red'))
>>> print('Green: ', my_values.get('green'))
>>> print('Opacity: ', my_values.get('opacity'))
Red: ['5']
Green: ['']
Opacity: None
It’d be nice if a default value of 0 were assigned when a parameter isn’t supplied or is blank. I might choose to do this with Boolean expressions because it feels like this logic doesn’t merit a whole if statement or helper function quite yet.
Python’s syntax makes this choice all too easy. The trick here is that the empty string, the empty list, and zero all evaluate to False implicitly. Thus, the expressions below will evaluate to the subexpression after the or operator when the first subexpression is False:
>>> # For query string 'red=5&blue=0&green='
>>> red = my_values.get('red', [''])[0] or 0
>>> green = my_values.get('green', [''])[0] or 0
>>> opacity = my_values.get('opacity', [''])[0] or 0
>>> print(f'Red: {red!r}')
>>> print(f'Green: {green!r}')
>>> print(f'Opacity: {opacity!r}')
Red: '5'
Green: 0
Opacity: 0
The red case works because the key is present in the my_values dictionary. The value is a list with one member: the string ‘5’. This string implicitly evaluates to True, so red is assigned to the first part of the or expression.
The green case works because the value in the my_values dictionary is a list with one member: an empty string. The empty string implicitly evaluates to False, causing the or expression to evaluate to 0.
The opacity case works because the value in the my_values dictionary is missing altogether. The behavior of the get method is to return its second argument if the key doesn’t exist in the dictionary (see Item 16: “Prefer get Over in and KeyError to Handle Missing Dictionary Keys”). The default value in this case is a list with one member: an empty string. When opacity isn’t found in the dictionary, this code does exactly the same thing as the green case.
However, this expression is difficult to read, and it still doesn’t do everything I need. I’d also want to ensure that all the parameter values are converted to integers so I can immediately use them in mathematical expressions. To do that, I’d wrap each expression with the int built-in function to parse the string as an integer:
>>> red = int(my_values.get('red', [''])[0] or 0)
This is now extremely hard to read. There’s so much visual noise. The code isn’t approachable. A new reader of the code would have to spend too much time picking apart the expression to figure out what it actually does. Even though it’s nice to keep things short, it’s not worth trying to fit this all on one line.
Python has if/else conditional—or ternary—expressions to make cases like this clearer while keeping the code short:
>>> red_str = my_values.get('red', [''])
>>> red = int(red_str[0]) if red_str[0] else 0
This is better. For less complicated situations, if/else conditional expressions can make things very clear. But the example above is still not as clear as the alternative of a full if/else statement over multiple lines. Seeing all of the logic spread out like this makes the dense version seem even more complex:
>>> green_str = my_values.get('green', [''])
>>> if green_str[0]:
>>> green = int(green_str[0])
>>> else:
>>> green = 0
If you need to reuse this logic repeatedly—even just two or three times, as in this example—then writing a helper function is the way to go:
>>> def get_first_int(values, key, default=0):
>>> found = values.get(key, [''])
>>>
>>> if found[0]:
>>> return int(found[0])
>>> return default
The calling code is much clearer than the complex expression using or and the two-line version using the if/else expression:
>>> green = get_first_int(my_values, 'green')
As soon as expressions get complicated, it’s time to consider splitting them into smaller pieces and moving logic into helper functions. What you gain in readability always outweighs what brevity may have afforded you. Avoid letting Python’s pithy syntax for complex expressions from getting you into a mess like this. Follow the DRY principle: Don’t repeat yourself.
1.5.1. Things to Remember¶
✦ Python’s syntax makes it easy to write single-line expressions that are overly complicated and difficult to read.
✦ Move complex expressions into helper functions, especially if you need to use the same logic repeatedly.
✦ An if/else expression provides a more readable alternative to using the Boolean operators or and and in expressions.