Decorators and some little things
Learn a little bit of Python from time to time.
1. F-string
!
for repr vs str!r
is for__repr__
, a formal, printable representation!s
is for__str__
, a shorter outputimport datetime now=datetime.datetime.now() print(f"{now!r}") #datetime.datetime(2025, 7, 6, 11, 36, 56, 148520) print(f"{now!s}") #2025-07-06 11:36:56.148520 print(f"{now=:%m/%d/%Y}") #now=07/06/2025
:
for formatingbalance = 5425.9292 f"Balance: ${balance:.2f}" #'Balance: $5425.93' f"Balance: ${balance:,.2f}" #'Balance: $5,425.93' heading = "Centered string" f"{heading:=^30}" #'=======Centered string========'
2.
*
and/
in the arugment list*
before a argument, meaning accept an arbitrary number of positional argumentsdef sum_all(*args): #type(args) = tuple total = 0 for arg in args: total += arg return total print(sum_all(1, 2, 3))
**
is for keyword argument and expands as dicdef print_info(**kwargs): # type(kwargs) == dict for key, value in kwargs.items(): print(f"{key}: {value}") print_info(name='Kyle', age=18)
Combine these two are the most common
fun(*args, **kwargs)
- All subsequent parameters after
*
must be passed as keyword argumentsdef configure_settings(setting1, *, option1=True, option2=False): ... # Valid calls configure_settings("value_a", option1=False) # configure_settings("value_a", False) # Invalid, raises TypeError
- All parameters preceding
/
are positional-only argumentsdef greet(name, /, message="Hello"): print(f"{message}, {name}!") # Valid calls: greet("Alice") greet("Bob", "Hi") # greet(name="Charlie") # raises a TypeError
3 Decorators
Notes for this Primer on Python Decorators
- Syntax sugar for a function which takes functions as argment and returns a function
def decorator(func): def wrapper(): print("Something is happening before the function is called.") func() print("Something is happening after the function is called.") return wrapper # Direct implementation say_whee = decorator(say_whee) # Syntax sugar @decorator def say_whee(): print("Whee!")
- Adding arguments and return values
- Take advantage of
*
introduced above - add
return
in func and wrapper - @functools helps to keep
func.__name__
(willl returnfunc
instead ofwrapper
)def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): value = func(*args, **kwargs) return value return wrapper @decorator def say_hi(name): print("in say_hi") return "hi "+name
- Take advantage of
- Decorators WITHOUT modifying function, as in Function registration
PLUGINS = dict() def register(func): """Register a function as a plug-in""" PLUGINS[func.__name__] = func return func @register def func(...): ...
- Class decorator
Function decorator can be worked as class decorator, but just apply to
class.__init__
method unless redesignedfrom dataclasses import dataclass # dataclass will implement most basic methods for data class. @dataclass class PlayingCard: rank: str suit: str
- Decorators with optional arguments
- Add one more layer of function to take in arguments
- If there is no argument,
func
will be argument for the decorator, likerepeat(func)
. So the return value of the decorator isdecorator_repeat(_func)
@repeat def func(...):
- If there is a keyword arguments,
repeat(num_times=3)
will be called. anddecorator_repeat
will be returned as the REAL decorator and takefunc
as argument.@repeat(num_times=3): def func(...):
- So the boilplate for the decorators w optional arguments is as below. It’s really smart to use
*
to force keyword arguments.import functools def repeat(_func=None, *, num_times=2): def decorator_repeat(func): @functools.wraps(func) def wrapper_repeat(*args, **kwargs): for _ in range(num_times): value = func(*args, **kwargs) return value return wrapper_repeat if _func is None: # repeat(num_times=3) return decorator_repeat else: # repeat(func) return decorator_repeat(_func)
- Stateful decorator
function attributes are used here(
wrapper_count_calls.num_calls
) to keep track of the state of the functionsdef count_calls(func): @functools.wraps(func) def wrapper_count_calls(*args, **kwargs): wrapper_count_calls.num_calls += 1 print(f"Call {wrapper_count_calls.num_calls} of {func.__name__}()") return func(*args, **kwargs) wrapper_count_calls.num_calls = 0 return wrapper_count_calls
- Using Classes as Decorators
- decorator syntax
@decorator
is just a quicker way of sayingfunc = decorator(func)
. - If decorator is a class, it needs to take func as an argument in its
.__init__()
initializer. - The class instance needs to be callable by implementing
.__call__()
. Actually you can use any callable expression as a decorator.import functools class CountCalls: def __init__(self, func): functools.update_wrapper(self, func) self.func = func self.num_calls = 0 def __call__(self, *args, **kwargs): self.num_calls += 1 print(f"Call {self.num_calls} of {self.func.__name__}()") return self.func(*args, **kwargs)
- Now we can use this class as a decorator
@CountCalls def say_whee(): print("Whee!") say_whee() #Call 1 of say_whee() #Whee! say_whee() #Call 2 of say_whee() #Whee! say_whee.num_calls #2
- decorator syntax