1 Local functions
def essentially binds the body of the function to a name in such a way that functions are simply objects like everything else in Python. It’s important to remember that def is executed at runtime, meaning that functions are defined at runtime.
Python allows to define functions inside other functions. Such functions are often referred to as
local functions since they’re defined local to a specific function’s scope.
#1 - Local functions are defined on each call
#2 - LEGB and local function
There are four types of
scope in Python, and they are arranged in a hierarchy. Each scope is a context in which names are stored and in which they can be looked up. The four scopes from narrowest to broadest are:
- Local - names defined inside the current function.
- Enclosing - names defined inside any and all enclosing functions.
- Global - names defined at the top-level of a module. Each module brings with it a new global scope.
- Built-in - names built-in to the Python language through the special builtins module.
The LEGB Rule
Names are looked up in the narrowest relevant context. It's important to note that scopes in Python do not, in general, correspond to the source-code blocks as demarcated by indentation. For-loops, with-blocks, and the like do not introduce new nested scopes.
name lookup: first the
Local scope is checked, then any
Enclosing scope, next the
Global scope, and finally the
Builtin scope.
#3 - Local functions are not “members”
It’s important to note that local functions are not “members” of their containing function in any way. As we’ve mentioned, local functions are simply local name bindings in the function body.
#4 - When are local functions useful?
It makes sense to define these close to the call-site if they’re one-off, specialized functions. So local functions are a code organization and readability aid.
#5 - Returning functions from functions
- function-factory
- This ability to return functions is part of the broader notion of “first class functions” where functions can be passed to and returned from other functions or, more generally, treated like any other piece of data. This concept can be very powerful, particularly when combined with closures which we’ll explore in the next section.
2 Closures and nested scopes
- an interesting question: How does a local function use bindings to objects defined in a scope that no longer exists? That is, once a local function is returned from its enclosing scope, that enclosing scope is gone, along with any local objects it defined. How can the local function operate without that enclosing scope?
- The answer is that the local function forms what is known as a closure. A closure essentially remembers the objects from the enclosing scope that the local function needs. It then keeps them alive so that when the local function is executed they can still be used. One way to think of this is that the local function “closes over” the objects it needs, preventing them from being garbage collected.
- Python implements closures with a special attribute named __closure__ . If a function closes over any objects, then that function has a __closure__ attribute which maintains the necessary references to those objects.
#1 - Function factories
- with closure, local functions can safely use objects from their enclosing scope
- A very common use for closures is in function factories.
#2 - The nonlocal keyword
#3 - Accessing enclosing scopes with nonlocal
- global allows you to insert module-level name bindings into a function in Python
- how can we make the function local() modify the binding for message defined in the function enclosing() ?
- The answer to that is that Python also provides the keyword nonlocal which inserts a name binding from an enclosing namespace into the local namespace. More precisely, nonlocal searches the enclosing namespaces from innermost to outermost for the name you give it. As soon as it finds a match, that binding is introduced into the scope where nonlocal was invoked.
#4 - nonlocal references to nonexistent names
- It’s important to remember that it’s an error to use nonlocal when no matching enclosing binding exists. If you do this, Python will raise a SyntaxError .
3 Function decorators
At a high level,
decorators are a way to modify or enhance existing functions in a non-intrusive and maintainable way.
In Python,
a decorator is a callable object that takes in a callable and returns a callable. If that sounds a bit abstract, it might be simpler for now to think of decorators as functions that take a function as an argument and return another function, but the concept is more general than that.
#1 - The @ syntax for decorators
Coupled with this definition is a special syntax that lets you “decorate” functions with decorators. The syntax looks like this:
@my_decorator
def my_function():
# . . .
- What is “decorating”?
- So what does this actually do?
- When Python sees decorator application like this, it first compiles the base function, which in this case is my_function - this produces a new function object. Python then passes this function object to the function my_decorator.
- After calling the decorator with the original function object, Python takes the return value from the decorator and binds it to the name of the original function. The end result is that the name my_function is bound to the result of calling my_decorator with the function created by the def my_function line.
#2 - What can be a decorator?
#2.1 - Classes as decorators
- by using a class object as a decorator you replace the decorated function with a new instance of the class. The decorated function will be passed to the constructor and thereby to __init__() . However, that the object returned by the decorator must itself be callable, so the decorator class must implement the __call__() method and thereby be callable.
- In other words, we can use class objects as decorators so long as __init__() accepts a single argument (besides self ) and the class implements __call__().
#2.2 - Instances as decorators
- when use a class instance as a decorator, Python calls that instance’s __call__() method with the original function and uses __call__() s return value as the new function. These kinds of decorators are useful for creating collections of decorated functions which you can dynamically control in some way.
#3 - Multiple decorators
#4 - Decorating methods
#5 - Decorators and function metadata
- By naively replacing a function with another callable, we lose important metadata about the original function, and this can lead to confusing results in some cases.
#5.1 - Manually updating decorator metadata
#5.2 - Updating decorator metadata with functools.wraps
functools.wraps() is itself a decorator-factory (more on these later) which you apply to your wrapper functions. The wraps() function takes the function to be decorated as its argument, and it returns a decorator that does the hard work of updating the wrapper function with the wrapped function’s attributes.
#6 - Summary on decorators
Use decorators when they are the right tool: when the improve maintainability, add clarity, and simplify your code.