# decorators are used to transform/alter function behaviour # usage syntax: # @ # def (): # ... # Pseudocode of what's happening under the hood: # def (): # ... # = () # we are overwriting the function with the new, altered version # Decorators are just functions that take another function as an argument # wrap some additional logic around it and then return the new - edited function # Creating decorators: # 1) Define a function with desired with a single argument (for the function) def decorator_example(func): print("This is called on func definition") # 2) Define a `wrapping` function that will `wrap` logic around the func we are transforming # We don't know what function might be used with our decorator # Because the function might require arguments we will use beforementioned *args **kwargs # That way it will handle functions with any number of arguments def wrapper(*args, **kwargs): # Logic before func call print(f"This is called before {func.__name__}()") # call the func with args and kwargs # This will "unfold" arguments into separate ones instead of passing it the list and dictionary ### func(args, kwargs) != func(*args, **kwargs) ### func(*args, **kwargs) # logic after func call print(f"This is called after {func.__name__}()") # 3) Return the function we used to wrap func return wrapper # Now we can do @decorator_example def helloworld(): print(f"Hello World!") ### Because the decorator itself contains a print statement we will get an output on helloworld definition ### >>> This is called on func definition if '__main__' == __name__: helloworld() # >>> This is called before helloworld() # >>> Hello World! # >>> This is called after helloworld()