Fading Coder

One Final Commit for the Last Sprint

Home > Notes > Content

Python Functions: Parameters, Return Values, and Scope

Notes May 10 3

Python Functions: Parameters, Return Values, and Scope

Parameter Passing in Python

In Python, function parameters are passed by value. This means that when a function is called, the values of the actual parameters are copied to the formal parameters. Any modifications made to these parameters inside the function do not affect the original variables outside the function.

This concept can be illustrated with an analogy from "Journey to the West": Monkey King creates a clone of himself with the same abilities, but whatever happens to the clone doesn't affect the original Monkey King. Similarly, when passing parameters to a function, we're working with copies of the original values.

Consider the following example:

def exchange_values(x, y):
    '''Function to swap values of two variables'''
    x, y = y, x
    print("Inside function: x =", x, "y =", y)

a = 10
b = 20
exchange_values(a, b)
print("Outside function: a =", a, "b =", b)

When executed, this code produces:

Inside function: x = 20 y = 10
Outside function: a = 10 b = 20

The output clearly shows that while the values of x and y were swapped inside the function, the original variables a and b remain unchanged. This demonstrates that Python uses pass-by-value semantics for function parameters.

Reference Passing for Mutable Objects

When the actual parameter is a mutable object (like lists or dictionaries), the behavior appears different. In such cases, the reference to the object is passed, allowing modifications to the original object.

Here's an example:

def modify_dict(data):
    # Swap values of 'key1' and 'key2' in the dictionary
    data['key1'], data['key2'] = data['key2'], data['key1']
    print("Inside function: key1 =", data['key1'], "key2 =", data['key2'])

sample_dict = {'key1': 100, 'key2': 200}
modify_dict(sample_dict)
print("Outside function: key1 =", sample_dict['key1'], "key2 =", sample_dict['key2'])

The output shows:

Inside function: key1 = 200 key2 = 100
Outside function: key1 = 200 key2 = 100

In this case, changes made to the dictionary inside the function are reflected outside the function. However, this is still technically pass-by-value - it's just that the value being passed is a reference to the mutable object. If we reassign the parameter inside the function, it won't affect the original reference:

def reassign_dict(data):
    data = None  # This only affects the local reference
    print("Inside function after reassignment:", data)

sample_dict = {'key1': 100, 'key2': 200}
reassign_dict(sample_dict)
print("Outside function after reassignment:", sample_dict)

Output:

Inside function after reassignment: None
Outside function after reassignment: {'key1': 100, 'key2': 200}

Positional Arguments

Positional arguments, also called required arguments, must be provided in the correct order when calling a function. The number and position of actual arguments must match the formal parameters defined in the function.

Matching Argument Count

The number of arguments passed to a function must exactly match the number of parameters defined:

def calculate_perimeter(width, height):
    return 2 * (width + height)

# Calling with too few arguments
try:
    print(calculate_perimeter(5))
except TypeError as e:
    print("Error:", e)

# Calling with too many arguments
try:
    print(calculate_perimeter(5, 10, 15))
except TypeError as e:
    print("Error:", e)

Output:

Error: calculate_perimeter() missing 1 required positional argument: 'height'
Error: calculate_perimeter() takes 2 positional arguments but 3 were given

Matching Argument Position

The position of arguments matters when calling a function. If arguments are passed in the wrong order, it can lead to incorrect results or errors:

def triangle_area(base, height):
    return base * height / 2

# Correct order
print("Correct area:", triangle_area(6, 8))

# Incorrect order
print("Incorrect area:", triangle_area(8, 6))

Output:

Correct area: 24.0
Incorrect area: 24.0

In this case, the result is incorrect because base and height are swapped. For some operations like multiplication, the order might not matter, but for others, it can cause significant issues.

Keyword Arguments

Keyword arguments allow you to specify arguments using parameter names rather than position. This makes function calls more readable and flexible:

def display_info(title, url):
    print("Title:", title)
    print("URL:", url)

# Positional arguments
display_info("Python Tutorial", "https://example.com/python")

# Keyword arguments
display_info(title="Python Tutorial", url="https://example.com/python")
display_info(url="https://example.com/python", title="Python Tutorial")

# Mixed arguments (positional before keyword)
display_info("Python Tutorial", url="https://example.com/python")

Output:

Title: Python Tutorial
URL: https://example.com/python
Title: Python Tutorial
URL: https://example.com/python
Title: Python Tutorial
URL: https://example.com/python
Title: Python Tutorial
URL: https://example.com/python

Note that when mixing positional and keyword arguments, all positional arguments must come before any keyword arguments.

Default Parameters

Python allows you to specify default values for parameters. If an argument is not provided for such a parameter, the default value is used:

def create_profile(name, website="https://default.com", bio="No bio provided"):
    print("Name:", name)
    print("Website:", website)
    print("Bio:", bio)

# Using default values
create_profile("Alice")

# Providing some arguments
create_profile("Bob", bio="Python enthusiast")

# Providing all arguments
create_profile("Charlie", "https://charlie.dev", "Full-stack developer")

Output:

Name: Alice
Website: https://default.com
Bio: No bio provided
Name: Bob
Website: https://default.com
Bio: Python enthusiast
Name: Charlie
Website: https://charlie.dev
Bio: Full-stack developer

When defining a function with default parameters, all parameters with default values must come after those without default values:

# This is valid
def valid_function(a, b=2, c=3):
    pass

# This is invalid and will raise a SyntaxError
try:
    def invalid_function(a=1, b, c=3):
        pass
except SyntaxError as e:
    print("Error:", e)

You can view the default values of a function's parameters using the `__defaults__` attribute:

print(create_profile.__defaults__)

Output:

('https://default.com', 'No bio provided')

Variable Arguments (*args and **kwargs)

Python allows functions to accept a variable number of arguments using special syntax:

*args for Variable Positional Arguments

The `*args` syntax allows a function to accept any number of positional arguments, which are collected into a tuple:

def process_data(primary, *additional):
    print("Primary:", primary)
    print("Additional items:", additional)
    for item in additional:
        print(" -", item)

process_data("First")
process_data("First", "Second", "Third", "Fourth")

Output:

Primary: First
Additional items: ()
Primary: First
Additional items: ('Second', 'Third', 'Fourth')
 - Second
 - Third
 - Fourth

Note that `*args` doesn't have to be the last parameter, but if it's not, subsequent parameters must be passed as keyword arguments:

def mixed_parameters(*args, keyword_param):
    print("Positional args:", args)
    print("Keyword param:", keyword_param)

mixed_parameters("A", "B", keyword_param="C")
# mixed_parameters("A", "B", "C")  # This would raise an error

**kwargs for Variable Keyword Arguments

The `**kwargs` syntax allows a function to accept any number of keyword arguments, which are collected into a dictionary:

def process_config(base_url, **options):
    print("Base URL:", base_url)
    print("Options:", options)
    for key, value in options.items():
        print(f" - {key}: {value}")

process_config("https://example.com")
process_config("https://example.com", timeout=30, retries=3, secure=True)

Output:

Base URL: https://example.com
Options: {}
Base URL: https://example.com
Options: {'timeout': 30, 'retries': 3, 'secure': True}
 - timeout: 30
 - retries: 3
 - secure: True

You can combine `*args` and `**kwargs` in the same function:

def flexible_function(required, *args, **kwargs):
    print("Required:", required)
    print("Args:", args)
    print("Kwargs:", kwargs)

flexible_function("req", "pos1", "pos2", kw1="val1", kw2="val2")

Output:

Required: req
Args: ('pos1', 'pos2')
Kwargs: {'kw1': 'val1', 'kw2': 'val2'}

Reverse Parameter Collection

Python also supports the reverse of parameter collection - unpacking sequences or dictionaries into function arguments:

Unpacking Lists/Tuples with *

You can use the `*` operator to unpack a list or tuple into positional arguments:

def format_name(first, last):
    return f"{first} {last}"

name_parts = ["John", "Doe"]
print(format_name(*name_parts))

Output:

John Doe

Unpacking Dictionaries with **

You can use the `**` operator to unpack a dictionary into keyword arguments:

def display_person(name, age, city):
    print(f"{name} is {age} years old and lives in {city}")

person_data = {"name": "Alice", "age": 30, "city": "New York"}
display_person(**person_data)

Output:

Alice is 30 years old and lives in New York

Reverse parameter collection can also be used with functions that have variable parameters:

def process_data(primary, *additional):
    print("Primary:", primary)
    print("Additional:", additional)

data = ["First", "Second", "Third"]
process_data("Primary", *data)

Output:

Primary: Primary
Additional: ('First', 'Second', 'Third')

Return Values

Functions can return values using the `return` statement. If no return value is specified, the function returns `None`:

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

result = add(5, 3)
print("Result:", result)

def no_return():
    print("This function doesn't return anything")

print(no_return())

Output:

Result: 8
This function doesn't return anything
None

A function can have multiple return statements, but only one will be executed when the function is called:

def check_number(n):
    if n > 0:
        return "Positive"
    elif n < 0:
        return "Negative"
    else:
        return "Zero"

print(check_number(10))
print(check_number(-5))
print(check_number(0))

Output:

Positive
Negative
Zero

Returning Multiple Values

While a function can only return a single value, Python makes it easy to return multiple values by packing them into a tuple:

def get_dimensions():
    return 10, 20, 30

dimensions = get_dimensions()
print("Dimensions:", dimensions)
print("Length:", dimensions[0])
print("Width:", dimensions[1])
print("Height:", dimensions[2])

# You can also unpack the return values directly
length, width, height = get_dimensions()
print("\nUnpacked dimensions:")
print("Length:", length)
print("Width:", width)
print("Height:", height)

Output:

Dimensions: (10, 20, 30)
Length: 10
Width: 20
Height: 30

Unpacked dimensions:
Length: 10
Width: 20
Height: 30

Recursion

A recursive function is one that calls itself. This is useful for solving problems that can be broken down into smaller, similar subproblems:

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

print("Factorial of 5:", factorial(5))

Output:

Factorial of 5: 120

When defining a recursive function, it's crucial to ensure that the recursion has a base case and progresses toward that base case. Without this, you'll have infinite recursion:

def infinite_recursion():
    print("This will never end...")
    infinite_recursion()

# Uncommenting the next line would cause a maximum recursion depth error
# infinite_recursion()

Recursion is particularly useful for tasks like traversing tree structures or solving problems with natural recursive definitions, such as the Fibonacci sequence:

def fibonacci(n):
    if n <= 1:
        return n
    else:
        return fibonacci(n - 1) + fibonacci(n - 2)

print("Fibonacci sequence (first 10 numbers):")
for i in range(10):
    print(fibonacci(i), end=" ")

Output:

Fibonacci sequence (first 10 numbers):
0 1 1 2 3 5 8 13 21 34 

Variable Scope

Variable scope determines where in a program a variable can be accessed. Python has several types of scope, but we'll focus on the most common: local and global.

Local Variables

Variables defined inside a function have local scope and are only accessible within that function:

def demonstrate_local():
    local_var = "I'm local"
    print("Inside function:", local_var)

demonstrate_local()
try:
    print("Outside function:", local_var)
except NameError as e:
    print("Error:", e)

Output:

Inside function: I'm local
Error: name 'local_var' is not defined

Global Variables

Variables defined outside of any function have global scope and can be accessed throughout the program:

global_var = "I'm global"

def access_global():
    print("Inside function:", global_var)

access_global()
print("Outside function:", global_var)

Output:

Inside function: I'm global
Outside function: I'm global

Modifying Global Variables Inside Functions

If you need to modify a global variable inside a function, you must declare it as global using the `global` keyword:

counter = 0

def increment():
    global counter
    counter += 1
    print("Inside function:", counter)

print("Initial value:", counter)
increment()
print("After function call:", counter)

Output:

Initial value: 0
Inside function: 1
After function call: 1

Scope-Related Functions

Python provides built-in functions to inspect scope:

global_var = "Global"

def scope_demo():
    local_var = "Local"
    print("Local variables:", locals())
    print("Global variables:", globals())

scope_demo()

Output:

Local variables: {'local_var': 'Local'}
Global variables: {'global_var': 'Global', ...}

Local Functions and the nonlocal Keyword

Python allows defining functions inside other functions. These inner functions have access to variables in the enclosing function's scope:

def outer_function(x):
    def inner_function(y):
        return x + y
    return inner_function

add_five = outer_function(5)
print("5 + 10 =", add_five(10))

Output:

5 + 10 = 15

The `nonlocal` keyword allows inner functions to modify variables in the enclosing (but not global) scope:

def counter_generator():
    count = 0
    
    def increment():
        nonlocal count
        count += 1
        return count
    
    def decrement():
        nonlocal count
        count -= 1
        return count
    
    return increment, decrement

increment, decrement = counter_generator()
print("Initial count:", increment())
print("After increment:", increment())
print("After decrement:", decrement())

Output:

Initial count: 1
After increment: 2
After decrement: 1

Related Articles

Deploying a Maven Web Application to Tomcat 9 Using the Tomcat Manager

Tomcat 9 does not provide a dedicated Maven plugin. The Tomcat Manager interface, however, is backward-compatible, so the Tomcat 7 Maven Plugin can be used to deploy to Tomcat 9. This guide shows two...

Skipping Errors in MySQL Asynchronous Replication

When a replica halts because the SQL thread encounters an error, you can resume replication by skipping the problematic event(s). Two common approaches are available. Methods to Skip Errors 1) Skip a...

Spring Boot MyBatis with Two MySQL DataSources Using Druid

Required dependencies application.properties: define two data sources and poooling Java configuration for both data sources MyBatis mappers for each data source Controller endpoints to verify both co...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.