In software development, there are different approaches to organizing code. Procedural programming involves writing code sequentially from top to bottom based on business logic. However, this approach often leads to code duplication and poor maintainability.
Functional programming addresses these issues by encapsulating specific functionality into reusable blocks. When a particular piece of logic is needed more than three times, it is recommended to extract it into a function. This approach significantly improves code extensibility and reusability.
Think of functions as building blocks, similar to Lego bricks. You can combine these modular components to construct complex applications. Functions can call other functions, and the main function serves as the orchestrator that connects and invokes these functional blocks.
# module_example.py
def initialize_app():
pass
def load_configuration():
pass
def connect_database():
pass
def start_server():
initialize_app()
load_configuration()
connect_database()
if __name__ == '__main__':
# This block only executes when the script runs directly
# When imported as a module, __name__ equals the filename
# When executed directly, __name__ equals '__main__'
start_server()
Defining Custom Functions
The Problem with Procedural Code
Consider a monitoring system that sends alerts when system resources exceed thresholds:
while True:
if cpu_usage > 90:
# Send CPU alert email
connect_to_smtp_server()
send_email()
disconnect_server()
if disk_usage > 90:
# Send disk alert email
connect_to_smtp_server()
send_email()
disconnect_server()
if memory_usage > 80:
# Send memory alert email
connect_to_smtp_server()
send_email()
disconnect_server()
This approach creates redundant code. Adding more alert types would require duplicating the email sending logic repeatedly.
The Functional Approach
def send_alert_email(message_content):
connect_to_smtp_server()
send_email(message_content)
disconnect_server()
while True:
if cpu_usage > 90:
send_alert_email('CPU utilization exceeded threshold')
if disk_usage > 90:
send_alert_email('Disk space critically low')
if memory_usage > 80:
send_alert_email('Memory usage too high')
The functional version offers better readability and maintainability. The key differences are:
- Procedural: Code written line by line, often with duplicated logic and poor reusability
- Functional: Logic encapsulated in functions, eliminating repetition
- Object-Oriented: Functions grouped and encapsulated within classes for better organization
Function Definition Syntax
def function_name(parameters):
# Function body
# Logic execution
return value
Key components:
def: Keyword to define a function- Function name: Identifier used to call the function
- Function body: The logic to be executed
- Parameters: Input data for the function
- Return value: Output data returned to the caller
Return Values
Functions communicate their execution status through return values:
def send_notification():
# Notification logic here
success = execute_send()
if success:
return True
return False
while True:
status = send_notification()
if not status:
log_error('Notification failed, retrying...')
Function Parameters
Standard Parameters
def greet_user(username):
print(f"Hello, {username}")
greet_user('Alice')
The number of argumenst must match the number of parameters defined:
def display_info(name, age):
print(f"{name} is {age} years old")
display_info('Bob') # TypeError: missing required argument
Default Parameters
def create_profile(name, role='Guest'):
print(f"Name: {name}, Role: {role}")
create_profile('Alice', 'Admin') # Name: Alice, Role: Admin
create_profile('Bob') # Name: Bob, Role: Guest
Default parameters must be placed after non-default parameters in the function definition.
Variable-Length Arguments
Using *args to accept multiple positional arguments (converted to a tuple):
def calculate_sum(*args):
total = 0
for num in args:
total += num
return total
calculate_sum(1, 2, 3, 4, 5) # Returns 15
numbers = [10, 20, 30]
calculate_sum(*numbers) # Unpacks list into individual arguments
Using **kwargs to accept keyword arguments (converted to a dictionary):
def display_info(**kwargs):
for key, value in kwargs.items():
print(f"{key}: {value}")
display_info(name='Alice', age=25, city='New York')
user_data = {'name': 'Bob', 'age': 30}
display_info(**user_data)
Combining both:
def process_data(*args, **kwargs):
print(f"Positional: {args}")
print(f"Keyword: {kwargs}")
process_data(1, 2, 3, name='Alice', status='active')
# Output:
# Positional: (1, 2, 3)
# Keyword: {'name': 'Alice', 'status': 'active'}
Practical Example: Email Alert System
import smtplib
from email.mime.text import MIMEText
from email.utils import formataddr
def send_alert_email(subject_line):
message = MIMEText('System Alert Notification', 'plain', 'utf-8')
message['From'] = formataddr(['Alert System', 'alerts@example.com'])
message['To'] = formataddr(['Admin', 'admin@example.com'])
message['Subject'] = subject_line
server = smtplib.SMTP('smtp.example.com', 25)
server.login('alerts@example.com', 'password')
server.sendmail('alerts@example.com', ['admin@example.com'], message.as_string())
server.quit()
if __name__ == '__main__':
cpu_threshold = 95
disk_threshold = 85
memory_threshold = 90
if cpu_threshold > 90:
send_alert_email('CPU Critical Alert')
if disk_threshold > 80:
send_alert_email('Disk Space Warning')
if memory_threshold > 85:
send_alert_email('Memory Usage Alert')
Built-in Functions
Python provides numerous built-in functions for common operations:
# Check data type
data = [1, 2, 3, 4]
print(type(data)) # <class>
# List available methods
print(dir(list))
# Get detailed help
help(list)
</class>
Variable Scope
Variables defined inside a function are local to that function and cannot be accessed from outside:
def show_name():
user_name = "Alice"
print(user_name)
show_name() # Output: Alice
print(user_name) # NameError: name not defined
However, functions can access global variables:
global_name = "Bob"
def display():
local_name = "Alice"
print(local_name)
print(global_name)
display()
# Output: Alice
# Output: Bob
Modifying a global variable inside a function creates a new local variable by default:
counter = 10
def increment():
counter = 20
print(f"Inside: {counter}")
increment() # Inside: 20
print(f"Outside: {counter}") # Outside: 10
To modify a global variable within a function, use the global keyword:
counter = 10
def increment():
global counter
counter = 20
print(f"Inside: {counter}")
increment() # Inside: 20
print(f"Outside: {counter}") # Outside: 20
Using global is generally discouraged as it can lead to confusing code and unexpected side effects.
The Return Statement
The return statement immediately exits the function and optionally returns a value:
def find_first_match(target, data_list):
for index, value in enumerate(data_list):
if value == target:
return index
print(f"Checking index {index}: {value}")
return -1
result = find_first_match(5, [1, 2, 3, 4, 5, 6, 7])
print(f"Found at index: {result}")
def process_user():
user_name = "Alice"
for i in range(5):
if i == 3:
print("Processing complete")
else:
print(i)
return user_name
returned_name = process_user()
if returned_name == "Alice":
print("User processed successfully")
File Operations
Opening Files
File operations typically involve three steps: opening, reading/writing, and closing.
file_handle = open('filepath.txt', 'mode')
Common file modes:
r- Read mode (default)w- Write mode (overwrites existing content)a- Append mode (adds to end of file)r+- Read and write modeb- Binary mode (for non-text files)
File Object Methods
# Read entire file
content = file_handle.read()
# Read one line
line = file_handle.readline()
# Read all lines into a list
lines = file_handle.readlines()
# Write to file
file_handle.write('content')
# Get current position
position = file_handle.tell()
# Move to specific position
file_handle.seek(0)
# Close the file
file_handle.close()
Using Context Managers
The with statement ensures proper file handling by automatically closing files:
with open('data.txt', 'r') as file:
content = file.read()
# File automatically closes when block exits
Managing multiple files simultaneously:
with open('source.txt', 'r') as source, open('destination.txt', 'w') as dest:
for line in source:
cleaned_line = line.strip()
dest.write(cleaned_line + '\n')
This pattern is particularly useful for configuration file processing or data transformation tasks.