Chapters Five and Six Python Program/Notes¶
Chapter 5 - Advanced Functions¶
Functions can be passed variables or values and this is called an argument.
You just do that like Java.
You can also assign variables in the function definition.
You can give a function place holder parameters in the event someone calls your function without the proper arguments.
Forcing parameters can be done in a couple ways. One with a / that is positional, and * which is positional or keyword.
If you’re making a function where you can accept any number of parameters you put a little * in front.
*name
That stores any number of positional parameters in a tuple, while this stores them in a dictionary.
**name
Return statements can return actually anything because in python everything is an object.
Pass by references allow parameters to change values, but in Python that doesn’t really work.
Python does pass objects, but only as a reference to that object, so it won’t change the original.
If you wanna change something you gotta make an object, I guess.
In Python it’s possible to return multiple values using a tuple, which can be unpacked into variables using destructing assignment.
Annotations can be applied to parameters and return values, usually to indicate type.
Chapter 5 Program¶
I wanted to keep the programs separate for chapter 5 and 6, because these chapters are getting a little juicer.
# -------------------------------------------------------------------
# 1. Arguments — passing variables or values into a function (like Java)
# -------------------------------------------------------------------
print("=== 1. Arguments ===")
def greet(name, greeting):
print(f" {greeting}, {name}!")
greet("Ada", "Hello") # passing values directly
language = "Python"
version = 3
greet(language, f"Welcome to version {version}")
# -------------------------------------------------------------------
# 2. Default (placeholder) parameters
# If the caller skips an argument, the default kicks in
# -------------------------------------------------------------------
print("\n=== 2. Default parameters ===")
def power(base, exp=2): # exp has a placeholder default of 2
return base ** exp
print(f" power(4) = {power(4)}") # uses default exp=2
print(f" power(4, 3) = {power(4, 3)}") # caller overrides it
# -------------------------------------------------------------------
# 3. Forcing parameter style
# / = everything before it must be POSITIONAL
# * = everything after it must be KEYWORD
# -------------------------------------------------------------------
print("\n=== 3. Forced positional ( / ) and keyword ( * ) ===")
def strict(pos_only, /, normal, *, kw_only):
print(f" pos_only={pos_only} normal={normal} kw_only={kw_only}")
# pos_only must be positional, kw_only must be keyword
strict(1, 2, kw_only=3)
strict(10, normal=20, kw_only=30)
# These would raise TypeErrors — uncomment to prove it:
# strict(pos_only=1, normal=2, kw_only=3) # pos_only can't be keyword
# strict(1, 2, 3) # kw_only can't be positional
# -------------------------------------------------------------------
# 4. *args — accept any number of positional arguments (stored as tuple)
# -------------------------------------------------------------------
print("\n=== 4. *args (any number of positional params → tuple) ===")
def total(*numbers):
print(f" Received tuple: {numbers}")
return sum(numbers)
print(f" total(1,2,3) = {total(1, 2, 3)}")
print(f" total(10,20,30,40) = {total(10, 20, 30, 40)}")
# -------------------------------------------------------------------
# 5. **kwargs — accept any number of keyword arguments (stored as dict)
# -------------------------------------------------------------------
print("\n=== 5. **kwargs (any number of keyword params → dict) ===")
def display_info(**details):
print(f" Received dict: {details}")
for key, value in details.items():
print(f" {key}: {value}")
display_info(name="Guido", language="Python", year=1991)
# -------------------------------------------------------------------
# 6. Mixing *args and **kwargs together
# -------------------------------------------------------------------
print("\n=== 6. *args + **kwargs together ===")
def mixed(required, *args, **kwargs):
print(f" required={required}")
print(f" extra positional (tuple): {args}")
print(f" extra keyword (dict): {kwargs}")
mixed("must-have", 1, 2, 3, color="blue", size="large")
# -------------------------------------------------------------------
# 7. Return statements — can return ANYTHING (everything is an object)
# -------------------------------------------------------------------
print("\n=== 7. Returning anything ===")
def get_function(mode):
if mode == "shout":
return lambda s: s.upper() + "!!!" # returns a function object
return lambda s: s.lower() + "..."
shout = get_function("shout")
whisper = get_function("quiet")
print(f" {shout('hello')}")
print(f" {whisper('HELLO')}")
def get_class():
class Surprise: # returns a class object
value = 42
return Surprise
MyClass = get_class()
print(f" Returned class value: {MyClass.value}")
# -------------------------------------------------------------------
# 8. Pass by object reference — Python passes a reference,
# but rebinding inside the function does NOT change the original.
# Mutating the object itself DOES affect the original.
# -------------------------------------------------------------------
print("\n=== 8. Pass-by-object-reference ===")
def try_rebind(x):
x = 9999 # just rebinds the local name — original unchanged
print(f" inside try_rebind: x = {x}")
num = 42
try_rebind(num)
print(f" after try_rebind: num = {num}") # still 42
# But mutating a mutable object DOES affect the caller
def append_item(lst, item):
lst.append(item) # mutates the object the reference points to
my_list = [1, 2, 3]
append_item(my_list, 99)
print(f" after append_item: my_list = {my_list}") # [1, 2, 3, 99]
# "If you wanna change something you gotta make an object" — use a list/dict
def increment(counter):
counter["value"] += 1 # mutate the dict object
c = {"value": 0}
increment(c)
increment(c)
print(f" counter after 2 increments: {c}")
# -------------------------------------------------------------------
# 9. Returning multiple values via tuple + destructuring assignment
# -------------------------------------------------------------------
print("\n=== 9. Multiple return values + tuple unpacking ===")
def min_max(numbers):
return min(numbers), max(numbers) # returns a tuple
data = [4, 1, 9, 2, 7]
result = min_max(data)
print(f" raw return (tuple): {result}")
low, high = min_max(data) # destructuring assignment
print(f" unpacked → low={low}, high={high}")
def divmod_custom(a, b):
return a // b, a % b # quotient and remainder
quotient, remainder = divmod_custom(17, 5)
print(f" 17 ÷ 5 → quotient={quotient}, remainder={remainder}")
# -------------------------------------------------------------------
# 10. Type annotations — hint at expected types for params and return
# Python does NOT enforce these at runtime; they're documentation
# -------------------------------------------------------------------
print("\n=== 10. Type annotations ===")
def add(a: int, b: int) -> int:
return a + b
def repeat_str(s: str, times: int = 2) -> str:
return s * times
def stats(numbers: list[float]) -> tuple[float, float, float]:
return min(numbers), max(numbers), sum(numbers) / len(numbers)
print(f" add(3, 4) = {add(3, 4)}")
print(f" repeat_str('ha') = {repeat_str('ha')}")
print(f" repeat_str('ha', 4) = {repeat_str('ha', 4)}")
lo, hi, avg = stats([3.0, 1.5, 9.2, 4.8])
print(f" stats([3.0,1.5,9.2,4.8]) → min={lo}, max={hi}, avg={avg:.2f}")
# Annotations are stored on the function object itself — totally inspectable
print(f"\n add.__annotations__ = {add.__annotations__}")
print(f" stats.__annotations__ = {stats.__annotations__}")
Chapter 6 - Decorators¶
Ts makes functions much more useful.
So first off you can pass functions into functions, because a function is an object and you can pass any object.
Wrapper functions can be used to pass all of the parameters of one function on to another.
In Python 3 a decorator is a little prefix before a function defection.
You gotta use @dec in front of a function.
They can add or modify stuff.
Decorator factories have parameters and provide parameters to a decorator.
Chapter 6 Program¶
import time
import functools
# -------------------------------------------------------------------
# 1. Functions are objects — you can pass them into other functions
# -------------------------------------------------------------------
print("=== 1. Functions as objects ===")
def shout(text):
return text.upper() + "!!!"
def whisper(text):
return text.lower() + "..."
def deliver_message(func, message):
# func is just an object being passed in like any other argument
result = func(message)
print(result)
deliver_message(shout, "hello there")
deliver_message(whisper, "hello there")
# -------------------------------------------------------------------
# 2. Wrapper function — passes ALL parameters through with *args/**kwargs
# This is the manual version of what decorators automate
# -------------------------------------------------------------------
print("\n=== 2. Wrapper function (manual) ===")
def announce(func):
def wrapper(*args, **kwargs): # captures everything
print(f" >> Calling {func.__name__}")
result = func(*args, **kwargs) # forwards everything
print(f" >> Finished {func.__name__}")
return result
return wrapper
def add(a, b):
print(f" {a} + {b} = {a + b}")
return a + b
announced_add = announce(add) # wrap manually — no @ yet
announced_add(3, 4)
# -------------------------------------------------------------------
# 3. The @ decorator syntax — syntactic sugar for the same thing
# @dec before a def is exactly: func = dec(func)
# -------------------------------------------------------------------
print("\n=== 3. @ decorator syntax ===")
def bold(func):
@functools.wraps(func) # keeps the original name/docstring intact
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return f"**{result}**"
return wrapper
@bold # same as: greet = bold(greet)
def greet(name):
return f"Hello, {name}"
print(greet("Guido"))
print(greet("Ada"))
# -------------------------------------------------------------------
# 4. Decorator that ADDS behavior (timing)
# -------------------------------------------------------------------
print("\n=== 4. Decorator that adds behavior (timer) ===")
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = (time.perf_counter() - start) * 1000
print(f" ⏱ {func.__name__} took {elapsed:.3f} ms")
return result
return wrapper
@timer
def slow_sum(n):
return sum(range(n))
result = slow_sum(1_000_000)
print(f" Result: {result}")
# -------------------------------------------------------------------
# 5. Decorator that MODIFIES behavior (block negative inputs)
# -------------------------------------------------------------------
print("\n=== 5. Decorator that modifies behavior (guard) ===")
def positive_only(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
if any(a < 0 for a in args if isinstance(a, (int, float))):
print(f" ✗ {func.__name__} blocked — negative inputs not allowed")
return None
return func(*args, **kwargs)
return wrapper
@positive_only
def square_root(n):
return n ** 0.5
print(f" √9 = {square_root(9)}")
print(f" √-1 = {square_root(-1)}")
# -------------------------------------------------------------------
# 6. Stacking decorators — applied bottom-up
# -------------------------------------------------------------------
print("\n=== 6. Stacked decorators (applied bottom → top) ===")
@bold
@timer
def slow_greeting(name):
time.sleep(0.001)
return f"Greetings, {name}"
# timer wraps first, then bold wraps the timed version
print(slow_greeting("World"))
# -------------------------------------------------------------------
# 7. Decorator factory — a decorator that accepts its own parameters
# factory(params) → returns a configured decorator
# That decorator → returns the wrapper
# -------------------------------------------------------------------
print("\n=== 7. Decorator factory (parameters) ===")
def repeat(times): # factory: takes a parameter
def decorator(func): # the actual decorator
@functools.wraps(func)
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator # factory returns the ready-to-use decorator
@repeat(times=3) # factory runs first, then decorates say()
def say(message):
print(f" {message}")
say("echo!")
# Another factory — stamp a label onto every return value
def tag(label):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return f"[{label}] {func(*args, **kwargs)}"
return wrapper
return decorator
@tag("INFO")
def status(msg):
return msg
@tag("WARNING")
def warn(msg):
return msg
print(status("All systems go"))
print(warn("Disk space low"))