Decorators and some little things
Learn a little bit of Python from time to time.
1. F-string
!for repr vs str!ris for__repr__, a formal, printable representation!sis 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
returnin func and wrapper - @functools helps to keep
func.__name__(willl returnfuncinstead 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,
funcwill 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_repeatwill be returned as the REAL decorator and takefuncas 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
@decoratoris 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