Effective Python #3 | Functions


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

  1. You can have functions return multiple values by putting them in a tuple and having the caller take advantage of Python’s unpacking syntax.

  2. Multiple return values from a function can also be unpacked by catch-all starred expressions.

  3. 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

  1. 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.

  2. Raise exceptions to indicate special situations instead of returning None. Expect the calling code to handle exceptions properly when they’re documented

  3. 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

  1. Closure functions can refer to variables from any of the scopes in which they were defined.

  2. By default, closures can’t affect enclosing scopes by assigning variables.

  3. Use the nonlocal statement to indicate when a closure can modify a variable in its enclosing scopes.

  4. 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.

  1. Functions can accept a variable number of positional arguments by using *args in the def statement.

  2. You can use the items from a sequence as the positional arguments for a function with the * operator.

  3. Using the * operator with a generator may cause a program to run out of memory and crash.

  4. 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

  1. Function arguments can be specified by position or by keyword.

  2. Keywords make it clear what the purpose of each argument is when it would be confusing with only positional arguments.

  3. Keyword arguments with default values make it easy to add new behaviors to a function without needing to migrate all existing callers

  4. 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 {}, [], or datetime.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

  1. 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.

  2. 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.

  3. 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

  1.  Decorators in Python are syntax to allow one function to modify another function at runtime.

  2. Using decorators can cause strange behaviors in tools that do intro-spection, such as debuggers.

  3. Use the wraps decorator from the functools built-in module when you define your own decorators to avoid issues.