- rtshkmr's digital garden/
- Readings/
- Books/
- Fluent Python: Clear, Concise, and Effective Programming – Luciano Ramalho/
- Chapter 9. Decorators and Closures/
Chapter 9. Decorators and Closures
Table of Contents
there’s a need to disambiguate the following:
“decorators” in the context of design patterns vs
“decorators” as a python source code syntax feature to do basic metaprogramming
“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
nonlocalkeywordprogramming 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:
- a decorator is a function or another callable
- a decorator may replace the decorated function with a different one
- decorators are executed IMMEDIATELY when a module is LOADED (@ load-time)
When you “decorate” a function in Python (either with the
@decoratorsyntax 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:
module global scope
the f3 function’s scope
the
nonlocalscopethough this part is not super obvious yet
1 2 3 4 5 6 7 8 9 10 11b = 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:
| |
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
cellitems / attrsavg.__closure__[0].cell_contentsalternatively, we could inspect the
__code__for that variable, just to observe this.avg.__code__.co_freevarswhich 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
xthat appears in function:if we have a
global x, thenxcomes from and is assigned to thexglobal variable moduleNOTE: python doesn’t have a program global scope, only module global scopes
if we have a
nonlocal xdeclaration thenxcomes from and is assigned to thexlocal variable of the nearest surrounding function wherexis definedIf
xis a parameter / assigned a value in the function body, thenxis the local variableIf
xis referenced (read only), but not assigned (no modification) and is NOT a parameter:xis 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_patternsTIP: using
@functools.wraps()will allow the decoration to have the same docs and variadic kwargs and suchthis 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_cacheis actually adictand 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
@cacheis 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
128which is 128 entries in the cache - for optimal performance,
maxsizeparameter should be a power of 2 to make it optimal typedparameter, if true, discriminates argument type within its store, so1.0and1are 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
htmlizeact as a dispatcher that dispatches multiple specific functions e.g.htmlize_strand so on
- let
so,
functools.singledispatchdecorator: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
singledispatchbecause 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>.registerto do the bundling of the specific functions
implementing it:
the function that we apply the
@singledispatchdecorator 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 genericObjecttype), will be found there.for the other specialised functions (contributors to the generic function) , we can use the custom
@<generic_fn>.registerto register themthe specialised function
only 1st argument matters
we can define them at varying levels of class-specificity
e.g.
boolis a subtype-ofnumbers.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>.registerdecorator 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 24registry = 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
@nameanymore, we have to actually call that factory functionread 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 #
| |
- 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''
| |
- the
clockclass here is our parameterized decorator factory
KIV class decorators for chapter 24.
Chapter Summary #
Further Reading #
- 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.