22. Organizzare il codice con moduli e pacchetti
Man mano che i tuoi programmi Python crescono, mantenere tutto il codice in un singolo file diventa poco pratico. Vorrai organizzare funzioni correlate, classi e variabili in file separati che possano essere riutilizzati in programmi diversi. Il sistema di moduli (module) e pacchetti (package) di Python fornisce esattamente questa capacità: un modo per organizzare, condividere e riutilizzare il codice in modo efficace.
In questo capitolo esploreremo come funziona il sistema di importazione di Python, come creare e usare i tuoi moduli personalizzati e come organizzare più moduli in pacchetti. Esamineremo anche la speciale variabile __name__ che ti consente di scrivere file che funzionano sia come moduli importabili sia come script autonomi.
22.1) Cosa sono i moduli e come funziona import
Comprendere i moduli
Un modulo è semplicemente un file Python che contiene definizioni e istruzioni. Qualsiasi file .py che crei è un modulo. Quando scrivi una funzione in un file chiamato calculator.py, quel file diventa un modulo chiamato calculator che altri file Python possono usare.
I moduli svolgono diversi scopi importanti:
- Riutilizzabilità del codice: scrivere una funzione una volta e usarla in più programmi
- Organizzazione: raggruppare insieme funzionalità correlate
- Gestione dello spazio dei nomi (namespace): mantenere i nomi separati per evitare conflitti
- Manutenibilità: file più piccoli e focalizzati sono più facili da capire e modificare
Creiamo un modulo semplice per vedere come funziona. Crea un file chiamato greetings.py:
# greetings.py
def say_hello(name):
"""Return a friendly greeting."""
return f"Hello, {name}!"
def say_goodbye(name):
"""Return a farewell message."""
return f"Goodbye, {name}. See you soon!"
# Una variabile a livello di modulo
default_greeting = "Welcome"Questo file ora è un modulo. Contiene due funzioni e una variabile che altri file Python possono usare.
L'istruzione import
Per usare il codice di un modulo, lo importi(import). L'istruzione import dice a Python di caricare un modulo e rendere disponibili i suoi contenuti. Crea un altro file nella stessa directory chiamato main.py:
# main.py
import greetings
message = greetings.say_hello("Alice")
print(message) # Output: Hello, Alice!
farewell = greetings.say_goodbye("Bob")
print(farewell) # Output: Goodbye, Bob. See you soon!
print(greetings.default_greeting) # Output: WelcomeQuando esegui main.py, Python esegue l'istruzione import greetings. Ecco cosa succede dietro le quinte:
Importante: Python esegue il codice di un modulo solo la prima volta che viene importato in un programma. Le importazioni successive nello stesso programma riutilizzano il modulo già caricato. Questo evita esecuzioni duplicate e fa risparmiare tempo.
Accedere ai contenuti di un modulo
Dopo aver importato un modulo, accedi ai suoi contenuti usando la notazione a punto(dot notation): module_name.item_name. È simile a come accediamo ai metodi delle stringhe come text.upper() o ai metodi delle liste come numbers.append(), come abbiamo imparato nei Capitoli 5 e 14.
import greetings
# Accedere alle funzioni
result = greetings.say_hello("Charlie")
# Accedere alle variabili
greeting = greetings.default_greeting
# Puoi persino controllare cosa c'è in un modulo
print(dir(greetings)) # Elenca tutti i nomi definiti nel moduloLa notazione a punto rende chiaro da dove proviene ciascun nome. Quando vedi greetings.say_hello(), sai subito che questa funzione proviene dal modulo greetings.
Percorso di ricerca dei moduli
Quando scrivi import greetings, come fa Python a trovare greetings.py? Python cerca i moduli in un ordine specifico:
- Directory corrente: la directory che contiene lo script che stai eseguendo
- PYTHONPATH: directory elencate nella variabile d'ambiente
PYTHONPATH(se impostata) - Libreria standard: le directory dei moduli integrati di Python
- Site-packages: pacchetti di terze parti installati con pip
Puoi vedere il percorso di ricerca di Python esaminando sys.path:
import sys
for path in sys.path:
print(path)Output (esempio: i tuoi percorsi effettivi varieranno in base al sistema e all'installazione di Python):
/home/user/projects/myproject
/usr/lib/python3.11
/usr/lib/python3.11/lib-dynload
/usr/local/lib/python3.11/site-packagesIl primo percorso nell’output è la directory di lavoro corrente. Python cerca prima in questa directory, quindi può trovare moduli nella stessa directory.
Nomi dei moduli e nomi dei file
Il nome del modulo è il nome del file senza l’estensione .py. Se il tuo file è string_utils.py, il nome del modulo è string_utils. I nomi dei moduli devono seguire le regole degli identificatori di Python (come abbiamo imparato nel Capitolo 3):
- Iniziare con una lettera o un underscore
- Contenere solo lettere, cifre e underscore
- Non possono essere parole chiave Python
# Nomi di moduli validi (e nomi file)
import data_processor # data_processor.py
import user_auth # user_auth.py
import _internal_helpers # _internal_helpers.py
# Non validi - causerebbero errori
# import 2d_graphics # Can't start with digit
# import my-module # Hyphens not allowed
# import class # 'class' is a keywordErrore comune: oscurare (shadowing) i moduli della libreria standard
Fai attenzione a non dare ai tuoi moduli lo stesso nome dei moduli della libreria standard. Se crei un file chiamato random.py nella directory del tuo progetto, Python importerà il tuo file invece del modulo random della libreria standard, causando errori confusi:
# Il tuo file: random.py
def my_function():
return 42
# Un altro file nel tuo progetto
import random
print(random.randint(1, 6)) # ERROR! Your random.py doesn't have randint()Per evitare questo, controlla se un nome è già usato dalla libreria standard prima di creare un modulo con quel nome. Puoi verificarlo provando a importarlo nella shell interattiva di Python. Se si importa senza errori, quel nome è già occupato.
Cosa succede durante l'importazione
Esaminiamo cosa succede realmente quando importi un modulo. Crea un file chiamato demo_module.py:
# demo_module.py
print("Module is being loaded!")
def greet():
print("Hello from demo_module")
print("Module loading complete!")Ora importalo:
# test_import.py
print("Before import")
import demo_module
print("After import")
demo_module.greet()Output:
Before import
Module is being loaded!
Module loading complete!
After import
Hello from demo_moduleNota che le istruzioni print() in demo_module.py vengono eseguite durante l’importazione. Questo dimostra che importare un modulo esegue tutto il suo codice a livello superiore. Le definizioni di funzione vengono memorizzate per un uso successivo, ma qualsiasi codice al di fuori delle funzioni viene eseguito immediatamente.
Se importi di nuovo lo stesso modulo nello stesso programma, i messaggi di caricamento non appariranno di nuovo:
import demo_module # First import - executes module code
import demo_module # Second import - uses cached module
import demo_module # Third import - still uses cached moduleOutput:
Module is being loaded!
Module loading complete!Il codice del modulo viene eseguito solo una volta, indipendentemente da quante volte lo importi.
22.2) Modi diversi di importare: import, from e as
Python offre diversi modi per importare moduli e i loro contenuti. Ogni approccio ha implicazioni diverse su come accedi ai nomi importati e su come influiscono sul tuo namespace.
Istruzione import di base
L’istruzione import di base che abbiamo già visto carica l’intero modulo:
import math
result = math.sqrt(16)
print(result) # Output: 4.0
pi_value = math.pi
print(pi_value) # Output: 3.141592653589793Con questo approccio, usi sempre il nome del modulo come prefisso. Questo rende il codice molto chiaro: puoi sempre capire da dove proviene un nome.
Importare nomi specifici con from
A volte ti serve solo uno o due elementi di un modulo. L’istruzione from ti consente di importare nomi specifici direttamente nel tuo namespace:
from math import sqrt, pi
result = sqrt(25) # Nessun prefisso 'math.' necessario
print(result) # Output: 5.0
print(pi) # Output: 3.141592653589793Ora puoi usare sqrt e pi direttamente senza il prefisso math.. Questo è comodo quando usi questi nomi frequentemente.
Vediamo un altro esempio con il nostro modulo greetings:
# Usare from import
from greetings import say_hello
message = say_hello("Diana") # Accesso diretto
print(message) # Output: Hello, Diana!
# Tuttavia, say_goodbye non è disponibile perché non l'abbiamo importato
# say_goodbye("Diana") # NameError: name 'say_goodbye' is not definedPuoi importare più nomi in un’unica istruzione:
from greetings import say_hello, say_goodbye, default_greeting
print(say_hello("Eve")) # Output: Hello, Eve!
print(say_goodbye("Frank")) # Output: Goodbye, Frank!
print(default_greeting) # Output: WelcomeL’importazione con wildcard (e perché evitarla)
Python consente di importare tutto da un modulo usando *:
from math import *
print(sqrt(9)) # Output: 3.0
print(cos(0)) # Output: 1.0
print(pi) # Output: 3.141592653589793Questo importa tutti i nomi pubblici dal modulo (nomi che non iniziano con underscore). Anche se sembra comodo, in genere è considerata una cattiva pratica perché:
- Inquinamento del namespace: non sai esattamente quali nomi stai importando
- Conflitti di nomi: i nomi importati potrebbero sovrascrivere le tue variabili
- Leggibilità: chi legge il codice non può capire da dove provengono i nomi
# Esempio problematico
from math import *
# Più avanti nel tuo codice...
def sqrt(x):
"""Your own square root function."""
return x ** 0.5
# Quale sqrt stai usando? La tua o quella di math?
result = sqrt(16) # Confusing!Best practice: importa nomi specifici o usa l’istruzione import di base. Evita from module import * tranne che nelle sessioni interattive in cui stai sperimentando.
Rinominare le importazioni con as
A volte i nomi dei moduli o delle funzioni sono lunghi, oppure vuoi evitare conflitti di nomi. La parola chiave as ti consente di creare un alias:
import math as m
result = m.sqrt(36)
print(result) # Output: 6.0Questo è particolarmente utile per moduli con nomi lunghi o quando si seguono convenzioni comuni:
import datetime as dt
today = dt.date.today()
print(today) # Output: 2025-12-19 (or current date)Puoi anche rinominare importazioni specifiche:
from math import sqrt as square_root
result = square_root(49)
print(result) # Output: 7.0Questo è utile quando hai conflitti di nomi:
from math import sqrt as math_sqrt
def sqrt(x):
"""Custom square root with input validation."""
if x < 0:
return None
return math_sqrt(x)
print(sqrt(25)) # Output: 5.0 (your function)
print(sqrt(-4)) # Output: None (your function)Combinare stili di importazione
Puoi mescolare diversi stili di importazione nello stesso file:
import math
from datetime import date, time
from random import randint as random_int
# Usare math con prefisso
radius = 5
area = math.pi * radius ** 2
# Usare date e time direttamente
today = date.today()
current_time = time(14, 30)
# Usare la funzione rinominata
dice_roll = random_int(1, 6)Scegliere lo stile di importazione giusto
Ecco una guida decisionale:
Usa import module quando:
- Ti servono più elementi del modulo
- Vuoi la massima chiarezza su da dove provengono i nomi
- Il nome del modulo è breve e chiaro
Usa from module import name quando:
- Ti serve solo uno o due elementi specifici
- I nomi sono distintivi e difficilmente andranno in conflitto
- Userai quei nomi frequentemente
Usa import module as alias quando:
- Il nome del modulo è molto lungo
- Segui una convenzione comune (come
import numpy as np) - Devi evitare conflitti con altri moduli
Evita from module import * nel codice di produzione:
- Usalo solo per esperimenti rapidi nella shell interattiva
- Non usarlo mai in moduli che altri importeranno
Vediamo un esempio completo che dimostra buone pratiche di importazione:
# data_processor.py
import math
from statistics import mean, median
from datetime import datetime as dt
def calculate_statistics(numbers):
"""Calculate various statistics for a list of numbers."""
if not numbers:
return None
avg = mean(numbers)
mid = median(numbers)
std_dev = math.sqrt(sum((x - avg) ** 2 for x in numbers) / len(numbers))
return {
'mean': avg,
'median': mid,
'std_dev': std_dev,
'timestamp': dt.now()
}
# Test the function
data = [10, 20, 30, 40, 50]
stats = calculate_statistics(data)
print(f"Mean: {stats['mean']}") # Output: Mean: 30.0
print(f"Median: {stats['median']}") # Output: Median: 30
print(f"Std Dev: {stats['std_dev']:.2f}") # Output: Std Dev: 14.14Questo esempio mostra:
import mathper il modulo completo (potremmo usare altre funzioni matematiche più avanti)from statistics import mean, medianper funzioni specifiche che usiamo frequentementefrom datetime import datetime as dtper un modulo comunemente abbreviato con un alias
22.3) Panoramica dei moduli comuni della libreria standard Python
Python include una ricca libreria standard (standard library), una raccolta di moduli che forniscono soluzioni a compiti di programmazione comuni. Questi moduli sono sempre disponibili; non devi installare nulla di extra. Capire cosa è disponibile nella libreria standard ti aiuta a evitare di “reinventare la ruota”.
Il modulo math
Il modulo math fornisce funzioni matematiche oltre l’aritmetica di base:
import math
# Funzioni trigonometriche
angle_rad = math.radians(45) # Convertire gradi in radianti
print(math.sin(angle_rad)) # Output: 0.7071067811865476
print(math.cos(angle_rad)) # Output: 0.7071067811865475
# Arrotondamento e valore assoluto
print(math.ceil(4.2)) # Output: 5 (arrotonda per eccesso)
print(math.floor(4.8)) # Output: 4 (arrotonda per difetto)
print(math.fabs(-7.5)) # Output: 7.5 (valore assoluto come float)
# Esponenziali e logaritmi
print(math.exp(2)) # Output: 7.38905609893065 (e^2)
print(math.log(100)) # Output: 4.605170185988092 (logaritmo naturale)
print(math.log10(100)) # Output: 2.0 (logaritmo in base 10)
# Costanti
print(math.pi) # Output: 3.141592653589793
print(math.e) # Output: 2.718281828459045Come abbiamo imparato nel Capitolo 4, il modulo math è essenziale per operazioni matematiche avanzate.
Il modulo random
Il modulo random genera numeri pseudo-casuali e fa selezioni casuali:
import random
# Interi casuali
dice = random.randint(1, 6) # Intero casuale da 1 a 6 (inclusi)
print(f"Dice roll: {dice}")
# Float casuali
probability = random.random() # Float casuale da 0.0 a 1.0
print(f"Probability: {probability:.4f}")
# Scelta casuale da una sequenza
colors = ['red', 'blue', 'green', 'yellow']
chosen_color = random.choice(colors)
print(f"Chosen color: {chosen_color}")
# Mescolare una lista in-place
deck = ['A', 'K', 'Q', 'J', '10']
random.shuffle(deck)
print(f"Shuffled deck: {deck}")
# Campione casuale senza reinserimento
lottery_numbers = random.sample(range(1, 50), 6)
print(f"Lottery numbers: {sorted(lottery_numbers)}")Output (example - will vary due to randomness):
Dice roll: 4
Probability: 0.7382
Chosen color: green
Shuffled deck: ['Q', 'A', '10', 'K', 'J']
Lottery numbers: [7, 15, 23, 31, 38, 42]Il modulo datetime
Il modulo datetime gestisce date e orari:
from datetime import date, time, datetime, timedelta
# Data e ora correnti
today = date.today()
now = datetime.now()
print(f"Today: {today}") # Output: Today: 2025-12-19
print(f"Now: {now}") # Output: Now: 2025-12-19 14:30:45.123456
# Creare date e orari specifici
birthday = date(1990, 5, 15)
meeting_time = time(14, 30)
appointment = datetime(2025, 12, 25, 10, 0)
print(f"Birthday: {birthday}") # Output: Birthday: 1990-05-15
print(f"Meeting: {meeting_time}") # Output: Meeting: 14:30:00
print(f"Appointment: {appointment}") # Output: Appointment: 2025-12-25 10:00:00
# Aritmetica sulle date con timedelta
tomorrow = today + timedelta(days=1)
next_week = today + timedelta(weeks=1)
print(f"Tomorrow: {tomorrow}") # Output: Tomorrow: 2025-12-20
print(f"Next week: {next_week}") # Output: Next week: 2025-12-26
# Estrarre componenti
print(f"Year: {today.year}") # Output: Year: 2025
print(f"Month: {today.month}") # Output: Month: 12
print(f"Day: {today.day}") # Output: Day: 19Il modulo os
Il modulo os fornisce funzionalità del sistema operativo. Lo esploreremo in dettaglio nel Capitolo 26, ma ecco un’anteprima:
import os
# Directory di lavoro corrente
current_dir = os.getcwd()
print(f"Current directory: {current_dir}")
# Elencare i file in una directory
files = os.listdir('.')
print(f"Files: {files[:3]}") # Mostra i primi 3 file
# Verificare se un percorso esiste
exists = os.path.exists('myfile.txt')
print(f"File exists: {exists}")
# Unire componenti di percorso (funziona su sistemi operativi diversi)
file_path = os.path.join('data', 'users', 'profile.txt')
print(f"Path: {file_path}") # Output: data/users/profile.txt (or data\users\profile.txt on Windows)Il modulo sys
Il modulo sys fornisce parametri e funzioni specifici del sistema:
import sys
# Informazioni sulla versione di Python
print(f"Python version: {sys.version}")
print(f"Version info: {sys.version_info}")
# Informazioni sulla piattaforma
print(f"Platform: {sys.platform}") # Output: linux, darwin, win32, etc.
# Dimensione massima degli interi
print(f"Max int: {sys.maxsize}")Il modulo statistics
Il modulo statistics fornisce funzioni per calcoli statistici:
import statistics
grades = [85, 92, 78, 90, 88, 95, 82]
# Tendenza centrale
avg = statistics.mean(grades)
mid = statistics.median(grades)
mode_val = statistics.mode([1, 2, 2, 3, 3, 3, 4])
print(f"Mean: {avg}") # Output: Mean: 87.14285714285714
print(f"Median: {mid}") # Output: Median: 88
print(f"Mode: {mode_val}") # Output: Mode: 3
# Dispersione
std_dev = statistics.stdev(grades)
variance = statistics.variance(grades)
print(f"Standard deviation: {std_dev:.2f}") # Output: Standard deviation: 5.90
print(f"Variance: {variance:.2f}") # Output: Variance: 34.81Il modulo collections
Il modulo collections fornisce tipi di contenitore specializzati. Lo esploreremo di più nel Capitolo 39, ma ecco un assaggio:
from collections import Counter, defaultdict
# Counter - contare le occorrenze
text = "hello world"
letter_counts = Counter(text)
print(letter_counts) # Output: Counter({'l': 3, 'o': 2, 'h': 1, 'e': 1, ' ': 1, 'w': 1, 'r': 1, 'd': 1})
print(letter_counts['l']) # Output: 3
# defaultdict - dizionario con valori di default
word_lists = defaultdict(list)
word_lists['fruits'].append('apple')
word_lists['fruits'].append('banana')
word_lists['vegetables'].append('carrot')
print(dict(word_lists)) # Output: {'fruits': ['apple', 'banana'], 'vegetables': ['carrot']}Trovare altri moduli della libreria standard
La libreria standard di Python contiene oltre 200 moduli. Puoi esplorarli in diversi modi:
# Vedere tutti i moduli disponibili (richiede un momento)
help('modules')
# Ottenere aiuto su un modulo specifico
import math
help(math)
# Vedere cosa c'è in un modulo
import random
print(dir(random))La documentazione di Python (https://docs.python.org/3/library/) fornisce informazioni complete su ogni modulo della libreria standard. Man mano che acquisisci esperienza, scoprirai quali moduli sono più utili per il tuo lavoro.
22.4) Creare e usare i tuoi moduli
Creare i tuoi moduli è semplice: qualsiasi file Python può essere un modulo. La chiave è organizzare il codice in modo ponderato, così che i moduli siano focalizzati, riutilizzabili e facili da capire.
Creare un modulo semplice
Creiamo un modulo per lavorare con i voti degli studenti. Crea un file chiamato grade_calculator.py:
# grade_calculator.py
"""Module for calculating and analyzing student grades."""
def calculate_average(grades):
"""Calculate the average of a list of grades."""
if not grades:
return 0
return sum(grades) / len(grades)
def get_letter_grade(numeric_grade):
"""Convert a numeric grade to a letter grade."""
if numeric_grade >= 90:
return 'A'
elif numeric_grade >= 80:
return 'B'
elif numeric_grade >= 70:
return 'C'
elif numeric_grade >= 60:
return 'D'
else:
return 'F'
def find_highest(grades):
"""Find the highest grade in a list."""
if not grades:
return None
return max(grades)
def find_lowest(grades):
"""Find the lowest grade in a list."""
if not grades:
return None
return min(grades)
# Costanti a livello di modulo
PASSING_GRADE = 60
HONOR_ROLL_THRESHOLD = 90Ora crea un altro file per usare questo modulo:
# student_report.py
import grade_calculator
# Punteggi dei test dello studente
test_scores = [85, 92, 78, 88, 95]
# Calcolare le statistiche
average = grade_calculator.calculate_average(test_scores)
letter = grade_calculator.get_letter_grade(average)
highest = grade_calculator.find_highest(test_scores)
lowest = grade_calculator.find_lowest(test_scores)
# Generare il report
print("Student Grade Report")
print("=" * 40)
print(f"Test Scores: {test_scores}")
print(f"Average: {average:.1f}")
print(f"Letter Grade: {letter}")
print(f"Highest Score: {highest}")
print(f"Lowest Score: {lowest}")
# Verificare l'honor roll
if average >= grade_calculator.HONOR_ROLL_THRESHOLD:
print("Status: HONOR ROLL!")
elif average >= grade_calculator.PASSING_GRADE:
print("Status: Passing")
else:
print("Status: Needs Improvement")Output:
Student Grade Report
========================================
Test Scores: [85, 92, 78, 88, 95]
Average: 87.6
Letter Grade: B
Highest Score: 95
Lowest Score: 78
Status: PassingDocumentazione del modulo
Nota la docstring in cima a grade_calculator.py. Questa docstring a livello di modulo descrive cosa fa il modulo. Compare quando qualcuno usa help():
import grade_calculator
help(grade_calculator)Questo mostra la documentazione del modulo, inclusa la docstring del modulo e tutte le docstring delle funzioni. Una buona documentazione rende i tuoi moduli più facili da usare.
Variabili e costanti a livello di modulo
I moduli possono contenere variabili condivise tra tutti gli utilizzi del modulo. Queste vengono spesso usate per configurazione o costanti:
# config.py
"""Application configuration settings."""
# Impostazioni database
DB_HOST = "localhost"
DB_PORT = 5432
DB_NAME = "myapp"
# Impostazioni dell'applicazione
MAX_LOGIN_ATTEMPTS = 3
SESSION_TIMEOUT = 1800 # secondi
DEBUG_MODE = False
# Percorsi dei file
DATA_DIR = "/var/data"
LOG_DIR = "/var/log"
# Feature flag
ENABLE_CACHING = True
ENABLE_LOGGING = TrueUsare la configurazione da un modulo:
# app.py
import config
def connect_database():
"""Connect to the database using config settings."""
print(f"Connecting to {config.DB_HOST}:{config.DB_PORT}")
print(f"Database: {config.DB_NAME}")
if config.DEBUG_MODE:
print("DEBUG: Connection details logged")
def check_login_attempts(attempts):
"""Check if login attempts exceed the limit."""
if attempts >= config.MAX_LOGIN_ATTEMPTS:
print(f"Too many attempts! Maximum is {config.MAX_LOGIN_ATTEMPTS}")
return False
return True
connect_database()
print(check_login_attempts(2)) # Output: True
print(check_login_attempts(4)) # Output: Too many attempts! Maximum is 3Output:
Connecting to localhost:5432
Database: myapp
True
Too many attempts! Maximum is 3Importante: le variabili a livello di modulo sono condivise tra tutte le importazioni. Se modifichi una variabile del modulo, la modifica influisce su tutto il codice che usa quel modulo:
# file1.py
import config
config.DEBUG_MODE = True
print(f"File1 - Debug mode: {config.DEBUG_MODE}")
# file2.py
import config
print(f"File2 - Debug mode: {config.DEBUG_MODE}") # Will be True!Questo comportamento può essere utile ma anche sorprendente. Sii cauto quando modifichi variabili a livello di modulo.
Nomi privati nei moduli
Per convenzione, i nomi che iniziano con un underscore sono considerati privati o interni al modulo:
# user_manager.py
"""Module for managing user accounts."""
# Funzione di supporto privata
def _validate_email(email):
"""Internal function to validate email format."""
return '@' in email and '.' in email
# Funzione pubblica
def create_user(username, email):
"""Create a new user account."""
if not _validate_email(email):
return None
user = {
'username': username,
'email': email,
'active': True
}
return user
# Costante privata
_MAX_USERNAME_LENGTH = 20
# Costante pubblica
MIN_PASSWORD_LENGTH = 8Quando usi from user_manager import *, i nomi privati (quelli che iniziano con underscore) non vengono importati. Tuttavia, puoi comunque accedervi esplicitamente se necessario:
import user_manager
# Funzione pubblica - pensata per l'uso
user = user_manager.create_user("alice", "alice@example.com")
# Funzione privata - ci si può accedere ma non bisognerebbe farci affidamento
# (potrebbe cambiare in versioni future)
is_valid = user_manager._validate_email("test@test.com")Il prefisso underscore è un segnale per gli altri programmatori: "Questo è un dettaglio di implementazione. Non dare per scontato che resterà uguale."
22.5) Comprendere i pacchetti e __init__.py
Man mano che i progetti crescono, vorrai organizzare più moduli correlati in un pacchetto. Un pacchetto è una directory che contiene moduli Python e uno speciale file __init__.py.
Che cos'è un pacchetto?
Un pacchetto è un modo per organizzare più moduli in una struttura gerarchica. Pensalo come una cartella che contiene file Python, in cui la cartella stessa può essere importata.
Ecco una semplice struttura di pacchetto:
myproject/
main.py
utilities/
__init__.py
text.py
math.py
file.pyIn questa struttura, utilities è un pacchetto che contiene tre moduli: text, math e file. Il file __init__.py (che può essere vuoto) dice a Python che utilities è un pacchetto.
Creare un pacchetto semplice
Creiamo un pacchetto per l’elaborazione dei dati. Per prima cosa, crea questa struttura di directory:
data_tools/
__init__.py
validators.py
formatters.pyCrea validators.py:
# data_tools/validators.py
"""Data validation functions."""
def is_valid_email(email):
"""Check if email has basic valid format."""
return '@' in email and '.' in email.split('@')[1]
def is_valid_phone(phone):
"""Check if phone number has valid format (simple check)."""
digits = ''.join(c for c in phone if c.isdigit())
return len(digits) == 10
def is_positive_number(value):
"""Check if value is a positive number."""
try:
return float(value) > 0
except (ValueError, TypeError):
return FalseCrea formatters.py:
# data_tools/formatters.py
"""Data formatting functions."""
def format_phone(phone):
"""Format phone number as (XXX) XXX-XXXX."""
digits = ''.join(c for c in phone if c.isdigit())
if len(digits) != 10:
return phone
return f"({digits[:3]}) {digits[3:6]}-{digits[6:]}"
def format_currency(amount):
"""Format number as currency."""
return f"${amount:,.2f}"
def format_percentage(value, decimals=1):
"""Format number as percentage."""
return f"{value * 100:.{decimals}f}%"Crea un __init__.py vuoto:
# data_tools/__init__.py
"""Data processing tools package."""Importare dai pacchetti
Ora puoi importare dal pacchetto in diversi modi:
# Metodo 1: importare il modulo dal pacchetto
import data_tools.validators
email = "user@example.com"
is_valid = data_tools.validators.is_valid_email(email)
print(f"Email valid: {is_valid}") # Output: Email valid: True
# Metodo 2: importare un modulo specifico con from
from data_tools import formatters
phone = "1234567890"
formatted = formatters.format_phone(phone)
print(f"Formatted phone: {formatted}") # Output: Formatted phone: (123) 456-7890
# Metodo 3: importare funzioni specifiche
from data_tools.validators import is_valid_phone
from data_tools.formatters import format_currency
print(is_valid_phone("555-1234")) # Output: False (not 10 digits)
print(format_currency(1234.56)) # Output: $1,234.56Il file __init__.py
Il file __init__.py serve a due scopi:
- Contrassegna la directory come un pacchetto: Python riconosce le directory con
__init__.pycome pacchetti - Codice di inizializzazione del pacchetto: il codice in
__init__.pyviene eseguito quando il pacchetto viene importato per la prima volta
Il file __init__.py può essere vuoto, ma può anche contenere codice per rendere il pacchetto più facile da usare:
# data_tools/__init__.py
"""Data processing tools package."""
# Importare funzioni usate di frequente nel namespace del pacchetto
from data_tools.validators import is_valid_email, is_valid_phone
from data_tools.formatters import format_phone, format_currency
# Versione del pacchetto
__version__ = '1.0.0'
# Costante a livello di pacchetto
DEFAULT_CURRENCY_SYMBOL = '$'Ora gli utenti possono importare direttamente dal pacchetto:
# Invece di: from data_tools.validators import is_valid_email
# Puoi scrivere:
from data_tools import is_valid_email, format_currency
print(is_valid_email("test@test.com")) # Output: True
print(format_currency(99.99)) # Output: $99.9922.6) La variabile __name__ e if __name__ == "__main__": eseguire un file come script
I file Python possono servire a due scopi: possono essere importati come moduli oppure eseguiti come script autonomi. La speciale variabile __name__ ti aiuta a scrivere codice che funziona bene in entrambe le situazioni.
Comprendere __name__
Ogni modulo Python ha una variabile incorporata chiamata __name__. Python imposta questa variabile in modo diverso a seconda di come viene usato il file:
- Quando viene importato:
__name__viene impostato al nome del modulo - Quando viene eseguito direttamente:
__name__viene impostato a"__main__"
Vediamolo in azione. Crea un file chiamato demo_name.py:
# demo_name.py
print(f"The __name__ variable is: {__name__}")Ora eseguilo direttamente:
python demo_name.pyOutput:
The __name__ variable is: __main__Ora importalo da un altro file:
# test_import.py
import demo_nameOutput:
The __name__ variable is: demo_nameQuando esegui demo_name.py direttamente, Python imposta __name__ a "__main__". Quando lo importi, Python imposta __name__ al nome del modulo ("demo_name").
Il pattern if __name__ == "__main__":
Questo comportamento ti permette di scrivere codice che viene eseguito solo quando il file viene eseguito direttamente, non quando viene importato. Questo si fa con il pattern:
if __name__ == "__main__":
# Il codice qui viene eseguito solo quando il file è eseguito direttamente
passEcco perché è utile. Crea math_utils.py:
# math_utils.py
"""Utility functions for mathematical operations."""
def calculate_area(radius):
"""Calculate the area of a circle."""
return 3.14159 * radius ** 2
def calculate_circumference(radius):
"""Calculate the circumference of a circle."""
return 2 * 3.14159 * radius
# Codice di test - viene eseguito solo quando il file è eseguito direttamente
if __name__ == "__main__":
print("Testing math_utils functions...")
test_radius = 5
area = calculate_area(test_radius)
circumference = calculate_circumference(test_radius)
print(f"Radius: {test_radius}")
print(f"Area: {area:.2f}")
print(f"Circumference: {circumference:.2f}")Quando esegui questo file direttamente:
python math_utils.pyOutput:
Testing math_utils functions...
Radius: 5
Area: 78.54
Circumference: 31.42Ma quando lo importi:
# use_math_utils.py
import math_utils
# Il codice di test non viene eseguito!
area = math_utils.calculate_area(10)
print(f"Area of circle: {area:.2f}") # Output: Area of circle: 314.16Il codice di test nel blocco if __name__ == "__main__": non viene eseguito durante l’importazione. Questo ti consente di includere codice di test, esempi o dimostrazioni nei tuoi moduli senza influenzare il codice che li importa.
Usi comuni di if __name__ == "__main__":
Test e dimostrazioni
Includere esempi che mostrano come usare il tuo modulo:
# string_tools.py
def reverse_string(text):
"""Reverse a string."""
return text[::-1]
def count_vowels(text):
"""Count vowels in text."""
vowels = 'aeiouAEIOU'
return sum(1 for char in text if char in vowels)
if __name__ == "__main__":
# Codice di dimostrazione
sample = "Hello, World!"
print(f"Original: {sample}")
print(f"Reversed: {reverse_string(sample)}")
print(f"Vowels: {count_vowels(sample)}")In questo capitolo abbiamo imparato come organizzare il codice Python usando moduli e pacchetti. Abbiamo esplorato come funziona il sistema di importazione, diversi modi di importare codice e come creare i nostri moduli e pacchetti. Abbiamo anche imparato a conoscere la variabile __name__ e il pattern if __name__ == "__main__": che consente ai file di funzionare sia come moduli importabili sia come script autonomi.
Questi strumenti di organizzazione diventano sempre più importanti man mano che i tuoi programmi crescono. Nel prossimo capitolo esploreremo come usare le funzioni come dati e applicare semplici tecniche di programmazione funzionale, costruendo sulla solida base di organizzazione del codice che abbiamo stabilito qui.