# The Decorator Pattern Here are some notes on Python decorators. ## Python decorator attributes A _decorator_ is a function that takes another function as input, extends its behavior, and returns a new function as output [2]. A decorator wraps a function inside another function that adds extra functionality before or after the wrapped function is executed [5]. The decorator is useful for logging, access control, memoization, and function timing. In a nutshell, decorators allow us to add behavior to functions without altering their core logic or structure [5]. This is possible because functions are first-class objects in Python which means they can be passed as arguments to functions and also be returned from functions just like other types of objects such as string, int, or float. Thus, a decorator can be used to decorate a function or a class. Here we discuss three special decorators: @staticmethod, @classmethod, and @property which are “magical” decorators that can be very handy for our development work and make your code more clean [2]. ### @staticmethod A `static` method is a method that does not require the creation of an instance of a class. ```py class Cellphone: def __init__(self, brand, number): self.brand = brand self.number = number def get_number(self): return self.number @staticmethod def get_emergency_number(): return "911" Cellphone.get_emergency_number() # '911' ``` ### @classmethod A class method requires the class itself as the first argument which is written as cls. A class method normally works as a factory method and returns an instance of the class with supplied arguments. However, it does not have to work as a factory class and return an instance. We can create an instance in the class method and do whatever you need without having to return it. Class methods are very commonly used in third-party libraries. Here, it is a factory method here and returns an instance of the Cellphone class with the brand preset to “Apple”. ```py class Cellphone: def __init__(self, brand, number): self.brand = brand self.number = number def get_number(self): return self.number @staticmethod def get_emergency_number(): return "911" @classmethod def iphone(cls, number): _iphone = cls("Apple", number) print("An iPhone is created.") return _iphone iphone = Cellphone.iphone("1112223333") # An iPhone is created. iphone.get_number() # "1112223333" iphone.get_emergency_number() # "911" ``` If you use class methods properly, you can reduce code redundancy dramatically and make your code more readable and more professional. The key idea is that we can create an instance of the class based on some specific arguments in a class method, so we do not have to repeatedly create instances in other places (DRY). ### @property In the code snippet above, there is a function called `get_number` which returns the number of a Cellphone instance. We can optimize the method a bit and return a formatted phone number. In Python, we can also use getter and setter to easily manage the attributes of the class instances. ```py class Cellphone: def __init__(self, brand, number): self.brand = brand self.number = number @property def number(self): _number = "-".join([self._number[:3], self._number[3:6],self._number[6:]]) return _number @number.setter def number(self, number): if len(number) != 10: raise ValueError("Invalid phone number.") self._number = number cellphone = Cellphone("Samsung", "1112223333") print(cellphone.number) # 111-222-3333 cellphone.number = "123" # ValueError: Invalid phone number. ``` Here is the complete example using the three decorators in Python: `@staticmethod`, `@classmethod`, and `@property`: ```py class Cellphone: def __init__(self, brand, number): self.brand = brand self.number = number @property def number(self): _number = "-".join([self._number[:3], self._number[3:6],self._number[6:]]) return _number @number.setter def number(self, number): if len(number) != 10: raise ValueError("Invalid phone number.") self._number = number @staticmethod def get_emergency_number(): return "911" @classmethod def iphone(cls, number): _iphone = cls("Apple", number) print("An iPhone is created.") return _iphone ``` ## Using Decorators Here are some useful topics on decorators discussed in [5]: - Decorators with Arguments - Chaining Multiple Decorators - The Importance of functools.wraps Practical Use Cases of Decorators Some of the most common use cases for decorators: - Logging: Track function calls, arguments, and results. - Memoization: Cache function results to improve performance. - Access Control: Restrict access to certain functions based on conditions (such as user permissions). - Timing: Measure how long a function takes to execute. ## New Type Annotation Features The improvement of type annotations in Python 3.11 can help to write bug-free code [3]. ### Self — the Class Type The following code does not use type hints which may cause problems. ```py class Box: def paint_color(self, color): self.color = color return self ``` We can use Self to indicate that the return value is an object in the type of “Self" which is interpreted as the Box class. ```py from typing import Self class Box: def paint_color(self, color: str) -> Self: self.color = color return self ``` ### Arbitrary Literal String When we want a function to take a string literal, we must specify the compatible string literals. Python 3.11 introduces a new general type named `LiteralString` which allows the users to enter any string literals. ```py from typing import LiteralString def paint_color(color: LiteralString): pass paint_color("cyan") paint_color("blue") ``` The `LiteralString` type gives the flexibility of using any string literals instead of specific string literals when we use the `Literal` type. ### Varying Generics We can use `TypeVar` to create generics with a single type, as we did previously for Box. When we do numerical computations (such as array-based operations in NumPy and TensorFlow), we use arrays that have varied dimensions and shapes. When we provide type annotations to these varied shapes, it can be cumbersome to provide type information for each possible shape which requires a separate definition of a class since the exiting TypeVar can only handle a single type at a time. Python 3.11 is introducing the TypeVarTuple that allows you to create generics using multiple types. Using this feature, we can refactor our code in the previous snippet, and have something like the below: ```py from typing import Generic, TypeVarTuple Dim = TypeVarTuple('Dim') class Shape(Generic[*Dim]): pass ``` Since it is a tuple object, we can use a starred expression to unpack its contained objects which is a variable number of types. The above Shape class can be of any shape which has more flexibility and eliminates the need of creating separate classes for different shapes. ### TypedDict — Flexible Key Requirements In Python, dictionaries are a powerful data type that saves data in the form of key-value pairs. The keys are arbitrary and you can use any applicable keys to store data. However, sometimes we may want to have a structured dictionary that has specific keys and the values of a specific type which means using TypedDict. ```py from typing import TypedDict class Name(TypedDict): first_name: str last_name: str ``` We know that some people may have middle names (middle_name) and some do not. There are no direct annotations to make a key optional and the current workaround is creating a superclass that uses all the required keys while the subclass includes the optional keys. Python 3.11 introduces NotRequired as a type qualifier to indicate that a key can be potentially missing for TypedDict. The usage is very straightforward. ```py from typing import TypedDict, NotRequired class Name(TypedDict): first_name: str middle_name: NotRequired[str] last_name: str ``` If we have too many optional keys, we can specify those keys that are required using `Required` instead of specifying those optional as not required. Thus, the alternative equivalent solution for the above issue: ```py from typing import TypedDict, Required class Name(TypedDict, total=False): first_name: Required[str] middle_name: str last_name: Required[str] ``` Note in the code snippet we specify `total=False` which makes all the keys optional. In the meantime, we mark these required keys as `Required` which means that the other keys are optional. ## Software Design There are eight built-in Python decorators discussed in [4] that can help you write more elegant and maintainable code. ```pre @atexit.register @dataclasses @enum.unique @partial @singledispatch @classmethod @staticmethod @property ``` ## References [1]: [A Gentle Introduction to Decorators in Python](https://machinelearningmastery.com/a-gentle-introduction-to-decorators-in-python/) [2]: [How to Use the Magical @staticmethod, @classmethod, and @property Decorators in Python](https://betterprogramming.pub/how-to-use-the-magical-staticmethod-classmethod-and-property-decorators-in-python-e42dd74e51e7?gi=8734ec8451fb) [3]: [4 New Type Annotation Features in Python 3.11](https://betterprogramming.pub/4-new-type-annotation-features-in-python-3-11-84e7ec277c29) [4]: [8 Built-in Python Decorators to Write Elegant Code](https://www.kdnuggets.com/8-built-in-python-decorators-to-write-elegant-code) [5]: [Mastering Python Decorators: A Deep Dive Into Function Wrapping and Enhancements](https://medium.com/codex/mastering-python-decorators-a-deep-dive-into-function-wrapping-and-enhancements-2092d70ff26f) ---------- [Function Wrappers in Python: Model Runtime and Debugging](https://builtin.com/data-science/python-wrapper) [The Python Decorator Handbook](https://www.freecodecamp.org/news/the-python-decorator-handbook/)