Explaining Function Decorators in Python
Passing my learning experience in a way that ChatGPT cannot
The aim of this page📝is to define decorators. Please note that
The “Decorator Pattern” ≠ Python “decorators”!
Also note that decorators are utilizing the core ideas of functional programming — the higher order function — which makes it possible for functions to be
- passed as an argument into another function
- returned as a product of another function
— for the classical treatment, see 2A: Higher-order Procedures < Structure and Interpretation of Computer Programs < Electrical Engineering and Computer Science < MIT OpenCourseWare
2. Decorators are a way to enhance an existing function which takes a function as an argument and returns another function
- Their benefits are that they are non-intrusive + maintainable
- Decorator is a callable object that:
- ..Accepts a callable (usually a function, but can be a class or object)
- ..A callable is the decorator’s only argument
- ..Returns a callable
- ..A callable is the decorator’s only product
A callable in Python is an object that can be invoked or called to produce a result. This could be a function, a method, or a class. All callables in Python can be called using parentheses
()
and passing in any necessary arguments.
— for more on callable classes, see https://medium.com/p/b25064da45be
3. Syntactically, to call a decorator function, it is located one line above the base function and begins with a special symbol @
- it denotes applying decorators to functions
4. Syntactically, the definition of a decorator has a specific structure with a nested wrapper utilizing closure
- define a name of a decorator with a single argument — this is a base function to be modified
- within the decorator, define a wrapper conventionally called
wrap(*args, **kwargs)
accepting any combination of arguments with*args
and**kwargs
- somewhere in the wrapper, call the base function ..you could assign its results to a variable ..you could run a command/create a side-effect ..run your transformations ..you could return transformed value / perform required side-effects/commands ..etc.
- from a top-level-decorator, return wrapper object
5. Functional steps illustrate how decorators are decoupled from their callers
- Compile
base_func
into a function object - The object is passed into
decorator
decorator
returns a decorated/enhanced function object- Python binds the modified function to the original name
Result: modification of existing functions without the need to modify their definitions In other words, Callers don’t need to change when decorators are applied.
6. A simple example#1: Escape Unicode characters and re-encode a string to ASCII
- We have many functions that return string
- We need to ensure the those strings only contain ASCII characters
- We could add many
ascii()
functions to convert non-ascii chars to escape sequences - This is not dry / Does not scale / Maintainance is harder ⟹ let’s use a single decorator which is applied when needed
- In 05 is important to note how
wrap
is returned - Decorator accepts callable and produces callable
- In this example, the returned callable) is the local function
wrap()
wrap()
uses a closure to accessf
afterescape_unicode()
returns- [local: 2022–03–14-Closures-in-Python.md][#3]
- .. see https://medium.com/p/33565833dd88 for a short closure explainer
- Use decorator by attaching it right above the function definition
7. Simple Example#2: Enhance a base function with a timer and printout the duration of its runtime
@timeit
statement callstimeit(func=foo)
decorator function- it returns a
wrapper
function object which is called instead of afoo
function object — whenfoo
is called - the feature that makes it possible that
wrapper
can accessfoo
is closure, this is the heart and enabler of decorators - each time
foo
is called, e.g. withfoo("hello world)
, you callwrapper("hello world")
- generate/bind the
start
timestamp - the function is called and does its job
>>> hello world
- ⟹ generate/bind the
end
timestamp - ⟹ print the difference between
end
andstart
to signify the duration of function execution - ⟹ done
>>> foo executed in 0.0000 seconds