Python Core Concepts: File I/O, Data Structures & Algorithms
Python File Handling Basics
Common modes for opening files:
open(filename, 'w')
: Open for writing (truncates file if exists, creates if not).open(filename, 'r')
: Open for reading (default mode).open(filename, 'a')
: Open for appending (creates file if not exists).
Common file handle (fh
) methods:
fh.read()
: Reads the entire file content.fh.readline()
: Reads a single line from the file.fh.readlines()
: Reads all lines into a list of strings.fh.write(string)
: Writes a string to the file.fh.writelines(list_of_strings)
: Writes a list of strings to the file.fh.close()
: Closes the file handle, releasing resources.
File Handling Examples
1. Without Using the with
Statement (Manual Close)
# Open file for writing
file = open('file_path', 'w')
# Write content
file.write('Hello world!')
# IMPORTANT: Close the file
file.close()
2. Without Using with
Statement (Using Try-Finally)
This ensures the file is closed even if errors occur during writing.
# Open file for writing
file = open('file_path', 'w')
try:
# Attempt to write content
file.write('Hello world')
finally:
# Ensure file is closed regardless of success or failure
file.close()
3. Using the with
Statement (Recommended)
The with
statement automatically handles closing the file, even if errors occur.
# Open file for writing using 'with'
with open('file_path', 'w') as file:
# Write content
file.write('Hello world!')
# File is automatically closed here
Python Data Structures & Operations
Tuples
- Variable Swapping: Tuples allow for easy variable swapping.
(x, y) = (y, x) # Swaps the values of x and y
Concatenation: Tuples can be concatenated using the +
operator.(2, 'two', 3) + (5, 6) # Results in (2, 'two', 3, 5, 6)
Lists
- Append: Adds an element to the end of the list.
L = [1, 2]
L.append(5) # L becomes [1, 2, 5]
Extend: Appends elements from another iterable to the end.L1 = [2, 1, 3]
L1.extend([0, 6]) # L1 becomes [2, 1, 3, 0, 6]
Delete by Index: Removes the element at a specific index.L = [10, 20, 30]
del(L[1]) # L becomes [10, 30]
Pop: Removes and returns the last element (or element at a specified index).L1 = [2, 1, 3, 0, 6]
last_element = L1.pop() # last_element is 6, L1 becomes [2, 1, 3, 0]
element_at_index_1 = L1.pop(1) # element_at_index_1 is 1, L1 becomes [2, 3, 0]
Remove by Value: Removes the first occurrence of a specific element.L1 = [2, 1, 3, 1]
L1.remove(1) # L1 becomes [2, 3, 1]
String Operations
- Convert String to List: Creates a list of characters from a string.
s = "hello"
char_list = list(s) # char_list becomes ['h', 'e', 'l', 'l', 'o']
Split String: Splits a string into a list based on a delimiter.s = "a-b-c"
parts = s.split('-') # parts becomes ['a', 'b', 'c']
Join List into String: Concatenates elements of a list into a single string.L = ['a', 'b', 'c']
joined_string = ''.join(L) # joined_string becomes "abc"
joined_with_underscore = '_'.join(L) # joined_with_underscore becomes "a_b_c"
Finding Common Letters (Example):s1 = 'itb u rock'
s2 = 'i rule itb'
# Find the first common letter
if len(s1) == len(s2):
for char1 in s1:
for char2 in s2:
if char1 == char2:
print('A common letter is:', char1)
break # Exits inner loop after finding the first match for char1
else:
continue # Only executed if the inner loop did NOT break
break # Exits outer loop after finding the first common letter overall
String Formatting (Legacy %-formatting):x = 'looked'
print("Misha %s and %s around" % ('walked', x))
# Output: Misha walked and looked around
Note: F-strings (f"Misha {verb1} and {verb2} around"
) are the modern and preferred way for string formatting in Python 3.6+.
Dictionaries
Dictionaries store key-value pairs.
- Creation:
my_dict = {}
grades = {"Anna": "B", "John": "A", "Ichi": "A"}
Accessing Values: Use the key in square brackets. Accessing a non-existent key raises a KeyError
.print(grades['John']) # Output: A
Adding/Updating Entries: Assign a value to a key.grades['Sylvan'] = "A" # Adds Sylvan: A
grades['Anna'] = "A+" # Updates Anna's grade
Testing Key Existence: Use the in
keyword.print('John' in grades) # Output: True
print('Peter' in grades) # Output: False
Deleting Entries: Use the del
keyword.del(grades["Anna"])
Getting Keys: Returns a view object containing the dictionary’s keys.keys_list = list(grades.keys()) # Convert view to list if needed
Getting Values: Returns a view object containing the dictionary’s values.values_list = list(grades.values()) # Convert view to list if needed
Matrix Operations in Python
Functions for basic matrix operations.
def create_matrix(rows, columns):
"""Creates a matrix filled with zeros."""
matrix = []
for i in range(rows):
row = []
for j in range(columns):
row.append(0)
matrix.append(row)
return matrix
def add_matrices(matrix1, matrix2):
"""Adds two matrices element-wise."""
rows1, columns1 = len(matrix1), len(matrix1[0])
rows2, columns2 = len(matrix2), len(matrix2[0])
if rows1 != rows2 or columns1 != columns2:
raise ValueError("Matrix dimensions must match for addition")
result_matrix = []
for i in range(rows1):
row = []
for j in range(columns1):
sum_element = matrix1[i][j] + matrix2[i][j]
row.append(sum_element)
result_matrix.append(row)
return result_matrix
def multiply_matrices(matrix_a, matrix_b):
"""Multiplies two matrices."""
rows_a, columns_a = len(matrix_a), len(matrix_a[0])
rows_b, columns_b = len(matrix_b), len(matrix_b[0])
if columns_a != rows_b:
raise ValueError("Matrix dimensions are incompatible for multiplication (columns_a != rows_b)")
result_matrix = []
for i in range(rows_a):
row = []
for j in range(columns_b):
# Initialize the element at (i, j) to 0
element = 0
for k in range(columns_a): # or range(rows_b)
element += matrix_a[i][k] * matrix_b[k][j]
row.append(element)
result_matrix.append(row)
return result_matrix
def create_submatrix(matrix, row_number_to_remove, column_number_to_remove):
"""Creates a submatrix by removing a specified row and column (1-based index)."""
# Adjust input row and column numbers to zero-based indexing
row_idx_to_remove = row_number_to_remove - 1
col_idx_to_remove = column_number_to_remove - 1
# Check if the input row and column numbers are within valid bounds
if not (0 <= row_idx_to_remove < len(matrix)) or not (0 <= col_idx_to_remove < len(matrix[0])):
raise ValueError("Invalid row or column number for submatrix creation")
# Create the submatrix using list comprehension (alternative 1)
# submatrix = [
# row[:col_idx_to_remove] + row[col_idx_to_remove + 1:]
# for row_index, row in enumerate(matrix)
# if row_index != row_idx_to_remove
# ]
# Create the submatrix using nested list comprehension (alternative 2)
submatrix = [
[matrix[i][j] for j in range(len(matrix[0])) if j != col_idx_to_remove]
for i in range(len(matrix)) if i != row_idx_to_remove
]
return submatrix
Python Exception Handling Example
This program demonstrates defining custom exceptions and handling them using try...except...finally
blocks.
# Define custom exception classes
class Exc0(Exception):
def __init__(self, msg):
self.message = msg
def __str__(self):
return self.message
class Exc1(Exc0): # Exc1 inherits from Exc0
def __init__(self, msg):
super().__init__(msg)
# --- First Try-Except-Finally Block ---
try:
print("Raising Exc1...")
raise Exc1("Exc1 occurred") # Raise an instance of Exc1
except Exc0 as e: # Catches Exc1 because Exc1 is a subclass of Exc0
print("Exc0 caught")
print(e) # Prints the message from the caught exception
finally:
# This block always executes
print("Finally done")
print("\n---\n") # Separator
# --- Second Try-Except-Finally Block ---
try:
print("Raising Exc0...")
raise Exc0("Exc0 occurred") # Raise an instance of Exc0
except Exc1 as e: # This block will NOT catch Exc0
print("Exc1 caught")
print(e)
except Exc0 as e: # This block WILL catch Exc0
print("Exc0 caught again")
print(e)
finally:
# This block always executes
print("Finally again")
print("\nEnd of program")
Explanation (Indonesian & English)
Program ini mendefinisikan dua kelas pengecualian (exception), Exc0
dan Exc1
, di mana Exc1
merupakan subkelas dari Exc0
. Kedua kelas ini memiliki metode __init__
untuk menginisialisasi pesan error dan metode __str__
untuk mengembalikan pesan error tersebut ketika objek pengecualian dicetak.
(This program defines two exception classes, Exc0
and Exc1
, where Exc1
is a subclass of Exc0
. Both classes have an __init__
method to initialize an error message and a __str__
method to return that error message when the exception object is printed.)
Blok Pertama Try-Except-Finally
Dalam blok try
pertama, sebuah objek pengecualian Exc1
dibangkitkan menggunakan pernyataan raise
.
Karena Exc1
adalah subkelas dari Exc0
, maka pengecualian ini ditangkap oleh blok except Exc0 as e
.
Program mencetak “Exc0 caught” dan kemudian mencetak pesan dari objek pengecualian Exc1
, yaitu “Exc1 occurred”.
Setelah itu, blok finally
dieksekusi, mencetak “Finally done”.
(In the first try
block, an Exc1
exception object is raised using the raise
statement.
Because Exc1
is a subclass of Exc0
, this exception is caught by the except Exc0 as e:
block.
The program prints “Exc0 caught” and then prints the message from the Exc1
exception object, which is “Exc1 occurred”.
After that, the finally
block is executed, printing “Finally done”.)
Blok Kedua Try-Except-Finally
Dalam blok try
kedua, sebuah objek pengecualian Exc0
dibangkitkan.
Pengecualian ini tidak dapat ditangkap oleh blok except Exc1 as e
karena Exc0
bukanlah subkelas dari Exc1
.
Blok except Exc0 as e
ditambahkan untuk menangkap pengecualian ini.
Program mencetak “Exc0 caught again” dan kemudian mencetak pesan dari objek pengecualian Exc0
, yaitu “Exc0 occurred”.
Setelah itu, blok finally
dieksekusi, mencetak “Finally again”.
(In the second try
block, an Exc0
exception object is raised.
This exception cannot be caught by the except Exc1 as e:
block because Exc0
is not a subclass of Exc1
.
The except Exc0 as e:
block is added to catch this exception.
The program prints “Exc0 caught again” and then prints the message from the Exc0
exception object, which is “Exc0 occurred”.
After that, the finally
block is executed, printing “Finally again”.)
Akhir Program
Program kemudian mencetak “End of program”, menandakan akhir dari eksekusi program.
(The program then prints “End of program”, indicating the end of the program’s execution.)
Expected Output:
Raising Exc1... Exc0 caught Exc1 occurred Finally done --- Raising Exc0... Exc0 caught again Exc0 occurred Finally again End of program
Object-Oriented Programming in Python
Example demonstrating classes, inheritance, method overriding, and operator overloading.
import math # Needed if using more complex shapes, not strictly for Rectangle perimeter
class Shape(object):
"""Base class for geometric shapes."""
def __init__(self, type_of_shape: str):
self.type_of_shape = type_of_shape
def __str__(self):
return "I am a " + self.type_of_shape
class Polygon(Shape):
"""Represents shapes with straight sides."""
def __init__(self, type_of_shape, side_lengths: list):
super().__init__(type_of_shape)
# It's generally better to initialize instance variables in __init__
self.side_lengths = side_lengths if side_lengths is not None else []
def compute_perimeter(self):
"""Calculates the perimeter of the polygon."""
perimeter = 0
for length in self.side_lengths:
perimeter += length
return perimeter
def get_number_of_sides(self):
"""Returns the number of sides of the polygon."""
return len(self.side_lengths)
def __gt__(self, other):
"""Overloads the greater than operator to compare perimeters."""
if not isinstance(other, Polygon):
return NotImplemented # Indicate comparison is not supported
self_perimeter = self.compute_perimeter()
other_perimeter = other.compute_perimeter()
if self_perimeter > other_perimeter:
return True
# Optional: Handle equality specifically if needed,
# but standard > doesn't return 'Same'.
# elif self_perimeter == other_perimeter:
# return 'Same' # Custom return, not standard for >
else:
return False
class Rectangle(Polygon):
"""Represents a rectangle, inheriting from Polygon."""
def __init__(self, side_lengths: list):
# Basic validation: A rectangle needs 4 sides, opposite sides equal
# For simplicity here, we assume valid input or could add checks.
# If just given [length, width], duplicate them: [l, w, l, w]
if len(side_lengths) == 2:
side_lengths = [side_lengths[0], side_lengths[1], side_lengths[0], side_lengths[1]]
elif len(side_lengths) != 4:
raise ValueError("Rectangle requires 2 or 4 side lengths.")
# Could add check: side_lengths[0] == side_lengths[2] and side_lengths[1] == side_lengths[3]
super().__init__('rectangle', side_lengths)
# self.side_lengths is already set by the Polygon's __init__
def __str__(self):
"""Provides a string representation of the rectangle."""
# More specific representation for a rectangle
if len(self.side_lengths) == 4:
return f"A rectangle with sides {self.side_lengths[0]}, {self.side_lengths[1]}, {self.side_lengths[2]}, {self.side_lengths[3]}"
else:
return super().__str__() # Fallback if sides aren't typical
def compute_area(self):
"""Calculates the area of the rectangle."""
if len(self.side_lengths) == 4:
# Assuming standard rectangle where sides are [l, w, l, w]
return self.side_lengths[0] * self.side_lengths[1]
else:
return 0 # Or raise error
# --- Example Usage ---
p1 = Rectangle([1, 2, 1, 2]) # Perimeter = 6
p2 = Rectangle([1, 2, 3, 5]) # Not a valid rectangle, but Polygon logic works. Perimeter = 11
p3 = Rectangle([2, 3]) # Creates [2, 3, 2, 3]. Perimeter = 10
print(f"p1: {p1}, Perimeter: {p1.compute_perimeter()}, Area: {p1.compute_area()}")
print(f"p2: {p2}, Perimeter: {p2.compute_perimeter()}") # Area method not applicable if not valid rect
print(f"p3: {p3}, Perimeter: {p3.compute_perimeter()}, Area: {p3.compute_area()}")
print("\nComparing p2 and p1 based on perimeter:")
if p2 > p1:
print(f"p2's perimeter ({p2.compute_perimeter()}) is greater than p1's ({p1.compute_perimeter()})")
print(p2)
elif p1 > p2: # Check the opposite explicitly
print(f"p1's perimeter ({p1.compute_perimeter()}) is greater than p2's ({p2.compute_perimeter()})")
print(p1)
else:
# This covers equality or if comparison returned something unexpected
print(f"The perimeters are equal (p1: {p1.compute_perimeter()}, p2: {p2.compute_perimeter()})")
Coin Change Problem: Dynamic Programming
Finding the minimum number of coins required to make a given amount using a set of denominations.
def coin_change(coins, amount):
"""Calculates the minimum number of coins to make the amount.
Args:
coins (list): A list of available coin denominations.
amount (int): The target amount.
Returns:
int: The minimum number of coins, or -1 if the amount cannot be made.
"""
# Initialize dp array with a value indicating impossibility (infinity)
# dp[i] will store the minimum coins needed to make amount i
dp = [float('inf')] * (amount + 1)
# Base case: 0 coins are needed to make amount 0
dp[0] = 0
# Iterate through each coin denomination
for coin in coins:
# Update dp array for amounts from the coin value up to the target amount
for i in range(coin, amount + 1):
# If the amount (i - coin) can be made,
# consider using the current 'coin'.
if dp[i - coin] != float('inf'):
# Update dp[i] if using this 'coin' results in fewer total coins
dp[i] = min(dp[i], dp[i - coin] + 1)
# If dp[amount] is still infinity, the amount cannot be made
# Otherwise, return the minimum number of coins found
return dp[amount] if dp[amount] != float('inf') else -1
# --- Example Usage ---
denominations = [2, 3, 5, 10, 20, 30, 50]
amount = 78
min_coins = coin_change(denominations, amount)
if min_coins != -1:
print(f"Minimum number of coins needed for amount {amount}: {min_coins}")
else:
print(f"Amount {amount} cannot be made with the given denominations.")
# Example Output for amount 78: Minimum number of coins needed: 4 (e.g., 50 + 20 + 5 + 3)
Complexity Analysis
- Time Complexity: O(
amount
*n
), whereamount
is the target amount andn
is the number of coin denominations. This is because we have nested loops: the outer loop iterates throughn
coins, and the inner loop iterates up toamount
times for each coin. - Space Complexity: O(
amount
). This is due to the space required for thedp
array, which has a size ofamount + 1
.