Decorators and some little things

3 minute read

Learn a little bit of Python from time to time.

1. F-string

  1. ! for repr vs str
    • !r is for __repr__, a formal, printable representation
    • !s is for __str__, a shorter output
      import 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
      
  2. : for formating
    balance = 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

  3. * before a argument, meaning accept an arbitrary number of positional arguments
    def sum_all(*args):
         #type(args) = tuple
         total = 0
         for arg in args:
             total += arg
         return total
    print(sum_all(1, 2, 3))
    
  4. ** is for keyword argument and expands as dic
    def 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)

  5. All subsequent parameters after * must be passed as keyword arguments
    def configure_settings(setting1, *, option1=True, option2=False):
     ...
    # Valid calls    
    configure_settings("value_a", option1=False)
    # configure_settings("value_a", False) # Invalid, raises TypeError
    
  6. All parameters preceding / are positional-only arguments
    def 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

  1. 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!")
    
  2. Adding arguments and return values
    • Take advantage of * introduced above
    • add return in func and wrapper
    • @functools helps to keep func.__name__(willl return func instead of wrapper)
      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
      
  3. 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(...):
      ...
    
  4. Class decorator Function decorator can be worked as class decorator, but just apply to class.__init__ method unless redesigned
    from dataclasses import dataclass
    # dataclass will implement most basic methods for data class.
    @dataclass
    class PlayingCard:
     rank: str
     suit: str
    
  5. 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, like repeat(func). So the return value of the decorator is decorator_repeat(_func)
      @repeat
      def func(...):
      
    • If there is a keyword arguments, repeat(num_times=3) will be called. and decorator_repeat will be returned as the REAL decorator and take func 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)
      
  6. Stateful decorator function attributes are used here(wrapper_count_calls.num_calls) to keep track of the state of the functions
    def 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
    
  7. Using Classes as Decorators
    • decorator syntax @decorator is just a quicker way of saying func = 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
      

Tags:

Categories:

Updated: