Explaining Function Decorators in Python

Passing my learning experience in a way that ChatGPT cannot

Pavol Kutaj
3 min readFeb 3, 2023

The aim of this page📝is to define decorators. Please note that

The “Decorator Pattern” ≠ Python “decorators”!

The Decorator Pattern

Also note that decorators are utilizing the core ideas of functional programming — the higher order function — which makes it possible for functions to be

  1. passed as an argument into another function
  2. 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

  1. define a name of a decorator with a single argument — this is a base function to be modified
  2. within the decorator, define a wrapper conventionally called wrap(*args, **kwargs) accepting any combination of arguments with *args and **kwargs
  3. 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.
  4. from a top-level-decorator, return wrapper object

5. Functional steps illustrate how decorators are decoupled from their callers

  1. Compile base_func into a function object
  2. The object is passed into decorator
  3. decorator returns a decorated/enhanced function object
  4. 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 access f after escape_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

  1. @timeit statement calls timeit(func=foo) decorator function
  2. it returns a wrapper function object which is called instead of a foo function object — when foo is called
  3. the feature that makes it possible that wrapper can access foo is closure, this is the heart and enabler of decorators
  4. each time foo is called, e.g. with foo("hello world), you call wrapper("hello world")
  5. generate/bind the start timestamp
  6. the function is called and does its job
>>> hello world
  • ⟹ generate/bind the end timestamp
  • ⟹ print the difference between end and start to signify the duration of function execution
  • ⟹ done
>>> foo executed in 0.0000 seconds

--

--

Pavol Kutaj

Today I Learnt | Infrastructure Support Engineer at snowplow.io with a passion for cloud infrastructure/terraform/python/docs. More at https://pavol.kutaj.com