3 Functions
Last edited
21 Prefer raising exceptions to returning None
- when writing utility functions
Dont use go’s style returning err or ok, instead only raise or returns proper value.
Incorrect:
# 1) Rely on falsy
def careful_divide(a, b) -> float | None:
try:
return a / b
except ZeroDivisonError:
return None
# What happens if the result is actually 0? This code triggers when it shouldn't
result = careful_divide(x, y)
if result is None:
print("Invalid inputs")
# 2) Go style
success, result = careful_divide(x, y)
if result is None:
print("Invalid inputs")
# same as go this can be ignored with _, resultCorrect:
def careful_divide(a: float, b: float) -> float:
"""Divides a by b.
Raises:
ValueError: When the inputs cannot be divided.
"""
try:
return a / b
except ZeroDivisionError:
raise ValueError('Invalid inputs')
try:
result = careful_divide(x, y)
except ValueError:
print("Invalid inputs")
else:
print(f"Result is {result}")The call site is more verbose but better.
23/25
Two take aways:
- set defaults to make parameters optional
- force keyword args so the api is easy to change for opts. To do this reduce positional args to what you know you’ll always need and everything else force keyword args so the change is easy to add or remove args.
def safe_division(numerator, denominator, /, ndigits=10, *, ignore_zero_division=False):
try:
return round(numerator / denominator, ndigits)
except ZeroDivisionError:
if ignore_zero_division:
return float('inf')
raise
safe_division(22, 7) # positional-only args
safe_division(22, 7, ndigits=2) # middle: either way
safe_division(22, 7, 2) # or .. <- not sure this is really useful, kinda just makes it confusing
safe_division(22, 7, ignore_zero_division=True) # keyword-only/= before it, position only.*= after it, keyword only- Between
/, ..., *= your choice (default for python parameters)
24 Use None to specify Dynamic Default Arguments
There’s a footgun with default arguments: They are evaluated at init time. This becomes a problem for all mutable types.
# this dictionary is created here
# all following calls to the function will reuse this one dictionary
def decode(default: dict = {})
return default
foo = decode()
foo['stuff'] = 5
bar = decode()
bar['stuff'] # 5, becaues we're sharing the same dictionary
# rather than a fresh one each callThe correct way to handle this for mutable types is always use None
def decode(default: dict | None = None) -> dict:
"""Return the given default, or a fresh empty dict.
Args:
default: Value to return. Defaults to an empty dictionary.
"""
if default is None:
default = {} # fresh dict every call
return default26 Decorators should use functools.wrap
Decorators - run code before or after a function.
Writing the decorator:
- first parameter must be a function
- the rest is all normal python function stuff, you can accept whatever you want as parameters
def trace(func): # accepting the func as the first param
def trace_wrapper(*args, **kwargs):
"""Decorator that traces input and ouput"""
return func(*args, **kwargs) # call the function, passing in the args
return trace_wrapper
@trace
def fibonacii(n):
"""Return the n-th Fibonacci number"""
...
# So with this typical looking setup if I try to get info about the fibonacii function
# i'll end getting info from the trace, since it's the first function.
# So fibonacii will perform normally, it doesn't identify itself correctly.
>>> fibonacii.__name__
'trace_wrapper'
>>> fibonacii.__doc__
'Decorator that traces input and ouput'
# As we can see, we're calling the trace_wrapper function because that's what trace is returning.Now let’s try with functools.wraps
from functools import wraps
def trace(func):
@wraps(func) # the only changed line
def trace_wrapper(*args, **kwargs):
"""Decorator that traces input and ouput"""
return func(*args, **kwargs)
return trace_wrapper
# Same as above..
>>> fibonacii.__name__
'fibonacii'
>>> fibonacii.__doc__
'Return the n-th Fibonacci number'So what does wraps do? Copies metadata (attributes like .__name__) from the base function to the wrapper.
Is this really needed? No not to run. But it can cause strange issues since other code may rely on resolving the attributes.
Tip
This is what a decorator does under the hood
fib_wrapped_in_trace = trace(fibonacci)
# We've got a first class function we can call
# - that will run trace passing in fibonacci
# - then trace can do what it wants and call the passed in function on it's own