Decorators Pycon


https://www.youtube.com/watch?v=MjHpMCIvwsY

Slides and Code Snippets can be downloaded here

  • Decorating a function creates three callables - the decorated function, the decorator, and the return value (function) from the decorator function assigned back to the decorated function.

  • Scope resolution in python follows the LEGB rule, looking for variables at each level before moving upwards. Source:

    • Local(L): Defined inside function/class

    • Enclosed(E): Defined inside enclosing functions(Nested function concept)

    • Global(G): Defined at the uppermost level

    • Built-in(B): Reserved names in Python built-in modules

  • The decorator function (the one that you apply to others with e.g. @decorator) executes once, when you decorate a function, but the inner wrapper function gets executed every time function is called.

  • Since the outside function is only executed once, you can use it to define additional functionality using nonlocal.

Snippet from Lecture:

import time

class CalledTooOftenError(Exception):
    pass

def once_per_minute(func):
    last_invoked = 0

    def wrapper(*args, **kwargs):
        nonlocal last_invoked

        elapsed_time = time.time() - last_invoked
        if elapsed_time < 60:
            raise CalledTooOftenError(f"Only {elapsed_time} has passed")
        last_invoked = time.time()

        return func(*args, **kwargs)

    return wrapper

if __name__ == '__main__':
    @once_per_minute
    def add(a, b):
        return a + b

    print(add(2, 2))
    print(add(3, 3))
  • nonlocal updates the variable in the enclosing scope, without having to make global variables. You only need to use nonlocal if you are assigning to the variable in the inner scope; if you’re accessing it follows LEGB scope.

  • Adding a decorator to a function passes the function as the first argument to the decorator. If arguments need to be passed to a decorator, you need to define an additional wrapper function that receives those arguments:

def some_decorator(n):
    def middle(func): # func is function being decorated
        function_specific_local = 0

        def wrapper(*args, **kwargs):
            nonlocal function_specific_local

            function_specific_local += n
            return func(*args, **kwargs)
        return wrapper
    return middle
  • This also increases the number of callables to 4. Applying this to a function manually would look like:
def my_function():
    ...
my_function = some_decorator(n=5)(my_function)

With a decorator:

@some_decorator(n=5)
def my_function()
  ...
  • Memoization wrappers: args in wrapper functions are defined as a tuple. As tuples are hashable, you can check if the same args have been passed before to memoize. If some of the arguments aren’t hashable, you can pickle args and kwargs before comparing it against your cache:

key = (pickle.dumps(args), pickle.dump(kwargs))

  • If you have distinct classes, but want similar behavior, instead of using inheritance/multiple inheritance, or setting class attributes after the fact, you can use a decorator. Since classes are callables, this is no different.

Snippet from Lecture:

def fancy_repr(self):
    return f"I'm a {type(self).__name__}, with vars {vars(self)}"

def repr_and_birthday(c):
    c.__repr__ = fancy_repr

    def wrapper(*args, **kwargs):
        o = c(*args, **kwargs)
        o._created_at = time.time()
        return o
    return wrapper


if __name__ == '__main__':
    @repr_and_birthday
    class Foo():
        def __init__(self, x, y):
            self.x = x
            self.y = y

    f = Foo(10, [10, 20, 30])
    print(f)
    print(f._created_at)
  • Use decorators instead of metaclasses if possible.

  • Wrapping your inner wrapper function (the one with *args and **kwargs) with functools.wraps allows you to keep attributes of a function (e.g. __name__, __doc__)

  • functools.partials acts as a wrapper function that passes *args or **kwargs to a function

Source:

from functools import partial

def power(base, exponent):
    return base ** exponent

square = partial(power, exponent=2)

assert square(2) == 4

https://www.youtube.com/watch?v=0LPuG825eAk

pdb notes

  • up - goes up a frame
  • down - goes down a frame
  • p - prints a variable
  • pp - pretty prints a variable
  • useful to use locals() to see all variables in a frame
  • list or ll - lists the code around the current line
  • n - next line
  • s - step into function, if there’s a function call on the current line
  • return or r - returns from the current function, useful in case you just stepped into a function and want to return to the caller

can set PYTHONBREAKPOINT=0 to disable all breakpoints

If you want to breakpoint when a crash happens in your program, you can use python3 -m pdb file.py, then hit c to continue. When a fatal error happens, you’ll be dropped into the debugger.

If the exception that causes the crash is caught/pretty-printed, you can use import pdb; pdb.post_mortem() to drop into the debugger in some higher stack frame.