Advanced Functions & Parameters

Reviewed & published by Brayan K

Master every advanced Python function technique — from closures and decorators to metaprogramming, function factories, and production-level patterns used by FAANG engineers.

Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

🎯 What Makes Python Functions Special?

🏠 Real-World Analogy:

In Python, functions are like physical tools you can hold. You can put them in a toolbox (list), hand them to a friend (pass as argument), get them back (return value), or even create new tools on the fly!

CapabilityWhat It MeansExample
First-class objectsFunctions are values like numbersf = print
Assignable to variablesStore in a variable for latergreet = say_hello
Storable in collectionsKeep functions in lists/dictsops = [add, sub]
Passable as argumentsGive to other functionsmap(func, data)
Returnable as valuesFunctions can create functionsreturn inner_func
Capture outer scopeRemember variables (closures)nonlocal count

💡 Why This Matters: This gives Python functional power similar to JavaScript—enabling decorators, callbacks, and advanced patterns.

🧠 2. Positional vs Keyword Parameters (Deep Dive)

# Positional vs Keyword Arguments

# Positional arguments - order matters
def greet(name, age):
    print(name, age)

greet("Alice", 25)

# Keyword arguments - order doesn't matter
greet(name="Alex", age=20)
greet(age=20, name="Alex")

# Keyword arguments improve:
# ✔ clarity
# ✔ safety
# ✔ order-independent calls
# ✔ API design

# ✅ Expected output:
# Alice 25
# Alex 20
# Alex 20

⚙️ 3. Default Parameters (Correct & Incorrect Ways)

⚠️ The Mutable Default Trap (VERY Common Bug!)

One of Python's most notorious gotchas! Default values are created once when the function is defined, not each time it's called. This causes bugs with mutable defaults like lists or dicts.

Default TypeSafe?Why?
x=5 (int)✅ YesImmutable - can't be changed
x="hello" (str)✅ YesImmutable - can't be changed
x=[] (list)❌ DANGERMutable - shared across all calls!
❌ DANGERMutable - shared across all calls!
x=None✅ YesThe correct fix for mutable defaults
def create_user(role="guest"):  # String is immutable - safe!
    return {"role": role}

print(create_user())        # {'role': 'guest'}
print(create_user("admin")) # {'role': 'admin'}

# ✅ Expected output:
# {'role': 'guest'}
# {'role': 'admin'}

❌ Dangerous default (THE BUG):

def add_item(item, items=[]):  # ❌ List created ONCE when function defined!
    items.append(item)
    return items

print(add_item(1))  # [1] - looks fine...
print(add_item(2))  # [1, 2] - WAIT, where did 1 come from?!
print(add_item(3))  # [1, 2, 3] - They all share the SAME list!

# ✅ Expected output:
# [1]
# [1, 2]
# [1, 2, 3]

✔ The Fix (use None pattern):

def add_item(item, items=None):  # ✅ None is safe
    if items is None:
        items = []  # Create NEW list each time
    items.append(item)
    return items

print(add_item(1))  # [1]
print(add_item(2))  # [2] - Fresh list each time!
print(add_item(3))  # [3] - Each call is independent

# ✅ Expected output:
# [1]
# [2]
# [3]

🔄 4. *args (Variadic Positional Parameters)

🤔 What is *args?

*args lets a function accept any number of positional arguments. They get collected into a tuple you can iterate over.

def total(*numbers):  # *numbers catches ALL positional args
    print(f"Received: {numbers}")  # It's a tuple!
    return sum(numbers)

print(total(1, 2, 3))         # Can pass 3 args
print(total(5, 10, 15, 20))   # Or 4 args
print(total(100))             # Or just 1!

# ✅ Expected output:
# Received: (1, 2, 3)
# 6
# Received: (5, 10, 15, 20)
# 50
# Received: (100,)
# 100

🔧 5. **kwargs (Variadic Keyword Parameters)

🤔 What is **kwargs?

**kwargs lets a function accept any number of keyword arguments. They get collected into a dictionary.

def build_user(**info):  # **info catches ALL keyword args
    print(f"Received: {info}")  # It's a dictionary!
    return info

user = build_user(name="Alex", age=20, role="admin")
print(user)  # {'name': 'Alex', 'age': 20, 'role': 'admin'}

# ✅ Expected output:
# Received: {'name': 'Alex', 'age': 20, 'role': 'admin'}
# {'name': 'Alex', 'age': 20, 'role': 'admin'}
SyntaxCollectsStored AsExample Call
*argsPositional argumentsTuplef(1, 2, 3)
**kwargsKeyword argumentsDictionaryf(a=1, b=2)
*args, **kwargsBoth!Tuple + Dictf(1, 2, a=3)

🧬 6. Argument Unpacking ( * and ** )

nums = [1, 2, 3]
print(*nums)  # 1 2 3

def add(a, b, c):
    return a + b + c

print(add(*nums))

# ✅ Expected output:
# 1 2 3
# 6
data = {"x": 1, "y": 2}

def show(x, y):
    print(x, y)

show(**data)

# ✅ Expected output:
# 1 2

This is deeply used in:

🧩 7. First-Class Functions & Higher-Order Functions

A function passed into another function:

def apply(fn, value):
    return fn(value)

print(apply(lambda n: n*n, 5))  # 25

def double(x):
    return x * 2

print(apply(double, 10))  # 20

# ✅ Expected output:
# 25
# 20

🧠 8. Lambda Functions (Real Usage)

Beginners think lambdas are "inline shortcuts". Experts know lambdas power:

users = [{"name": "Alex", "age": 20}, {"name": "Sam", "age": 30}]
sorted_users = sorted(users, key=lambda u: u["age"])
print(sorted_users)

# ✅ Expected output:
# [{'name': 'Alex', 'age': 20}, {'name': 'Sam', 'age': 30}]

🔒 9. Closures (Python's Most Important Function Feature)

A closure is like a backpack that a function carries. When you create a function inside another function, the inner function "packs" any variables it needs from the outer function and keeps them forever—even after the outer function finishes!

TermMeaning
ClosureA function that "remembers" variables from where it was created
nonlocalKeyword to modify (not just read) an outer variable
Free variableA variable used in a function but defined outside it
def counter():
    count = 0  # This is the "backpack" variable
    
    def inc():
        nonlocal count  # ← "I want to MODIFY count, not create a new one"
        count += 1
        return count
    
    return inc  # Return the inner function (with its backpack!)

# Create a counter - it remembers its own count
c = counter()
print(c())  # 1 - count is remembered!
print(c())  # 2 - still remembering!
print(c())  # 3 - it persists!

# Create another counter - it has its OWN backpack
c2 = counter()
print(c2()) # 1 - separate count!

# ✅ Expected output:
# 1
# 2
# 3
# 1

🎁 10. Decorators (The Real Advanced-Level Skill)

Decorators transform functions and classes — used everywhere in Python frameworks.

def logger(fn):
    def wrapper(*args, **kwargs):
        print("Calling:", fn.__name__)
        return fn(*args, **kwargs)
    return wrapper

@logger
def greet():
    print("Hello!")

greet()

# ✅ Expected output:
# Calling: greet
# Hello!

This is one of Python's signature advanced features.

🧪 10b. Worked Example — Everything So Far in One Decorator

This is where the last few sections join up. A decorator is a closure (it remembers the function it was handed), it uses *args and **kwargs so it can wrap any function, and it has to hand the return value back or the decorated function silently starts returning None.

Read the comments before running it — each one says what the line does and what it costs you if you leave it out.

import functools

# A decorator is just a function that takes a function and returns a new one.
def log_calls(func):

    # @functools.wraps copies the original name and docstring onto the wrapper.
    # Leave it out and greet.__name__ comes back as "wrapper", which breaks
    # help(), debuggers, and anything that inspects your functions.
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # *args collects positional arguments into a tuple.
        # **kwargs collects keyword arguments into a dict.
        # Together they let one wrapper accept ANY call signature.
        shown = [repr(a) for a in args] + [f"{k}={v!r}" for k, v in kwargs.items()]
        print(f"-> {func.__name__}({', '.join(shown)})")

        result = func(*args, **kwargs)   # unpack them straight back out again

        print(f"<- {func.__name__} returned {result!r}")
        return result                    # forget this line and greet() gives you None
    return wrapper


@log_calls                 # exactly the same as writing:  greet = log_calls(greet)
def greet(name, greeting="Hello"):
    """Return a greeting line."""
    return f"{greeting}, {name}!"


print(greet("Ada"))                      # one positional argument
print(greet("Grace", greeting="Hi"))     # positional + keyword

# functools.wraps kept the identity of the original function:
print(greet.__name__)
print(greet.__doc__)

# ✅ Expected output:
# -> greet('Ada')
# <- greet returned 'Hello, Ada!'
# Hello, Ada!
# -> greet('Grace', greeting='Hi')
# <- greet returned 'Hi, Grace!'
# Hi, Grace!
# greet
# Return a greeting line.

Notice the order of the printed lines. The -> line happens before the real function runs and the <- line after it, which is why decorators are the natural home for timing, logging, authentication and caching: they own the moment before and the moment after every call.

🎯 10c. Your Turn — A Decorator That Counts Calls

Two blanks, both of them the ideas this lesson is built on: the keyword that lets an inner function modify an outer variable, and the syntax that forwards every argument through untouched. Run it and check against the expected output.

import functools

# 🎯 YOUR TURN — finish the decorator that counts how often a function is called
# Fill in the two blanks marked ___

def count_calls(func):
    calls = 0                          # the closure variable — the "backpack"

    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # 👉 replace ___ with the keyword that lets you MODIFY calls
        #    (without it, calls += 1 raises UnboundLocalError)
        ___ calls
        calls += 1
        print(f"{func.__name__} has been called {calls} time(s)")

        # 👉 replace ___ so the original function receives whatever it was given
        return func(___)
    return wrapper


@count_calls
def add(a, b):
    return a + b


print(add(2, 3))        # two positional arguments
print(add(10, b=5))     # one positional, one keyword

# ✅ Expected output:
# add has been called 1 time(s)
# 5
# add has been called 2 time(s)
# 15

🚀 11. Function Factories (Dynamic Function Creation)

def multiply_by(n):
    def inner(x):
        return n * x
    return inner

double = multiply_by(2)
triple = multiply_by(3)
print(double(10))  # 20
print(triple(10))  # 30

# ✅ Expected output:
# 20
# 30

🔁 12. Recursion (Pythonic Patterns)

def factorial(n):
    return 1 if n <= 1 else n * factorial(n-1)

print(factorial(5))  # 120

# ✅ Expected output:
# 120

Tail recursion isn't optimised in Python—so you must use it strategically.

🧮 13. Memoization (Caching for High Performance)

🤔 What is Memoization?

Memoization means remembering the results of expensive function calls. If you call the function with the same inputs again, it returns the cached result instantly instead of recalculating.

Without MemoizationWith Memoization
fib(30) → 1+ million calculationsfib(30) → ~30 calculations
Exponential time O(2ⁿ)Linear time O(n)
Takes seconds/minutesInstant ⚡
from functools import lru_cache

@lru_cache(maxsize=None)  # ← This one line adds caching!
def fib(n):
    if n < 2: 
        return n
    return fib(n-1) + fib(n-2)

# Without @lru_cache, fib(30) would take forever
# With it, it's instant!
print(fib(30))  # 832040 - calculated instantly!
print(fib(50))  # 12586269025 - still instant!

# ✅ Expected output:
# 832040
# 12586269025

🧱 14. Pure vs Impure Functions

def add(a, b):
    return a + b

print(add(5, 3))

# ✅ Expected output:
# 8
x = 0
def inc():
    global x
    x += 1

inc()
print(x)

# ✅ Expected output:
# 1

🎛 15. Context Managers as Functionality

Context managers extend function behavior beyond decorators.

from contextlib import contextmanager

@contextmanager
def timer():
    import time
    start = time.time()
    yield
    print(f"Elapsed: {time.time() - start}s")

with timer():
    sum(range(1000000))

🕹 16. Putting It All Together — Advanced Example

A rate-limited API wrapper using:

import time

def rate_limit(max_calls, interval):
    calls = []

    def decorator(fn):
        def wrapper(*args, **kwargs):
            nonlocal calls
            now = time.time()

            calls = [t for t in calls if now - t < interval]

            if len(calls) >= max_calls:
                raise Exception("Rate limit exceeded")

            calls.append(now)
            return fn(*args, **kwargs)
        return wrapper
    return decorator

@rate_limit(3, 5)
def fetch_data():
    print("Fetching data...")

for _ in range(4):
    try:
        fetch_data()
    except Exception as e:
        print(e)

This is real production-level engineering — used in APIs, SaaS, AI tools, and infra services.

🧩 17. Currying & Partial Function Application

🤔 What's the Difference?

TechniqueWhat It DoesExample
CurryingTransform f(a,b) into f(a)(b)multiply(2)(3)
PartialPre-fill some argumentsdouble = partial(multiply, 2)
def multiply(a):      # First call: pass 'a'
    def inner(b):     # Second call: pass 'b'
        return a * b  # Use both!
    return inner

# Create specialized functions
double = multiply(2)  # "Lock in" 2 as first argument
triple = multiply(3)  # "Lock in" 3 as first argument

print(double(10))  # 20 (2 * 10)
print(triple(10))  # 30 (3 * 10)

# ✅ Expected output:
# 20
# 30
from functools import partial

def power(base, exponent):
    return base ** exponent

# Pre-fill exponent=2 → creates a "square" function
square = partial(power, exponent=2)

# Pre-fill exponent=3 → creates a "cube" function  
cube = partial(power, exponent=3)

print(square(5))  # 25 (5²)
print(cube(5))    # 125 (5³)

# ✅ Expected output:
# 25
# 125

🧬 18. Function Introspection

def hello(name: str) -> str:
    return f"Hello {name}"

print(hello.__name__)
print(hello.__annotations__)
print(hello.__code__.co_argcount)

# ✅ Expected output:
# hello
# {'name': <class 'str'>, 'return': <class 'str'>}
# 1

🧱 19. Metaprogramming With Functions

def modify(func):
    def wrapper(*a, **kw):
        result = func(*a, **kw)
        return f"[Modified] {result}"
    return wrapper

@modify
def greet():
    return "Hello"

print(greet())  # [Modified] Hello

# ✅ Expected output:
# [Modified] Hello

🔁 20. Function Pipelines (Functional Composition)

def compose(f, g):
    return lambda x: f(g(x))

def increment(x): return x + 1
def double(x): return x * 2

pipeline = compose(double, increment)
print(pipeline(5))  # (5+1)*2 = 12

# ✅ Expected output:
# 12

Functional composition is used in:

📦 21. Callables Beyond Functions

class Multiplier:
    def __init__(self, factor):
        self.factor = factor

    def __call__(self, value):
        return value * self.factor

double = Multiplier(2)
print(double(10))  # 20

# ✅ Expected output:
# 20

🧲 22. Dynamic Dispatch (Single Dispatch)

from functools import singledispatch

@singledispatch
def display(x):
    print("Default:", x)

@display.register(int)
def _(x):
    print("Integer:", x)

@display.register(list)
def _(x):
    print("List:", x)

display(10)
display([1,2,3])

# ✅ Expected output:
# Integer: 10
# List: [1, 2, 3]

⚙️ 23. Decorators With Arguments (Decorator Factories)

def repeat(n):
    def wrapper(func):
        def inner(*a, **kw):
            for _ in range(n):
                func(*a, **kw)
        return inner
    return wrapper

@repeat(3)
def greet():
    print("Hello")

greet()

# ✅ Expected output:
# Hello
# Hello
# Hello

This is essential for:

🧪 24. Creating Custom Decorators for Error Handling

def safe(func):
    def wrapper(*a, **kw):
        try:
            return func(*a, **kw)
        except Exception as e:
            print("Error:", e)
    return wrapper

@safe
def risky(x):
    return 10 / x

risky(0)  # prints error instead of crashing

# ✅ Expected output:
# Error: division by zero

🔍 25. Benchmarking Functions

import time

def slow():
    sum(i*i for i in range(20000))

start = time.perf_counter()
slow()
end = time.perf_counter()

print("Time:", end - start)

🚀 26. Real-World: Dynamic API Client Generator

def api_client(base_url):
    def make_request(endpoint):
        def call(**params):
            print("GET", base_url + endpoint, params)
        return call
    return make_request

github = api_client("https://api.github.com")
get_user = github("/users")
get_user(username="torvalds")

# ✅ Expected output:
# GET https://api.github.com/users {'username': 'torvalds'}

This is how real SDKs are created.

🧨 27. Using Functions to Create DSLs

def select(*fields):
    return {"select": fields}

def where(**conditions):
    return {"where": conditions}

query = {**select("name", "age"), **where(id=5)}
print(query)

# ✅ Expected output:
# {'select': ('name', 'age'), 'where': {'id': 5}}

DSLs are used in:

🧠 28. Advanced Factory Patterns With Functions

def serializer(fmt):
    if fmt == "json":
        import json
        return json.dumps
    if fmt == "repr":
        return repr

to_json = serializer("json")
print(to_json({"key": "value"}))

# ✅ Expected output:
# {"key": "value"}

🎛 29. Using Functions as Middleware Chains

def middleware_chain(func_list):
    def call(value):
        for f in func_list:
            value = f(value)
        return value
    return call

pipeline = middleware_chain([
    lambda x: x + 1,
    lambda x: x * 5,
    str
])

print(pipeline(10))  # "55"

# ✅ Expected output:
# 55

🧨 30. Part 1 Summary

You can now work with:

🧠 31. Descriptors — The Hidden Power Behind Properties

class Descriptor:
    def __get__(self, instance, owner):
        return "Got value"

    def __set__(self, instance, value):
        print("Set:", value)

class Demo:
    x = Descriptor()

d = Demo()
print(d.x)
d.x = 10

# ✅ Expected output:
# Got value
# Set: 10

🔧 32. Metaclasses + Functions = Dynamic Class Construction

class Meta(type):
    def __new__(cls, name, bases, attrs):
        attrs['created_by'] = 'Meta'
        return super().__new__(cls, name, bases, attrs)

class User(metaclass=Meta):
    pass

print(User.created_by)

# ✅ Expected output:
# Meta

🎛 33. Using Functions to Build Plugins

PLUGINS = {}

def plugin(name):
    def register(func):
        PLUGINS[name] = func
        return func
    return register

@plugin("compress")
def compress(data):
    return data[:10]

print(PLUGINS)

Plugins are used in:

🧩 34. Higher-Order Error Handling Patterns

def rescue(default=None):
    def wrapper(func):
        def inner(*a, **kw):
            try: return func(*a, **kw)
            except: return default
        return inner
    return wrapper

@rescue(default="failed")
def risky(x):
    return 10/x

print(risky(0))  # "failed"

# ✅ Expected output:
# failed

🧬 35. Partial Evaluation & Runtime Specialisation

from functools import partial

def logistic(x, L, k, x0):
    return L / (1 + 2.71828 ** (-k*(x-x0)))

fast_logistic = partial(logistic, L=1, k=0.5)

print(fast_logistic(10, x0=5))

# ✅ Expected output:
# 0.9241417020900337

Partial evaluation is used in:

⚡ 36. Memoization Variants (Custom TTL Cache)

import time

def ttl_cache(seconds):
    def wrapper(func):
        store = {}
        def inner(*a):
            now = time.time()
            if a in store:
                value, t = store[a]
                if now - t < seconds:
                    return value
            result = func(*a)
            store[a] = (result, now)
            return result
        return inner
    return wrapper

@ttl_cache(5)
def expensive():
    return "computed"

This is the basis of:

🧱 37. Turning Functions Into Stateful Machines

def counter():
    n = 0
    def inner():
        nonlocal n
        n += 1
        return n
    return inner

c = counter()
print(c())  # 1
print(c())  # 2

# ✅ Expected output:
# 1
# 2

Stateful function techniques create:

🧠 38. Function Composition for Data Pipelines

def compose(*functions):
    def pipeline(value):
        for f in functions:
            value = f(value)
        return value
    return pipeline

clean = compose(
    lambda x: x.strip(),
    lambda x: x.lower(),
    lambda x: x.replace("!", "")
)

print(clean("  HELLO!  "))

# ✅ Expected output:
# hello

🧲 39. Callback Systems (Events, Hooks & Signals)

callbacks = []

def on_event(func):
    callbacks.append(func)
    return func

@on_event
def notify():
    print("Notified!")

for f in callbacks:
    f()

# ✅ Expected output:
# Notified!

Callbacks are the foundation of:

🧨 40. Advanced Use of yield for Coroutines

def processor():
    total = 0
    while True:
        x = yield total
        total += x

p = processor()
next(p)
print(p.send(5))  # 5
print(p.send(3))  # 8

🧮 41. Decorators That Modify Function Signatures

from functools import wraps
import inspect

def rename_args(**new_names):
    def wrapper(func):
        sig = inspect.signature(func)
        print("Original sig:", sig)
        return func
    return wrapper

@rename_args(x="value")
def test(x):
    pass

# ✅ Expected output:
# Original sig: (x)

This kind of technique builds:

🎛 42. Building an Advanced Pipeline Framework

class Pipeline:
    def __init__(self):
        self.steps = []

    def step(self, func):
        self.steps.append(func)
        return func

    def run(self, input):
        for f in self.steps:
            input = f(input)
        return input

p = Pipeline()

@p.step
def clean(x): return x.strip()

@p.step
def lower(x): return x.lower()

print(p.run("  HELLO  "))

# ✅ Expected output:
# hello

This mirrors real frameworks like Airflow or spaCy.

🎉 Final Wrap-Up

You now understand every advanced Python function mechanism:

You've reached a level of Python function mastery that only senior engineers, framework authors, or ML system architects typically achieve.

🎯 Mini-Challenge: Build a Decorator Factory

A decorator factory is a decorator you can configure — @repeat(times=3) rather than plain @repeat. It is three nested functions deep, which is exactly why it is worth writing once by hand. Only the outline is given below; no logic.

# 🎯 MINI-CHALLENGE: a decorator factory —  @repeat(times=3)
#
# Three layers, from the outside in:
# 1. def repeat(times):          the FACTORY — takes the setting, returns a decorator
# 2.     def decorator(func):    the decorator — takes the function, returns a wrapper
# 3.         def wrapper(*args, **kwargs):
# 4.             results = []
# 5.             call func(*args, **kwargs) "times" times, appending each result
# 6.             return results
# 7.         return wrapper
# 8.     return decorator
#
# Then use it:
#     @repeat(times=3)
#     def roll():
#         return 4
#
#     print(roll())
#
# ✅ Expected output:
# [4, 4, 4]

# your code here

If you get TypeError: decorator() missing 1 required positional argument, the usual cause is returning decorator(func) instead of decorator — the factory must hand back the function itself, not the result of calling it.

📋 Quick Reference — Advanced Functions

SyntaxWhat it does
*argsCollect extra positional arguments
**kwargsCollect extra keyword arguments
functools.partial(fn, x)Fix one or more arguments
inspect.signature(fn)Introspect function parameters
lambda x: x * 2Create an inline anonymous function

🏆 Lesson Complete!

You've mastered every advanced function technique Python has — from variadic arguments to partial application and function introspection.

Practice quiz

What is the bug in 'def add_item(item, items=[])'?

  • Lists cannot be parameters
  • items must come first
  • The default list is created once and shared across every call, so items leak between calls
  • It is too slow

Answer: The default list is created once and shared across every call, so items leak between calls. Default values are created once at definition time, so a mutable default like [] is reused across calls and accumulates data.

What is the correct fix for a mutable default argument?

  • Default to None, then do 'if items is None: items = []' inside
  • Use items=() instead
  • Make items required
  • Use a global list

Answer: Default to None, then do 'if items is None: items = []' inside. The None sentinel pattern gives each call a fresh list, avoiding the shared-default trap.

Inside a function, what does *args collect arguments into?

  • A list
  • A dictionary
  • A set
  • A tuple

Answer: A tuple. *args gathers extra positional arguments into a tuple you can iterate over.

Inside a function, what does **kwargs collect arguments into?

  • A tuple
  • A dictionary
  • A list
  • A namedtuple

Answer: A dictionary. **kwargs gathers extra keyword arguments into a dictionary mapping names to values.

What keyword lets an inner function MODIFY a variable from its enclosing function?

  • nonlocal
  • global
  • static
  • extern

Answer: nonlocal. nonlocal lets a closure modify (not just read) a variable in the enclosing function's scope; without it Python creates a new local.

What does @lru_cache add to a function like a recursive fib?

  • Logging
  • Type checking
  • Memoization — cached results so repeated inputs return instantly
  • Parallel execution

Answer: Memoization — cached results so repeated inputs return instantly. @lru_cache from functools caches results, turning exponential recursive fib into linear time by reusing computed values.

With 'square = partial(power, exponent=2)', what does square(5) return for power(base, exponent)=base**exponent?

  • 10
  • 25
  • 32
  • 7

Answer: 25. partial pre-fills exponent=2, so square(5) computes 5**2 = 25.

What does 'compose(double, increment)' (f(g(x))) return for input 5, where increment adds 1 and double multiplies by 2?

  • 11
  • 10
  • 7
  • 12

Answer: 12. compose(double, increment)(5) = double(increment(5)) = double(6) = 12.

What makes a function 'pure'?

  • It uses global variables
  • Same input always gives the same output with no side effects
  • It prints its result
  • It modifies its arguments

Answer: Same input always gives the same output with no side effects. A pure function has no side effects and always returns the same output for the same input — easier to test and parallelize.

How is a callable class created?

  • By defining __init__ only
  • By inheriting from function
  • By defining a __call__ method so instances can be invoked like functions
  • By using @callable

Answer: By defining a __call__ method so instances can be invoked like functions. Defining __call__ makes instances callable, e.g. double = Multiplier(2); double(10) — giving function-like objects with state.

Continue this course