Supprimer Rendre public Rendre privé Add tags Delete tags
  Ajouter un tag   Annuler
  Supprimer le tag   Annuler
  • • DevOps notes •
  •  
  • AI
  • Tags
  • Connexion

Enhancing Functions: Decorators/shaare/OPgX0g

  • python
  • python

Enhancing Functions: Decorators

  • A decorator is a callable that takes another function, adds behaviour before and/or after it runs, and returns a new callable.
  • They solve cross‑cutting concerns such as logging, timing, permission checks, or retries without cluttering core logic.
  • The magic @decorator_name syntax is shorthand for passing the target function to the decorator and re‑binding the original name to the returned wrapper.

Decorator Anatomy (Manual View)

  • Outer decorator function accepts the target function and creates a wrapper inside it.
  • The wrapper usually takes *args, **kwargs so it can handle any signature.
  • Wrapper executes optional "before" code, calls the original, maybe does "after" code, and returns the original’s result.
  • Returning the wrapper from the decorator completes the transformation.

Using decorators:

  • Manually wrapping illustrates what @ syntax really does behind the scenes.
  • This approach is clear but repetitive: @ eliminates the manual reassignment step.
import time

def simple_task(sleep_duration):
    time.sleep(sleep_duration)
    print("Running a simple task...")

def timing_decorator(original_function):
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = original_function(*args, **kwargs)
        duration = time.perf_counter() - start
        print(f"{original_function.__name__} took {duration:.3f}s")

        return result

    return wrapper

simple_task = timing_decorator(simple_task)
simple_task(0.3)

The @ Syntax

  • Placing @decorator_name directly above def my_func(): triggers my_func = decorator_name(my_func) at definition time.
  • After that line is executed, my_func refers to the wrapper returned by the decorator, so callers automatically get enhanced behaviour.
  • This keeps the decoration visible and close to the function definition, improving readability.
@timing_decorator
def another_task():
    print("Running another task...")

another_task()

Configurable Decorators: Decorators with Arguments

  • A basic decorator adds fixed behavior; sometimes you need to configure that behaviour (e.g. how many retries, which log level).
  • You cannot pass options directly to a plain @decorator, because that decorator receives only the target function.
  • Solution: call a factory that takes options and returns a decorator, then apply it with @factory(option=value).
def timing_decorator(original_function):
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = original_function(*args, **kwargs)
        duration = time.perf_counter() - start
        print(f"{original_function.__name__} took {duration:.3f}s")

        return result

    return wrapper

The Decorator Factory Pattern

  • Factory function receives configuration arguments and returns the actual decorator.
  • The actual decorator still takes the target function and builds a wrapper.
  • The wrapper can access both the factory’s configuration (via a closure) and the call‑time *args / **kwargs for the target function.
  • Three nested layers keep concerns separated: configuration ➜ decoration ➜ runtime.

Applying Decorators with Arguments

  • Use @factory(arg1, arg2…) above the function definition.
  • At definition time Python calls the factory, gets back a decorator, and applies that decorator to the function.
  • Callers of the function automatically get the behaviour configured by the factory.

Example: Retry Decorator Factory

  • A practical DevOps scenario: retry a flaky operation a configurable number of times.
  • The factory takes max_attempts; the wrapper loops until success or until attempts are exhausted, re‑raising the last error.
import random

def retry(max_attempts=3):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for attempt in range(1, max_attempts + 1):
                try:
                    print(f"Attempt {attempt}/{max_attempts}")
                    return func(*args, **kwargs)
                except Exception as e:
                    print(f" Error: {e}")
                    if attempt == max_attempts:
                        raise

        return wrapper
    return decorator

@retry(4)
def sometimes_fails():
    if random.random() < 0.7:
        raise RuntimeError("Flaky failure")
    return "Success!"

print(f"Result: {sometimes_fails()}")

Configurable Decorators: Decorators with Arguments

  • A basic decorator adds fixed behavior; sometimes you need to configure that behaviour (e.g. how many retries, which log level).
  • You cannot pass options directly to a plain @decorator, because that decorator receives only the target function.
  • Solution: call a factory that takes options and returns a decorator, then apply it with @factory(option=value).
def timing_decorator(original_function):
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = original_function(*args, **kwargs)
        duration = time.perf_counter() - start
        print(f"{original_function.__name__} took {duration:.3f}s")

        return result

    return wrapper

The Decorator Factory Pattern

  • Factory function receives configuration arguments and returns the actual decorator.
  • The actual decorator still takes the target function and builds a wrapper.
  • The wrapper can access both the factory’s configuration (via a closure) and the call‑time *args / **kwargs for the target function.
  • Three nested layers keep concerns separated: configuration ➜ decoration ➜ runtime.

Applying Decorators with Arguments

  • Use @factory(arg1, arg2…) above the function definition.
  • At definition time Python calls the factory, gets back a decorator, and applies that decorator to the function.
  • Callers of the function automatically get the behaviour configured by the factory.

Example: Retry Decorator Factory

  • A practical DevOps scenario: retry a flaky operation a configurable number of times.
  • The factory takes max_attempts; the wrapper loops until success or until attempts are exhausted, re‑raising the last error.
import random

def retry(max_attempts=3):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for attempt in range(1, max_attempts + 1):
                try:
                    print(f"Attempt {attempt}/{max_attempts}")
                    return func(*args, **kwargs)
                except Exception as e:
                    print(f" Error: {e}")
                    if attempt == max_attempts:
                        raise

        return wrapper
    return decorator

@retry(4)
def sometimes_fails():
    if random.random() < 0.7:
        raise RuntimeError("Flaky failure")
    return "Success!"

print(f"Result: {sometimes_fails()}")

Decorators & Return Values

  • A decorator’s wrapper replaces the original function, so if it forgets to return the original result the caller receives None.
  • Many real‑world functions produce critical data (e.g. status strings, dictionaries, numeric results); the decorator must be transparent about that value.
  • Fixing this means capturing the result of func(*args, **kwargs) inside the wrapper and returning it unchanged.
def log_calls_broken(func):
    def wrapper(*args, **kwargs):
        print(f"LOG: Calling {func.__name__}")
        func(*args, **kwargs)
        print(f"LOG: Finished {func.__name__}")
    return wrapper

@log_calls_broken
def add(x, y):
    return x + y

print(f"Result seen by caller: {add(2, 3)}")

The Wrapper’s Responsibility

  • The wrapper is the public face of the decorated function; it must faithfully:
    • Call the original with all arguments.
    • Capture its return value.
    • Perform any extra behaviour (log, time, validate).
    • Return the captured value so callers remain unaware of the wrapper.
  • Failure to return breaks contracts and causes subtle bugs.

Capturing return values

  • Capturing is a one‑liner: value = func(*args, **kwargs).
  • After post‑call logic, return value preserves behaviour.
  • You can also inspect or transform value before returning if the decorator’s purpose demands it.
def log_calls(func):
    def wrapper(*args, **kwargs):
        print(f"LOG: Calling {func.__name__}")
        value = func(*args, **kwargs)
        print(f"LOG: Finished {func.__name__}")
        return value
    return wrapper

@log_calls
def multiply(a, b):
    return a * b

print(f"Result seen by caller: {multiply(2, 3)}")

Handling Exceptions in Decorators

  • Wrappers often log exceptions for observability but should re‑raise them so callers can still handle or see errors.
  • Use try ... except ... raise around the call; log inside the except, then re‑raise without arguments to preserve traceback.
  • A decorator that swallows exceptions changes program semantics unless that is its explicit purpose (e.g. retry).
def log_and_reraise(func):
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except Exception as err:
            print(f"[ERROR] {func.__name__} raised {err.__class__.__name__}")
            raise
    return wrapper

@log_and_reraise
def fail():
    raise ValueError("simulated problem")

fail()

functools.wraps

  • A decorator replaces the original function object with its wrapper, so introspection tools see the wrapper’s metadata instead of the original’s.
  • Attributes such as __name__, __doc__, __module__, and type‑hint annotations are lost or altered.
  • This confuses debuggers, documentation generators, and anyone relying on help(), inspect, or error traces that reference the function name.
  • Python’s functools module supplies @wraps(original_func); apply it inside your decorator to the wrapper.
  • @wraps copies key metadata from the original function onto the wrapper, so the decorated function still looks like the original externally.
def broken_decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@broken_decorator
def add(a, b):
    """Return the sum of two numbers."""
    return a + b

print("Introspection without @wraps:")
print(f"  __name__: {add.__name__}")
print(f"  __doc__: {add.__doc__}")
from functools import wraps

def correct_decorator(func):
    @wraps(func) # Best practice: Always use it!
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@correct_decorator
def multiply(a, b):
    """Return the product of two numbers."""
    return a * b

print("Introspection with @wraps:")
print(f"  __name__: {multiply.__name__}")
print(f"  __doc__: {multiply.__doc__}")

Stacking Decorators: Applying Multiple Layers

  • Python lets you attach more than one decorator to a single function by writing multiple @decorator lines above the def.
  • Each decorator contributes a distinct slice of behaviour (logging, timing, caching, auth checks) keeping the core function clean.

Application vs. Execution Order

  • Decoration happens bottom‑up when the function is defined:
    1. Decorator nearest the def wraps the original first.
    2. Each line above wraps the result of the previous decoration.
  • Execution happens top‑down (outside‑in) when the decorated function is called: the outermost wrapper runs first, then calls the inner wrapper, and so on until the original function runs.

Order Matters

  • Swapping decorator order changes both side‑effects and final result if wrappers transform the return value.

from functools import wraps

def decorator_A(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("A before")
        result = func(*args, **kwargs)
        print("A after")
        return result
    return wrapper

def decorator_B(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("B before")
        result = func(*args, **kwargs)
        print("B after")
        return result
    return wrapper

@decorator_A
@decorator_B
def foo():
    print("  >>> inside function foo")

@decorator_B
@decorator_A
def bar():
    print("  >>> inside function bar")

foo()

print("----")

bar()
2 months ago Permalien
cluster icon
  • Mocking : Mocking Fundamentals Introduction When unit testing DevOps scripts that interact with external systems, tests can become slow, unreliable, difficult ...
  • Filesystem Paths : Working with Filesystem Paths in Python Manipulating paths as plain strings is error-prone and OS-specific. pathlib provides an object-oriented, cr...
  • Dictionaries : Dictionaries (dict) Dictionaries are mutable, insertion-ordered collections of key-value pairs. Keys must be unique and immutable; values can be of an...
  • Lambda Functions : Lambda Functions Python functions defined with def allow multiple statements, clear naming, and support for docstrings, making them ideal for complex...
  • Handling Authentication : Handling Authentication APIs often require authentication to control access, rate limits, and auditing. Without authentication, requests to protected...


(110)
Filtrer par liens sans tag
Replier Replier tout Déplier Déplier tout Êtes-vous sûr de vouloir supprimer ce lien ? Êtes-vous sûr de vouloir supprimer ce tag ? Le gestionnaire de marque-pages personnel, minimaliste, et sans base de données par la communauté Shaarli