Introduction
-
Functions in Python have a variety of extra features that make a programmer’s life easier.
- Some are similar to capabilities in other programming languages, but many are unique to Python.
-
These extras can make a function’s purpose more obvious.
-
They can eliminate noise and clarify the intention of callers.
-
They can significantly reduce subtle bugs that are difficult to find.
-
Item 19. Never Unpack More Than Three Variables When Functions Return Multiple Values
-
You can have functions return multiple values by putting them in a tuple and having the caller take advantage of Python’s unpacking syntax.
-
Multiple return values from a function can also be unpacked by catch-all starred expressions.
-
Unpacking into four or more variables is error prone and should be avoided; instead, return a small class or namedtuple instance.
# example
def get_avg_ratio(numbers):
average = sum(numbers) / len(numbers)
scaled = [x / average for x in numbers]
scaled.sort(reverse=True)
return scaled
longest, *middle, shortest = get_avg_ratio(lengths)
Item 20: Prefer Raising Exceptions to Returning None
-
Functions that return None to indicate special meaning are error prone because None and other values (e.g., zero, the empty string) all evaluate to False in conditional expressions.
-
Raise exceptions to indicate special situations instead of returning None. Expect the calling code to handle exceptions properly when they’re documented
-
Type annotations can be used to make it clear that a function will never return the value None, even in special situations.
# before
def careful_divide(a, b):
try:
return a / b
except ZeroDivisionError:
return None
# after
def careful_divide(a, b):
try:
return a / b
except ZeroDivisionError as e:
raise ValueError('Invalid inputs')
Item 21: Know How Closures Interact with Variable Scope
-
Closure functions can refer to variables from any of the scopes in which they were defined.
-
By default, closures can’t affect enclosing scopes by assigning variables.
-
Use the nonlocal statement to indicate when a closure can modify a variable in its enclosing scopes.
-
Avoid using
nonlocal
statements for anything beyond simple functions.
# Avoid using nonlocal
def sort_priority3(numbers, group):
found = False
def helper(x):
nonlocal found # Added
if x in group:
found = True
return (0, x)
return (1, x)
numbers.sort(key=helper)
return found
def sort_priority(values, group):
def helper(x):
if x in group:
return (0, x)
return (1, x)
values.sort(key=helper)
Item22 : Reduce Visual Noise with Variable Positional Arguments.
-
Functions can accept a variable number of positional arguments by using *args in the def statement.
-
You can use the items from a sequence as the positional arguments for a function with the * operator.
-
Using the * operator with a generator may cause a program to run out of memory and crash.
-
Adding new positional parameters to functions that accept *args can introduce hard-to-detect bugs.
# Do not use *args (Positional Args)
favorites = [7, 33, 99]
log('Favorite colors', *favorites)
Item23. Provide Optional Behavior with Keyword Arguments
-
Function arguments can be specified by position or by keyword.
-
Keywords make it clear what the purpose of each argument is when it would be confusing with only positional arguments.
-
Keyword arguments with default values make it easy to add new behaviors to a function without needing to migrate all existing callers
-
Optional keyword arguments should always be passed by keyword instead of by position.
# Before
pounds_per_hour = flow_rate(weight_diff, time_diff, 3600, 2.2)
# after
pounds_per_hour = flow_rate(weight_diff, time_diff, period=3600, units_per_kg=2.2)
Item24. Use None and Docstrings to Specify Dynamic Default Arguments
-
A default argument value is evaluated only once: during function definition at module load time. This can cause odd behaviors for dynamic values (like
{}
,[]
, ordatetime.now()
). -
Use None as the default value for any keyword argument that has a dynamic value. Document the actual default behavior in the function’s docstring.
-
Using None to represent keyword argument default values also works correctly with type annotations.
# example1
def log(message, when=datetime.now()):
print(f'{when}: {message}')
log('Hi there!') # >>> 2019-07-06 14:06:15.120124: Hi there!
sleep(0.1)
log('Hello again!') # >>> 2019-07-06 14:06:15.120124: Hello again!
# example2
def decode(data, default={}):
try:
return json.loads(data)
except ValueError:
return default
foo = decode('bad data')
foo['stuff'] = 5
bar = decode('also bad')
bar['meep'] = 1
print(foo) # >> {'stuff': 5, 'meep': 1}
Item25. Enforce Clarity with Keyword-Only and Positional-Only Arguments
-
Keyword-only arguments force callers to supply certain arguments by keyword (instead of by position), which makes the intention of a function call clearer. Keyword-only arguments are defined after a single * in the argument list.
-
Positional-only arguments ensure that callers can’t supply certain parameters using keywords, which helps reduce coupling. Positional-only arguments are defined before a single / in the argument list.
-
Parameters between the / and * characters in the argument list may be supplied by position or keyword, which is the default for Python parameters.
# example
def safe_division_e(numerator, denominator, # Positional-only
/, ndigits=10, *, # Both are available
ignore_overflow=False, # Keyword-only
ignore_zero_division=False):
Item 26. Define Function Decorators with functools.wraps
-
Decorators in Python are syntax to allow one function to modify another function at runtime.
-
Using decorators can cause strange behaviors in tools that do intro-spection, such as debuggers.
-
Use the
wraps
decorator from the functools built-in module when you define your own decorators to avoid issues.