40. Scrivere codice pulito e leggibile
Nel corso di questo libro, hai imparato la sintassi di Python, le strutture dati, il flusso di controllo, le funzioni e le classi, oltre a molti altri concetti di programmazione. Ora puoi scrivere programmi che funzionano. Ma c’è una differenza cruciale tra codice che funziona e codice che è manutenibile—codice che tu e gli altri potete comprendere, modificare e fare debugging mesi o anni dopo.
Questo capitolo si concentra sulla scrittura di codice pulito e leggibile. Imparerai le convenzioni e le pratiche che rendono il codice Python professionale e manutenibile. Non sono solo regole arbitrarie: sono linee guida collaudate sul campo che rendono più semplice collaborare, riducono i bug e ti aiutano a capire il tuo stesso codice quando ci ritorni più avanti.
40.1) Perché lo stile conta: leggere vs. scrivere codice
40.1.1) Il codice viene letto più spesso di quanto venga scritto
Quando scrivi codice, spendi minuti o ore per crearlo. Ma quel codice verrà letto molte volte: quando fai debugging, quando aggiungi funzionalità, quando altri sviluppatori ci lavorano e quando ci ritorni mesi dopo cercando di ricordare cosa fa.
Considera questo codice funzionante ma con uno stile scadente:
# ATTENZIONE: stile scadente - solo a scopo dimostrativo
def c(l):
t=0
for i in l:
t=t+i
return t/len(l)
data=[85,92,78,90,88]
result=c(data)
print(result) # Output: 86.6Questo codice funziona perfettamente. Calcola la media di una lista(list) di numeri. Ma capire cosa fa richiede un’analisi accurata. Ora confrontalo con questa versione:
def calculate_average(numbers):
"""Calculate the arithmetic mean of a list of numbers."""
total = 0
for number in numbers:
total = total + number
return total / len(numbers)
test_scores = [85, 92, 78, 90, 88]
average_score = calculate_average(test_scores)
print(average_score) # Output: 86.6Cosa rende migliore la seconda versione?
- Il nome della funzione (
calculate_average) dichiara chiaramente lo scopo - I nomi delle variabili (
numbers,total,test_scores) sono descrittivi - La docstring spiega cosa fa la funzione
- La spaziatura corretta rende la struttura chiara
- Chiunque può capire questo codice senza studiarlo
Entrambe le versioni producono risultati identici, ma la seconda versione è immediatamente comprensibile.
L’intuizione chiave: scrivi codice una volta, ma lo leggi decine o centinaia di volte. Investire pochi secondi in più in naming e formattazione chiari ti fa risparmiare ore di confusione più avanti.
40.1.2) La leggibilità riduce i bug
Un codice chiaro è più facile da sottoporre a debugging perché puoi capire rapidamente cosa fa ciascuna parte. Quando i nomi delle variabili sono descrittivi e la struttura è pulita, puoi individuare più facilmente gli errori logici.
# Difficile da sottoporre a debugging - cosa rappresentano queste variabili?
# ATTENZIONE: stile scadente - solo a scopo dimostrativo
def process(x, y):
if x > y:
return x * (1 - y)
return x
result = process(100, 0.1)# Facile da sottoporre a debugging - è chiaro cosa sta succedendo
def apply_discount(price, discount_rate):
"""Calculate price after applying discount rate (0.0 to 1.0)."""
discount_amount = price * discount_rate
final_price = price - discount_amount
return final_price
original_price = 100
discount = 0.1 # sconto del 10%
final_price = apply_discount(original_price, discount)
print(f"Final price: ${final_price}")
# Output: Final price: $90.0Nella seconda versione, puoi vedere immediatamente la logica: "Calcoliamo l'importo dello sconto e poi lo sottraiamo dal prezzo." Nella prima versione, devi tenere mentalmente traccia di cosa rappresentano x e y e capire cosa significhi x * (1 - y).
40.1.3) La coerenza favorisce la collaborazione
Quando tutti in un team seguono le stesse convenzioni di stile, il codice diventa prevedibile. Non sprechi energia mentale a decifrare stili di formattazione diversi: puoi concentrarti sulla comprensione della logica.
Python ha una guida di stile ufficiale chiamata PEP 8 (Python Enhancement Proposal 8). PEP 8 definisce convenzioni per:
- Come nominare variabili, funzioni e classi
- Come formattare il codice (spaziatura, lunghezza delle righe, indentazione)
- Quando usare commenti e docstring
- Come organizzare gli import
Seguire PEP 8 significa che il tuo codice apparirà familiare agli altri programmatori Python, rendendo la collaborazione più fluida. Copriremo le linee guida essenziali di PEP 8 nelle prossime sezioni.
40.2) Convenzioni di naming: variabili, funzioni e classi (PEP 8)
40.2.1) Principi generali di naming
I buoni nomi sono descrittivi e non ambigui. Dovrebbero dirti cosa rappresenta o fa qualcosa senza richiedere di leggere l’implementazione.
Principi chiave:
- Usa parole complete, non abbreviazioni (tranne quelle molto comuni come
id,url,html) - Sii specifico:
user_countè meglio dicount,calculate_total_priceè meglio dicalculate - Evita nomi a lettera singola tranne che per cicli molto brevi o formule matematiche
- Non includere informazioni sul tipo nei nomi (Python è dinamicamente tipizzato)
# Nomi scarsi - non è chiaro cosa rappresentino
# ATTENZIONE: stile scadente - solo a scopo dimostrativo
# Cos'è 'n'? Un numero? Un nome? Un nodo?
# Cos'è 'd'? Una data? Una distanza? Una durata?
# Cos'è 'l'? Sembra il numero 1!
n = "Alice"
d = 25
l = [1, 2, 3]
calc = lambda x: x * 2
# Buoni nomi - chiari e descrittivi
student_name = "Alice"
age_in_years = 25
test_scores = [1, 2, 3]
double_value = lambda x: x * 2Eccezione: variabili brevi nei cicli
# Accettabile: molto corto, contesto chiaro
for i in range(10):
print(i)
for x, y in coordinates:
distance = (x**2 + y**2) ** 0.5
# Ma preferisci nomi descrittivi per chiarezza
for student_index in range(len(students)):
print(students[student_index])
for point_x, point_y in coordinates:
distance = (point_x**2 + point_y**2) ** 0.540.2.2) Nomi di variabili e funzioni: snake_case
In Python, variabili e funzioni usano snake_case: tutto minuscolo con parole separate da underscore.
# Variabili
user_name = "Bob"
total_price = 99.99
is_valid = True
max_retry_count = 3
# Funzioni
def calculate_tax(amount, rate):
"""Calculate tax on a given amount."""
return amount * rate
def send_email_notification(recipient, message):
"""Send an email to the specified recipient."""
print(f"Sending to {recipient}: {message}")
# Uso delle funzioni
tax_amount = calculate_tax(100, 0.08)
send_email_notification("user@example.com", "Welcome!")Perché snake_case? È altamente leggibile. Gli underscore creano confini chiari tra le parole, rendendo i nomi facili da scansionare. Confronta calculatetotalprice (difficile da leggere) con calculate_total_price (immediatamente chiaro).
40.2.3) Nomi delle costanti: UPPER_SNAKE_CASE
Le costanti—valori che non dovrebbero cambiare durante l’esecuzione del programma—usano UPPER_SNAKE_CASE: tutto maiuscolo con underscore.
# Costanti a livello di modulo
MAX_LOGIN_ATTEMPTS = 3
DEFAULT_TIMEOUT_SECONDS = 30
PI = 3.14159
DATABASE_URL = "postgresql://localhost/mydb"
def validate_password_length(password):
"""Check if password meets minimum length requirement."""
MIN_PASSWORD_LENGTH = 8 # Costante all'interno della funzione
return len(password) >= MIN_PASSWORD_LENGTH
# Uso delle costanti
if login_attempts > MAX_LOGIN_ATTEMPTS:
print("Account locked")Importante: Python non ha una sintassi built-in per le costanti. A differenza di alcuni linguaggi (come const in JavaScript o final in Java), Python non ha un modo per dichiarare che una variabile non può essere cambiata.
Invece, i programmatori Python usano una convenzione di naming per segnalare l’intento:
UPPER_SNAKE_CASEsignifica: "intendo che questa sia una costante—non modificarla"- È uno strumento di comunicazione tra programmatori, non una feature del linguaggio
# Python non ha una sintassi per le costanti - questa è solo una variabile normale
MAX_LOGIN_ATTEMPTS = 3
# Python non ti impedirà di modificarla
MAX_LOGIN_ATTEMPTS = 5 # ❌ Tecnicamente funziona, ma viola la convenzione
# La convenzione di naming è un segnale sull'INTENTO:
# "L'ho chiamata in MAIUSCOLO per mostrare che non voglio che cambi"Best practice: se un valore deve davvero cambiare durante l’esecuzione del programma, non chiamarlo come una costante:
# Questo valore cambierà - usa il minuscolo
max_login_attempts = 3
max_login_attempts = 5 # ✅ OK - il nome indica che può cambiare
# Questo valore non dovrebbe mai cambiare - usa il MAIUSCOLO
MAX_LOGIN_ATTEMPTS = 3
# Non riassegnarlo più avanti nel codiceLa convenzione aiuta i programmatori a capire il tuo intento ed evitare bug. Quando vedi MAX_LOGIN_ATTEMPTS, sai di non doverlo modificare.
40.2.4) Nomi delle classi: PascalCase
I nomi delle classi usano PascalCase (chiamato anche CapWords): ogni parola inizia con una lettera maiuscola, senza underscore.
# Definizioni di classi
class Student:
"""Represent a student with name and grades."""
def __init__(self, name):
self.name = name
self.grades = []
class ShoppingCart:
"""Manage items in a shopping cart."""
def __init__(self):
self.items = []
def add_item(self, item):
"""Add an item to the cart."""
self.items.append(item)
class DatabaseConnection:
"""Handle database connection and queries."""
def __init__(self, url):
self.url = url
# Creazione di istanze (nota: le istanze usano nomi variabile in snake_case)
student = Student("Alice")
shopping_cart = ShoppingCart()
db_connection = DatabaseConnection("localhost")Perché PascalCase per le classi? Distingue visivamente le classi da funzioni e variabili. Quando vedi Student(), sai subito che sta creando un’istanza di una classe. Quando vedi calculate_average(), sai che sta chiamando una funzione.
40.2.5) Nomi privati e interni: underscore iniziale
I nomi che iniziano con un singolo underscore (_name) indicano uso interno—sono pensati per l’uso all’interno del modulo o della classe, non da codice esterno.
Python non ha una sintassi per marcare metodi o attributi come "privati" (a differenza di private in Java o C++). Invece, Python usa una convenzione di naming con un underscore iniziale (_name) per comunicare l’intento.
Cosa significa _name:
- "Questo è solo per uso interno"
- "L’ho creato per l’uso all’interno di questa classe/modulo, non per codice esterno"
- "Questo potrebbe cambiare in qualsiasi momento in versioni future—non farci affidamento"
class BankAccount:
"""Represent a bank account with balance tracking."""
def __init__(self, account_number, initial_balance):
self.account_number = account_number
self._balance = initial_balance # Attributo interno
def deposit(self, amount):
"""Add money to the account."""
if self._validate_amount(amount): # Metodo interno
self._balance += amount
def _validate_amount(self, amount):
"""Internal helper to validate transaction amounts."""
return amount > 0
def get_balance(self):
"""Return the current balance."""
return self._balance
# Uso della classe
account = BankAccount("12345", 1000)
account.deposit(500)
print(account.get_balance()) # Output: 1500
# Tecnicamente funziona, ma viola la convenzione
print(account._balance)
# Output: 1500 (works, but you shouldn't do this!)
# Tecnicamente funziona, ma viola la convenzione
result = account._validate_amount(100)
# Output: True (works, but you shouldn't do this!)Punto chiave: Python non può impedirti di accedere a _balance o chiamare _validate_amount(). L’underscore è un segnale tra programmatori, non una feature di sicurezza.
Perché esiste questa convenzione
Dato che Python non può imporre la privacy, l’underscore è il modo in cui gli autori di classi comunicano il loro intento:
Cosa segnala l’underscore:
- "Questa è un’implementazione interna—potrebbe cambiare in versioni future"
- "Usa invece i metodi pubblici—sono garantiti come stabili"
- "Se dipendi dai dettagli interni, il tuo codice potrebbe rompersi quando aggiorno la libreria"
La convenzione crea un contratto: gli autori delle classi possono cambiare liberamente l’implementazione interna (qualunque cosa con _), ma devono mantenere stabile l’interfaccia pubblica. Questo permette alle librerie di evolvere senza rompere il codice degli utenti.
40.2.6) Nomi speciali: doppi underscore
I nomi con doppio underscore iniziale e finale (__name__) sono metodi speciali o magic methods definiti da Python. Non creare i tuoi nomi con questo schema: è riservato all’uso di Python.
class Point:
"""Represent a 2D point."""
def __init__(self, x, y): # Metodo speciale: inizializzazione
self.x = x
self.y = y
def __str__(self): # Metodo speciale: rappresentazione come stringa
return f"Point({self.x}, {self.y})"
def __add__(self, other): # Metodo speciale: operatore di addizione
return Point(self.x + other.x, self.y + other.y)
p1 = Point(1, 2)
p2 = Point(3, 4)
print(p1) # Output: Point(1, 2)
print(p1 + p2) # Output: Point(4, 6)Come abbiamo imparato nel Capitolo 31, questi metodi speciali abilitano l’overloading degli operatori e l’integrazione con le funzioni built-in di Python.
40.2.7) Tabella riepilogativa del naming
| Tipo | Convenzione | Esempio |
|---|---|---|
| Variabili | snake_case | user_name, total_count |
| Funzioni | snake_case | calculate_tax(), send_email() |
| Costanti | UPPER_SNAKE_CASE | MAX_SIZE, DEFAULT_TIMEOUT |
| Classi | PascalCase | Student, ShoppingCart |
| Interno/Privato | _leading_underscore | _balance, _validate() |
| Speciale/Magic | double_underscore | __init__, __str__ |
40.3) Layout del codice: indentazione, spaziatura e righe vuote
40.3.1) Indentazione: quattro spazi
Python usa l’indentazione per definire i blocchi di codice. Usa sempre 4 spazi per livello di indentazione—mai tab, e non mescolare mai tab e spazi.
def calculate_grade(score):
"""Determine letter grade from numeric score."""
if score >= 90:
return "A"
elif score >= 80:
return "B"
elif score >= 70:
return "C"
else:
return "F"
# Indentazione annidata: 4 spazi per livello
def process_students(students):
"""Process a list of student records."""
for student in students:
if student["active"]:
grade = calculate_grade(student["score"])
print(f"{student['name']}: {grade}")
students = [
{"name": "Alice", "score": 92, "active": True},
{"name": "Bob", "score": 78, "active": True}
]
process_students(students)
# Output:
# Alice: A
# Bob: CPerché 4 spazi? È lo standard della community Python. La maggior parte del codice Python che troverai usa 4 spazi, quindi seguire questa convenzione rende il tuo codice coerente con l’ecosistema.
Configurare il tuo editor: gli editor di codice moderni possono essere impostati per inserire 4 spazi quando premi Tab. Questo ti dà la comodità del tasto Tab mantenendo lo standard dei 4 spazi.
40.3.2) Lunghezza massima delle righe: 79 caratteri
PEP 8 raccomanda di limitare le righe a 79 caratteri (con fino a 99 caratteri per docstring e commenti). Può sembrare restrittivo, ma ha benefici pratici:
- Il codice resta leggibile su schermi più piccoli
- Puoi visualizzare due file affiancati
- Incoraggia a spezzare espressioni complesse in parti più semplici
Nota: molti progetti moderni usano limiti leggermente più lunghi (88, 100 o 120 caratteri). La chiave è la coerenza all’interno del tuo progetto. Scegli un limite e rispettalo.
# Troppo lungo - difficile da leggere
# ATTENZIONE: stile scadente - solo a scopo dimostrativo
def calculate_monthly_payment(principal, annual_rate, years):
return principal * (annual_rate / 12) * (1 + annual_rate / 12) ** (years * 12) / ((1 + annual_rate / 12) ** (years * 12) - 1)
# Meglio - spezzato in righe leggibili
def calculate_monthly_payment(principal, annual_rate, years):
"""Calculate monthly loan payment using amortization formula."""
monthly_rate = annual_rate / 12
num_payments = years * 12
numerator = principal * monthly_rate * (1 + monthly_rate) ** num_payments
denominator = (1 + monthly_rate) ** num_payments - 1
return numerator / denominator
payment = calculate_monthly_payment(200000, 0.045, 30)
print(f"Monthly payment: ${payment:.2f}") # Output: Monthly payment: $1013.37Spezzare righe lunghe: quando devi spezzare una riga, usa la continuazione implicita all’interno di parentesi tonde, quadre o graffe:
# Chiamata di funzione lunga
result = some_function(
first_argument,
second_argument,
third_argument,
fourth_argument
)
# Lista lunga
colors = [
"red", "green", "blue",
"yellow", "orange", "purple",
"pink", "brown", "gray"
]
# Stringa lunga
message = (
"This is a very long message that needs to be broken "
"across multiple lines for readability. Python automatically "
"concatenates adjacent string literals."
)
print(message)
# Output: This is a very long message that needs to be broken across multiple lines for readability. Python automatically concatenates adjacent string literals.40.3.3) Spaziatura attorno agli operatori e dopo le virgole
Usa spazi attorno agli operatori e dopo le virgole per migliorare la leggibilità:
# Spaziatura scarsa - tutto stretto e difficile da leggere
# ATTENZIONE: stile scadente - solo a scopo dimostrativo
x=5
y=x*2+3
result=calculate_tax(100,0.08)
data=[1,2,3,4,5]
# Buona spaziatura - chiaro e leggibile
x = 5
y = x * 2 + 3
result = calculate_tax(100, 0.08)
data = [1, 2, 3, 4, 5]
# Spaziatura nelle espressioni
total = (price * quantity) + shipping_cost
is_valid = (age >= 18) and (has_license == True)
# Spaziatura nelle definizioni di funzione
def calculate_discount(price, discount_rate, minimum_purchase=0):
"""Calculate discounted price if minimum purchase is met."""
if price >= minimum_purchase:
return price * (1 - discount_rate)
return priceEccezione: non usare spazi attorno a = negli argomenti keyword o nei valori di default dei parametri:
# Spaziatura corretta per argomenti keyword
result = calculate_discount(price=100, discount_rate=0.1, minimum_purchase=50)
# Spaziatura corretta per parametri di default
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"40.3.4) Righe vuote per separazione logica
Usa righe vuote per separare sezioni logiche del codice:
Due righe vuote tra funzioni e classi di primo livello:
def first_function():
"""First function."""
pass
def second_function():
"""Second function."""
pass
class MyClass:
"""A class definition."""
passUna riga vuota tra i metodi all’interno di una classe:
class Student:
"""Represent a student with grades."""
def __init__(self, name):
self.name = name
self.grades = []
def add_grade(self, grade):
"""Add a grade to the student's record."""
self.grades.append(grade)
def get_average(self):
"""Calculate the student's grade average."""
if not self.grades:
return 0
return sum(self.grades) / len(self.grades)Righe vuote dentro le funzioni per separare passaggi logici:
def process_order(order_items, customer):
"""Process a customer order and calculate total."""
# Calcola il subtotale
subtotal = 0
for item in order_items:
subtotal += item["price"] * item["quantity"]
# Applica lo sconto cliente
discount = 0
if customer["is_premium"]:
discount = subtotal * 0.1
# Calcola la tassa
tax = (subtotal - discount) * 0.08
# Calcola il totale finale
total = subtotal - discount + tax
return {
"subtotal": subtotal,
"discount": discount,
"tax": tax,
"total": total
}Queste righe vuote agiscono come "paragrafi" visivi, rendendo immediatamente evidente la struttura del codice.
40.3.5) Evitare gli spazi finali
Non lasciare spazi alla fine delle righe: sono invisibili ma possono causare problemi con i sistemi di controllo versione e alcuni editor.
# Cattivo - spazi finali invisibili (mostrati come · solo per illustrazione)
# ATTENZIONE: stile scadente - solo a scopo dimostrativo
def calculate(x):···
return x * 2···
# Buono - nessuno spazio finale
def calculate(x):
return x * 2La maggior parte degli editor moderni può essere configurata per rimuovere automaticamente gli spazi finali quando salvi un file.
40.4) Documentazione: scrivere commenti e docstring utili
40.4.1) Quando scrivere commenti
I commenti spiegano perché il codice fa qualcosa, non cosa fa. Variabili e funzioni ben nominate dovrebbero rendere ovvio il "cosa".
# Commento scarso - dice l'ovvio
# ATTENZIONE: stile scadente - solo a scopo dimostrativo
x = x + 1 # Aggiungi 1 a x
# Commento buono - spiega il perché
x = x + 1 # Adatta per indicizzazione basata su zero
# Commento scarso - ridondante rispetto al codice
# ATTENZIONE: stile scadente - solo a scopo dimostrativo
# Controlla se age è maggiore o uguale a 18
if age >= 18:
print("Adult")
# Commento buono - spiega la logica di business
# Età legale per bere negli Stati Uniti
if age >= 21:
print("Can purchase alcohol")Quando i commenti sono preziosi:
- Spiegare algoritmi complessi:
def binary_search(sorted_list, target):
"""Search for target in sorted list using binary search."""
left = 0
right = len(sorted_list) - 1
while left <= right:
# Calcola il punto medio, evitando overflow di interi
# (right + left) // 2 potrebbe andare in overflow con indici molto grandi
mid = left + (right - left) // 2
if sorted_list[mid] == target:
return mid
elif sorted_list[mid] < target:
left = mid + 1 # Il target è nella metà destra
else:
right = mid - 1 # Il target è nella metà sinistra
return -1 # Target non trovato- Chiarire regole di business non ovvie:
def calculate_shipping_cost(weight, distance):
"""Calculate shipping cost based on weight and distance."""
base_cost = 5.00
# Promozione spedizione gratuita per articoli pesanti (policy aziendale al 2024)
# Questo incoraggia ordini in grandi quantità e riduce i costi di spedizione per unità
if weight > 50:
return 0
# Tariffa standard: $0.50 per libbra più $0.10 per miglio
# Basata sul contratto con il corriere negoziato nel Q1 2024
return base_cost + (weight * 0.50) + (distance * 0.10)- Documentare workaround o soluzioni temporanee:
def process_data(data):
"""Process incoming data records."""
# TODO: Questa è una correzione temporanea per record malformati
# Rimuovere una volta implementata la validazione dei dati a monte
if not isinstance(data, list):
data = [data]
for record in data:
# Elabora ogni record
pass40.4.2) Scrivere docstring efficaci
Le docstring sono commenti speciali che documentano moduli, classi e funzioni. Sono racchiuse tra triple virgolette e compaiono come prima istruzione nella definizione.
def calculate_bmi(weight_kg, height_m):
"""
Calculate Body Mass Index (BMI).
BMI is calculated as weight in kilograms divided by the square of height in meters.
Args:
weight_kg: Weight in kilograms (float or int)
height_m: Height in meters (float or int)
Returns:
float: The calculated BMI value
Example:
>>> calculate_bmi(70, 1.75)
22.857142857142858
"""
return weight_kg / (height_m ** 2)
# Accedere alle docstring
print(calculate_bmi.__doc__)
# Output:
# Calculate Body Mass Index (BMI).
#
# BMI is calculated as weight in kilograms divided by the square of height in meters.
# ...Docstring su una sola riga per funzioni semplici:
def square(x):
"""Return the square of x."""
return x * x
def is_even(n):
"""Return True if n is even, False otherwise."""
return n % 2 == 0Docstring multi-linea per funzioni complesse:
def find_prime_factors(n):
"""
Find all prime factors of a positive integer.
This function returns a list of prime numbers that, when multiplied
together, equal the input number. The factors are returned in ascending order.
Args:
n: A positive integer greater than 1
Returns:
list: Prime factors in ascending order
Raises:
ValueError: If n is less than 2
Example:
>>> find_prime_factors(12)
[2, 2, 3]
>>> find_prime_factors(17)
[17]
"""
if n < 2:
raise ValueError("n must be at least 2")
factors = []
divisor = 2
while n > 1:
while n % divisor == 0:
factors.append(divisor)
n = n // divisor
divisor += 1
return factorsDocstring delle classi:
class BankAccount:
"""
Represent a bank account with deposit and withdrawal operations.
This class maintains an account balance and provides methods for
depositing and withdrawing money. All transactions are validated to prevent negative balances.
Attributes:
account_number: Unique identifier for the account
balance: Current account balance in dollars
"""
def __init__(self, account_number, initial_balance=0):
"""
Initialize a new bank account.
Args:
account_number: Unique account identifier (string)
initial_balance: Starting balance (default: 0)
"""
self.account_number = account_number
self.balance = initial_balance
def deposit(self, amount):
"""
Add money to the account.
Args:
amount: Amount to deposit (must be positive)
Raises:
ValueError: If amount is not positive
"""
if amount <= 0:
raise ValueError("Deposit amount must be positive")
self.balance += amount40.4.3) Convenzioni per le docstring
Prima riga: breve riepilogo di cosa fa la funzione/classe. Dovrebbe stare su una riga.
Riga vuota: separa il riepilogo dalla descrizione dettagliata.
Descrizione dettagliata: spiega cosa fa la funzione, eventuali dettagli importanti e come usarla.
Args/Parameters: elenca ogni parametro con il suo tipo e scopo.
Returns: descrive cosa restituisce la funzione e il suo tipo.
Raises: documenta le eccezioni che la funzione potrebbe sollevare.
Example: mostra un uso tipico (opzionale ma utile).
def calculate_compound_interest(principal, rate, time, compounds_per_year=1):
"""
Calculate compound interest on an investment.
Uses the compound interest formula: A = P(1 + r/n)^(nt)
where A is the final amount, P is principal, r is annual rate,
n is compounds per year, and t is time in years.
Args:
principal: Initial investment amount (float)
rate: Annual interest rate as decimal (e.g., 0.05 for 5%)
time: Investment period in years (float)
compounds_per_year: Number of times interest compounds annually
(default: 1 for annual compounding)
Returns:
float: Final amount after compound interest
Example:
>>> calculate_compound_interest(1000, 0.05, 10, 12)
1647.0095
"""
return principal * (1 + rate / compounds_per_year) ** (compounds_per_year * time)40.4.4) Commenti TODO per lavori futuri
Usa commenti TODO per segnare aree che richiedono attenzione in futuro:
def process_payment(amount, payment_method):
"""Process a payment transaction."""
# TODO: Aggiungere supporto per pagamenti in criptovaluta
# TODO: Implementare controlli di rilevamento frodi
if payment_method == "credit_card":
return process_credit_card(amount)
elif payment_method == "paypal":
return process_paypal(amount)
else:
raise ValueError(f"Unsupported payment method: {payment_method}")Molti editor possono cercare commenti TODO, rendendo facile trovare aree che necessitano di lavoro.
40.5) Organizzare il codice: import, costanti, funzioni e main
40.5.1) Struttura standard di un modulo
Un modulo Python ben organizzato segue questa struttura:
- Docstring del modulo: descrive cosa fa il modulo
- Import: libreria standard, terze parti, poi import locali
- Costanti: costanti a livello di modulo
- Funzioni e classi: codice principale
- Blocco di esecuzione main: codice che viene eseguito quando lo script viene avviato
"""
student_manager.py
Gestisce i record degli studenti, inclusi voti e calcoli del GPA.
Questo modulo fornisce funzioni per aggiungere studenti, registrare i voti
e calcolare le medie dei voti (grade point averages).
"""
# Import della libreria standard
import sys
from datetime import datetime
# Import di terze parti (se presenti)
# import requests
# Import locali (se presenti)
# from .database import save_student
# Costanti
MAX_GRADE = 100
MIN_GRADE = 0
PASSING_GRADE = 60
# Funzioni
def calculate_gpa(grades):
"""
Calculate GPA from a list of numeric grades.
Args:
grades: List of numeric grades (0-100)
Returns:
float: GPA on 4.0 scale
"""
if not grades:
return 0.0
average = sum(grades) / len(grades)
# Conversione alla scala 4.0
if average >= 90:
return 4.0
elif average >= 80:
return 3.0
elif average >= 70:
return 2.0
elif average >= 60:
return 1.0
else:
return 0.0
def validate_grade(grade):
"""
Check if a grade is within valid range.
Args:
grade: Numeric grade to validate
Returns:
bool: True if grade is valid, False otherwise
"""
return MIN_GRADE <= grade <= MAX_GRADE
# Esecuzione main
if __name__ == "__main__":
# Codice che viene eseguito quando lo script viene avviato direttamente
test_grades = [85, 92, 78, 88]
gpa = calculate_gpa(test_grades)
print(f"GPA: {gpa}") # Output: GPA: 3.040.5.2) Organizzazione degli import
Raggruppa gli import in tre sezioni, separate da righe vuote:
- Import della libreria standard: moduli Python built-in
- Import di terze parti: pacchetti installati (come
requests,numpy) - Import locali: i tuoi moduli
# Import della libreria standard
import os
import sys
from datetime import datetime, timedelta
from pathlib import Path
# Import di terze parti
import requests
from flask import Flask, render_template
# Import locali dell'applicazione
from myapp.database import connect_db
from myapp.models import User, Product
from myapp.utils import format_currencyStili di import:
# Importa l'intero modulo
import math
result = math.sqrt(16) # Output: 4.0
# Importa elementi specifici
from math import sqrt, pi
result = sqrt(16) # Output: 4.0
# Import con alias
import numpy as np
array = np.array([1, 2, 3])
# Importa elementi multipli
from os import path, getcwd, listdirEvita gli import wildcard (from module import *)—rendono poco chiaro da dove provengano i nomi:
# Scarso - non è chiaro da dove provenga sqrt
# ATTENZIONE: stile scadente - solo a scopo dimostrativo
from math import *
result = sqrt(16)
# Buono - import esplicito
from math import sqrt
result = sqrt(16)40.5.3) Organizzare le costanti
Posiziona le costanti a livello di modulo vicino all’inizio, dopo gli import:
"""Configuration settings for the application."""
import os
# Costanti dell'applicazione
APP_NAME = "Student Manager"
VERSION = "1.0.0"
DEBUG_MODE = True
# Configurazione database
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///students.db")
MAX_CONNECTIONS = 10
# Regole di business
MAX_STUDENTS_PER_CLASS = 30
PASSING_GRADE = 60
GRADE_WEIGHTS = {
"homework": 0.3,
"midterm": 0.3,
"final": 0.4
}
def calculate_final_grade(homework, midterm, final):
"""Calculate weighted final grade."""
return (
homework * GRADE_WEIGHTS["homework"] +
midterm * GRADE_WEIGHTS["midterm"] +
final * GRADE_WEIGHTS["final"]
)40.5.4) Ordinamento logico delle funzioni
Organizza le funzioni in un ordine logico:
- Funzioni pubbliche per prime: funzioni pensate per essere usate da altri moduli
- Funzioni di supporto dopo: funzioni interne che supportano quelle pubbliche
- Funzioni correlate insieme: raggruppa funzioni che lavorano insieme
"""Order processing module."""
# Funzioni dell'API pubblica
def process_order(order_items, customer):
"""
Process a customer order.
This is the main entry point for order processing.
"""
subtotal = _calculate_subtotal(order_items)
discount = _calculate_discount(subtotal, customer)
tax = _calculate_tax(subtotal - discount)
total = subtotal - discount + tax
return {
"subtotal": subtotal,
"discount": discount,
"tax": tax,
"total": total
}
def validate_order(order_items):
"""Validate that an order contains valid items."""
if not order_items:
return False
for item in order_items:
if not _validate_item(item):
return False
return True
# Funzioni helper interne
def _calculate_subtotal(items):
"""Calculate order subtotal (internal use)."""
total = 0
for item in items:
total += item["price"] * item["quantity"]
return total
def _calculate_discount(subtotal, customer):
"""Calculate customer discount (internal use)."""
if customer.get("is_premium"):
return subtotal * 0.1
return 0
def _calculate_tax(amount):
"""Calculate sales tax (internal use)."""
TAX_RATE = 0.08
return amount * TAX_RATE
def _validate_item(item):
"""Validate a single order item (internal use)."""
required_fields = ["name", "price", "quantity"]
return all(field in item for field in required_fields)Nota come le funzioni pubbliche (process_order, validate_order) vengono prima, e le funzioni helper (prefissate con _) vengono dopo. Questo rende chiaro quali funzioni sono l’API principale.
40.5.5) Organizzazione delle classi nei moduli
Quando un modulo contiene classi, organizzale in modo logico:
"""User management system."""
# Costanti
DEFAULT_ROLE = "user"
ADMIN_ROLE = "admin"
# Prima le classi base
class User:
"""Base user class."""
def __init__(self, username, email):
self.username = username
self.email = email
self.role = DEFAULT_ROLE
def can_edit(self, resource):
"""Check if user can edit a resource."""
return resource.owner == self.username
# Poi le classi derivate dopo le classi base
class AdminUser(User):
"""Administrator with elevated privileges."""
def __init__(self, username, email):
super().__init__(username, email)
self.role = ADMIN_ROLE
def can_edit(self, resource):
"""Admins can edit any resource."""
return True
# Classi correlate raggruppate insieme
class Resource:
"""Represent a resource that can be owned and edited."""
def __init__(self, name, owner):
self.name = name
self.owner = owner
# Funzioni di utilità correlate alle classi
def create_user(username, email, is_admin=False):
"""Factory function to create appropriate user type."""
if is_admin:
return AdminUser(username, email)
return User(username, email)Principi di organizzazione delle classi:
- Classi base prima delle classi derivate (i lettori devono capire prima la base)
- Classi correlate raggruppate insieme (User e Resource sono correlate)
- Le funzioni di utilità che lavorano con le classi vengono dopo le definizioni delle classi
- Ogni classe dovrebbe avere una docstring chiara che spiega il suo scopo
40.6) Il pattern if name == "main"
40.6.1) Comprendere il pattern
Ogni file Python ha una variabile built-in chiamata __name__. Python imposta automaticamente il valore di questa variabile a seconda di come viene usato il file:
- Quando esegui un file direttamente (ad esempio
python my_script.py), Python imposta__name__a"__main__" - Quando importi un file come modulo, Python imposta
__name__al nome del modulo (il nome del file senza.py)
Questo ti permette di scrivere codice che viene eseguito solo quando il file viene avviato direttamente, non quando viene importato:
"""math_utils.py - Mathematical utility functions."""
def add(a, b):
"""Add two numbers."""
return a + b
def multiply(a, b):
"""Multiply two numbers."""
return a * b
# Questo codice viene eseguito solo quando il file viene avviato direttamente
if __name__ == "__main__":
# Testa le funzioni
print(f"5 + 3 = {add(5, 3)}") # Output: 5 + 3 = 8
print(f"5 * 3 = {multiply(5, 3)}") # Output: 5 * 3 = 15Quando esegui python math_utils.py, vedrai l’output. Ma quando lo importi in un altro file:
# another_file.py
from math_utils import add, multiply
result = add(10, 20)
print(result) # Output: 30
# Il codice di test da math_utils.py NON viene eseguitoNota che il codice di test (dentro if __name__ == "__main__":) NON viene eseguito quando viene importato!
40.6.2) Perché questo pattern è importante
Questo pattern serve a diversi scopi importanti:
1. Test e dimostrazione: puoi includere esempi d’uso nello stesso file delle tue funzioni:
"""temperature.py - Temperature conversion utilities."""
def celsius_to_fahrenheit(celsius):
"""Convert Celsius to Fahrenheit."""
return (celsius * 9/5) + 32
def fahrenheit_to_celsius(fahrenheit):
"""Convert Fahrenheit to Celsius."""
return (fahrenheit - 32) * 5/9
if __name__ == "__main__":
# Dimostra le funzioni
print("Temperature Conversion Examples:")
print(f"0°C = {celsius_to_fahrenheit(0)}°F") # Output: 0°C = 32.0°F
print(f"100°C = {celsius_to_fahrenheit(100)}°F") # Output: 100°C = 212.0°F
print(f"32°F = {fahrenheit_to_celsius(32)}°C") # Output: 32°F = 0.0°C2. Moduli riutilizzabili: lo stesso file può essere sia uno script standalone sia un modulo importabile:
"""data_processor.py - Process and analyze data files."""
import sys
def load_data(filename):
"""Load data from a file."""
with open(filename) as f:
return [line.strip() for line in f]
def analyze_data(data):
"""Perform analysis on data."""
return {
"count": len(data),
"average_length": sum(len(item) for item in data) / len(data)
}
if __name__ == "__main__":
# Quando viene eseguito come script, elabora gli argomenti da riga di comando
if len(sys.argv) < 2:
print("Usage: python data_processor.py <filename>")
sys.exit(1)
filename = sys.argv[1]
data = load_data(filename)
results = analyze_data(data)
print(f"Processed {results['count']} items")
print(f"Average length: {results['average_length']:.2f}")Puoi eseguirlo come script:
$ python data_processor.py data.txt
Processed 42 items
Average length: 15.23Oppure importarlo in un altro file:
# my_analysis.py
from data_processor import load_data, analyze_data
my_data = load_data("myfile.txt")
results = analyze_data(my_data)
print(f"Found {results['count']} items")40.6.3) Pattern comuni per i blocchi main
Pattern 1: casi di test semplici
"""calculator.py - Basic calculator operations."""
def add(a, b):
"""Add two numbers."""
return a + b
def subtract(a, b):
"""Subtract b from a."""
return a - b
if __name__ == "__main__":
# Test rapidi
assert add(2, 3) == 5
assert subtract(10, 4) == 6
print("All tests passed!") # Output: All tests passed!Pattern 2: funzione main
Per script più complessi, definisci una funzione main():
"""report_generator.py - Generate reports from data."""
import sys
def load_data(filename):
"""Load data from file."""
# Implementazione qui
pass
def generate_report(data):
"""Generate report from data."""
# Implementazione qui
pass
def save_report(report, output_file):
"""Save report to file."""
# Implementazione qui
pass
def main():
"""Main entry point for the script."""
if len(sys.argv) < 3:
print("Usage: python report_generator.py <input> <output>")
return 1
input_file = sys.argv[1]
output_file = sys.argv[2]
try:
data = load_data(input_file)
report = generate_report(data)
save_report(report, output_file)
print(f"Report saved to {output_file}")
return 0
except Exception as e:
print(f"Error: {e}")
return 1
if __name__ == "__main__":
# Esci con il codice di stato restituito da main (0 = successo, 1 = errore)
sys.exit(main())Questo pattern ha diversi vantaggi:
- La funzione
main()può essere testata in modo indipendente - Punto di ingresso chiaro per lo script
- Codici di uscita corretti (0 per successo, diversi da zero per errori)
- Separazione pulita tra logica dello script e funzioni del modulo
40.6.4) Best practice per i blocchi main
Mantieni i blocchi main focalizzati: il codice dentro if __name__ == "__main__" dovrebbe gestire principalmente l’esecuzione dello script, non contenere logica complessa:
# Scarso - logica complessa nel blocco main
# ATTENZIONE: stile scadente - solo a scopo dimostrativo
if __name__ == "__main__":
data = []
for i in range(100):
if i % 2 == 0:
data.append(i * 2)
result = sum(data) / len(data)
print(result)
# Buono - logica nelle funzioni, il blocco main coordina
def generate_even_doubles(limit):
"""Generate doubled even numbers up to limit."""
return [i * 2 for i in range(limit) if i % 2 == 0]
def calculate_average(numbers):
"""Calculate average of numbers."""
return sum(numbers) / len(numbers)
if __name__ == "__main__":
data = generate_even_doubles(100)
result = calculate_average(data)
print(result) # Output: 99.0Usa una funzione main() per script complessi: come mostrato in precedenza, definire una funzione main() rende lo script più testabile e organizzato.
Documenta l’uso dello script: se il tuo script accetta argomenti da riga di comando, documentali nella docstring del modulo:
"""
file_processor.py - Process text files with various operations.
Usage:
python file_processor.py <input_file> <output_file> [--uppercase]
Arguments:
input_file: Path to input file
output_file: Path to output file
--uppercase: Convert text to uppercase (optional)
"""
import sys
def process_file(input_path, output_path, uppercase=False):
"""Process file with specified options."""
with open(input_path) as f:
content = f.read()
if uppercase:
content = content.upper()
with open(output_path, 'w') as f:
f.write(content)
if __name__ == "__main__":
if len(sys.argv) < 3:
print(__doc__) # Stampa la docstring del modulo
sys.exit(1)
input_file = sys.argv[1]
output_file = sys.argv[2]
uppercase = "--uppercase" in sys.argv
process_file(input_file, output_file, uppercase)
print(f"Processed {input_file} -> {output_file}")Scrivere codice pulito e leggibile è un’abilità che si sviluppa con la pratica. Le convenzioni e i pattern di questo capitolo non sono regole arbitrarie: sono pratiche dimostrate che rendono il codice più facile da capire, mantenere e sottoporre a debugging. Man mano che scrivi più codice Python, questi pattern diventeranno una seconda natura.
Ricorda: il codice viene letto molto più spesso di quanto venga scritto. I pochi secondi extra che spendi per scegliere un nome chiaro, aggiungere un commento utile o organizzare correttamente gli import ti faranno risparmiare ore di confusione più avanti—per te stesso e per gli altri che lavorano con il tuo codice.
Nel prossimo capitolo, esploreremo tecniche di debugging e test che si basano su queste pratiche di clean code, aiutandoti a scrivere non solo codice leggibile, ma codice corretto e affidabile.