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
  • Filesystem Operations : Filesystem Operations (os & shutil) DevOps scripts often need to create, delete, copy, and move files and directories as part of automation workflows...
  • Fixtures in Pytest : Fixtures in Pytest As tests grow more complex, repeating setup and cleanup steps makes tests harder to read and maintain. Pytest fixtures allow centr...
  • Declarative Logging : Declarative Logging Configuration Declarative configuration separates setup from code, making it easier to maintain and adjust. Python’s logging.conf...
  • Functions: return vs yield : Functions: return vs yield Regular functions execute immediately, run to completion, and return a single value (or None). Generator functions retur...
  • 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...


(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