Python Basics II#
Welcome to Python Basics II! In this lecture, we’ll cover:
Functions — Writing reusable code
Data Structures — Lists, tuples, dictionaries, and sets
File I/O — Reading and writing files
Error Handling — Making robust code with try/except
By the end of this notebook, you’ll be able to write well-organized Python programs that can read data from files, process it with functions, and handle errors gracefully.
I Functions in Python#
1. What is a Function?#
In Python, a function is a reusable block of code that performs a specific task. It helps you:
Organize code into logical sections.
Avoid repetition by reusing the same logic multiple times.
Enhance readability by making your code self-contained and modular.
Python functions are defined with the def keyword. A typical function looks like this:
def function_name(parameters):
"""
Docstring: A description of the function’s purpose, inputs, and outputs.
"""
# function body
# ...
return result
A Simple Example#
def greet(name):
"""Return a greeting for the given name."""
return f"Hello, {name}!"
print(greet("Rutgers")) # Expect: Hello, Alice!
Hello, Rutgers!
2. Function Parameters and Arguments#
Functions can have different types of parameters and arguments to accommodate a variety of use cases.
2.1 Positional Arguments#
By default, arguments are matched to parameters by position:
def add(x, y):
"""Return the sum of x and y."""
return x + y
result = add(3, 5) # x=3, y=5
print(result) # Expect: 8
8
2.2 Keyword Arguments#
You can also match arguments by explicitly naming them:
def connect(host, port):
"""Simulate connecting to a network host and port."""
return f"Connecting to {host} on port {port}"
print(connect(port=8080, host="rutgers.edu"))
# Output: Connecting to localhost on port 8080
print(connect(host="rutgers.edu", port=8888))
Connecting to rutgers.edu on port 8080
Connecting to rutgers.edu on port 8888
2.3 Default Arguments#
If a parameter has a default value, it becomes optional for the caller:
def power(base, exponent=2):
"""Raise base to a given exponent; default exponent is 2."""
return base ** exponent
print(power(4)) # exponent defaults to 2 -> 16
print(power(4, 3)) # -> 64
power(5)
16
64
Note: Be cautious when using mutable default arguments (e.g., lists or dictionaries) since they can lead to unexpected behavior if modified in-place.
def add_item(item, items=[]):
items.append(item)
return items
print (add_item(1))
print (add_item(2))
print (add_item(3))
list1 = add_item(,list)
print (add_item(1, [44]))
[1]
[1, 2]
[1, 2, 3]
[44, 1]
def add_item2(item, items=None):
if items is None:
items = []
items.append(item)
return items
print (add_item2(1))
print (add_item2(2))
print (add_item2(3))
[1]
[2]
[3]
3. Return Values#
Single Return Value: A function returns a single object—this object can be anything in Python (string, number, list, dictionary, etc.).
Multiple Return Values: Python’s tuple-unpacking allows for multiple outputs.
def min_and_max(values):
"""Return both the minimum and maximum of a list of values."""
return min(values), max(values)
mn, mx = min_and_max([3, 1, 5, 9, 2])
print(mn, mx) # Expect: 1 9
mm = min_and_max([3, 1, 5, 9, 2])
print (mm)
def somefunc():
return 4+5
print(somefunc())
1 9
(1, 9)
9
No Explicit Return: If a function ends without a
returnstatement, Python implicitly returnsNone.
4. Documentation with Docstrings#
A docstring is a specialized string literal that documents a function’s purpose, parameters, and return values. Typically enclosed with triple quotes ("""). Good docstrings:
Start with a one-sentence summary.
Optionally include more detailed explanations or examples in subsequent paragraphs.
Can use conventions like NumPy Docstrings or Google Python Style.
def linear_transform(x, a, b):
"""
Compute a linear transformation of x: y = a*x + b.
Parameters
----------
x : float
The input variable.
a : float
The slope of the linear function.
b : float
The intercept of the linear function.
Returns
-------
float
The value of y = a*x + b.
"""
return a*x + b
help(linear_transform)
Help on function linear_transform in module __main__:
linear_transform(x, a, b)
Compute a linear transformation of x: y = a*x + b.
Parameters
----------
x : float
The input variable.
a : float
The slope of the linear function.
b : float
The intercept of the linear function.
Returns
-------
float
The value of y = a*x + b.
5. Scoping and Namespaces#
Variables defined inside a function are local to that function, meaning they are not accessible outside unless explicitly returned. This principle (known as scope) protects your variables from unintentional modifications.
def foo():
x = 10 # local variable
return x
def foo2():
x = 20
return x
print(foo())
# A local variable is not available outside the function.
# Running `print(x)` here would raise a NameError.
6. An Introduction to Recursion#
Though recursion can be powerful, it needs to be used judiciously:
def factorial(n):
if n <= 1:
return 1
else:
return n * factorial(n - 1)
Be mindful of recursion depth limits in Python.
def factorial(n):
if n <= 1:
return 1
else:
return n * factorial(n - 1)
print(factorial(5))
120
7. Best Practices#
Keep functions small and focused.
Name functions clearly and descriptively.
Use docstrings and type hints for clarity.
Validate inputs, watch for edge cases.
Consider unit tests (e.g.,
unittest,pytest).
Summary#
Functions are fundamental to writing modular, maintainable Python code. For a graduate student working on complex projects—perhaps in computational research, data science, or advanced engineering simulations—understanding how to design, document, and structure functions effectively is essential.
II Python Containers#
This section provides an overview of lists, tuples, and dictionaries, including slicing and key/value manipulation. We’ll explore usage and performance considerations, especially in large-scale or research scenarios.
1. Overview#
In Python, a container is any object that holds an arbitrary number of other objects. The most commonly used containers are:
Lists: Ordered, mutable sequences.
Tuples: Ordered, immutable sequences.
Dictionaries: Key-value mappings, mutable in nature.
These containers are essential for organizing data in scientific computing and complex research applications.
2. Lists#
2.1 Basic Usage#
A Python list is defined using square brackets [ ]. It is mutable, meaning you can add, remove, or change elements in-place.
# Creating a list
data = [10, 20, 30, 40]
# Accessing elements by index
print(data[0]) # Output: 10
print(data[-1]) # Output: 40
print(data[3])
print(data[-2])
# Modifying elements
data[0] = 100
print('data', data) # [100, 20, 30, 40]
data2 = data[:] # data2 = data or data2=data[:]
print('data2', data2)
data2[0] = 1000
print('data', data)
print('data2', data2)
data2.append(10000)
print('data2', data2)
data2.reverse()
print('data2', data2)
10
40
40
30
data [100, 20, 30, 40]
data2 [100, 20, 30, 40]
data [100, 20, 30, 40]
data2 [1000, 20, 30, 40]
data2 [1000, 20, 30, 40, 10000]
data2 [10000, 40, 30, 20, 1000]
Common Methods#
append(x): Add an item to the end.
extend(iterable): Add multiple items (another list or iterator).
insert(i, x): Insert
xat indexi.pop(i): Remove and return the item at index
i(default last).remove(x): Remove the first occurrence of
x.sort(): Sort the list in-place.
reverse(): Reverse the list in-place.
Inserting or removing elements from the beginning of a list can be costly (O(n)), as other elements shift.
2.2 List Slicing#
Slicing allows you to extract a subsequence of a list:
numbers = [0, 1, 2, 3, 4, 5, 6, 7]
# Basic slice: list[start:end:step]
sub_list = numbers[2:6] # [2, 3, 4, 5]
step_slice = numbers[1:7:2] # [1, 3, 5]
reverse_slice = numbers[::-1] # [7, 6, 5, 4, 3, 2, 1, 0]
print("sub_list:", sub_list)
print("step_slice:", step_slice)
print("reverse_slice:", reverse_slice)
sub_list: [2, 3, 4, 5]
step_slice: [1, 3, 5]
reverse_slice: [7, 6, 5, 4, 3, 2, 1, 0]
Note: For large-scale data, slicing can help avoid copying the entire list, especially if you’re using libraries like NumPy that implement slicing via views.
2.3 Advanced Considerations: List Comprehensions#
A list comprehension is a concise way to create lists:
squares = [x**2 for x in range(5)]
print(squares)
squares2 = []
for x in range(5):
squares2.append(x**2)
print(squares2)
[0, 1, 4, 9, 16]
[0, 1, 4, 9, 16]
List comprehensions are often more Pythonic and can be faster than building lists with a standard for loop in many cases.
3. Tuples#
3.1 Basic Usage#
A tuple is an immutable ordered sequence, typically created using parentheses ( ). Similar to lists, but you cannot modify them in-place.
coords = (3.2, 4.1)
print(coords[0]) # 3.2
lcoords = [3.2, 4.1]
print(lcoords[0])
lcoords[0] = 100
print(lcoords[0])
# Tuples are immutable, so this assignment would raise a TypeError:
# coords[0] = 100
Trying to modify a tuple element will raise a TypeError:
3.2 Performance and Immutability#
Tuples can sometimes be more memory-efficient and faster to process than lists, especially for read-only data.
4. Dictionaries#
4.1 Basic Usage#
A dictionary in Python is a mutable mapping of key-value pairs, created using curly braces { } or the dict() constructor:
person = {
"name": "Alice",
"age": 29,
"department": "Physics"
}
print(person["name"]) # Accessing values by key
person["age"] = 30 # Modifying values
person
print("shool" in person)
print(person.keys())
print(person.values())
print(person.items())
Alice
False
dict_keys(['name', 'age', 'department'])
dict_values(['Alice', 30, 'Physics'])
dict_items([('name', 'Alice'), ('age', 30), ('department', 'Physics')])
4.2 Key/Value Access and Methods#
Check existence:
if "department" in person:Add/Update:
person["title"] = "Research Associate"Remove entries:
person.pop("title")ordel person["age"]
Methods:
keys(),values(),items()
4.3 Dictionary Comprehensions#
Similar to list comprehensions, you can create a dictionary using dictionary comprehensions:
squares_dict = {x: x**2 for x in range(5)}
squares_dict
4.4 Use Cases#
Metadata: Store experiment parameters.
Lookup Tables: Fast mapping from IDs or keys to values.
Sparse Data: Represent data that only has a few non-zero entries.
5. Nested Data Structures#
You can nest these containers for more complex data:
lab_data = {
"experiment1": {
"timestamp": "2025-01-01",
"results": [1.2, 2.3, 3.4],
},
"experiment2": {
"timestamp": "2025-01-02",
"results": [2.1, 4.5, 9.0],
}
}
print(lab_data["experiment1"]["results"][0]) # 1.2
1.2
6. Performance Considerations#
6.1 Lists vs. Tuples#
Lists: Good for data that you need to mutate frequently.
Tuples: Lighter and faster if the data is static.
6.2 Large-Scale Data#
Consider NumPy, pandas, or SciPy for specialized or large-scale data.
6.3 Dictionary Optimizations#
Hash tables can degrade if collisions become frequent.
For read-only dictionaries, consider
types.MappingProxyType.collections.OrderedDictcan preserve insertion order if needed (though normal dicts do in 3.7+).
7. Best Practices#
Start with Built-ins.
Document nested containers.
Validate that keys exist and list lengths match.
Immutable Where Possible.
Leverage Comprehensions.
Time Complexity awareness.
Summary#
Python’s lists, tuples, and dictionaries form the backbone of most data structures in Python. Understanding their nuances, especially in research or large-scale code, is essential.
5. Sets#
A set is an unordered collection of unique elements. Sets are useful for:
Removing duplicates from a list
Fast membership testing (O(1) lookup)
Mathematical set operations (union, intersection, difference)
## Creating a set
unique_numbers = {1, 2, 3, 2, 1} # {1, 2, 3} - duplicates removed!
empty_set = set() # Note: {} creates an empty dict, not a set
## From a list
numbers = [1, 2, 2, 3, 3, 3]
unique = set(numbers) # {1, 2, 3}
# Creating sets
particles = {'electron', 'proton', 'neutron', 'electron'} # duplicate removed
print(f"Particles: {particles}")
print(f"Number of unique particles: {len(particles)}")
# Fast membership testing
print(f"Is 'proton' in the set? {'proton' in particles}")
print(f"Is 'muon' in the set? {'muon' in particles}")
Particles: {'proton', 'electron', 'neutron'}
Number of unique particles: 3
Is 'proton' in the set? True
Is 'muon' in the set? False
5.1 Set Operations#
Sets support mathematical operations:
Union (
|): Elements in either setIntersection (
&): Elements in both setsDifference (
-): Elements in first but not secondSymmetric Difference (
^): Elements in either but not both
# Set operations
fermions = {'electron', 'proton', 'neutron', 'quark'}
leptons = {'electron', 'muon', 'tau', 'neutrino'}
print(f"Fermions: {fermions}")
print(f"Leptons: {leptons}")
print(f"Union (all particles): {fermions | leptons}")
print(f"Intersection (both categories): {fermions & leptons}")
print(f"Fermions but not leptons: {fermions - leptons}")
Fermions: {'proton', 'electron', 'quark', 'neutron'}
Leptons: {'tau', 'electron', 'neutrino', 'muon'}
Union (all particles): {'tau', 'muon', 'electron', 'quark', 'neutron', 'proton', 'neutrino'}
Intersection (both categories): {'electron'}
Fermions but not leptons: {'proton', 'neutron', 'quark'}
5.2 Common Use Case: Removing Duplicates#
# Removing duplicates from experimental data
measurements = [1.2, 3.4, 1.2, 5.6, 3.4, 7.8, 1.2]
print(list(set(measurements)))
unique_measurements = list(set(measurements))
print(f"Original: {measurements}")
print(f"Unique values: {unique_measurements}")
# Note: set() doesn't preserve order. Use dict.fromkeys() to preserve order:
ordered_unique = list(dict.fromkeys(measurements))
print(f"Unique (order preserved): {ordered_unique}")
[1.2, 3.4, 5.6, 7.8]
Original: [1.2, 3.4, 1.2, 5.6, 3.4, 7.8, 1.2]
Unique values: [1.2, 3.4, 5.6, 7.8]
Unique (order preserved): [1.2, 3.4, 5.6, 7.8]
III. File Input/Output#
Working with files is essential for any scientific programming. You’ll need to:
Read experimental data from files
Save results for later analysis
Process large datasets that don’t fit in your code
Python makes file operations straightforward.
1. Opening and Closing Files#
The basic pattern for working with files:
## Open a file
file = open('filename.txt', 'mode')
## Do something with the file
content = file.read()
## Always close when done!
file.close()
File modes:
'r'— Read (default). File must exist.'w'— Write. Creates new file or overwrites existing!'a'— Append. Adds to end of file.'r+'— Read and write.
2. The with Statement (Context Manager)#
Always use with when working with files! It automatically closes the file, even if an error occurs:
with open('filename.txt', 'r') as file:
content = file.read()
# File is automatically closed when we exit this block
This is the recommended way to handle files in Python.
3. Reading Files#
# First, let's create a sample file to work with
sample_data = """Temperature Data
Day 1: 23.5 C
Day 2: 24.1 C
Day 3: 22.8 C
Day 4: 25.0 C
Day 5: 23.9 C
"""
with open('temperature.txt', 'w') as f:
f.write(sample_data)
print("Created temperature.txt")
Created temperature.txt
Reading Methods#
read()— Read entire file as one stringreadline()— Read one line at a timereadlines()— Read all lines into a list
# Method 1: read() - entire file as string
with open('temperature.txt', 'r') as f:
content = f.read()
print("=== Using read() ===")
print(content)
# Method 2: readlines() - list of lines
with open('temperature.txt', 'r') as f:
lines = f.readlines()
print("=== Using readlines() ===")
print(lines)
print (len(lines))
=== Using read() ===
Temperature Data
Day 1: 23.5 C
Day 2: 24.1 C
Day 3: 22.8 C
Day 4: 25.0 C
Day 5: 23.9 C
=== Using readlines() ===
['Temperature Data\n', 'Day 1: 23.5 C\n', 'Day 2: 24.1 C\n', 'Day 3: 22.8 C\n', 'Day 4: 25.0 C\n', 'Day 5: 23.9 C\n']
6
Iterating Over Lines (Most Common Pattern)#
# Best practice: iterate directly over the file
with open('temperature.txt', 'r') as f:
for line_number, line in enumerate(f, 1):
print(f"Line {line_number}: {line.strip()}")
Line 1: Temperature Data
Line 2: Day 1: 23.5 C
Line 3: Day 2: 24.1 C
Line 4: Day 3: 22.8 C
Line 5: Day 4: 25.0 C
Line 6: Day 5: 23.9 C
4. Writing Files#
# Writing to a file
results = [
("Experiment 1", 3.14159),
("Experiment 2", 2.71828),
("Experiment 3", 1.41421),
]
with open('results.txt', 'w') as f:
f.write("Experimental Results\n") # write() for strings
f.write("=" * 30 + "\n")
for name, value in results:
f.write(f"{name}: {value:.4f}\n")
print("Wrote results.txt")
# Verify by reading it back
with open('results.txt', 'r') as f:
print(f.read())
Wrote results.txt
Experimental Results
==============================
Experiment 1: 3.1416
Experiment 2: 2.7183
Experiment 3: 1.4142
Appending to Files#
Use mode 'a' to add to the end of a file without erasing existing content:
# Append more results
with open('results.txt', 'a') as f:
f.write("Experiment 4: 1.73205\n")
f.write("Experiment 5: 2.23607\n")
# Check the updated file
with open('results.txt', 'r') as f:
print(f.read())
5. Reading Numerical Data with NumPy#
For scientific data, numpy.loadtxt() and numpy.savetxt() are more convenient:
import numpy as np
# Create a numerical data file
data = """# Time (s), Position (m), Velocity (m/s)
0.0, 0.0, 10.0
1.0, 9.5, 9.0
2.0, 18.0, 8.0
3.0, 25.5, 7.0
4.0, 32.0, 6.0
"""
with open('motion_data.csv', 'w') as f:
f.write(data)
# Read with numpy (skip header row, comma delimiter)
time, position, velocity = np.loadtxt('motion_data.csv',
delimiter=',',
skiprows=1,
unpack=True)
print(f"Time: {time}")
print(f"Position: {position}")
print(f"Velocity: {velocity}")
Saving Numerical Data#
# Calculate acceleration and save results
acceleration = np.gradient(velocity, time)
# Stack columns and save
output_data = np.column_stack((time, position, velocity, acceleration))
np.savetxt('motion_analysis.csv',
output_data,
delimiter=',',
header='Time,Position,Velocity,Acceleration',
fmt='%.4f',
comments='')
print("Saved motion_analysis.csv")
# Verify
with open('motion_analysis.csv', 'r') as f:
print(f.read())
File I/O Summary#
Task |
Method |
|---|---|
Open file safely |
|
Read entire file |
|
Read lines |
|
Write string |
|
Read numerical data |
|
Save numerical data |
|
IV. Error Handling with try/except#
Errors happen. Users enter invalid input. Files don’t exist. Networks fail. Good programs handle errors gracefully instead of crashing.
Python uses exceptions to handle errors. When something goes wrong, Python “raises” an exception. We can “catch” these exceptions and respond appropriately.
1. Common Python Exceptions#
Exception |
Cause |
|---|---|
|
Wrong value type (e.g., |
|
Wrong type for operation (e.g., |
|
Division by zero |
|
File doesn’t exist |
|
List index out of range |
|
Dictionary key doesn’t exist |
2. Basic try/except#
try:
# Code that might cause an error
risky_operation()
except SomeException:
# Code to run if that error occurs
handle_error()
x = 0
if x != 0:
result = 10 / x
print(result)
else:
print("Cannot divide by zero.")
# Without error handling - this would crash!
# result = 10 / 0 # ZeroDivisionError
# With error handling
try:
result = 10 / 0
print(f"Result: {result}")
except ZeroDivisionError:
print("Error: Cannot divide by zero!")
print("Program continues running...")
Error: Cannot divide by zero!
Program continues running...
3. Handling Multiple Exception Types#
def safe_divide(a, b):
"""Safely divide two numbers with error handling."""
try:
result = a / b
return result
except ZeroDivisionError:
print("Error: Division by zero!")
return None
except TypeError:
print("Error: Invalid types for division!")
return None
# Test cases
print(f"10 / 2 = {safe_divide(10, 2)}")
print(f"10 / 0 = {safe_divide(10, 0)}")
print(f"'10' / 2 = {safe_divide('10', 2)}")
10 / 2 = 5.0
Error: Division by zero!
10 / 0 = None
Error: Invalid types for division!
'10' / 2 = None
4. The else and finally Clauses#
else: Runs if NO exception occurredfinally: ALWAYS runs, whether or not an exception occurred
try:
risky_operation()
except SomeException:
handle_error()
else:
# Runs only if no exception
success_actions()
finally:
# Always runs
cleanup()
def read_data_file(filename):
"""Read a data file with full error handling."""
try:
f = open(filename, 'r')
data = f.read()
except FileNotFoundError:
print(f"Error: File '{filename}' not found!")
return None
else:
print(f"Successfully read {len(data)} characters")
return data
finally:
print("Cleanup: Closing file (if open)")
try:
f.close()
except:
pass # File was never opened
# Test with existing and non-existing files
print("=== Reading existing file ===")
result = read_data_file('temperature.txt')
print("\n=== Reading non-existing file ===")
result = read_data_file('nonexistent.txt')
=== Reading existing file ===
Successfully read 87 characters
Cleanup: Closing file (if open)
=== Reading non-existing file ===
Error: File 'nonexistent.txt' not found!
Cleanup: Closing file (if open)
5. Practical Example: Input Validation#
A common use case is validating user input:
def get_positive_number(prompt):
"""Keep asking until user enters a valid positive number."""
while True:
try:
value = float(input(prompt))
if value <= 0:
print("Please enter a positive number!")
continue
return value
except ValueError:
print("Invalid input! Please enter a number.")
# Uncomment to test interactively in a live notebook:
# mass = get_positive_number("Enter mass (kg): ")
# print(f"You entered: {mass} kg")
6. Physics Example: Robust Calculation Function#
import math
def calculate_orbital_period(semi_major_axis, central_mass):
"""
Calculate orbital period using Kepler's third law.
T = 2π * sqrt(a³ / (G * M))
Parameters:
semi_major_axis: in meters
central_mass: in kg
Returns:
Period in seconds, or None if invalid input
"""
G = 6.674e-11 # gravitational constant
try:
# Validate inputs
if semi_major_axis <= 0:
raise ValueError("Semi-major axis must be positive")
if central_mass <= 0:
raise ValueError("Central mass must be positive")
# Calculate
period = 2 * math.pi * math.sqrt(semi_major_axis**3 / (G * central_mass))
return period
except ValueError as e:
print(f"Invalid input: {e}")
return None
except TypeError as e:
print(f"Type error: {e}")
return None
# Earth's orbit around the Sun
a_earth = 1.496e11 # meters
M_sun = 1.989e30 # kg
T = calculate_orbital_period(a_earth, M_sun)
if T:
print(f"Earth's orbital period: {T:.2e} seconds")
print(f"That's {T / (24*3600):.1f} days")
# Test error handling
print("\nTesting with invalid input:")
calculate_orbital_period(-1e11, M_sun) # negative distance
calculate_orbital_period(a_earth, 0) # zero mass
Earth's orbital period: 3.16e+07 seconds
That's 365.2 days
Testing with invalid input:
Invalid input: Semi-major axis must be positive
Invalid input: Central mass must be positive
import math
def calculate_orbital_period(semi_major_axis, central_mass):
"""
Calculate orbital period using Kepler's third law.
T = 2π * sqrt(a³ / (G * M))
Parameters:
semi_major_axis: in meters
central_mass: in kg
Returns:
Period in seconds, or None if invalid input
"""
if semi_major_axis <= 0 or central_mass <= 0:
return None
G = 6.674e-11 # gravitational constant
period = 2 * math.pi * math.sqrt(semi_major_axis**3 / (G * central_mass))
return period
a_earth = 1.496e11 # meters
M_sun = 1.989e30 # kg
T = calculate_orbital_period(a_earth, M_sun)
print(T)
7. Raising Your Own Exceptions#
Use raise to signal errors in your own code:
def set_temperature(kelvin):
"""Set temperature, ensuring it's physically valid."""
if kelvin < 0:
raise ValueError(f"Temperature {kelvin}K is below absolute zero!")
print(f"Temperature set to {kelvin}K ({kelvin - 273.15:.1f}°C)")
# Valid temperature
set_temperature(300)
# Invalid temperature - this will raise an exception
try:
set_temperature(-50)
except ValueError as e:
print(f"Caught error: {e}")
Error Handling Summary#
Keyword |
Purpose |
|---|---|
|
Code that might raise an exception |
|
Handle specific exception types |
|
Run if no exception occurred |
|
Always run (cleanup) |
|
Raise your own exception |
Best Practices:
Catch specific exceptions, not bare
except:Use
finallyfor cleanup (closing files, connections)Validate inputs early with meaningful error messages
Don’t silently ignore errors — at least log them
V. Summary & Next Steps#
In this lecture, we covered:
Functions#
Defining functions with
defParameters: positional, keyword, default
Return values and docstrings
Scope and namespaces
Data Structures#
Lists: Ordered, mutable, indexed
Tuples: Ordered, immutable, faster
Dictionaries: Key-value pairs, fast lookup
Sets: Unique elements, set operations
File I/O#
Reading and writing text files
The
withstatement for safe file handlingNumPy’s
loadtxtandsavetxtfor numerical data
Error Handling#
try/exceptfor catching errorselseandfinallyclausesRaising exceptions with
raise
Next Lecture: Python Basics III#
NumPy arrays in depth
Advanced plotting with Matplotlib
Introduction to numerical methods
VI. Practice Exercises#
Exercise 1: Function Practice#
Write a function quadratic_formula(a, b, c) that:
Takes coefficients a, b, c of a quadratic equation ax² + bx + c = 0
Returns both roots (as a tuple)
Handles the case where the discriminant is negative (return
None)Includes a docstring
Exercise 2: Data Structure Practice#
Create a dictionary called element_data that stores:
Element symbols as keys
A dictionary with ‘name’, ‘atomic_number’, and ‘mass’ as values
Include at least 5 elements
Write a function that takes an element symbol and prints its properties
Exercise 3: File I/O Practice#
Write a program that:
Creates a file with 10 random numbers (one per line)
Reads the file back
Calculates and prints the mean and standard deviation
Saves the statistics to a new file
Exercise 4: Error Handling Practice#
Write a function safe_sqrt(x) that:
Returns the square root of x
Handles negative numbers gracefully (print warning, return None)
Handles non-numeric input (print error, return None)
Works correctly for valid positive numbers
# Your solutions here
# Exercise 1: quadratic_formula
# Exercise 2: element_data
# Exercise 3: File I/O
# Exercise 4: safe_sqrt
VII. Advanced Topics (Optional Reading)#
The following sections cover more advanced Python features. These are useful for writing sophisticated code but are not essential for the core computational physics content.
Variable Arguments (
*args,**kwargs)Iterators and Generators
Decorators
Feel free to explore these topics if you’re comfortable with the main material.
Variable Arguments: *args and **kwargs#
Sometimes you want a function to accept any number of arguments:
*args: Collects extra positional arguments as a tuple**kwargs: Collects extra keyword arguments as a dictionary
def flexible_function(*args, **kwargs):
"""A function that accepts any arguments."""
print(f"Positional args: {args}")
print(f"Keyword args: {kwargs}")
flexible_function(1, 2, 3, name="physics", value=42)
Generators#
Generators are functions that produce a sequence of values lazily (one at a time), using yield instead of return. They’re memory-efficient for large sequences.
def fibonacci(n):
"""Generate first n Fibonacci numbers."""
a, b = 0, 1
count = 0
while count < n:
yield a # Return value and pause
a, b = b, a + b
count += 1
# Use the generator
print("First 10 Fibonacci numbers:")
for num in fibonacci(10):
print(num, end=" ")
Decorators#
Decorators wrap functions to add extra behavior without modifying the original function.
import time
def timing_decorator(func):
"""Measure how long a function takes to run."""
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} took {end - start:.4f} seconds")
return result
return wrapper
@timing_decorator
def slow_function():
"""A function that takes some time."""
total = sum(i**2 for i in range(100000))
return total
result = slow_function()