diff --git a/tech_docs/python/python_functions.md b/tech_docs/python/python_functions.md new file mode 100644 index 0000000..63b29f5 --- /dev/null +++ b/tech_docs/python/python_functions.md @@ -0,0 +1,408 @@ +# Python Functions Cheatsheet + +## Basic Function Types + +### Built-in Functions +```python +# Common built-in functions +len(sequence) # Returns length of a sequence +print(*objects) # Prints objects to console +type(object) # Returns the type of an object +range(start, stop) # Creates sequence of numbers +max(iterable) # Returns maximum value +min(iterable) # Returns minimum value +sum(iterable) # Sums items in an iterable +``` + +### User-defined Functions +```python +# Basic function definition +def function_name(parameters): + """Docstring: explains what function does""" + # Function body + return value # Optional return statement + +# Example +def square(x): + """Returns the square of a number.""" + return x * x +``` + +### Lambda Functions (Anonymous) +```python +# lambda arguments: expression +square = lambda x: x * x +add = lambda x, y: x + y + +# With higher-order functions +list(map(lambda x: x * 2, [1, 2, 3])) # [2, 4, 6] +``` + +## Function Parameters + +### No Parameters +```python +def say_hello(): + return "Hello, world!" + +# Call with no arguments +say_hello() +``` + +### Required Parameters +```python +def multiply(a, b): + return a * b + +# Must provide all required arguments +result = multiply(5, 3) # 15 +``` + +### Default Parameters +```python +def power(base, exponent=2): + return base ** exponent + +# Default parameters are optional +power(5) # 25 (uses default exponent=2) +power(5, 3) # 125 (overrides default) +``` + +### Keyword Arguments +```python +def create_person(name, age, job): + return f"{name} is {age} years old and works as a {job}" + +# Order doesn't matter with keyword args +create_person(age=30, job="developer", name="Alice") +``` + +### Positional-Only and Keyword-Only Arguments (Python 3.8+) +```python +def process_data(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2): + """ + pos1, pos2: positional-only (before /) + pos_or_kwd: positional or keyword + kwd1, kwd2: keyword-only (after *) + """ + return f"Processing: {pos1}, {pos2}, {pos_or_kwd}, {kwd1}, {kwd2}" + +# Valid calls: +process_data(1, 2, 3, kwd1=4, kwd2=5) # All specified +process_data(1, 2, pos_or_kwd=3, kwd1=4, kwd2=5) # pos_or_kwd as keyword + +# Invalid calls: +# process_data(pos1=1, pos2=2, pos_or_kwd=3, kwd1=4, kwd2=5) # Error: pos1, pos2 can't be keyword +# process_data(1, 2, 3, 4, 5) # Error: kwd1, kwd2 can't be positional +``` + +### Variable-length Arguments (*args and **kwargs) +```python +def sum_all(*args): + """Accept any number of positional arguments""" + return sum(args) + +def user_info(**kwargs): + """Accept any number of keyword arguments""" + for key, value in kwargs.items(): + print(f"{key}: {value}") + +# Usage +sum_all(1, 2, 3, 4, 5) # 15 +user_info(name="Alice", age=30, city="New York") + +# Combination +def process_all(*args, **kwargs): + print(f"Args: {args}") + print(f"Kwargs: {kwargs}") + +process_all(1, 2, 3, name="Alice", age=30) # Args: (1, 2, 3), Kwargs: {'name': 'Alice', 'age': 30} +``` + +## Variable Scope and the LEGB Rule + +```python +# The LEGB rule determines how Python looks up variable names: +# Local → Enclosing → Global → Built-in + +x = 10 # Global scope + +def outer_function(): + y = 20 # Enclosing scope for inner_function + + def inner_function(): + z = 30 # Local scope + print(f"Local: {z}") + print(f"Enclosing: {y}") + print(f"Global: {x}") + print(f"Built-in: {len}") # Built-in function + + return inner_function + +func = outer_function() +func() # Prints all variables from different scopes + +# Modifying variables in outer scopes +def modify_global(): + global x + x = 100 # Modifies the global x + +def modify_enclosing(): + y = 20 + + def inner(): + nonlocal y + y = 200 # Modifies the enclosing y + + inner() + print(y) # 200 + +modify_global() +print(x) # 100 +modify_enclosing() +``` + +## Advanced Function Concepts + +### Recursive Functions +```python +def factorial(n): + if n <= 1: + return 1 + return n * factorial(n-1) +``` + +### Closures +```python +def counter_factory(): + count = 0 # This variable is "enclosed" in the returned function + + def increment(): + nonlocal count + count += 1 + return count + + return increment + +counter = counter_factory() +print(counter()) # 1 +print(counter()) # 2 +print(counter()) # 3 + +# Another example: function with "memory" +def make_multiplier(factor): + def multiply(number): + return number * factor # Uses factor from enclosing scope + return multiply + +double = make_multiplier(2) +triple = make_multiplier(3) +print(double(5)) # 10 +print(triple(5)) # 15 +``` + +### Higher-order Functions +```python +# Functions that take/return other functions +def apply(func, value): + return func(value) + +# Usage +apply(lambda x: x*2, 5) # 10 +``` + +### Generator Functions +```python +def count_up_to(n): + i = 1 + while i <= n: + yield i + i += 1 + +# Creates a generator object +counter = count_up_to(3) +next(counter) # 1 +next(counter) # 2 +next(counter) # 3 +``` + +### Decorators +```python +from functools import wraps + +def my_decorator(func): + @wraps(func) # Preserves metadata of the original function + def wrapper(*args, **kwargs): + print("Before function call") + result = func(*args, **kwargs) + print("After function call") + return result + return wrapper + +@my_decorator +def greet(name): + """Greet someone by name.""" + return f"Hello, {name}!" + +# The metadata is preserved +print(greet.__name__) # 'greet' (not 'wrapper') +print(greet.__doc__) # 'Greet someone by name.' +``` + +### Type Hints (PEP 484) +```python +from typing import List, Dict, Tuple, Optional, Union, Any, Callable + +def headline(text: str, align: bool = True) -> str: + """Format text as a headline.""" + if align: + return f"{text.title()}\n{'-' * len(text)}" + else: + return f"{text.title()}" + +# More complex type hints +def process_items(items: List[str]) -> Dict[str, int]: + return {item: len(item) for item in items} + +# Optional parameters +def greeting(name: Optional[str] = None) -> str: + if name is None: + return "Hello, Guest!" + return f"Hello, {name}!" + +# Union types +def process_id(id: Union[int, str]) -> str: + return str(id) + +# Function type hints +def apply_formatter(value: str, formatter: Callable[[str], str]) -> str: + return formatter(value) +``` + +## Docstring Conventions (PEP 257) + +```python +def calculate_discount(price: float, rate: float = 0.1) -> float: + """ + Calculate the discounted price. + + Args: + price: The original price + rate: The discount rate (default: 0.1) + + Returns: + The price after applying the discount + + Raises: + ValueError: If price or rate is negative + """ + if price < 0 or rate < 0: + raise ValueError("Price and rate must be non-negative") + return price * (1 - rate) + +# Google style +def fetch_data(resource_id: str, max_results: int = 100) -> List[Dict]: + """Fetches data from the specified resource. + + Args: + resource_id: The ID of the resource to fetch. + max_results: The maximum number of results to return. + + Returns: + A list of dictionaries containing the fetched data. + + Raises: + ConnectionError: If unable to connect to the data source. + """ + pass +``` + +## Class Methods + +### Instance Methods +```python +class MyClass: + def instance_method(self, x): + """Access instance attributes with self""" + self.value = x +``` + +### Class Methods +```python +class MyClass: + @classmethod + def class_method(cls, x): + """Access class attributes with cls""" + return cls(x) # Can create instances +``` + +### Static Methods +```python +class MyClass: + @staticmethod + def static_method(x): + """No access to instance or class""" + return x * 2 +``` + +## Functional Programming + +### map() +```python +# Apply function to all items in an iterable +numbers = [1, 2, 3, 4] +squares = list(map(lambda x: x**2, numbers)) # [1, 4, 9, 16] +``` + +### filter() +```python +# Filter items based on a function +numbers = [1, 2, 3, 4, 5, 6] +evens = list(filter(lambda x: x % 2 == 0, numbers)) # [2, 4, 6] +``` + +### reduce() +```python +from functools import reduce +# Apply function cumulatively to items +numbers = [1, 2, 3, 4] +product = reduce(lambda x, y: x * y, numbers) # 24 +``` + +### Other functools Utilities +```python +from functools import partial, lru_cache + +# partial: Create new functions with pre-filled arguments +def power(base, exponent): + return base ** exponent + +square = partial(power, exponent=2) +cube = partial(power, exponent=3) + +print(square(4)) # 16 +print(cube(4)) # 64 + +# lru_cache: Memoization decorator (caches results) +@lru_cache(maxsize=128) +def fibonacci(n): + if n <= 1: + return n + return fibonacci(n-1) + fibonacci(n-2) + +# First call computes result, subsequent calls with same args use the cache +print(fibonacci(30)) # Fast, even for large values +``` + +## Function Design Best Practices + +1. **Single Responsibility**: Functions should do one thing well +2. **Pure Functions**: Avoid side effects when possible +3. **Descriptive Names**: Use clear, descriptive function names +4. **Parameter Count**: Aim for fewer than 4 parameters +5. **Default Values**: Use sensible defaults for optional parameters +6. **Docstrings**: Document what the function does, parameters, and return values +7. **Type Hints**: Use type annotations for better tooling and clarity +8. **Error Handling**: Validate inputs and handle errors gracefully +9. **Return Values**: Be consistent with return types +10. **Early Returns**: Use early returns to avoid deep nesting \ No newline at end of file