Today we'll cover more advanced function topics such as Scope, Defaults, and Typed Functions <div style="position: relative; padding-bottom: 64.90384615384616%; height: 0;"><iframe src="https://www.loom.com/embed/6a6f9e7393d84a21bd8db93173421ebb?sid=60fdb5fa-198c-476d-8e8c-312d69b36b0d" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"></iframe></div> Welcome to this lesson on advanced Python functions. You've already learned the basics of functions—how to define them and call them. Now, we'll dive deeper into some of the more complex aspects that can make your functions more powerful and efficient. #### 1. Understanding Scope: Local vs Global Variables **Scope** refers to the region of the program where a variable is accessible. Python has two basic scopes of variables: local and global. - **Local Variables:** A variable declared inside a function is local to that function. It is accessible only within that function and not outside of it. ```python def function_example(): local_var = 5 # Local variable, accessible only here print(local_var) function_example() # print(local_var) # This would raise an error because local_var is not accessible here. ``` - **Global Variables:** A variable declared outside any function is a global variable and is accessible from any part of the code, including inside functions. ```python global_var = 10 # Global variable, accessible anywhere in this script def function_example(): print(global_var) # Accessible here function_example() print(global_var) # And here ``` #### 2. Typed Functions Python 3.5 introduced support for optional type hints. These hints help programmers understand what type of value should be passed to or returned from functions. ```python def add_numbers(a: int, b: int) -> int: return a + b result = add_numbers(5, 3) print(result) # Output will be 8 ``` Type hints do not affect the runtime behavior of programs but are useful for type checking in larger codebases. #### 3. Default Values for Function Parameters You can specify default values for parameters in Python functions. This feature is helpful when you expect a function to be called with the same value frequently. ```python def greet(name, message="Hello"): print(f"{message}, {name}!") greet("Alice") # Uses default message "Hello" greet("Bob", "Good morning") # Overrides the default message ``` #### 4. Higher-Order Functions A higher-order function is a function that takes another function as an argument, or that returns a function as its result. - **Function as an Argument:** ```python def greet(name): return f"Hello {name}" def shout(text_function, name): result = text_function(name) return result.upper() print(shout(greet, "Alice")) # Outputs: HELLO ALICE ``` - **Returning a Function:** ```python def outer_function(text): text = text def inner_function(): print(text) return inner_function my_function = outer_function("Hello") my_function() # This will output: Hello ``` #### 5. Using *args and **kwargs in Python Functions In Python, you can use `*args` and `**kwargs` to handle variable numbers of arguments in functions. These are particularly useful when you are not sure how many arguments might be passed to your function. - **`*args`** is used to pass a variable number of non-keyword arguments to a function. It is typically used to capture all the arguments provided into a tuple. ```python def sum_all(*args): return sum(args) print(sum_all(1, 2, 3, 4)) # Outputs: 10 print(sum_all(1, 2)) # Outputs: 3 ``` - **`**kwargs`** is used to pass variable number of keyword arguments to a function. This allows you to handle named arguments that you have not defined in advance. The arguments are captured into a dictionary. ```python def print_user_data(**kwargs): for key, value in kwargs.items(): print(f"{key}: {value}") print_user_data(name="Alice", age=30, city="New York") ``` #### 6. Practical Applications of `*args` and `**kwargs` These features are incredibly useful in several scenarios: - When you want to create wrapper functions that pass through arguments to another function. - When you are working with functions that may evolve over time, where new parameters might be added in the future. - For subclassing and extending methods in object-oriented programming without needing to replicate the full method signatures of the base class. ```python # Wrapper function example def wrapper(*args, **kwargs): print("Arguments were:", args, kwargs) result = sum_all(*args) # passing all received args to sum_all return result print(wrapper(1, 2, 3, 4, 5)) # Outputs: 15 ``` #### Cheat Sheet - **Scope:** Understand where your variables can be accessed. - **Type Hints:** Use `variable: type` in function definitions to indicate expected types. - **Default Values:** Use `parameter=default_value` in function definitions for common defaults. - **Higher-Order Functions:** Functions that take other functions as arguments or return functions. #### Exercise Create a higher-order function `operate` that takes two arguments: a function `operation` and a list of numbers `nums`. The function `operation` should be capable of performing a mathematical operation (like sum, min, max). The `operate` function should apply the `operation` to the list `nums` and return the result. ```python def operate(operation, nums): return operation(nums) # Example usage result = operate(sum, [1, 2, 3, 4, 5]) print(result) # Outputs: 15 ``` #### Additional Resources 1. [Real Python on Function Arguments](https://realpython.com/defining-your-own-python-function/) 2. [Python Documentation on Function Annotations](https://docs.python.org/3/tutorial/controlflow.html#function-annotations) 3. [Higher-Order Functions in Python](https://www.geeksforgeeks.org/higher-order-functions-in-python/)