Click here for the first post, which contains the context of this series.
Item #21: Understand how closures interact with variable scope.
Suppose that you have a list L of numbers, have a list G of important numbers, want to sort L while giving priority to the numbers of L that are in G, and want to know if a number in L is in G:
def my_sort(L, G):
flag = False
def helper(x):
if x in G:
flag = True
return (0, x)
return (1, x)
L.sort(key=helper)
return flag
This sorts L as expected but returns False: the problem is that flag = True in helper is a new variable due to how scoping works in Python. The fix is to use the keyword nonlocal:
def my_sort(L, G):
flag = False
def helper(x):
nonlocal flag
if x in G:
flag = True
return (0, x)
return (1, x)
L.sort(key=helper)
return flag
However, it is better practice to wrap states in classes:
class MySort:
def __init__(self, G):
self.G = G
self.flag = False
def __call__(self, x):
if x in self.G:
self.flag = True
return (0, x)
return (1, x)
def my_sort(L, G):
my_sort = MySort(G)
L.sort(key=my_sort)
return my_sort.flag
Item #22: Reduce visual noise with variable positional arguments.
Suppose that you have the following function:
def my_logger(message, items):
return f'{message}{", ".join([str(x) for x in items])}'
numbers = [1, 2, 3]
print(my_logger('I like these numbers: ', numbers))
print(my_logger('I like no numbers.', []))
Passing an empty list is noisy. Instead, use positional arguments:
def my_logger(message, *items):
return f'{message}{", ".join([str(x) for x in items])}'
numbers = [1, 2, 3]
print(my_logger('I like these numbers: ', *numbers))
print(my_logger('I like no numbers.'))
Note that *numbers converts numbers into a tuple, which means that if numbers were a massive generator, this would be resource-intensive.
Moreover, if you were to update the signature of my_logger to something like def my_logger(date, message, *items):, then not updating all of the calls to my_logger would introduce bugs that are hard to detect.
Item #23: Provide optional behavior with keyword arguments.
Note the use of **:
def flow_rate(weight_diff, time_diff, period=1, units_per_kg=1):
return weight_diff * units_per_kg * period / time_diff
kwargs = {
'weight_diff': 0.5,
'time_diff': 3,
'period': 3600,
'units_per_kg': 2.2
}
print(flow_rate(**kwargs))
With optional arguments, do not do this: flow_rate(0.5, 3, 3600, 2.2). Instead, do this: flow_rate(0.5, 3, period=3600, units_per_kg=2.2).
Item #24: Use None and Docstring to specify dynamic default arguments.
Suppose that you run the following script:
def append_zero(x=[]):
x.append(0)
return x
a = append_zero()
b = append_zero()
It turns out that a and b are the same list, so both look like [0, 0]. This is because lists are dynamic and not static, like strings.
Initialize keyword arguments that have dynamic values with None, and document this in the docstring:
def append_zero(x=None):
'''Append a zero to a list.
Args:
x: list. Defaults to an empty list.
'''
if not x:
x = []
x.append(0)
return x
Item #25: Enforce clarity with keyword-only and positional-only arguments.
Suppose that you have the following division function:
def safe_division(number, divisor, ignore_overflow, ignore_zero_division):
try:
return number / divisor
except OverflowError:
if ignore_overflow:
return 0
else:
raise
except ZeroDivisionError:
if ignore_zero_division:
return float('inf')
else:
raise
safe_division(12, 3, True, False)
This is noisy. An improvement would be to change the signature to
def safe_division(number, divisor, ignore_overflow=False, ignore_zero_division=False):
# ...
safe_division(12, 3, ignore_overflow=True)
The problem is that this is still possible:
safe_division(12, 3, True, False)
Keyword-only arguments cannot be passed by position:
def safe_division(number, divisor, *, ignore_overflow=False, ignore_zero_division=False):
# ...
safe_division(12, 3, True, False) # This gives an error.
Now, suppose that we change the signature to
def safe_division(numerator, denominator, *, ignore_overflow=False, ignore_zero_division=False):
# ...
Then this name change could break multiple existing calls to the function.
Positional-only arguments cannot be passed by keyword:
def safe_division(numerator, denominator, /, *, ignore_overflow=False, ignore_zero_division=False):
# ...
safe_division(numerator=10, denominator=2) # This gives an error.