Pacchetti Python e il sistema di importazione
Scopri come funzionano i pacchetti Python: crea un pacchetto con __init__.py, usa importazioni assolute e relative, esponi un'API pubblica e evita gli errori comuni.
Un pacchetto è una directory di moduli Python che si tratta come un'unica unità importabile. Mentre un modulo è un singolo file .py, un pacchetto è una cartella — che può contenere molti moduli e sotto-pacchetti — che il sistema di importazione di Python può navigare come un albero. Questo capitolo spiega come creare pacchetti, controllare ciò che espongono, scrivere correttamente importazioni assolute e relative ed evitare le insidie che mettono in difficoltà i principianti.
Moduli vs. Pacchetti — la differenza fondamentale
Un modulo è un singolo file .py:
greetings.py ← moduleUn pacchetto è una directory che contiene almeno un file speciale chiamato __init__.py:
greetings/ ← package
__init__.py
english.py
spanish.pyEntrambi vengono importati con la stessa parola chiave import, ma un pacchetto fornisce una gerarchia di spazi dei nomi: greetings.english e greetings.spanish sono moduli separati, eppure condividono lo spazio dei nomi greetings.
Quando usare un modulo vs. un pacchetto:
| Situazione | Usa |
|---|---|
| Un'utilità piccola e autonoma | Modulo (un singolo file .py) |
| Più moduli correlati da raggruppare sotto un unico nome | Pacchetto (una directory) |
| Una libreria da distribuire su PyPI | Pacchetto (con layout src/) |
Il file __init__.py
__init__.py è ciò che rende una directory un pacchetto. Python lo esegue la prima volta che il pacchetto (o uno qualsiasi dei suoi moduli) viene importato. Può essere vuoto, oppure può:
- Importare nomi dai sotto-moduli per renderli disponibili a livello di pacchetto
- Eseguire l'inizializzazione a livello di pacchetto (configurazione del logging, controlli di versione, ecc.)
- Definire
__all__per controllarefrom package import *
Layout minimo del pacchetto
myapp/
__init__.py ← can be empty
utils.py
config.py# myapp/__init__.py (empty — that is fine)# myapp/utils.py
def greet(name):
return f"Hello, {name}!"Importazione dall'esterno del pacchetto:
from myapp.utils import greet
print(greet("Alice")) # Hello, Alice!Rendere i nomi disponibili a livello di pacchetto
Un pattern comune è importare i nomi più utilizzati in __init__.py in modo che i chiamanti possano scrivere from myapp import greet invece di from myapp.utils import greet.
# myapp/__init__.py
from .utils import greet
from .config import MAX_RETRIESOra entrambi i nomi sono disponibili direttamente nel pacchetto:
import myapp
print(myapp.greet("Bob")) # Hello, Bob!
print(myapp.MAX_RETRIES) # whatever config.py definesImportazioni assolute
Un'importazione assoluta parte sempre dal pacchetto di livello superiore o da una directory presente in sys.path. Non dipende mai dalla posizione del file che effettua l'importazione.
project/
myapp/
__init__.py
utils.py
services/
__init__.py
email.pyAll'interno di email.py, un'importazione assoluta si presenta così:
# myapp/services/email.py
from myapp.utils import greet # absolute — starts from the top-level package
def send_welcome(user):
message = greet(user)
print(f"Sending: {message}")Le importazioni assolute sono lo stile predefinito e raccomandato (PEP 8). Sono non ambigue indipendentemente da come viene invocato Python.
Importazioni relative
Un'importazione relativa usa i punti (.) per navigare nell'albero dei pacchetti relativamente alla posizione del file corrente.
.indica il pacchetto corrente..indica il pacchetto padre...indica il pacchetto nonno, e così via
# myapp/services/email.py
# One dot — import from myapp.services (same directory)
from . import sms
# Two dots — import from myapp (parent directory)
from ..utils import greetQuando usare le importazioni relative
Le importazioni relative sono utili all'interno di un pacchetto quando si vuole rendere chiaro che greet proviene da questo pacchetto e non da una libreria esterna con lo stesso nome. Facilitano anche il refactoring perché le importazioni si spostano insieme al pacchetto se si rinomina la directory di livello superiore.
Attenzione: le importazioni relative funzionano solo all'interno di un pacchetto. Se esegui python myapp/utils.py direttamente, Python lo tratta come uno script autonomo, non come parte di un pacchetto, e un'importazione relativa genera ImportError: attempted relative import with no known parent package. Esegui invece il pacchetto con python -m myapp.utils.
# Wrong — runs utils.py as a script, breaking relative imports
$ python myapp/utils.py
# Right — runs utils.py as part of the myapp package
$ python -m myapp.utilsControllare l'API pubblica con __all__
__all__ è un elenco di nomi che from package import * esporta. Documenta anche ciò che il pacchetto considera pubblico.
# myapp/__init__.py
from .utils import greet, farewell
from .config import MAX_RETRIES
__all__ = ["greet", "MAX_RETRIES"] # farewell is intentionally not exportedOra from myapp import * importa solo greet e MAX_RETRIES. La funzione farewell esiste ancora; semplicemente non fa parte dell'interfaccia pubblica dichiarata. I nomi con prefisso underscore singolo (_private) sono esclusi da import * per convenzione.
Pacchetti annidati (sotto-pacchetti)
I pacchetti possono contenere altri pacchetti. Ogni sotto-directory necessita del proprio __init__.py.
analytics/
__init__.py
reports/
__init__.py
daily.py
weekly.py
charts/
__init__.py
bar.py
pie.pyImporta un modulo profondamente annidato con il percorso completo con punti:
from analytics.reports.daily import generate_report
from analytics.charts.bar import BarChartOppure, se analytics/__init__.py li espone:
# analytics/__init__.py
from .reports.daily import generate_report# caller
from analytics import generate_reportQuanto in profondità conviene scendere?
Un pacchetto con tre o quattro livelli di profondità è generalmente un segnale che è cresciuto troppo e dovrebbe essere suddiviso in pacchetti di livello superiore separati (installabili separatamente). Per la maggior parte dei progetti, due livelli (pacchetto.modulo) sono sufficienti.
Esempio pratico: costruire un pacchetto geometry
Costruiamo passo dopo passo un pacchetto piccolo ma realistico.
Layout della directory
geometry/
__init__.py
shapes.py
conversions.pyshapes.py
# geometry/shapes.py
import math
def circle_area(radius):
"""Return the area of a circle with the given radius."""
if radius < 0:
raise ValueError("radius must be non-negative")
return math.pi * radius ** 2
def rectangle_area(width, height):
"""Return the area of a rectangle."""
return width * height
def triangle_area(base, height):
"""Return the area of a triangle."""
return 0.5 * base * heightconversions.py
# geometry/conversions.py
def degrees_to_radians(degrees):
"""Convert degrees to radians."""
import math
return degrees * math.pi / 180
def radians_to_degrees(radians):
"""Convert radians to degrees."""
import math
return radians * 180 / math.pi__init__.py — esporre i nomi chiave
# geometry/__init__.py
"""
geometry — simple 2-D geometry utilities.
Public API:
circle_area(radius) -> float
rectangle_area(width, height) -> float
triangle_area(base, height) -> float
degrees_to_radians(degrees) -> float
radians_to_degrees(radians) -> float
"""
from .shapes import circle_area, rectangle_area, triangle_area
from .conversions import degrees_to_radians, radians_to_degrees
__all__ = [
"circle_area",
"rectangle_area",
"triangle_area",
"degrees_to_radians",
"radians_to_degrees",
]Usare il pacchetto
# main.py (sits next to the geometry/ directory)
import geometry
print(geometry.circle_area(5)) # 78.53981633974483
print(geometry.rectangle_area(4, 6)) # 24
print(geometry.degrees_to_radians(90)) # 1.5707963267948966Oppure con importazioni selettive:
from geometry import circle_area, degrees_to_radians
print(circle_area(3)) # 28.274333882308138
print(degrees_to_radians(180)) # 3.141592653589793Pacchetti di spazio dei nomi (Python 3.3+)
Da Python 3.3, una directory senza __init__.py è un namespace package. Python unisce in un unico pacchetto logico tutte le directory con lo stesso nome presenti in sys.path. Questo è principalmente utile per grandi organizzazioni che suddividono un singolo pacchetto tra più repository o directory di installazione.
Per lo sviluppo quotidiano, includi sempre __init__.py. Rende le tue intenzioni non ambigue e funziona con tutte le versioni di Python.
Come Python trova i pacchetti
Quando scrivi import geometry, Python ricerca in sys.path nell'ordine:
- La directory dello script in esecuzione (o la directory corrente in modalità interattiva)
- Le directory nella variabile d'ambiente
PYTHONPATH - Le directory della libreria standard
- La directory
site-packages(dove risiedono i pacchetti installati con pip)
import sys
print(sys.path)La directory del pacchetto deve trovarsi direttamente all'interno di una di queste posizioni. Se geometry/ si trova in /home/alice/projects/, Python non la troverà a meno che /home/alice/projects/ non sia presente in sys.path.
Suggerimento: usa un ambiente virtuale e installa il tuo pacchetto in modalità sviluppo (pip install -e .) in modo che Python lo trovi sempre senza manipolazioni manuali di sys.path.
Distribuire un pacchetto
Per condividere un pacchetto con altri (o installarlo con pip), hai bisogno di un file pyproject.toml nella radice del progetto:
my_project/
pyproject.toml ← build metadata
src/
geometry/
__init__.py
shapes.py
conversions.pyUn pyproject.toml minimale:
[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.backends.legacy:build"
[project]
name = "geometry"
version = "0.1.0"
description = "Simple 2-D geometry utilities"
requires-python = ">=3.9"Installa localmente in modalità modificabile durante lo sviluppo:
pip install -e .Ora import geometry funziona ovunque nel tuo ambiente virtuale, indipendentemente dalla directory corrente.
Errori comuni
__init__.py mancante
Se dimentichi __init__.py, Python 3 tratta la directory come un namespace package (il che di solito funziona ancora), ma Python 2 la ignora completamente. Sii esplicito: aggiungi sempre __init__.py.
Nominare un pacchetto come un modulo della libreria standard
Evita nomi come math/, json/, os/, email/. Python potrebbe importare il tuo pacchetto al posto di quello della libreria standard, rompendo codice non correlato.
Eseguire un modulo di pacchetto come script
Come notato in precedenza, eseguire python myapp/services/email.py direttamente rompe le importazioni relative. Usa invece python -m myapp.services.email.
Importazioni circolari tra moduli dello stesso pacchetto
Se shapes.py importa da conversions.py e conversions.py importa da shapes.py, si crea un'importazione circolare. I sintomi includono ImportError o nomi che appaiono inaspettatamente come None. La soluzione è solitamente spostare la logica condivisa in un terzo modulo, oppure ritardare l'importazione all'interno del corpo di una funzione.
# Delayed import — breaks the cycle at module load time
def some_function():
from .shapes import circle_area # imported only when the function is called
...ImportError quando si usano importazioni relative fuori da un pacchetto
# Will raise: ImportError: attempted relative import with no known parent package
# if run as: python myapp/utils.py
from . import config # relative import inside utils.pyEseguilo come python -m myapp.utils oppure ristruttura in modo che il punto di ingresso sia uno script separato che importa il pacchetto.
Riepilogo
| Concetto | Descrizione sintetica |
|---|---|
| Pacchetto | Una directory con __init__.py contenente moduli |
__init__.py | Rende una directory un pacchetto; viene eseguito alla prima importazione |
| Importazione assoluta | from myapp.utils import greet — sempre dalla radice |
| Importazione relativa | from ..utils import greet — relativa al file corrente |
__all__ | Elenca i nomi esportati da from package import * |
| Namespace package | Directory senza __init__.py; solo Python 3.3+ |
| Installazione modificabile | pip install -e . — pacchetto trovato ovunque nel venv |
Vedi Python Modules per l'organizzazione del codice in un singolo file, Python pip per installare pacchetti di terze parti e Python Virtual Environments per mantenere isolate le dipendenze del progetto.