Skip to main content
  1. Readings/
  2. Books/
  3. Fluent Python: Clear, Concise, and Effective Programming – Luciano Ramalho/

Chapter 9. Decorators and Closures

··4942 words·24 mins
  • there’s a need to disambiguate the following:

    1. “decorators” in the context of design patterns vs

    2. “decorators” as a python source code syntax feature to do basic metaprogramming

    3. “decorators” in the context of source code that alters the syntax tree when walking and annotating it

      b and c are related

      See elaboration here:

             Absolutely—the term “decorator” is overloaded in the programming lexicon, and disambiguating its different meanings is essential, especially for a technical audience steeped in first-principles thinking.
      
             ## 1. **Decorator as an Object-Oriented Design Pattern**
      
             **Definition:**
      ​       - The **Decorator Pattern** (from the Gang of Four design patterns) is a structural object-oriented pattern.
      ​       - **Mental Model:** It’s about composing behaviors at runtime by “wrapping” objects so you can add, remove, or alter responsibilities dynamically, without modifying the original object’s code.
      ​       - **Canonical Use:** You create a base interface, concrete components, and “decorator” classes implementing the same interface but containing an instance of the component (or another decorator). The decorator adds extra functionality before/after delegating calls to the wrapped object.
      
             **Example:**
             ```python
             class Coffee:
                 def cost(self):
                     return 5
      
             class MilkDecorator:
                 def __init__(self, coffee):
                     self._coffee = coffee
      
                 def cost(self):
                     return self._coffee.cost() + 2  # adds cost of milk
      
             coffee = Coffee()
             milk_coffee = MilkDecorator(coffee)
             print(milk_coffee.cost())  # Output: 7
             ```
             **Key Points:**
      ​       - The object’s interface remains constant.
      ​       - You can layer (nest) decorators to compose features.
      ​       - Pattern is about **runtime behavior extension** via object composition.
      
             ## 2. **Decorator as a Python Source Code Syntax Feature (Function/Method/Class Annotation)**
      
             **Definition:**
      ​       - In Python, a *decorator* is language syntax (the `@decorator` notation) that allows alteration or augmentation of functions, methods, or classes.
      ​       - **Mental Model:** This is a form of metaprogramming: you take a callable (“the decorated”) and replace it with the result of passing it to a decorator function (or class). The original entity is replaced—effectively rewiring its meaning at definition time.
      ​       - This mechanism operates during **definition time**—when the source code is executed—and can be used for logging, access control, transformation, memoization, etc.
      
             **Example:**
             ```python
             def my_decorator(func):
                 def wrapper(*args, **kwargs):
                     print("Before")
                     return func(*args, **kwargs)
                 return wrapper
      
             @my_decorator
             def greet():
                 print("Hello")
             ```
      ​       - At load-time, `greet = my_decorator(greet)`.
      
             **Key Points:**
      ​       - Not limited to adding or layering behavior: you can totally replace functionality, register, introspect, or annotate.
      ​       - Strongly linked to Python’s first-class, runtime-accessible function objects.
      ​       - Used pervasively in frameworks (Flask, Django, pytest, etc.).
      
             ## 3. **Decorator as an Abstract Syntax Tree (AST) Transformer**
      
             **Definition:**
      ​       - In advanced metaprogramming scenarios, *decorator* can also refer to code that inspects and modifies the **AST itself**—that is, the parse tree Python generates before executing code.
      ​       - **Mental Model:** These are meta-tools (not the `@decorator` feature) that programmatically traverse, annotate, or even rewrite code during, for example, static analysis, linting, or source-to-source transformation.
      ​       - Examples include tools like `ast` module in Python’s standard library, or third-party packages for code analysis, type-checking, or metaclass generation.
      
             **Example (AST transformation):**
             ```python
             import ast
      
             class FunctionAnnotator(ast.NodeTransformer):
                 def visit_FunctionDef(self, node):
                     # Insert or rewrite a decorator in the AST node
                     ...
                     return node
      
             tree = ast.parse(open('somefile.py').read())
             # Walk/transform the tree
             FunctionAnnotator().visit(tree)
             ```
             **Key Points:**
      ​       - This usage of “decorator” is about source-level transformations, not runtime behavior or object composition.
      ​       - Such tools might annotate or rewrite code to reflect design-time properties or enforce invariants.
      ​       - Operates at a different abstraction layer: **syntax tree instead of runtime objects**.
      
             ## **Disambiguating the Terms**
      
             - **Decorator Pattern (OOP):**
             Object composition to add functionality via wrappers, used in class/instance structures. “Decorator” is a *pattern* in the sense of design, independent of language syntax.
      ​       - **Python Decorator Syntax:**
             Metaprogramming at definition time (`@decorator`), altering functions/methods through higher-order functions or callables. This is a *language feature* deeply integrated into Python’s runtime and loading mechanisms.
      ​       - **AST/Syntax Tree Decorator/Transformer:**
             Tools that mutate or annotate the language’s syntax tree itself, often before code ever runs. This is a *meta-level code transformation* technique, not a runtime object operation.
      
             ## **Mental Model and Practical Takeaway**
      
             - Whenever we encounter the word “decorator,” first clarify: **Are we talking about a design pattern, a Python syntactic feature, or code transformation tools working with the syntax tree?**
      ​       - For a **tech leader**, it’s crucial to recognize **the same term can encode different mental models**, and context defines which conceptual layer we’re operating in—object structure, runtime function composition, or compile-time/source-level transformation.
      
             > **In summary:**
             > - The term **“decorator”** in software can mean (1) a design pattern for dynamic behavioral extension, (2) the Python syntax feature for code transformation at definition time, or (3) a concept/tool for manipulating code at the syntax-tree level. These meanings are distinct in logic, purpose, and implementation, and using their precise terminology and mental models prevents architectural ambiguities in code review, design discussions, and tooling selection.
      
             **References:**
             1. Gamma, Helm, Johnson, Vlissides. “Design Patterns: Elements of Reusable Object-Oriented Software.”
             2. Python Language Reference, section on function definitions and decorators.
             3. Python `ast` module documentation and real-world linter/analysis tools (e.g., Black, mypy internals).
      
  • closures are a foundational concept that help us reason about:

    • how to write decorators and use the nonlocal keyword

    • programming that uses callbacks, or when writing python code in a functional fashion.

precursor learning objectives #

• How Python evaluates decorator syntax • How Python decides whether a variable is local • Why closures exist and how they work • What problem is solved by nonlocal With this grounding, we can tackle further decorator topics: • Implementing a well-behaved decorator • Powerful decorators in the standard library: @cache, @lru_cache, and @single dispatch • Implementing a parameterized decorator

What’s New in This Chapter #

Decorators 101 #

definition: “callable that takes another function as argument” #

  • 3 facts to summarise it:

    1. a decorator is a function or another callable
    2. a decorator may replace the decorated function with a different one
    3. decorators are executed IMMEDIATELY when a module is LOADED (@ load-time)
  • When you “decorate” a function in Python (either with the @decorator syntax or by manual assignment), you are:

    • Passing the original function object to a decorator callable (function or class).

    • Receiving back the return value of the decorator, which is typically—but not always—an inner function that wraps (or sometimes replaces) the original.

    • Binding a variable (often the same name as the original function) to this new object.

  • class decorators also exist!

When Python Executes Decorators - import-time vs run-time #

  • import-time vs runtime import time would refer to when the module is loaded.
    • within a script \(\rightarrow\) when the script starts
    • within a different module \(\rightarrow\) when the importing is actually done

to emphasize that function decorators are executed as soon as the module is imported, but the decorated functions only run when they are explicitly invoked. This highlights the difference between what Pythonistas call import time and runtime.

Registration Decorators #

  • typically decorators define an inner function that uses the decorated function and return that inner function.

    this in a way, supports the GOF decorator pattern as well

Variable Scope Rules #

this is a fundamental concept:

  • compare with JS, if we have a variable defined local to the scope of a function,

    then in python, it will always assume that it’s a local variable and it won’t look further in the outer scopes to resolve that name.

    in JS, the name resolution may go to higher scopes and we might potentially modify a global variable unknowingly

    in python, refs to a locally defined name will never bubble out to a larger scope

    But the fact is, when Python compiles the body of the function, it decides that b is a local variable because it is assigned within the function. The generated bytecode reflects this decision and will try to fetch b from the local scope. Later, when the call f2(3) is made, the body of f2 fetches and prints the value of the local variable a, but when trying to fetch the value of local variable b, it discovers that b is unbound.

  • so in this example, we have 3 scopes in action actually:

    1. module global scope

    2. the f3 function’s scope

    3. the nonlocal scope

      though this part is not super obvious yet

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      
             b = 6
             def f3(a):
                 global b
                 print(a)
                 print(b)
      
                 b = 9
      
             print(f3(3))
      
             print(b)
      
  • the nature of the variable (local vs. global) can’t change in the body of a function

Closures #

  • summary:

    a closure is a function that retains the bindings of the free variables that exist when the function is defined, so that they can be used later when the function is invoked and the defining scope is no longer available.

    the only situation in which a function may need to deal with external variables that are nonglobal is when it is nested in another function and those variables are part of the local scope of the outer function.

  • a closure is a function—let’s call it f—with an extended scope that encompasses variables referenced in the body of f that are not global variables or local variables of f.

    Such variables must come from the local scope of an outer function that encompasses f.

    It does not matter whether the function is anonymous or not; what matters is that it can access nonglobal variables that are defined outside of its body.

disambiguating closures from anon functions #

Actually, a closure is a function—let’s call it f—with an extended scope that encom‐ passes variables referenced in the body of f that are not global variables or local vari‐ ables of f. Such variables must come from the local scope of an outer function that encompasses f. It does not matter whether the function is anonymous or not; what matters is that it can access nonglobal variables that are defined outside of its body.

  • refer to this for the verbose disambiguation between closures and anon functions
      To disambiguate **closures** and **anonymous functions** in programming languages, particularly in the context of Python, we need to clarify their definitions, characteristics, and how they relate to each other.
    
      ### Definitions
    
      1. **Anonymous Functions**:
    ​     - An **anonymous function** (often referred to as a **lambda function** in Python) is simply a function that does not have a name. It is defined using the `lambda` keyword and can be used wherever function objects are required.
    ​     - Example in Python:
           ```python
           add = lambda x, y: x + y
           print(add(2, 3))  # Output: 5
           ```
    ​     - Anonymous functions are useful for short, throwaway functions that are not reused elsewhere.
    
      2. **Closures**:
    ​     - A **closure** is a function that captures the lexical scope in which it was defined, allowing it to access variables from that scope even when it is executed outside of that scope.
    ​     - In Python, closures are typically created when a nested function references variables from its enclosing scope.
    ​     - Example in Python:
           ```python
           def outer_function(x):
               def inner_function(y):
                   return x + y  # 'x' is captured from the outer scope
               return inner_function
    
           closure = outer_function(10)
           print(closure(5))  # Output: 15
           ```
    ​     - Here, `inner_function` forms a closure that captures the variable `x` from `outer_function`.
    
      ### Key Differences
    
      1. **Nature**:
    ​     - **Anonymous Function**: A function without a name; it can be defined and used inline.
    ​     - **Closure**: A function that retains access to its lexical scope even when invoked outside of that scope.
    
      2. **Creation**:
    ​     - **Anonymous Function**: Can be created anywhere, typically for short-lived operations (e.g., as arguments to higher-order functions).
    ​     - **Closure**: Created when a nested function references variables from its enclosing scope.
    
      3. **Scope Access**:
    ​     - **Anonymous Function**: Does not inherently capture any external state unless it is defined within another function.
    ​     - **Closure**: Specifically designed to capture and retain access to its surrounding state.
    
      4. **Use Cases**:
    ​     - **Anonymous Functions**: Often used for callbacks or functional programming constructs (like `map`, `filter`, etc.).
    ​     - **Closures**: Useful for data hiding and encapsulation, maintaining state across function calls.
    
      ### Python Context
    
      In Python, both anonymous functions and closures are supported:
    
      - **Anonymous Functions (Lambdas)**:
        ```python
        square = lambda x: x * x
        print(square(4))  # Output: 16
        ```
    
      - **Closures**:
        ```python
        def make_multiplier(factor):
            def multiplier(x):
                return x * factor  # 'factor' is captured from the outer scope
            return multiplier
    
        double = make_multiplier(2)
        print(double(5))  # Output: 10
        ```
    
      ### Summary
    
      - All closures can be considered anonymous functions if they are defined without a name, but not all anonymous functions create closures.
    ​  - The distinction lies in the ability of closures to capture and maintain access to their lexical environment, which anonymous functions do not inherently possess unless they are part of a nested structure.
    ​  - In Python, this distinction allows for powerful programming patterns such as decorators and higher-order functions while maintaining clean and concise code.
    
      By understanding these concepts, developers can effectively utilize both anonymous functions and closures in their programming practices, leveraging their unique characteristics for various scenarios.
    
      Citations:
      [1] https://stackoverflow.com/questions/4912116/closure-vs-anonymous-function-difference
      [2] https://steemit.com/php/@hvonsteemit/the-power-of-anonymous-functions-closures-in-php
      [3] https://www.reddit.com/r/PHP/comments/4s1f9l/anonymous_functions_vs_closures_the_way_you_like/
      [4] https://thoughtbot.com/blog/back-to-basics-anonymous-functions-and-closures
      [5] https://doc.rust-lang.org/book/ch13-01-closures.html
      [6] https://www.tutorialspoint.com/javascript-closures-vs-anonymous-functions
      [7] https://www.php.net/manual/en/functions.anonymous.php
      [8] https://realpython.com/python-type-hints-multiple-types/
    

free variables: variables that is not bound in the local scope #

Consider this example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def make_averager():
        series = []

        def averager(new_value):
                # this assignment here makes series locally bound for the lifespan of this averager function
                series.append(new_value)
                total = sum(series)
                return total / len(series)

        return averager

so avg is a reference to the inner function, averager which has the outer series in its scope.

within averager, series WAS originally a local variable because of the assignment within its function body

however after make_averager returns, that local scope is gone.

within averager, series is a free variable, a variable that is NOT bound in the local scope.

the closure for averager extends the scope of that function to include the binding for the free variable, series

Figure 9-1. The closure for averager extends the scope of that function to include the binding for the free variable series. Inspecting the returned averager object shows how Python keeps the names of local and free variables in the code attribute that represents the compiled body of the function. Example 9-10 demonstrates. Example 9-10. Inspecting the function created by make_averager in Example 9-8 >>> avg._code_.co_varnames (’new_value’, ’total’) >>> avg._code_.co_freevars

how free variables are kept within __closure__ attributes #

:NOTER_PAGE: (344 0.5105177993527509 . 0.33121019108280253)

  • closures are actually represented using cell items / attrs

    avg.__closure__[0].cell_contents

  • alternatively, we could inspect the __code__ for that variable, just to observe this.

    avg.__code__.co_freevars which gives the free variables for that function

is kept in the closure attribute of the returned function avg. Each item in avg.__closure__ corresponds to a name in avg. code .co_freevars. These items are cells, and they have an attribute called cell_con tents where the actual value can be found.

only situation for why a function may require access to variables outside its scope #

that the only situation in which a function may need to deal with external vari‐ ables that are nonglobal is when it is nested in another function and those variables are part of the local scope of the outer function.

The nonlocal Declaration #

using nonlocal keyword to explicitly define free variables #

  • lets you declare a variable as a free variable even when it is assigned within the function. If a new value is assigned to a nonlocal variable, the binding stored in the closure is changed.

  • prevents the use-before-definition error because the variable will be regarded as a free-variable if it’s indicated as nonlocal

To work around this, the nonlocal keyword was introduced in Python 3. It lets you declare a variable as a free variable even when it is assigned within the function. If a new value is assigned to a nonlocal variable, the binding stored in the closure is changed.

Variable Lookup Logic - how python resolves names #

  • Rules for defining how to fetch a variable x that appears in function:
    1. if we have a global x, then x comes from and is assigned to the x global variable module

      NOTE: python doesn’t have a program global scope, only module global scopes

    2. if we have a nonlocal x declaration then x comes from and is assigned to the x local variable of the nearest surrounding function where x is defined

    3. If x is a parameter / assigned a value in the function body, then x is the local variable

    4. If x is referenced (read only), but not assigned (no modification) and is NOT a parameter:

      • x is looked up the local scopes of the surrounding function bodies (non-local scopes) \(\rightarrow\) free variable!

      • if nothing, then will be read from the module global scope

      • if still nothing, then read from __builtins__.__dict__

:NOTER_PAGE: (346 . 0.496385)

Implementing a Simple Decorator #

  • typical behavior of a decorator: it replaces the decorated function with a new function that accepts the same arguments and (usually) returns whatever the decorated function was supposed to return, while also doing some extra processing.

  • this is partly aligned with the GOF Decorator Pattern in OOP.

    In both cases, we extend behaviour, but the way we do it is different.

      Your summary of a typical Python decorator—that it “replaces the decorated function with a new function that accepts the same arguments and (usually) returns whatever the decorated function was supposed to return, while also doing some extra processing”—captures how decorators are commonly used in Python. However, this pattern only partly aligns with the **Gang of Four (GOF) Decorator Pattern** as described in object-oriented design.
    
      ### GOF Decorator Pattern (Design Pattern)
    ​  - The **GOF Decorator Pattern** is about **dynamically attaching new responsibilities to objects** at runtime by wrapping them in decorator objects[2][7].
    ​  - In the traditional pattern, decorators are classes that implement the same interface as the objects they wrap. Additional behavior is layered by composition, and you can recursively chain decorators together[2][3][5].
    ​  - The emphasis is on **object composition** and runtime extension, not on function replacement or metaprogramming.
    
      ### Python Decorators (Language Feature)
    ​  - **Python decorators** (via the `@decorator` syntax) are a **language feature** for transforming functions or classes at definition time[1]. A Python decorator is usually a higher-order function or class that takes a function (or class) and returns a replacement—often a function that wraps the original, accepting the same parameters, returning the same output, and adding pre- or post-processing.
    ​  - This approach is similar in spirit to the design pattern because it allows extension or modification of behavior without changing the original function directly[1]. The decorator often "wraps" the original so that calling the decorated function results in the new ("decorator") logic being executed, often around a call to the original.
    
      ### Key Distinctions
    ​  - The **GOF pattern operates at the object/class level**, using composition and polymorphism.
    ​  - The **Python feature generally operates at the function (or class) level**, using higher-order functions for metaprogramming and code transformation at definition time.
    ​  - Both systems enable behavioral extension, and Python decorators can be used to implement the decorator pattern, but they're not always identical in mechanics or intention[1][5].
    
      ### In summary:
    ​  - Your description of the typical behavior matches how Python decorators are commonly implemented and used.
    ​  - This overlap is **inspired by the GOF Decorator Pattern**; the intent—to extend or alter behavior without modifying the original—is present in both.
    ​  - However, **Python's decorator syntax and idioms are a language feature enabling, but not limited to, the classical design pattern**. The mechanics and use cases often go beyond, including metadata registration, memoization, method validation, and many scenarios not contemplated by the original OOP design pattern[1][3][7].
    
      So: **your summary matches common Python decorator behavior, which parallels the GOF Decorator Pattern but is not restricted to it—they share intent, but not all implementation details**[1][2][5].
    
      [1] https://stackoverflow.com/questions/17927129/is-python-decorator-related-to-the-decorator-design-pattern
      [2] https://www.digitalocean.com/community/tutorials/gangs-of-four-gof-design-patterns
      [3] https://refactoring.guru/design-patterns/decorator
      [4] https://www.reddit.com/r/Python/comments/r0apia/which_of_the_design_patterns_of_the_og_gang_of/
      [5] https://python-patterns.guide/gang-of-four/decorator-pattern/
      [6] https://www.geeksforgeeks.org/system-design/decorator-pattern/
      [7] https://en.wikipedia.org/wiki/Decorator_pattern
      [8] https://github.com/tuvo1106/python_design_patterns
    
  • TIP: using @functools.wraps() will allow the decoration to have the same docs and variadic kwargs and such

    this is an example of a standard, ready-to-use decorator

``def clock(func): @functools.wraps(func) def clocked(*args, **kwargs):''

Decorators in the Standard Library #

Memoization with functools.cache #

  • stacking decorators composes them, starting from the bottom of the stack then applies outwards towards the top of the stack

    1
    2
    3
    
      @alpha
      @beta
      def my_fn():
    

    which is equivalent to nested functions my_fn = alpha(beta(my_fn))

  • args to the decorated function needs to be hashable

    this is because the internal representation for the lru_cache is actually a dict and the keys are from the positional and kwargs used in the calls.

    arguments taken by the decorated function must be hashable, because the underlying lru_cache uses a dict to store the results, and the keys are made from the positional and keyword arguments used in the calls.

  • a good usecase for @cache is caching remote API calls!

  • careful that we don’t end up using all the available memory ( may happen since this cache is unrestricted ). we can use functools.lru_cache(maxsize=<mymaxsize>) instead

Using @lru_cache to keep the memory bounded #

  • there’s a default of 128 which is 128 entries in the cache
  • for optimal performance, maxsize parameter should be a power of 2 to make it optimal
  • typed parameter, if true, discriminates argument type within its store, so 1.0 and 1 are treated as two different entries in the store.

Single Dispatch to Contribute to Generic Functions (aka Multimethods) #

  • python doesn’t have method overloading (like in Java, for example)

  • in this example for htmlize, with multiple rules, we can:

    • let htmlize act as a dispatcher that dispatches multiple specific functions e.g. htmlize_str and so on
  • so, functools.singledispatch decorator:

    • allows different modules to contribute to the overall solution, and lets you easily provide specialized functions even for types that belong to third-party packages that you can’t edit.

    • a generic function here is a group of functions to perform the same operation in different ways, depending on the type of the first argument

    • it’s singledispatch because only the first argument is used as the determinant, else it would have been called “multiple dispatch”

    • it’s going to use a custom @<generic_fn_name>.register to do the bundling of the specific functions

  • implementing it:

    • the function that we apply the @singledispatch decorator to will end up being the name of the generic function that we want to bundle functionality for. Also, the base case implementation (e.g. something that works with a generic Object type), will be found there.

    • for the other specialised functions (contributors to the generic function) , we can use the custom @<generic_fn>.register to register them

      • the specialised function

      • only 1st argument matters

      • we can define them at varying levels of class-specificity

        e.g. bool is a subtype-of numbers.Integral, but the singledispatch logic seeks the implementation with the most specific matching type, regardless of the order they appear in the code.

      • we can also stack other decorators atop the register decorator because the @<base>.register decorator returns the undecorated function.

    • TIP: register specialised functions to handle ABCs or Protocols to enjoy more polymorphic behaviour

      Using ABCs or typing.Protocol with @singledispatch allows your code to support existing or future classes that are actual or virtual subclasses of those ABCs, or that implement those protocols.

      KIV virtual subclasses till chapter 13

    • remember that we can register functions that works with 3rd party libraries!

this analogous to function overloading in typed languages

here’s the functools docs for it

NOTE it’s NOT supposed to be a parallel to java method overloading, it’s to support MODULAR EXTENSION #

  • modular extension in the sense that each module can register a specialized function for each type it supports.

@singledispatch is not designed to bring Java-style method over‐ loading to Python. A single class with many overloaded variations of a method is better than a single function with a lengthy stretch of if/elif/elif/elif blocks. But both solutions are flawed because they concentrate too much responsibility in a single code unit—the class or the function. The advantage of @singledispatch is supporting modular extension: each module can register a speci‐ alized function for each type it supports. In a realistic use case, you would not have all the implementations of generic functions in the same module as in

Parameterized Decorators #

  • Key Idea: use a decorator factory to create the decorator

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    
      registry = set()
    
      # this here is a decorator factory
      def register(active=True):
              def decorate(func):
                      print('running register'
                            f'(active={active})->decorate({func})')
              if active:
                      registry.add(func)
              else:
                      registry.discard(func)
              return func
           return decorate
    
      @register(active=False)
      def f1():
              print('running f1()')
    
      @register()
      def f2():
              print('running f2()')
    
      def f3():
              print('running f3()')
    
  • NOTE: we can’t just call @name anymore, we have to actually call that factory function

    read more here:

      It is correct to say that a **decorator factory** in Python must always be called using `@name()` (with parentheses), because it is a function that returns a decorator. The result of calling the factory—i.e., `name()`—must itself be a callable that accepts the function being decorated, and only this returned callable is applied as the actual decorator.
    
      - Using `@name` is for *simple decorators*: the decorator is itself a callable that takes a function as input (e.g., `def decorator(fn): ...`), and is applied as `@decorator`.
    ​  - A **decorator factory** is a function returning a decorator, often parameterized (e.g., `def decorator_factory(arg): ...`). You must invoke the factory—`@decorator_factory(args)`—so that what is returned will accept and wrap the underlying function[2][3][5].
    
      ### Does `@name` Ever Act as a Nullary Call?
    
      No, `@name` does **not** cause an implicit function call. The interpreter does *not* call `name()` for you. `@name` without parentheses applies exactly what’s bound to `name` as a decorator; it is not treated as `@name()`. This makes explicit invocation with parentheses (`@name()`) necessary if you intend to use a decorator factory, even with no arguments[2][3].
    
      - If you accidentally use `@name` (no parentheses) when `name` is actually a decorator factory, you will get a `TypeError` because the decorator factory is not the correct type—it hasn’t yet received its arguments and it doesn’t accept the function to be decorated as its first input.
    
      ### Example:
    
      ```python
      def decorator_factory(arg):
          def decorator(fn):
              ...
              return fn
          return decorator
    
      @decorator_factory(42)  # correct usage: applies the decorator returned from factory
      def foo(): ...
      ```
      If you write `@decorator_factory` (no parentheses) above, `foo` is actually passed to `decorator_factory`, not to the decorator it returns, and the protocol is broken.
    
      ### Summary Table
    
      | Decorator Syntax    | Used For              | Calls Made Implicitly  |
      |---------------------|-----------------------|------------------------|
      | `@name`             | Simple decorator      | No call (just binding) |
      | `@name()`           | Decorator factory     | Calls factory, supplies result as decorator |
      | `@name(arg1, arg2)` | Decorator factory     | Calls factory with arguments |
    
      In conclusion:
    ​  - **Decorator factories must be used as `@name()`** (or with arguments: `@name(arg, ...)`).
    ​  - **`@name` does NOT act as a nullary call** and does not call the factory.
    ​  - Using `@name` when only a decorator factory is defined will result in errors or unexpected behavior[2][3][5].
    
      [1] https://stackoverflow.com/questions/44358027/why-is-using-decorator-factory-factory-functions-a-syntax-error
      [2] https://www.thepythoncodingstack.com/p/demystifying-decorators-parts-3-to-7
      [3] https://blog.devgenius.io/decorator-factory-by-example-e3f2774b0baa
      [4] https://www.geeksforgeeks.org/python/decorators-in-python/
      [5] https://realpython.com/primer-on-python-decorators/
      [6] https://www.freecodecamp.org/news/the-python-decorator-handbook/
      [7] http://simeonfranklin.com/blog/2012/jul/1/python-decorators-in-12-steps/
    

possible to call the decorator factory function directly too without the @ #

  • though we would have to call it like so: register(active=False)(f)

If, instead of using the @ syntax, we used register as a regular function, the syntax needed to decorate a function f would be register()(f) to add f to the registry, or register(active=False)(f) to not add it (or remove it).

The Parameterized Clock Decorator #

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import time

DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}'

def clock(fmt=DEFAULT_FMT):
        def decorate(func):
                def clocked(*_args):
                        t0 = time.perf_counter()
                        _result = func(*_args)
                        elapsed = time.perf_counter() - t0
                        name = func.__name__
                        args = ', '.join(repr(arg) for arg in _args)
                        result = repr(_result)
                        print(fmt.format(**locals())) # NB: see the locals passing like it's done here
                        return _result
                return clocked
        return decorate


if __name__ == '__main__':
        @clock()
        def snooze(seconds):
                time.sleep(seconds)
  • we can pass local variables to a fn like fmt like so: print(fmt.format(**locals()))

A Class-Based Clock Decorator #

  • these examples just used functions as a demo example
  • for non-trivial cases, better to think of decorators as classes with a __call__()=

``implemented as a class with call''

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import time

DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}'

class clock:
        def __init__(self, fmt=DEFAULT_FMT):
                self.fmt = fmt
        def __call__(self, func):
                def clocked(*_args):
                        t0 = time.perf_counter()
                        _result = func(*_args)
                        elapsed = time.perf_counter() - t0
                        name = func.__name__
                        args = ', '.join(repr(arg) for arg in _args)
                        result = repr(_result)
                        print(self.fmt.format(**locals()))
                        return _result
                return clocked
  1. the clock class here is our parameterized decorator factory

KIV class decorators for chapter 24.

Chapter Summary #

Further Reading #

  1. general series on python decorators, starting with “how you implemented your Python decorator is wrong”

dynamic vs lexical scoping #

  • lexical scope is the norm: free variables are evaluated considering the environment where the function is defined. Lexical scope complicates the implementation of languages with first-class functions, because it requires the support of closures. On the other hand, lexical scope makes source code easier to read.

``Dynamic Scope Versus Lexical Scope''

coming to terms b/w decorator pattern and python decorators #

Note that I am not suggesting that function decorators should be used to implement the decorator pattern in Python programs. Although this can be done in specific sit‐ uations, in general the decorator pattern is best implemented with classes to represent the decorator and the components it will wrap.