I'm currently working through the book Data Science from Scratch by Joel Grus, and I've run across a function that I don't really understand:
def safe(f):
def safe_f(*args, **kwargs):
try:
return f(*args, **kwargs)
except:
return float('inf')
return safe_f
The safe function is called within a Gradient Descent algorithm to remove infinity values.
def minimize_batch(target_fn, gradient_fn, theta_0, tolerance=0.000001):
step_sizes = [100, 10, 1, 0.1, 0.001, 0.0001, 0.00001]
theta = theta_0
target_fn = safe(target_fn)
value = target_fn(theta)
while True:
gradient = gradient_fn(theta)
next_thetas = [step(theta, gradient, -step_size) for step_size in step_sizes]
next_theta = min(next_thetas, key=target_fn)
next_value = target_fn(next_theta)
if abs(value - next_value) < tolerance:
return theta
else:
theta, value = next_theta, next_value
I get the gist of what safe is doing, but I don't understand how it's doing it. For example, how does safe evaluate target_fn if there are no inputs for target_fn? What is safe doing such that it knows how to remove infinity values?
Gradient Descent aside, would this safe function work on a crazy functions that was undefined at uncountably many places?
It might help you to understand if we replace the variable names step by step:
target_fn = safe(target_fn)
means that f
in safe
is target_fn
:
def safe(target_fn):
def safe_f(*args, **kwargs):
try:
return target_fn(*args, **kwargs)
except:
return float('inf')
return safe_f
and:
target_fn = safe_f
i.e. we replace the function originally bound to target_fn
with the function safe_f
we just created in the decorator function safe
, retaining access to the original as f
, via a closure.
So the arguments are passed through *args, **kwargs
(see What does ** (double star) and * (star) do for parameters?):
next_value = target_fn(next_theta)
gets resolved to:
def safe_f(next_theta):
try:
return target_fn(next_theta)
except:
return float('inf')
i.e. either return the result from calling the original target_fn
with the argument next_theta
or, if any errors occur when doing so, float('inf')
.
would this
safe
function work on a crazy functions that was undefined at uncountably many places?
As it uses *args, **kwargs
you can use it to wrap any function, and it will quietly suppress any error raised by the function and return float('inf')
instead - this won't always be desirable, though! There are many other uses for decorator functions, which are often applied using @decorator
syntax (e.g. for class and static methods and properties on classes); see e.g. What are some common uses for Python decorators?