Skip to content

Instantly share code, notes, and snippets.

@nikolaysm
Last active August 29, 2024 23:20
Show Gist options
  • Save nikolaysm/e899d6364db09000a812c784ff43ad69 to your computer and use it in GitHub Desktop.
Save nikolaysm/e899d6364db09000a812c784ff43ad69 to your computer and use it in GitHub Desktop.
Decorator - A function that takes another function as an argument, adds functionality, and returns the decorated function.

Timing Decorator

# Function decorator that times execution
from time import time

def timer(func):
    # Nested wrapper function
    def wrapper():
        start = time()
        func()
        end = time()
        print(f"Duration: {end-start}")
    return wrapper

@timer
def sum_nums():
    result = 0
    for x in range(1000000):
        result += x

sum_nums()
Duration: 0.04120516777038574


from time import time

def timer(func):
    # Initialize with extreme values to ensure they get updated correctly
    fastest = float('inf')  # Initialize with infinity so any time will be faster
    slowest = float('-inf') # Initialize with negative infinity so any time will be slower

    # The wrapper function now uses nonlocal to access the fastest and slowest variables
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        nonlocal fastest, slowest
        start = time()
        result = func(*args, **kwargs)
        end = time()
        duration = end - start

        # Update fastest and slowest times
        if duration < fastest:
            fastest = duration
        if duration > slowest:
            slowest = duration

        print(f"Duration: {duration:.6f} seconds")
        print(f"Fastest: {fastest:.6f} seconds, Slowest: {slowest:.6f} seconds")
        return result

    return wrapper

# Example usage:
@timer
def sum_nums():
    result = 0
    for x in range(1000000):
        result += x
    return result

sum_nums()  # Run several times to see the changes in fastest and slowest times

Logging Decorator

def logger(func):
    def wrapper(*args, **kwargs):
        print(f"Ran {func.__name__} with args: {args}, and kwargs: {kwargs}")
        return func(*args, **kwargs)
    return wrapper

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

@logger
def sub(x, y):
    return x - y

add(10, 20)
sub(30, 20)
Ran add with args: (10, 20), and kwargs: {}
Ran sub with args: (30, 20), and kwargs: {}
10

Caching Decorator

import functools

def cache(func):
    cache_data = {}
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # Sorting kwargs to ensure consistent key structure
        sorted_kwargs = tuple(sorted(kwargs.items()))
        key = args + sorted_kwargs
        
        if key not in cache_data:
            cache_data[key] = func(*args, **kwargs)
        return cache_data[key]
    return wrapper

import time
@cache
def expensive_func(x):
    start_time = time.time()
    time.sleep(2)
    print(f"{expensive_func.__name__} ran in {time.time() - start_time:.2f} secs")
    return x

%time print(expensive_func(1))
expensive_func ran in 2.00 secs
1
CPU times: user 10.4 ms, sys: 2.82 ms, total: 13.2 ms
Wall time: 2 s
%time print(expensive_func(1))
1
CPU times: user 619 µs, sys: 100 µs, total: 719 µs
Wall time: 725 µs
@cache
def fibonacci(n):
    if n < 2:
        return n
    else:
        return fibonacci(n-1) + fibonacci(n-2)
fibonacci(10)
55
  • Enhance cache with maximum size and LRU cache expiration
import time
from functools import lru_cache

@lru_cache(maxsize=128)  # Set maxsize to whatever limit you want
def expensive_func(x):
    start_time = time.time()
    time.sleep(2)
    print(f"{expensive_func.__name__} ran in {time.time() - start_time:.2f} secs")
    return x
    
%time print(expensive_func(1))
expensive_func ran in 2.00 secs
1
CPU times: user 1.17 ms, sys: 4 µs, total: 1.17 ms
Wall time: 2 s

%time print(expensive_func(1))
1
CPU times: user 282 µs, sys: 0 ns, total: 282 µs
Wall time: 240 µs

Delay

import time
from functools import wraps

def delay(seconds):
    def inner(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print(f"Sleeping for {seconds} seconds before running {func.__name__}")
            time.sleep(seconds)
            return func(*args, **kwargs)
        return wrapper
    return inner


@delay(seconds=3)
def print_text():
    print("Hello World")
  
print_text()
Sleeping for 3 seconds before running print_text
Hello World

# import time
from functools import wraps

# parameterization so delays can be customized

def delay(seconds):
    def inner(func):
        @wraps(func)
        def wrapper(delay_seconds=seconds, *args, **kwargs):
            print(f"Sleeping for {delay_seconds} seconds before running {func.__name__}")
            time.sleep(delay_seconds)
            return func(*args, **kwargs)
        return wrapper
    return inner
# import time
from functools import wraps

def delay(seconds):
    def inner(func):
        @wraps(func)
        def wrapper(delay_seconds=seconds, *args, **kwargs):
            print(f"Sleeping for {delay_seconds} seconds before running {func.__name__}")
            time.sleep(delay_seconds)
            return func(*args, **kwargs)
        return wrapper
    return inner
 
@delay(seconds=3)
def print_text(text="Hello World"):
    print(text)

print_text()
print_text(5, "Hello World 2")
Sleeping for 3 seconds before running print_text
Hello World
Sleeping for 5 seconds before running print_text
Hello World 2

Debug

def debug(func):
    # Nested wrapper function
    def wrapper():
        import pdb; pdb.set_trace()
        func()
    return wrapper

@debug
def sum_nums():
    result = 0
    for x in range(1000000):
        result += x
        
sum_nums()
> <ipython-input-22-f5d6cfd2b688>(5)wrapper()
-> func()
(Pdb) l
  1  	def debug(func):
  2  	    # Nested wrapper function
  3  	    def wrapper():
  4  	        import pdb; pdb.set_trace()
  5  ->	        func()
  6  	    return wrapper
[EOF]
(Pdb) c

Retry

import time
from functools import wraps


def retry(attempts=3, delay=1):
    """
    A decorator for retrying a function if it raises any exception.

    :param attempts: The maximum number of attempts to make.
    :param delay: The delay (in seconds) between attempts.
    """
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            att = attempts
            while att > 0:
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    att -= 1
                    if att > 0:
                        print(f"Retry {attempts - att} of {attempts}: Error occurred: {e}")
                        time.sleep(delay)
                    else:
                        print("All retries failed. Last error: ", str(e))
                        raise  # Re-raise the last exception after final attempt
        return wrapper
    return decorator
 
@retry(attempts=5, delay=2)
def test_function():
    import random
    if random.random() < 0.5:
        print("Function succeeded")
        return "Success"
    else:
        print("Function failed")
        raise Exception("Simulated failure")

# Call the function and allow any final exceptions to bubble up naturally
for i in range(3):
    print(f"Function returned: {test_function()}")
Function failed
Retry 1 of 5: Error occurred: Simulated failure
Function failed
Retry 2 of 5: Error occurred: Simulated failure
Function succeeded
Function returned: Success
Function succeeded
Function returned: Success
Function succeeded
Function returned: Success
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment