Python Functions, Recursion & Advanced

Python Functions Interview Questions

What is a function in Python?
A function is a reusable block of code that performs a specific task. Functions help in organizing code, reducing repetition, and making programs more modular and maintainable.
What is the basic syntax of a Python function?
# Function definition def function_name(parameters): """docstring""" # Optional documentation # function body # ... return value # Optional return statement # Function call result = function_name(arguments) # Example: def greet(name): """Return a greeting message""" return f"Hello, {name}!" message = greet("Alice") print(message) # Hello, Alice!
What are the different types of function arguments in Python?
# 1. Positional arguments (required, in order) def add(a, b): return a + b print(add(5, 3)) # 8 # 2. Keyword arguments (by parameter name) print(add(b=3, a=5)) # 8 # 3. Default arguments (optional with default values) def greet(name, message="Hello"): return f"{message}, {name}!" print(greet("Alice")) # Hello, Alice! print(greet("Bob", "Hi")) # Hi, Bob! # 4. Variable-length arguments (*args) def sum_all(*numbers): return sum(numbers) print(sum_all(1, 2, 3, 4)) # 10 # 5. Keyword variable-length arguments (**kwargs) def print_info(**info): for key, value in info.items(): print(f"{key}: {value}") print_info(name="Alice", age=30, city="NYC") # 6. Combination of all def complex_func(a, b=10, *args, **kwargs): print(f"a={a}, b={b}") print(f"args: {args}") print(f"kwargs: {kwargs}") complex_func(1, 2, 3, 4, 5, x=10, y=20)
What is the difference between parameters and arguments?
# Parameters are variables in function definition # Arguments are actual values passed when calling function def add(a, b): # a and b are parameters return a + b result = add(5, 3) # 5 and 3 are arguments # Types of parameters: # 1. Formal parameters: Names in function definition # 2. Actual parameters: Values passed during call # Example with different terms: def calculate(principal, rate, years): # These are parameters return principal * (1 + rate) ** years # These values are arguments: interest = calculate(1000, 0.05, 5) # 1000, 0.05, 5 are arguments # Summary: # - Parameters = Placeholders in function definition # - Arguments = Actual values passed to function
What is function scope in Python?
# Python has 4 levels of scope (LEGB rule): # L: Local - Inside current function # E: Enclosing - Inside enclosing functions # G: Global - At module level # B: Built-in - Python built-in names # 1. Local scope def func(): x = 10 # Local variable print(x) # Accessible here func() # print(x) # Error: x is not defined (out of scope) # 2. Enclosing scope (nested functions) def outer(): x = "outer" def inner(): print(f"Inner accessing: {x}") # Access enclosing scope inner() outer() # 3. Global scope global_var = "I'm global" def show_global(): print(global_var) # Access global variable show_global() # 4. Modifying global variable counter = 0 def increment(): global counter # Declare we want to use global variable counter += 1 increment() print(counter) # 1 # 5. Built-in scope print(len("hello")) # len is a built-in function print(max(1, 2, 3)) # max is built-in # 6. Nonlocal keyword (for nested functions) def outer(): count = 0 def inner(): nonlocal count # Refer to count in enclosing scope count += 1 return count return inner() print(outer()) # 1
What are lambda functions in Python?
# Lambda functions are anonymous, single-expression functions # Syntax: lambda arguments: expression # 1. Basic lambda add = lambda x, y: x + y print(add(5, 3)) # 8 # Equivalent to: def add(x, y): return x + y # 2. Immediately invoked result = (lambda x, y: x * y)(4, 5) print(result) # 20 # 3. With map() numbers = [1, 2, 3, 4, 5] squared = list(map(lambda x: x**2, numbers)) print(squared) # [1, 4, 9, 16, 25] # 4. With filter() evens = list(filter(lambda x: x % 2 == 0, numbers)) print(evens) # [2, 4] # 5. With sorted() pairs = [(1, 9), (2, 8), (3, 7)] sorted_pairs = sorted(pairs, key=lambda x: x[1]) # Sort by second element print(sorted_pairs) # [(3, 7), (2, 8), (1, 9)] # 6. Multiple conditions process = lambda x: x * 2 if x > 0 else x * -1 print(process(5)) # 10 print(process(-5)) # 5 # 7. Returning lambda from function def make_multiplier(n): return lambda x: x * n double = make_multiplier(2) triple = make_multiplier(3) print(double(5)) # 10 print(triple(5)) # 15 # Limitations of lambda: # - Only single expression # - No statements (if, for, while, etc.) # - No annotations # - Limited to one line
What are decorators in Python?
# Decorators modify or enhance functions without changing their code # They are functions that take another function as argument # 1. Basic decorator def my_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 @my_decorator def say_hello(): print("Hello!") say_hello() # 2. Decorator with arguments def repeat(n): def decorator(func): def wrapper(*args, **kwargs): for _ in range(n): result = func(*args, **kwargs) return result return wrapper return decorator @repeat(3) def greet(name): print(f"Hello {name}!") greet("Alice") # 3. Preserving function metadata from functools import wraps def timer(func): @wraps(func) # Preserves function metadata def wrapper(*args, **kwargs): import time start = time.time() result = func(*args, **kwargs) end = time.time() print(f"{func.__name__} took {end - start:.2f} seconds") return result return wrapper @timer def slow_function(): import time time.sleep(1) return "Done" slow_function() # 4. Class-based decorator class CountCalls: def __init__(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) @CountCalls def say_hello(): print("Hello!") say_hello() say_hello() # 5. Multiple decorators (applied bottom to top) @timer @repeat(2) def example(): print("Example function") example()
What is recursion in Python?
# Recursion: function calls itself to solve smaller instances # Must have: Base case (stopping condition) and Recursive case # 1. Factorial (classic example) def factorial(n): if n == 0: # Base case return 1 else: # Recursive case return n * factorial(n - 1) print(factorial(5)) # 120 # 2. Fibonacci sequence def fibonacci(n): if n <= 1: # Base case return n else: # Recursive case return fibonacci(n - 1) + fibonacci(n - 2) print([fibonacci(i) for i in range(10)]) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] # 3. Recursive sum of list def recursive_sum(lst): if not lst: # Base case: empty list return 0 else: # Recursive case return lst[0] + recursive_sum(lst[1:]) print(recursive_sum([1, 2, 3, 4, 5])) # 15 # 4. Binary search (recursive) def binary_search(arr, target, low, high): if low > high: # Base case: not found return -1 mid = (low + high) // 2 if arr[mid] == target: # Base case: found return mid elif arr[mid] > target: # Recursive case: search left half return binary_search(arr, target, low, mid - 1) else: # Recursive case: search right half return binary_search(arr, target, mid + 1, high) arr = [1, 3, 5, 7, 9, 11, 13] print(binary_search(arr, 7, 0, len(arr) - 1)) # 3 # 5. Tower of Hanoi def tower_of_hanoi(n, source, target, auxiliary): if n == 1: # Base case print(f"Move disk 1 from {source} to {target}") return # Move n-1 disks from source to auxiliary tower_of_hanoi(n - 1, source, auxiliary, target) # Move nth disk from source to target print(f"Move disk {n} from {source} to {target}") # Move n-1 disks from auxiliary to target tower_of_hanoi(n - 1, auxiliary, target, source) tower_of_hanoi(3, 'A', 'C', 'B') # Recursion vs Iteration: # - Recursion: Elegant for tree-like structures, but uses more memory # - Iteration: More memory efficient, sometimes faster # Tail recursion optimization (Python doesn't optimize it)
What are generators in Python?
# Generators are functions that produce sequence of values lazily # Use 'yield' instead of 'return' # 1. Simple generator def count_up_to(n): i = 1 while i <= n: yield i # Pauses here, returns i i += 1 # Using generator counter = count_up_to(5) print(next(counter)) # 1 print(next(counter)) # 2 print(next(counter)) # 3 # Using in loop for num in count_up_to(3): print(num) # 1, 2, 3 # 2. Generator expression (like list comprehension but lazy) squares = (x**2 for x in range(5)) print(list(squares)) # [0, 1, 4, 9, 16] # 3. Infinite generator def infinite_counter(): i = 0 while True: yield i i += 1 inf = infinite_counter() print(next(inf)) # 0 print(next(inf)) # 1 # 4. Generator with send() def accumulator(): total = 0 while True: value = yield total if value is None: break total += value acc = accumulator() next(acc) # Initialize generator print(acc.send(10)) # 10 print(acc.send(20)) # 30 print(acc.send(5)) # 35 acc.close() # 5. Generator for file reading def read_large_file(file_path): with open(file_path, 'r') as file: for line in file: yield line.strip() # Memory efficient for large files # for line in read_large_file("large_file.txt"): # process(line) # 6. Generator for Fibonacci def fibonacci_generator(n): a, b = 0, 1 for _ in range(n): yield a a, b = b, a + b print(list(fibonacci_generator(10))) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] # Advantages of generators: # - Memory efficient (lazy evaluation) # - Can represent infinite sequences # - Cleaner code for certain patterns
What are first-class functions in Python?
# First-class functions: Functions are treated like any other object # Can be assigned, passed as arguments, returned from functions # 1. Assign function to variable def greet(name): return f"Hello, {name}!" my_func = greet # No parentheses! print(my_func("Alice")) # Hello, Alice! # 2. Pass function as argument def apply_twice(func, arg): return func(func(arg)) def add_five(x): return x + 5 print(apply_twice(add_five, 10)) # 20 # 3. Return function from function def make_adder(n): def adder(x): return x + n return adder add_three = make_adder(3) print(add_three(10)) # 13 # 4. Store functions in data structures def square(x): return x**2 def cube(x): return x**3 def double(x): return x * 2 operations = [square, cube, double] for func in operations: print(func(5)) # 25, 125, 10 # 5. Functions as dictionary values def add(a, b): return a + b def subtract(a, b): return a - b def multiply(a, b): return a * b def divide(a, b): return a / b if b != 0 else "Error" calculator = { '+': add, '-': subtract, '*': multiply, '/': divide } print(calculator['+'](10, 5)) # 15 print(calculator['*'](10, 5)) # 50 # 6. Higher-order functions from functools import reduce numbers = [1, 2, 3, 4, 5] # map: apply function to each element squared = list(map(lambda x: x**2, numbers)) print(squared) # [1, 4, 9, 16, 25] # filter: keep elements where function returns True evens = list(filter(lambda x: x % 2 == 0, numbers)) print(evens) # [2, 4] # reduce: apply function cumulatively product = reduce(lambda x, y: x * y, numbers) print(product) # 120 # 7. Function attributes def example(): """Example function with attributes""" pass example.custom_attr = "I'm a function attribute" print(example.__name__) # example print(example.__doc__) # Example function with attributes print(example.custom_attr) # I'm a function attribute
What are function annotations in Python?
# Function annotations provide metadata about types and parameters # Syntax: def func(param: annotation) -> return_annotation: # 1. Type hints (PEP 484) def greet(name: str) -> str: """Return greeting message""" return f"Hello, {name}!" # 2. Multiple parameters def add(a: int, b: int) -> int: return a + b # 3. Complex type hints from typing import List, Tuple, Dict, Optional, Union def process_items(items: List[str]) -> Dict[str, int]: """Process list of items and return dictionary""" return {item: len(item) for item in items} # 4. Optional parameters def find_item(items: List[str], target: str) -> Optional[int]: """Return index if found, None otherwise""" try: return items.index(target) except ValueError: return None # 5. Union types def square_root(x: Union[int, float]) -> float: return x ** 0.5 # 6. Using typing module from typing import Callable def apply_func(func: Callable[[int, int], int], a: int, b: int) -> int: return func(a, b) # 7. Variable annotations pi: float = 3.14159 names: List[str] = ["Alice", "Bob", "Charlie"] # 8. Accessing annotations def example(a: int, b: str = "hello") -> bool: return True print(example.__annotations__) # {'a': , 'b': , 'return': } # 9. Annotations are not enforced at runtime # They're just metadata for documentation and type checkers # 10. Using mypy for type checking # Install: pip install mypy # Run: mypy your_script.py # 11. Forward references (for recursive types) from typing import List class TreeNode: def __init__(self, value: int, children: List['TreeNode'] = None): self.value = value self.children = children or [] # Benefits of annotations: # - Better documentation # - IDE autocompletion # - Static type checking # - Runtime introspection
What are closure functions in Python?
# Closure: Function that remembers values from enclosing scope # Even after the outer function has finished execution # 1. Basic closure def outer_func(x): def inner_func(y): return x + y # x is "remembered" from outer scope return inner_func add_five = outer_func(5) # x = 5 is remembered print(add_five(3)) # 8 print(add_five(10)) # 15 # 2. Multiple closures def make_multiplier(n): def multiplier(x): return x * n return multiplier double = make_multiplier(2) triple = make_multiplier(3) print(double(5)) # 10 print(triple(5)) # 15 # 3. Closure with mutable state def counter(): count = 0 def increment(): nonlocal count count += 1 return count return increment c1 = counter() print(c1()) # 1 print(c1()) # 2 print(c1()) # 3 c2 = counter() # Separate closure with its own state print(c2()) # 1 # 4. Closure for configuration def configure_logger(level): def log(message): print(f"[{level}] {message}") return log info_log = configure_logger("INFO") error_log = configure_logger("ERROR") info_log("System started") # [INFO] System started error_log("Something wrong!") # [ERROR] Something wrong! # 5. Closure with multiple variables def make_power(exponent): def power(base): return base ** exponent return power square = make_power(2) cube = make_power(3) print(square(5)) # 25 print(cube(5)) # 125 # 6. Practical example: HTML tag wrapper def make_html_tag(tag): def wrap_text(text): return f"<{tag}>{text}" return wrap_text bold = make_html_tag("b") italic = make_html_tag("i") print(bold("Hello")) # Hello print(italic("World")) # World # 7. Checking closure variables def outer(x): y = 10 def inner(): z = x + y return z return inner func = outer(5) print(func.__closure__) # Contains cell objects print(func.__code__.co_freevars) # ('x', 'y') - captured variables # When to use closures: # - When you need to maintain state between calls # - For creating specialized functions # - In decorators # - For callback functions
What are built-in higher-order functions?
# Higher-order functions: Functions that take other functions as arguments # 1. map(function, iterable) - Apply function to each element numbers = [1, 2, 3, 4, 5] # Using lambda squared = list(map(lambda x: x**2, numbers)) print(squared) # [1, 4, 9, 16, 25] # Using named function def square(x): return x**2 squared = list(map(square, numbers)) print(squared) # [1, 4, 9, 16, 25] # Multiple iterables list1 = [1, 2, 3] list2 = [4, 5, 6] sums = list(map(lambda x, y: x + y, list1, list2)) print(sums) # [5, 7, 9] # 2. filter(function, iterable) - Keep elements where function returns True numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] evens = list(filter(lambda x: x % 2 == 0, numbers)) print(evens) # [2, 4, 6, 8, 10] # Filter with None (removes falsy values) mixed = [0, 1, False, True, "", "hello", [], [1, 2]] truthy = list(filter(None, mixed)) print(truthy) # [1, True, 'hello', [1, 2]] # 3. reduce(function, iterable, initial) - Cumulative operation from functools import reduce numbers = [1, 2, 3, 4, 5] # Sum total = reduce(lambda x, y: x + y, numbers) print(total) # 15 # With initial value total = reduce(lambda x, y: x + y, numbers, 10) print(total) # 25 (10 + 1 + 2 + 3 + 4 + 5) # Product product = reduce(lambda x, y: x * y, numbers) print(product) # 120 # Find maximum maximum = reduce(lambda x, y: x if x > y else y, numbers) print(maximum) # 5 # 4. sorted(iterable, key=None, reverse=False) words = ["banana", "apple", "cherry", "date"] # Sort by length sorted_by_length = sorted(words, key=len) print(sorted_by_length) # ['date', 'apple', 'banana', 'cherry'] # Sort by second letter sorted_by_second = sorted(words, key=lambda x: x[1]) print(sorted_by_second) # ['date', 'banana', 'apple', 'cherry'] # 5. any(iterable) and all(iterable) numbers = [1, 2, 3, 4, 5] # any: True if any element is truthy print(any(x > 3 for x in numbers)) # True (4 and 5 are > 3) print(any(x > 10 for x in numbers)) # False # all: True if all elements are truthy print(all(x > 0 for x in numbers)) # True print(all(x > 3 for x in numbers)) # False (1, 2, 3 are not > 3) # 6. zip(*iterables) - Combine iterables names = ["Alice", "Bob", "Charlie"] ages = [25, 30, 35] cities = ["NYC", "LA", "Chicago"] combined = list(zip(names, ages, cities)) print(combined) # [('Alice', 25, 'NYC'), ('Bob', 30, 'LA'), ('Charlie', 35, 'Chicago')] # 7. enumerate(iterable, start=0) - Add counter fruits = ["apple", "banana", "cherry"] for index, fruit in enumerate(fruits, start=1): print(f"{index}. {fruit}") # 1. apple # 2. banana # 3. cherry
What is memoization and how to implement it?
# Memoization: Caching results of expensive function calls # 1. Manual memoization for factorial def factorial(n, memo={}): if n in memo: return memo[n] if n == 0: result = 1 else: result = n * factorial(n - 1, memo) memo[n] = result return result print(factorial(5)) # 120 (computes once) print(factorial(5)) # 120 (returns cached) # 2. Fibonacci with memoization def fibonacci(n, memo={}): if n in memo: return memo[n] if n <= 1: result = n else: result = fibonacci(n - 1, memo) + fibonacci(n - 2, memo) memo[n] = result return result print(fibonacci(10)) # 55 (much faster than naive recursion) # 3. Using lru_cache decorator (Python 3.2+) from functools import lru_cache @lru_cache(maxsize=None) # Unlimited cache def fibonacci_cached(n): if n <= 1: return n return fibonacci_cached(n - 1) + fibonacci_cached(n - 2) print(fibonacci_cached(50)) # 12586269025 (fast!) # 4. Custom memoization decorator def memoize(func): cache = {} def wrapper(*args): if args in cache: return cache[args] result = func(*args) cache[args] = result return result return wrapper @memoize def expensive_function(x): print(f"Computing for {x}...") return x * x # Simulating expensive computation print(expensive_function(5)) # Computing for 5... 25 print(expensive_function(5)) # 25 (cached, no computation) print(expensive_function(10)) # Computing for 10... 100 # 5. Memoization with keyword arguments def memoize_kwargs(func): cache = {} def wrapper(*args, **kwargs): key = (args, tuple(sorted(kwargs.items()))) if key in cache: return cache[key] result = func(*args, **kwargs) cache[key] = result return result return wrapper @memoize_kwargs def complex_calc(a, b, multiplier=1): print(f"Calculating {a}, {b}, {multiplier}...") return (a + b) * multiplier print(complex_calc(2, 3, multiplier=2)) # Calculating... 10 print(complex_calc(2, 3, multiplier=2)) # 10 (cached) # 6. Class-based memoization class Memoize: def __init__(self, func): self.func = func self.cache = {} def __call__(self, *args): if args in self.cache: return self.cache[args] result = self.func(*args) self.cache[args] = result return result @Memoize def factorial_class(n): if n == 0: return 1 return n * factorial_class(n - 1) # 7. When to use memoization: # - Expensive pure functions (same input → same output) # - Recursive functions with overlapping subproblems # - Dynamic programming problems # 8. Cache statistics with lru_cache @lru_cache(maxsize=3) def get_data(key): print(f"Fetching {key}...") return key.upper() print(get_data("a")) # Fetching a... A print(get_data("b")) # Fetching b... B print(get_data("a")) # A (cached) print(get_data("c")) # Fetching c... C print(get_data("d")) # Fetching d... D (a is evicted) print(get_data.cache_info()) # CacheInfo(hits=1, misses=4, maxsize=3, currsize=3)
What are partial functions in Python?
# Partial functions: Fix some arguments of a function # Create new function with fewer arguments from functools import partial # 1. Basic partial function def power(base, exponent): return base ** exponent # Create square function (fix exponent=2) square = partial(power, exponent=2) print(square(5)) # 25 (5²) # Create cube function (fix exponent=3) cube = partial(power, exponent=3) print(cube(5)) # 125 (5³) # 2. Fixing multiple arguments def greet(greeting, name, punctuation="!"): return f"{greeting}, {name}{punctuation}" say_hello = partial(greet, "Hello") print(say_hello("Alice")) # Hello, Alice! say_hi = partial(greet, "Hi", punctuation=".") print(say_hi("Bob")) # Hi, Bob. # 3. Using with built-in functions numbers = [1, 2, 3, 4, 5] # Create function that always adds 10 add_ten = partial(map, lambda x: x + 10) result = list(add_ten(numbers)) print(result) # [11, 12, 13, 14, 15] # 4. Partial with keyword arguments def connect(host, port, timeout=30, retries=3): return f"Connecting to {host}:{port} (timeout={timeout}, retries={retries})" connect_localhost = partial(connect, "localhost", 8080) print(connect_localhost()) # Connecting to localhost:8080 (timeout=30, retries=3) connect_fast = partial(connect, timeout=5, retries=1) print(connect_fast("example.com", 443)) # Connecting to example.com:443 (timeout=5, retries=1) # 5. Manual partial function def manual_partial(func, *args, **kwargs): def new_func(*more_args, **more_kwargs): all_args = args + more_args all_kwargs = {**kwargs, **more_kwargs} return func(*all_args, **all_kwargs) return new_func def multiply(a, b, c): return a * b * c double = manual_partial(multiply, 2) print(double(3, 4)) # 24 (2 * 3 * 4) # 6. Using partial with class methods class Calculator: def add(self, a, b): return a + b calc = Calculator() add_five = partial(calc.add, 5) print(add_five(3)) # 8 # 7. Practical examples # URL builder def build_url(scheme, host, path, query=None): url = f"{scheme}://{host}/{path}" if query: url += f"?{query}" return url build_https = partial(build_url, "https") build_github = partial(build_https, "github.com") print(build_github("user/repo")) # https://github.com/user/repo print(build_github("org/project", query="branch=main")) # https://github.com/org/project?branch=main # Log formatter def log(level, message, timestamp): return f"[{timestamp}] {level}: {message}" from datetime import datetime log_now = partial(log, timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S")) error_log = partial(log_now, "ERROR") info_log = partial(log_now, "INFO") print(error_log("Something went wrong!")) print(info_log("System started"))
What are function attributes and introspection?
# Function introspection: Examining function properties at runtime def example_func(a, b=10, *args, **kwargs): """Example function for demonstrating introspection.""" return a + b # 1. Basic attributes print(example_func.__name__) # example_func print(example_func.__doc__) # Example function for demonstrating introspection. print(example_func.__module__) # __main__ (or module name) # 2. Code object attributes print(example_func.__code__.co_argcount) # 2 (a, b) print(example_func.__code__.co_nlocals) # Number of local variables print(example_func.__code__.co_varnames) # ('a', 'b', 'args', 'kwargs') print(example_func.__code__.co_filename) # File where function is defined print(example_func.__code__.co_firstlineno) # Line number where function starts # 3. Default arguments print(example_func.__defaults__) # (10,) - default values for positional args print(example_func.__kwdefaults__) # None - default values for keyword-only args # 4. Annotations def typed_func(a: int, b: str = "hello") -> bool: return True print(typed_func.__annotations__) # {'a': , 'b': , 'return': } # 5. Closure attributes def outer(x): def inner(): return x return inner closure_func = outer(10) print(closure_func.__closure__) # Cell objects print(closure_func.__code__.co_freevars) # ('x',) # 6. Custom attributes def custom_func(): pass custom_func.author = "Alice" custom_func.version = "1.0" custom_func.tags = ["utility", "helper"] print(custom_func.author) # Alice print(custom_func.version) # 1.0 print(custom_func.tags) # ['utility', 'helper'] # 7. Using inspect module for advanced introspection import inspect # Get source code source = inspect.getsource(example_func) print(source[:50]) # First 50 chars of source # Get signature sig = inspect.signature(example_func) print(sig) # (a, b=10, *args, **kwargs) # Parameter details for param_name, param in sig.parameters.items(): print(f"{param_name}:") print(f" kind: {param.kind}") print(f" default: {param.default}") print(f" annotation: {param.annotation}") # 8. Check if callable print(callable(example_func)) # True print(callable("hello")) # False # 9. Get all members of function members = inspect.getmembers(example_func) for name, value in members[:5]: # First 5 members print(f"{name}: {type(value).__name__}") # 10. Practical use cases def validate_arguments(func, *args, **kwargs): """Validate arguments against function signature.""" sig = inspect.signature(func) try: sig.bind(*args, **kwargs) return True except TypeError as e: print(f"Argument error: {e}") return False def add(a: int, b: int) -> int: return a + b print(validate_arguments(add, 5, 3)) # True print(validate_arguments(add, 5)) # False (missing b) print(validate_arguments(add, 5, "3")) # True (types not checked at runtime)
How to handle function errors and exceptions?
# Error handling in functions # 1. Basic try-except def safe_divide(a, b): try: return a / b except ZeroDivisionError: return "Error: Division by zero" except TypeError: return "Error: Invalid types" print(safe_divide(10, 2)) # 5.0 print(safe_divide(10, 0)) # Error: Division by zero print(safe_divide(10, "2")) # Error: Invalid types # 2. Multiple exceptions in one except def process_number(num): try: result = int(num) * 2 if result > 100: raise ValueError("Result too large") return result except (ValueError, TypeError) as e: return f"Error: {e}" # 3. Else clause (executes if no exception) def read_file(filename): try: file = open(filename, 'r') except FileNotFoundError: print(f"File {filename} not found") else: content = file.read() file.close() return content # 4. Finally clause (always executes) def process_data(data): result = None try: result = data.process() except AttributeError: print("Invalid data object") finally: print("Cleanup completed") return result # 5. Raising exceptions def validate_age(age): if age < 0: raise ValueError("Age cannot be negative") if age > 150: raise ValueError("Age is unrealistically high") return age try: validate_age(-5) except ValueError as e: print(f"Validation failed: {e}") # 6. Custom exceptions class InvalidEmailError(Exception): """Exception raised for invalid email addresses.""" def __init__(self, email, message="Invalid email address"): self.email = email self.message = message super().__init__(self.message) def __str__(self): return f"{self.message}: {self.email}" def validate_email(email): if "@" not in email: raise InvalidEmailError(email) return True try: validate_email("invalid-email") except InvalidEmailError as e: print(e) # Invalid email address: invalid-email # 7. Exception chaining def process_file(filename): try: with open(filename, 'r') as f: return f.read() except FileNotFoundError as e: raise RuntimeError(f"Failed to process {filename}") from e # 8. Assertions for debugging def calculate_discount(price, discount): assert 0 <= discount <= 1, "Discount must be between 0 and 1" assert price >= 0, "Price cannot be negative" return price * (1 - discount) # 9. Context managers for resource handling from contextlib import contextmanager @contextmanager def managed_resource(name): resource = open(name, 'w') try: yield resource finally: resource.close() print(f"Resource {name} closed") with managed_resource("test.txt") as f: f.write("Hello World") # 10. Logging instead of printing import logging logging.basicConfig(level=logging.INFO) def complex_operation(data): try: result = data.process() logging.info(f"Operation successful: {result}") return result except Exception as e: logging.error(f"Operation failed: {e}", exc_info=True) raise
What are coroutines and async functions?
# Coroutines and async/await (Python 3.5+) import asyncio # 1. Basic async function async def hello(): print("Hello") await asyncio.sleep(1) # Non-blocking sleep print("World") # Running async function asyncio.run(hello()) # 2. Multiple async functions async def say_hello(name, delay): await asyncio.sleep(delay) print(f"Hello {name}!") return f"Greeted {name}" async def main(): # Run sequentially result1 = await say_hello("Alice", 1) result2 = await say_hello("Bob", 2) # Run concurrently task1 = asyncio.create_task(say_hello("Charlie", 1)) task2 = asyncio.create_task(say_hello("David", 2)) await task1 await task2 # Using gather results = await asyncio.gather( say_hello("Eve", 1), say_hello("Frank", 2), say_hello("Grace", 3) ) print(f"Results: {results}") asyncio.run(main()) # 3. Async generator async def async_counter(n): for i in range(n): await asyncio.sleep(0.5) yield i async def use_async_gen(): async for number in async_counter(5): print(f"Got: {number}") asyncio.run(use_async_gen()) # 4. Async context manager import aiohttp async def fetch_url(url): async with aiohttp.ClientSession() as session: async with session.get(url) as response: return await response.text() # 5. Coroutine with send() and throw() async def accumulator(): total = 0 while True: try: value = yield total if value is None: break total += value except ValueError as e: print(f"Error: {e}") total = 0 async def use_accumulator(): acc = accumulator() await acc.send(None) # Initialize print(await acc.send(10)) # 10 print(await acc.send(20)) # 30 # Throw exception into coroutine try: await acc.throw(ValueError("Reset!")) except StopIteration: pass print(await acc.send(5)) # 5 (reset to 0) acc.close() asyncio.run(use_accumulator()) # 6. Async comprehensions async def async_comprehension(): # Async list comprehension results = [i async for i in async_counter(5)] print(f"Async list: {results}") # Async set comprehension unique = {i % 2 async for i in async_counter(5)} print(f"Async set: {unique}") # Async dict comprehension squares = {i: i**2 async for i in async_counter(5)} print(f"Async dict: {squares}") asyncio.run(async_comprehension()) # 7. Asynchronous iteration class AsyncCounter: def __init__(self, n): self.n = n self.current = 0 def __aiter__(self): return self async def __anext__(self): if self.current >= self.n: raise StopAsyncIteration await asyncio.sleep(0.1) value = self.current self.current += 1 return value async def use_async_iterator(): async for num in AsyncCounter(5): print(num) asyncio.run(use_async_iterator())
What are common function design patterns?
# Common function design patterns # 1. Factory pattern (function version) def create_animal(animal_type): if animal_type == "dog": def dog(): return "Woof!" return dog elif animal_type == "cat": def cat(): return "Meow!" return cat else: def unknown(): return "Unknown animal" return unknown dog = create_animal("dog") cat = create_animal("cat") print(dog()) # Woof! print(cat()) # Meow! # 2. Strategy pattern (function version) def add_strategy(a, b): return a + b def multiply_strategy(a, b): return a * b def calculator(a, b, strategy): return strategy(a, b) print(calculator(5, 3, add_strategy)) # 8 print(calculator(5, 3, multiply_strategy)) # 15 # 3. Observer pattern (callback functions) class EventEmitter: def __init__(self): self.listeners = {} def on(self, event, callback): if event not in self.listeners: self.listeners[event] = [] self.listeners[event].append(callback) def emit(self, event, *args, **kwargs): if event in self.listeners: for callback in self.listeners[event]: callback(*args, **kwargs) def log_event(message): print(f"LOG: {message}") def notify_user(message): print(f"USER: {message}") emitter = EventEmitter() emitter.on("message", log_event) emitter.on("message", notify_user) emitter.emit("message", "Hello World") # 4. Command pattern class Command: def __init__(self, execute, undo): self.execute = execute self.undo = undo def add_command(a, b): return a + b def subtract_command(a, b): return a - b commands = [ Command(lambda: add_command(5, 3), lambda: subtract_command(8, 3)), Command(lambda: subtract_command(10, 2), lambda: add_command(8, 2)) ] for cmd in commands: result = cmd.execute() print(f"Execute: {result}") undo_result = cmd.undo() print(f"Undo: {undo_result}") # 5. Template method pattern def template_method(algorithm): def wrapper(): print("Step 1: Initialize") result = algorithm() print("Step 3: Cleanup") return result return wrapper def specific_algorithm(): print("Step 2: Specific algorithm") return "Result" algorithm = template_method(specific_algorithm) print(algorithm()) # 6. Chain of responsibility def make_handler(handler_func, next_handler=None): def handler(request): result = handler_func(request) if result is None and next_handler: return next_handler(request) return result return handler def authenticate(request): if request.get("token") == "valid": print("Authenticated") return None # Pass to next handler return "Authentication failed" def authorize(request): if request.get("role") == "admin": print("Authorized") return None # Pass to next handler return "Authorization failed" def process(request): print("Processing request") return "Success" # Create handler chain handler = make_handler(process) handler = make_handler(authorize, handler) handler = make_handler(authenticate, handler) # Test chain valid_request = {"token": "valid", "role": "admin"} print(handler(valid_request)) # Success invalid_request = {"token": "invalid"} print(handler(invalid_request)) # Authentication failed
What are best practices for writing functions?
# Function best practices # 1. Single Responsibility Principle # BAD: Function does multiple things def process_user_data(user_data): # Validate if not user_data.get("name"): return "Error: Name required" # Process user_data["name"] = user_data["name"].title() # Save database.save(user_data) # Send notification email.send(user_data["email"]) return "Success" # GOOD: Separate functions def validate_user_data(user_data): if not user_data.get("name"): raise ValueError("Name required") return True def process_name(user_data): user_data["name"] = user_data["name"].title() return user_data def save_user(user_data): return database.save(user_data) def notify_user(user_data): return email.send(user_data["email"]) # 2. Keep functions small (ideally < 20 lines) # 3. Use meaningful names # BAD: def p(d): return d*2 # GOOD: def calculate_discount(price): return price * 0.9 # 4. Limit number of parameters (ideally ≤ 3) # BAD: Too many parameters def create_user(name, email, password, age, city, country, phone, role): pass # GOOD: Use dictionary or class def create_user(user_data): # user_data is a dictionary with all fields pass # 5. Use type hints def calculate_total(items: List[float], discount: float = 0.0) -> float: total = sum(items) return total * (1 - discount) # 6. Document with docstrings def fibonacci(n: int) -> int: """ Calculate the nth Fibonacci number. Args: n: The position in the Fibonacci sequence (0-based). Returns: The nth Fibonacci number. Raises: ValueError: If n is negative. Examples: >>> fibonacci(0) 0 >>> fibonacci(5) 5 """ if n < 0: raise ValueError("n must be non-negative") if n <= 1: return n return fibonacci(n - 1) + fibonacci(n - 2) # 7. Avoid side effects # BAD: Modifies input parameter def add_to_list(items, new_item): items.append(new_item) # Side effect return items # GOOD: Returns new list def add_to_list(items, new_item): return items + [new_item] # No side effect # 8. Use default arguments carefully # BAD: Mutable default argument def add_item(item, items=[]): # Shared list! items.append(item) return items # GOOD: Use None as default def add_item(item, items=None): if items is None: items = [] items.append(item) return items # 9. Handle errors appropriately def read_config_file(filename): try: with open(filename, 'r') as f: return json.load(f) except FileNotFoundError: logger.error(f"Config file {filename} not found") raise except json.JSONDecodeError as e: logger.error(f"Invalid JSON in {filename}: {e}") raise # 10. Write pure functions when possible # Pure function: Same input → Same output, no side effects def pure_multiply(a, b): return a * b # Pure # Impure function impure_counter = 0 def impure_increment(): global impure_counter # Side effect impure_counter += 1 return impure_counter # 11. Use function composition def compose(f, g): return lambda x: f(g(x)) add_one = lambda x: x + 1 square = lambda x: x ** 2 add_one_then_square = compose(square, add_one) print(add_one_then_square(5)) # 36 ((5+1)²) # 12. Consider performance # Use caching for expensive pure functions from functools import lru_cache @lru_cache(maxsize=128) def expensive_calculation(x): # Simulate expensive calculation time.sleep(1) return x * 2 # 13. Test your functions def test_functions(): assert add(2, 3) == 5 assert multiply(2, 3) == 6 print("All tests passed!")
Note: These questions cover Python functions comprehensively. Remember: Functions are first-class objects in Python. Use recursion for problems with recursive structure, but be mindful of stack limits. Decorators, generators, and lambda functions are powerful tools for writing clean, efficient code. Always follow best practices: keep functions small, use meaningful names, document with docstrings, and handle errors appropriately. For performance-critical code, consider memoization and generator expressions.
Python Functions, Recursion & Advanced Next