Okay, Nattar. Day 3.
Yesterday was comprehensions and generators. Today, I went down a different Python rabbit hole: Decorators
I had seen decorators before, but seeing @something above a function and actually understanding what happens underneath are two very different things.
So instead of just reading about them, I decided to build a small decorator library.
First... what is a decorator?
The simplest way I can explain it to myself:
A decorator lets me add extra behaviour to a function without changing the original function.
For example:
@log_calls
def add(a, b):
return a + b
The add() function still does what it was supposed to do.
The decorator simply adds something around it. And that's the idea I wanted to practice.
My five decorators
1. log_calls
This one logs the function name and the arguments passed to it.
def log_calls(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(
f"Calling function {func.__name__} "
f"with arguments {args} and keyword arguments {kwargs}"
)
result = func(*args, **kwargs)
return result
return wrapper
This helped me understand *args and **kwargs a little better too.
2. timeit
I wanted to know how long a function takes to execute.
start = time.time()
result = func(*args, **kwargs)
end = time.time()
elapsed = end - start
So instead of adding this timing logic inside every function, I can simply use:
@timeit
def my_function():
...
3. retry
What happens if a function fails?
Try again. That's what this decorator does.
for i in range(3):
try:
result = func(*args, **kwargs)
return result
except Exception as e:
error = e
print(f"Attempt {i+1} failed..")
If the function fails, it gets three attempts before the error is raised.
4. cache
This one was interesting. If I call a function with the same arguments multiple times, why calculate the same result again?
cache = {}
key = (args, tuple(kwargs.items()))
if key in cache:
return cache[key]
result = func(*args, **kwargs)
cache[key] = result
return result
Now the result can be reused when the same arguments are passed again. This was my first hands-on experience with implementing caching rather than just hearing about it.
5. validate
Finally, I wanted to experiment with validating function arguments using type annotations.
I used inspect.signature() to bind the arguments and then checked their types:
expected_type = annotations.get(name)
if not isinstance(value, expected_type):
raise TypeError(...)
So I could write something like:
@validate
def add(a: int, b: int):
return a + b
And the decorator checks whether the arguments are actually integers before running the function.
And then I thought...
Why stop at five .py files?
I wanted to try turning these decorators into an installable Python package. So I created a package structure, added the necessary package configuration, and tried installing it locally.
Something like:
pip install .
And then I could import the decorators into another project. That was probably my favourite part of today's challenge.
What did I learn today?
Today I got hands-on with:
- Decorators
- Wrapper functions
@wraps-
*argsand**kwargs - Caching
- Logging
- Retry logic
- Function timing
- Type validation
inspect.signature()- Python package structure
- Making a local package installable
Today started with: "Let's learn decorators."
And ended with: "Wait... I actually built a package."
Not bad. Still a long way to go.
But that's exactly why I started this 10 Week AI Challenge. I'm not trying to become an AI engineer in ten days. I'm trying to build the foundation that will help me become one.
Day 3: Done. ✅
Three days in. Three different learning experiences.
See you on Day 4. 🚀
I've added my decorator library to GitHub as part of my 10 Week AI Challenge.
👉 View the Decorator Library on GitHub
Keep going, Nattar.
Top comments (0)