├── README.md ├── LICENSE ├── decorator_func.py ├── decorator_functools_wrap.py ├── decorator_pass_obj.py ├── decorator_pass_obj_partial.py ├── decorator_pattern_call.py ├── decorator_pattern_classic.py └── decorator_type_propagation.py /README.md: -------------------------------------------------------------------------------- 1 | # Python Decorators: The Complete Guide 2 | 3 | Python decorators are a great way to add functionality to your Python functions. In this video, I'll show you what they are, how they work, and some of the most useful decorators you can use in your code. 4 | 5 | Video: https://youtu.be/QH5fw9kxDQA. 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 ArjanCodes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /decorator_func.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from math import sqrt 3 | from time import perf_counter 4 | from typing import Any, Callable 5 | 6 | 7 | def with_logging(func: Callable[..., Any]) -> Callable[..., Any]: 8 | def wrapper(*args: Any, **kwargs: Any) -> Any: 9 | logging.info(f"Calling {func.__name__}") 10 | value = func(*args, **kwargs) 11 | logging.info(f"Finished {func.__name__}") 12 | return value 13 | 14 | return wrapper 15 | 16 | 17 | def benchmark(func: Callable[..., Any]) -> Callable[..., Any]: 18 | def wrapper(*args: Any, **kwargs: Any) -> Any: 19 | start_time = perf_counter() 20 | value = func(*args, **kwargs) 21 | end_time = perf_counter() 22 | run_time = end_time - start_time 23 | logging.info(f"Execution of {func.__name__} took {run_time:.2f} seconds.") 24 | return value 25 | 26 | return wrapper 27 | 28 | 29 | def is_prime(number: int) -> bool: 30 | if number < 2: 31 | return False 32 | for element in range(2, sqrt(number) + 1): 33 | if number % element == 0: 34 | return False 35 | return True 36 | 37 | 38 | @with_logging 39 | @benchmark 40 | def count_prime_numbers(upper_bound: int) -> int: 41 | count = 0 42 | for number in range(upper_bound): 43 | if is_prime(number): 44 | count += 1 45 | return count 46 | 47 | 48 | def main() -> None: 49 | logging.basicConfig(level=logging.INFO) 50 | count_prime_numbers(50000) 51 | 52 | 53 | if __name__ == "__main__": 54 | main() 55 | -------------------------------------------------------------------------------- /decorator_functools_wrap.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import logging 3 | from math import sqrt 4 | from time import perf_counter 5 | from typing import Any, Callable 6 | 7 | 8 | def with_logging(func: Callable[..., Any]) -> Callable[..., Any]: 9 | @functools.wraps(func) 10 | def wrapper(*args: Any, **kwargs: Any) -> Any: 11 | logging.info(f"Calling {func.__name__}") 12 | value = func(*args, **kwargs) 13 | logging.info(f"Finished {func.__name__}") 14 | return value 15 | 16 | return wrapper 17 | 18 | 19 | def benchmark(func: Callable[..., Any]) -> Callable[..., Any]: 20 | @functools.wraps(func) 21 | def wrapper(*args: Any, **kwargs: Any) -> Any: 22 | start_time = perf_counter() 23 | value = func(*args, **kwargs) 24 | end_time = perf_counter() 25 | run_time = end_time - start_time 26 | logging.info(f"Execution of {func.__name__} took {run_time:.2f} seconds.") 27 | return value 28 | 29 | return wrapper 30 | 31 | 32 | def is_prime(number: int) -> bool: 33 | if number < 2: 34 | return False 35 | for element in range(2, sqrt(number) + 1): 36 | if number % element == 0: 37 | return False 38 | return True 39 | 40 | 41 | @with_logging 42 | @benchmark 43 | def count_prime_numbers(upper_bound: int) -> int: 44 | count = 0 45 | for number in range(upper_bound): 46 | if is_prime(number): 47 | count += 1 48 | return count 49 | 50 | 51 | def main() -> None: 52 | logging.basicConfig(level=logging.INFO) 53 | count_prime_numbers(50000) 54 | 55 | 56 | if __name__ == "__main__": 57 | main() 58 | -------------------------------------------------------------------------------- /decorator_pass_obj.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import logging 3 | from math import sqrt 4 | from time import perf_counter 5 | from typing import Any, Callable 6 | 7 | logger = logging.getLogger("my_app") 8 | 9 | 10 | def with_logging(logger: logging.Logger): 11 | def decorator(func: Callable[..., Any]) -> Callable[..., Any]: 12 | @functools.wraps(func) 13 | def wrapper(*args: Any, **kwargs: Any) -> Any: 14 | logger.info(f"Calling {func.__name__}") 15 | value = func(*args, **kwargs) 16 | logger.info(f"Finished {func.__name__}") 17 | return value 18 | 19 | return wrapper 20 | 21 | return decorator 22 | 23 | 24 | def benchmark(func: Callable[..., Any]) -> Callable[..., Any]: 25 | @functools.wraps(func) 26 | def wrapper(*args: Any, **kwargs: Any) -> Any: 27 | start_time = perf_counter() 28 | value = func(*args, **kwargs) 29 | end_time = perf_counter() 30 | run_time = end_time - start_time 31 | logging.info(f"Execution of {func.__name__} took {run_time:.2f} seconds.") 32 | return value 33 | 34 | return wrapper 35 | 36 | 37 | def is_prime(number: int) -> bool: 38 | if number < 2: 39 | return False 40 | for element in range(2, sqrt(number) + 1): 41 | if number % element == 0: 42 | return False 43 | return True 44 | 45 | 46 | @with_logging(logger) 47 | @benchmark 48 | def count_prime_numbers(upper_bound: int) -> int: 49 | count = 0 50 | for number in range(upper_bound): 51 | if is_prime(number): 52 | count += 1 53 | return count 54 | 55 | 56 | def main() -> None: 57 | logging.basicConfig(level=logging.INFO) 58 | count_prime_numbers(50000) 59 | 60 | 61 | if __name__ == "__main__": 62 | main() 63 | -------------------------------------------------------------------------------- /decorator_pass_obj_partial.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import logging 3 | from math import sqrt 4 | from time import perf_counter 5 | from typing import Any, Callable 6 | 7 | logger = logging.getLogger("my_app") 8 | 9 | 10 | def with_logging( 11 | func: Callable[..., Any], logger: logging.Logger 12 | ) -> Callable[..., Any]: 13 | @functools.wraps(func) 14 | def wrapper(*args: Any, **kwargs: Any) -> Any: 15 | logger.info(f"Calling {func.__name__}") 16 | value = func(*args, **kwargs) 17 | logger.info(f"Finished {func.__name__}") 18 | return value 19 | 20 | return wrapper 21 | 22 | 23 | def benchmark(func: Callable[..., Any]) -> Callable[..., Any]: 24 | @functools.wraps(func) 25 | def wrapper(*args: Any, **kwargs: Any) -> Any: 26 | start_time = perf_counter() 27 | value = func(*args, **kwargs) 28 | end_time = perf_counter() 29 | run_time = end_time - start_time 30 | logging.info(f"Execution of {func.__name__} took {run_time:.2f} seconds.") 31 | return value 32 | 33 | return wrapper 34 | 35 | 36 | def is_prime(number: int) -> bool: 37 | if number < 2: 38 | return False 39 | for element in range(2, sqrt(number) + 1): 40 | if number % element == 0: 41 | return False 42 | return True 43 | 44 | 45 | with_default_logging = functools.partial(with_logging, logger=logger) 46 | 47 | 48 | @with_default_logging 49 | @benchmark 50 | def count_prime_numbers(upper_bound: int) -> int: 51 | count = 0 52 | for number in range(upper_bound): 53 | if is_prime(number): 54 | count += 1 55 | return count 56 | 57 | 58 | def main() -> None: 59 | logging.basicConfig(level=logging.INFO) 60 | count_prime_numbers(50000) 61 | 62 | 63 | if __name__ == "__main__": 64 | main() 65 | -------------------------------------------------------------------------------- /decorator_pattern_call.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from abc import ABC, abstractmethod 3 | from math import sqrt 4 | from time import perf_counter 5 | from typing import Any, Callable 6 | 7 | 8 | class AbstractDecorator(ABC): 9 | def __init__(self, decorated: Callable[..., Any]) -> None: 10 | self._decorated = decorated 11 | 12 | @abstractmethod 13 | def __call__(self, *args: Any, **kwargs: Any) -> Any: 14 | pass 15 | 16 | 17 | class BenchmarkDecorator(AbstractDecorator): 18 | def __call__(self, *args: Any, **kwargs: Any) -> Any: 19 | start_time = perf_counter() 20 | value = self._decorated(*args, **kwargs) 21 | end_time = perf_counter() 22 | run_time = end_time - start_time 23 | logging.info( 24 | f"Execution of {self._decorated.__class__.__name__} took {run_time:.2f} seconds." 25 | ) 26 | return value 27 | 28 | 29 | class LoggingDecorator(AbstractDecorator): 30 | def __call__(self, *args: Any, **kwargs: Any) -> Any: 31 | logging.info(f"Calling {self._decorated.__class__.__name__}") 32 | value = self._decorated(*args, **kwargs) 33 | logging.info(f"Finished {self._decorated.__class__.__name__}") 34 | return value 35 | 36 | 37 | def is_prime(number: int) -> bool: 38 | if number < 2: 39 | return False 40 | for element in range(2, sqrt(number) + 1): 41 | if number % element == 0: 42 | return False 43 | return True 44 | 45 | 46 | def count_prime_numbers(upper_bound: int) -> int: 47 | count = 0 48 | for number in range(upper_bound): 49 | if is_prime(number): 50 | count += 1 51 | return count 52 | 53 | 54 | def main() -> None: 55 | logging.basicConfig(level=logging.INFO) 56 | benchmark = BenchmarkDecorator(count_prime_numbers) 57 | with_logging = LoggingDecorator(benchmark) 58 | with_logging(50000) 59 | 60 | 61 | if __name__ == "__main__": 62 | main() 63 | -------------------------------------------------------------------------------- /decorator_pattern_classic.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from abc import ABC, abstractmethod 3 | from math import sqrt 4 | from time import perf_counter 5 | 6 | 7 | def is_prime(number: int) -> bool: 8 | if number < 2: 9 | return False 10 | for element in range(2, sqrt(number) + 1): 11 | if number % element == 0: 12 | return False 13 | return True 14 | 15 | 16 | class AbstractComponent(ABC): 17 | @abstractmethod 18 | def execute(self, upper_bound: int) -> int: 19 | pass 20 | 21 | 22 | class AbstractDecorator(AbstractComponent): 23 | def __init__(self, decorated: AbstractComponent) -> None: 24 | self._decorated = decorated 25 | 26 | 27 | class ConcreteComponent(AbstractComponent): 28 | def execute(self, upper_bound: int) -> int: 29 | 30 | count = 0 31 | for number in range(upper_bound): 32 | if is_prime(number): 33 | count += 1 34 | return count 35 | 36 | 37 | class BenchmarkDecorator(AbstractDecorator): 38 | def execute(self, upper_bound: int) -> int: 39 | start_time = perf_counter() 40 | value = self._decorated.execute(upper_bound) 41 | end_time = perf_counter() 42 | run_time = end_time - start_time 43 | logging.info( 44 | f"Execution of {self._decorated.__class__.__name__} took {run_time:.2f} seconds." 45 | ) 46 | return value 47 | 48 | 49 | class LoggingDecorator(AbstractDecorator): 50 | def execute(self, upper_bound: int) -> int: 51 | logging.info(f"Calling {self._decorated.__class__.__name__}") 52 | value = self._decorated.execute(upper_bound) 53 | logging.info(f"Finished {self._decorated.__class__.__name__}") 54 | return value 55 | 56 | 57 | def main() -> None: 58 | logging.basicConfig(level=logging.INFO) 59 | component = ConcreteComponent() 60 | component = LoggingDecorator(component) 61 | component = BenchmarkDecorator(component) 62 | component.execute(50000) 63 | 64 | 65 | if __name__ == "__main__": 66 | main() 67 | -------------------------------------------------------------------------------- /decorator_type_propagation.py: -------------------------------------------------------------------------------- 1 | """ 2 | One downside of using decorators is that they don't play nice with type checkers by default. 3 | 4 | In `decorator_functools_wrap.py`, the typing system thinks the signature of `count_prime_numbers` is: 5 | 6 | > Callable[..., Any] 7 | 8 | However, we can help the type checker out by using some generics available in the typing library: 9 | - [TypeVar](https://docs.python.org/3/library/typing.html#typing.TypeVar) 10 | - [ParamSpec](https://docs.python.org/3/library/typing.html#typing.ParamSpec) 11 | 12 | ParamSpec is available since 3.10, but is also available in the `typing-extensions` library. 13 | 14 | This also helps check to make sure that your original signature doesn't change, like in the `hydra` example you mentioned; the type checker would raise a warning. 15 | """ 16 | 17 | import functools 18 | import logging 19 | from math import sqrt 20 | from time import perf_counter 21 | from typing import Callable, ParamSpec, TypeVar 22 | 23 | # First, create the generic types 24 | # TypeVar represents a single value 25 | WrappedReturn = TypeVar("WrappedReturn") 26 | # ParamSpec represents args and kwargs of a function signature 27 | WrappedParams = ParamSpec("WrappedParams") 28 | 29 | # Change the decorator definitions to reflect the generic types 30 | def with_logging( 31 | func: Callable[WrappedParams, WrappedReturn] 32 | ) -> Callable[WrappedParams, WrappedReturn]: 33 | @functools.wraps(func) 34 | # Assign generic types to the args and kwargs 35 | def wrapper(*args: WrappedParams.args, **kwargs: WrappedParams.kwargs) -> WrappedReturn: 36 | logging.info(f"Calling {func.__name__}") 37 | value = func(*args, **kwargs) 38 | logging.info(f"Finished {func.__name__}") 39 | return value 40 | 41 | return wrapper 42 | 43 | 44 | # Same changes here 45 | def benchmark( 46 | func: Callable[WrappedParams, WrappedReturn] 47 | ) -> Callable[WrappedParams, WrappedReturn]: 48 | @functools.wraps(func) 49 | def wrapper(*args: WrappedParams.args, **kwargs: WrappedParams.kwargs) -> WrappedReturn: 50 | start_time = perf_counter() 51 | value = func(*args, **kwargs) 52 | end_time = perf_counter() 53 | run_time = end_time - start_time 54 | logging.info(f"Execution of {func.__name__} took {run_time:.2f} seconds.") 55 | return value 56 | 57 | return wrapper 58 | 59 | 60 | def is_prime(number: int) -> bool: 61 | if number < 2: 62 | return False 63 | for element in range(2, int(sqrt(number)) + 1): 64 | if number % element == 0: 65 | return False 66 | return True 67 | 68 | 69 | @with_logging 70 | @benchmark 71 | def count_prime_numbers(upper_bound: int) -> int: 72 | count = 0 73 | for number in range(upper_bound): 74 | if is_prime(number): 75 | count += 1 76 | return count 77 | 78 | 79 | def main() -> None: 80 | logging.basicConfig(level=logging.INFO) 81 | # The type checker will reflect the correct signature: Callable[int, int] 82 | count_prime_numbers(50000) 83 | 84 | 85 | if __name__ == "__main__": 86 | main() 87 | --------------------------------------------------------------------------------