Introduction to Python
1. Basic Elements of Python
Objects, Expressions, and Numerical Types
Python is a dynamically-typed, object-oriented programming language. Everything in Python is an
object, including numbers, strings, and functions.
Numerical Types
Python supports several numerical types:
1. Integer (int): Whole numbers without decimal points
2. x = 10 # Positive integer
3. y = -5 # Negative integer
4. Float: Numbers with decimal points
5. pi = 3.14159
6. temperature = -273.15
7. Complex Numbers: Supported natively in Python
8. z = 3 + 4j # Complex number with real and imaginary parts
Variables and Assignments
Variables are containers for storing data values. Python uses dynamic typing, meaning you don't need
to declare the type explicitly.
# Variable assignment
name = "John" # String
age = 25 # Integer
height = 1.75 # Float
Branching Programs
Branching allows different code execution based on conditions using if, elif, and else statements.
# Example of branching
score = 85
if score >= 90:
grade = 'A'
elif score >= 80:
grade = 'B'
elif score >= 70:
grade = 'C'
elif score >= 60:
grade = 'D'
else:
grade = 'F'
print(f"Your grade is: {grade}")
Strings and Input
Strings are sequences of characters. Python provides powerful string manipulation capabilities.
# String operations
greeting = "Hello, World!"
print(greeting.upper()) # Uppercase
print(greeting.lower()) # Lowercase
print(len(greeting)) # Length of string
# User input
name = input("Enter your name: ")
print(f"Hello, {name}!")
Iteration
Iteration allows repeating a block of code multiple times using loops.
# For loop
fruits = ["apple", "banana", "cherry"]
for fruit in fruits:
print(fruit)
# While loop
count = 0
while count < 5:
print(count)
count += 1
Structured Types
Tuples
Immutable ordered sequences
Defined using parentheses
coordinates = (10, 20)
x, y = coordinates # Unpacking
Ranges
Generates sequences of numbers
# Range examples
range(5) # 0, 1, 2, 3, 4
range(2, 7) # 2, 3, 4, 5, 6
range(1, 10, 2) # 1, 3, 5, 7, 9
Lists
Mutable ordered sequences
Most versatile structured type
# List operations
numbers = [1, 2, 3, 4, 5]
numbers.append(6) # Add element
numbers.remove(3) # Remove element
sorted_numbers = sorted(numbers) # Sort list
# List comprehension
squares = [x**2 for x in range(5)] # [0, 1, 4, 9, 16]
Dictionaries
Key-value pairs
Unordered, mutable
student = {
"name": "Alice",
"age": 22,
"courses": ["Math", "Computer Science"]
}
print(student["name"]) # Access by key
student["grade"] = "A" # Add new key-value pair
Mutability
Mutable: Can be changed after creation (lists, dictionaries)
Immutable: Cannot be changed after creation (tuples, strings, numbers)
# Demonstrating mutability
# Mutable list
numbers = [1, 2, 3]
numbers[1] = 10 # Allowed
# Immutable tuple
coordinates = (1, 2)
# coordinates[1] = 10 # This would raise an error
# Functions, Exceptions, Modules, and Files
## 1. Functions
### Function Basics
A function is a block of organized, reusable code that performs a specific task.
```python
# Basic function definition
def greet(name):
"""
This function greets the person passed in as a parameter
:param name: Name of the person to greet
:return: Greeting message
"""
return f"Hello, {name}!"
# Function calling
print(greet("Alice"))
```
### Function Types and Characteristics
#### Difference between Function and Method
- **Function**: A standalone block of code
- **Method**: A function associated with an object or class
#### Function Characteristics
1. **First-Class Objects**: Functions can be:
- Assigned to variables
- Passed as arguments
- Returned from other functions
```python
# Functions as first-class objects
def multiply(x, y):
return x * y
# Assigning function to a variable
operation = multiply
print(operation(4, 5)) # Output: 20
# Function as an argument
def apply_operation(func, x, y):
return func(x, y)
print(apply_operation(multiply, 3, 4)) # Output: 12
```
#### Argument Types
1. **Positional Arguments**
```python
def add(a, b):
return a + b
print(add(3, 4)) # Positional arguments
```
2. **Keyword Arguments**
```python
def introduce(name, age):
print(f"Name: {name}, Age: {age}")
introduce(age=25, name="John") # Order doesn't matter
```
3. **Default Arguments**
```python
def power(base, exponent=2):
return base ** exponent
print(power(3)) # 9 (3^2)
print(power(3, 3)) # 27 (3^3)
```
4. **Variable Length Arguments**
```python
# *args for multiple positional arguments
def sum_all(*args):
return sum(args)
print(sum_all(1, 2, 3, 4, 5)) # 15
# **kwargs for keyword arguments
def print_info(**kwargs):
for key, value in kwargs.items():
print(f"{key}: {value}")
print_info(name="Alice", age=30)
```
#### Recursive Functions
```python
def factorial(n):
if n == 0 or n == 1:
return 1
else:
return n * factorial(n-1)
print(factorial(5)) # 120
```
#### Lambda Functions (Anonymous Functions)
```python
# Simple lambda function
square = lambda x: x ** 2
print(square(4)) # 16
# Using lambda with built-in functions
numbers = [1, 2, 3, 4, 5]
squared_numbers = list(map(lambda x: x**2, numbers))
# Filter even numbers
even_numbers = list(filter(lambda x: x % 2 == 0, numbers))
```
### Function Decorators
```python
def timer_decorator(func):
import time
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"Function took {end - start} seconds to run")
return result
return wrapper
@timer_decorator
def slow_function():
import time
time.sleep(2)
print("Function completed")
slow_function()
```
### Generators
```python
def fibonacci_generator(n):
a, b = 0, 1
count = 0
while count < n:
yield a
a, b = b, a + b
count += 1
# Using the generator
for num in fibonacci_generator(5):
print(num)
```
## 2. Exceptions
### Types of Errors
1. **Compile-Time Errors**: Syntax errors detected before running the code
2. **Runtime Errors**: Errors that occur during program execution
3. **Logical Errors**: Errors in program logic that produce incorrect results
### Exception Handling
```python
try:
# Code that might raise an exception
x = int(input("Enter a number: "))
result = 10 / x
except ZeroDivisionError:
print("Cannot divide by zero!")
except ValueError:
print("Invalid input. Please enter a number.")
else:
# Runs if no exception occurs
print("Division successful")
finally:
# Always runs, regardless of exceptions
print("Execution completed")
# User-defined exceptions
class CustomError(Exception):
def __init__(self, message):
self.message = message
super().__init__(self.message)
def check_age(age):
if age < 0:
raise CustomError("Age cannot be negative")
try:
check_age(-5)
except CustomError as e:
print(e)
```
## 3. Modules
### Creating Custom Modules
```python
# math_operations.py
def add(a, b):
return a + b
def multiply(a, b):
return a * b
# In another file
import math_operations
print(math_operations.add(5, 3))
```
### Special `__name__` Variable
```python
# This allows code to be run only when the script is the main program
if __name__ == "__main__":
print("This module is being run directly")
```
## 4. File Handling
### File Operations
```python
# Writing to a text file
with open('example.txt', 'w') as file:
file.write("Hello, World!")
# Reading from a text file
with open('example.txt', 'r') as file:
content = file.read()
print(content)
# Binary file handling with Pickle
import pickle
# Storing complex objects
data = {'name': 'John', 'age': 30}
with open('data.pkl', 'wb') as file:
pickle.dump(data, file)
# Reading pickled data
with open('data.pkl', 'rb') as file:
loaded_data = pickle.load(file)
print(loaded_data)
# File pointer manipulation
with open('example.txt', 'r') as file:
file.seek(0) # Move to the beginning of the file
position = file.tell() # Get current file position
```
### File Types
1. **Text Files**: Human-readable, store data as text
2. **Binary Files**: Store data in binary format (e.g., images, pickled objects)
# Classes and Object-Oriented Programming
## Object-Oriented Programming (OOP) Fundamentals
### Abstract Data Types and Classes
Object-Oriented Programming is a programming paradigm that uses objects and classes to organize
and structure code.
#### Basic Class Definition
```python
class BankAccount:
# Class attribute
bank_name = "MyBank"
# Constructor method
def __init__(self, account_number, balance=0):
# Instance attributes
self.account_number = account_number
self.balance = balance
self.transaction_history = []
# Instance method
def deposit(self, amount):
if amount > 0:
self.balance += amount
self.transaction_history.append(f"Deposit: +${amount}")
return True
return False
# Another instance method
def withdraw(self, amount):
if 0 < amount <= self.balance:
self.balance -= amount
self.transaction_history.append(f"Withdrawal: -${amount}")
return True
return False
# Method to display account info
def get_balance(self):
return self.balance
```
### Inheritance
Inheritance allows a class to inherit attributes and methods from another class.
```python
class SavingsAccount(BankAccount):
def __init__(self, account_number, interest_rate=0.05):
# Call parent class constructor
super().__init__(account_number)
self.interest_rate = interest_rate
def apply_interest(self):
interest = self.balance * self.interest_rate
self.deposit(interest)
return interest
# Multilevel Inheritance
class PremiumSavingsAccount(SavingsAccount):
def __init__(self, account_number, interest_rate=0.07, min_balance=1000):
super().__init__(account_number, interest_rate)
self.min_balance = min_balance
def check_min_balance(self):
return self.balance >= self.min_balance
```
### Encapsulation and Information Hiding
Encapsulation involves restricting direct access to some of an object's components.
```python
class SecureBankAccount:
def __init__(self, account_number):
# Private attributes (convention in Python)
self.__account_number = account_number
self.__balance = 0
self.__pin = None
# Getter method
def get_account_number(self):
return self.__account_number
# Setter method with validation
def set_pin(self, pin):
if len(str(pin)) == 4:
self.__pin = pin
return True
return False
# Method with access control
def withdraw(self, amount, entered_pin):
if entered_pin == self.__pin:
if amount > 0 and amount <= self.__balance:
self.__balance -= amount
return True
return False
```
### Case Study: Banking Application
A comprehensive example demonstrating OOP principles:
```python
class Bank:
def __init__(self, name):
self.name = name
self.accounts = {}
def create_account(self, account_type, initial_deposit=0):
account_number = len(self.accounts) + 1
if account_type == "basic":
account = BankAccount(account_number, initial_deposit)
elif account_type == "savings":
account = SavingsAccount(account_number)
elif account_type == "premium":
account = PremiumSavingsAccount(account_number)
else:
raise ValueError("Invalid account type")
self.accounts[account_number] = account
return account_number
def get_account(self, account_number):
return self.accounts.get(account_number)
def transfer_funds(self, from_account, to_account, amount):
if from_account.withdraw(amount):
to_account.deposit(amount)
return True
return False
# Example usage
def banking_simulation():
# Create a bank
my_bank = Bank("Global Banking Corporation")
# Create accounts
savings_account = my_bank.create_account("savings", 1000)
checking_account = my_bank.create_account("basic", 500)
# Retrieve accounts
savings = my_bank.get_account(savings_account)
checking = my_bank.get_account(checking_account)
# Perform operations
savings.deposit(500)
savings.apply_interest()
# Transfer funds
checking.withdraw(200)
savings.deposit(200)
print(f"Savings Balance: ${savings.get_balance()}")
print(f"Checking Balance: ${checking.get_balance()}")
# Run the simulation
banking_simulation()
```
### Advanced OOP Concepts
#### Class Methods and Static Methods
```python
class MathOperations:
# Class method
@classmethod
def create_zero_matrix(cls, size):
return [[0 for _ in range(size)] for _ in range(size)]
# Static method
@staticmethod
def is_prime(n):
if n < 2:
return False
for i in range(2, int(n**0.5) + 1):
if n % i == 0:
return False
return True
```
### Polymorphism
```python
class Shape:
def area(self):
pass
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * self.radius ** 2
# Polymorphic behavior
def print_area(shape):
print(f"Area: {shape.area()}")
# Usage
rect = Rectangle(5, 3)
circle = Circle(4)
print_area(rect) # Calls Rectangle's area method
print_area(circle) # Calls Circle's area method
```
### Key OOP Principles
1. **Encapsulation**: Bundling data and methods that operate on that data
2. **Inheritance**: Creating new classes based on existing classes
3. **Polymorphism**: Ability of different classes to be treated as instances of the same class
4. **Abstraction**: Hiding complex implementation details
### Best Practices
- Use composition over inheritance when possible
- Keep classes focused and following Single Responsibility Principle
- Use type hinting and docstrings for better code readability
- Prefer immutable data when possible
- Use property decorators for controlled attribute access