Liste non vide (NonEmptyList)¶
Le module fptk.adt.nelist définit le type NonEmptyList, une structure de liste garantissant, par construction, la présence d'au moins un élément.
Concept : Des collections jamais vides¶
De nombreuses opérations sur les listes classiques échouent ou produisent des résultats incohérents lorsqu'elles sont appliquées à une séquence vide :
max([]) # ValueError: max() arg is an empty sequence
min([]) # ValueError
premier = xs[0] # IndexError si la liste est vide
moyenne = sum(xs) / len(xs) # ZeroDivisionError si la liste est vide
NonEmptyList transforme cette exigence de « non-vacuité » en une garantie au niveau du type. Posséder une NonEmptyList vous assure qu'elle contient au moins un élément, éliminant ainsi le besoin de vérifications manuelles lors de l'exécution.
Le problème : les tests de vacuité répétitifs¶
def calculer_moyenne(xs: list[float]) -> float:
if not xs:
raise ValueError("Impossible de calculer la moyenne d'une liste vide")
return sum(xs) / len(xs)
def obtenir_premier(xs: list[T]) -> T:
if not xs:
raise ValueError("La liste est vide")
return xs[0]
# Chaque fonction doit valider la donnée, et chaque appelant doit gérer l'exception potentielle.
La solution : NonEmptyList¶
from fptk.adt.nelist import NonEmptyList
def calculer_moyenne(xs: NonEmptyList[float]) -> float:
# Aucun test requis : xs contient forcément au moins un élément.
return sum(xs) / len(list(xs))
def obtenir_premier(xs: NonEmptyList[T]) -> T:
return xs.head # Toujours sûr
# Construction sécurisée aux frontières du code
resultat = NonEmptyList.from_iter(donnees) # Renvoie Option[NonEmptyList]
if resultat:
moyenne = calculer_moyenne(resultat)
else:
# On gère le cas vide une seule fois, ici même.
API¶
Types¶
| Type | Description |
|---|---|
NonEmptyList[E] |
Liste comportant obligatoirement au moins un élément. |
Constructeurs¶
from fptk.adt.nelist import NonEmptyList
# Construction directe (toujours non vide)
nel = NonEmptyList(1) # [1]
nel = NonEmptyList(1, (2, 3, 4)) # [1, 2, 3, 4]
# À partir d'un itérable (peut être vide)
resultat = NonEmptyList.from_iter([1, 2]) # Renvoie une NonEmptyList ou None
resultat = NonEmptyList.from_iter([]) # Renvoie None
Propriétés¶
| Propriété | Type | Description |
|---|---|---|
head |
E |
Le premier élément (dont l'existence est garantie). |
tail |
tuple[E, ...] |
Les éléments restants (peut être un tuple vide). |
Méthodes¶
| Méthode | Signature | Description |
|---|---|---|
append(e) |
(E) -> NonEmptyList[E] |
Ajoute un élément à la fin. |
to_list() |
() -> list[E] |
Convertit en une liste standard de Python. |
from_iter(it) |
staticmethod (Iterable[E]) -> NonEmptyList[E] | None |
Tente de créer une NonEmptyList depuis un itérable. |
__iter__() |
() -> Iterator[E] |
Permet de parcourir l'ensemble des éléments. |
Fonctionnement technique¶
Structure de données¶
NonEmptyList s'appuie sur un head obligatoire et un tail facultatif :
@dataclass(frozen=True, slots=True)
class NonEmptyList[E]:
head: E # Premier élément (requis)
tail: tuple[E, ...] = () # Éléments suivants (tuple pour garantir l'immuabilité)
L'immuabilité est assurée par le décorateur @dataclass(frozen=True).
Construction sécurisée¶
@staticmethod
def from_iter(it: Iterable[E]) -> NonEmptyList[E] | None:
iterator = iter(it)
try:
h = next(iterator)
except StopIteration:
return None # L'itérable était vide
return NonEmptyList(h, tuple(iterator))
La méthode from_iter renvoie None si l'itérable fourni est vide. C'est le seul moyen d'obtenir une instance de NonEmptyList à partir de données dynamiques.
Exemples d'utilisation¶
Accès sécurisé au premier élément¶
from fptk.adt.nelist import NonEmptyList
# Avec une liste classique : risque d'erreur
def premier_element_dangereux(xs: list[int]) -> int:
return xs[0] # IndexError si vide !
# Avec NonEmptyList : sécurité totale
def premier_element_sur(xs: NonEmptyList[int]) -> int:
return xs.head # Garanti d'exister
# Validation aux limites
donnees_brutes = recuperer_donnees() # list[int]
nel = NonEmptyList.from_iter(donnees_brutes)
if nel:
print(premier_element_sur(nel))
else:
print("Aucune donnée disponible")
Calcul de statistiques sans crainte¶
from fptk.adt.nelist import NonEmptyList
def statistiques(xs: NonEmptyList[float]) -> dict:
"""Calcule des statistiques sans avoir à vérifier la vacuité de la liste."""
valeurs = list(xs)
return {
"nombre": len(valeurs),
"somme": sum(valeurs),
"moyenne": sum(valeurs) / len(valeurs), # Sûr (pas de division par zéro possible)
"min": min(valeurs), # Sûr
"max": max(valeurs), # Sûr
"premier": xs.head, # Sûr
}
Usage avec validate_all¶
from fptk.validate import validate_all
from fptk.adt.nelist import NonEmptyList
# validate_all renvoie Result[T, NonEmptyList[E]]
# En cas d'échec, vous avez la garantie d'obtenir au moins une erreur.
resultat = validate_all([test1, test2, test3], donnees)
resultat.match(
ok=lambda d: traiter(d),
err=lambda erreurs: print(f"Validation échouée : {erreurs.head}")
# erreurs est une NonEmptyList[str], donc .head est parfaitement sûr.
)
Quand utiliser NonEmptyList ?¶
Privilégiez NonEmptyList lorsque :
- Votre logique métier exige impérativement la présence d'au moins un élément.
- Vous voulez supprimer les tests de vacuité redondants dans vos fonctions.
- Vous accumulez des erreurs (comme dans un processus de validation).
- Vous effectuez des agrégations nécessitant une entrée non vide (moyenne, extremums, etc.).
Évitez NonEmptyList lorsque :
- Une collection vide est une donnée tout à fait valide dans votre contexte.
- Vous avez besoin d'accès aléatoires très fréquents (préférez
list). - Vous effectuez de nombreux ajouts d'éléments (la concaténation de tuples est en O(n)).
NonEmptyList vs Option[list]¶
| Type | Signification |
|---|---|
list[T] |
Zéro, un ou plusieurs éléments. |
Option[list[T]] |
Une liste facultative (qui pourrait tout de même être vide). |
NonEmptyList[T] |
Un ou plusieurs éléments (garanti). |
Option[NonEmptyList[T]] |
Une liste facultative qui, si elle existe, contient forcément au moins un élément. |
Voir aussi¶
validate_all— UtiliseNonEmptyListpour collecter les erreurs.Option— Pour les valeurs potentiellement absentes.Result— Pour les calculs susceptibles d'échouer.