0

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?

razalfuhl
  • 3
  • 1
  • It doesn't look like it's removing infinity values to me. It looks like it's _adding_ infinity values, in place of raising an exception. For instance, the unsafe version of `return 1 / x` raises a `ZeroDivisionError` when x is zero; the safe-decorated version does not raise an exception, instead returning infinity. – Kevin Jun 03 '15 at 15:33
  • It might help you look up information on this if you know that `safe` is a **decorator** function, and that `f` is accessible inside `safe_f` via a **closure**. – jonrsharpe Jun 03 '15 at 15:40

1 Answers1

0

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?

Community
  • 1
  • 1
jonrsharpe
  • 115,751
  • 26
  • 228
  • 437