Cursus
La fonction Python « reduce() » est issue du domaine de la programmation fonctionnelle. La programmation fonctionnelle (FP) est un paradigme de programmation dans lequel les programmes produisent des résultats en appliquant des fonctions à des données immuables.
Un modèle courant dans ce style est le « pli », qui réduit une séquence en un seul résultat. Par exemple, plier la liste des nombres [2, 4, 5, 3] sous l'addition 14 par étapes successives : [2, 4, 5, 3] → [6, 5, 3] → [11, 3] → 14.
reduce() généralise cette idée. Il applique une opération binaire à un itérable jusqu'à ce que seul le résultat subsiste.
Dans cet article, j'examinerai les éléments clés de l'reduce() de Python et donnerai quelques exemples pratiques. Si vous avez besoin d'une remise à niveau sur les bases de Python, je vous recommande de consulter les ressources suivantes :
- Tranche Python : Méthodes utiles pour le codage quotidien Tutoriel sur l'
- Python pour les utilisateurs de R cours
Histoire
Python inclut d'autres fonctions de programmation fonctionnelle, telles que map() et filter().
Les constructions fonctionnelles telles que map() et filter() étaient présentes dans Python 1.0. Guido van Rossum n'appréciait pas ces constructions, soulignant que l'instruction « reduce() » était difficile à analyser et qu'une boucle « for » était presque toujours plus lisible. Dans Python 3.0, conformément à la PEP 3100, les développeurs ont supprimé la fonction intégrée ` reduce() ` et l'ont déplacée vers le module ` functools `. Le passage à functools a essentiellement relégué cet outil au statut d'outil de niche.
Pourquoi utiliser reduce() ?
La plupart du temps, je considère qu'il est probablement préférable d'utiliser une fonction intégrée ou une boucle. Cependant, reduce() reste un choix judicieux dans certains cas d'utilisation.
- Pipelines de fonctions. Enchaînez une série (éventuellement dynamique) de transformations de manière claire.
- Pliages algébriques. À utiliser pour les opérations qui ont des valeurs d'identité naturelles, telles que l'union d'ensembles avec l'ensemble vide ou les opérations de masque de bits avec zéro.
- Pli personnalisé sans intégration. Définissez votre propre fusion de manière spécifique au domaine lorsqu'il n'existe aucune fusion intégrée.
- Accumulateurs structurés. Menez simultanément plusieurs éléments d'état dans une fonction d'accumulateur personnalisée.
Fonctionnement de la fonction reduce() en Python
Veuillez examiner le fonctionnement de reduce().
La signature de la fonction de base d'reduce() est la suivante :
functools.reduce(function, iterable, [initializer])
La fonction reduce() fonction requiert deux arguments obligatoires et un troisième facultatif.
function: une fonction binaire qui définit comment combiner deux éléments,iterable: la séquence ou l'itérable à réduire, tel qu'une liste ou un tuple,initializer(facultatif) : une valeur de départ pour initialiser la fonction.
Exemple étape par étape :
- [1, 3, 2, 7] → [4, 2, 7].
- [4, 2, 7] → [6, 7].
- [6, 7] → [13].
Le résultat final est 13.
Exemples simples de reduce()
Les exemples suivants illustrent le fonctionnement de l'utilisation de reduce().
from functools import reduce
numbers = [2, 4, 6]
product = reduce(lambda x, y: x * y, numbers) # ((2 x 4) x 6) = 8 x 6 = 48
min_value = reduce(lambda x, y: x if x < y else y, numbers) # 2
words = ['dog', 'cat', 'tree', 'pony']
str_concat = reduce(lambda x, y: x + y, words) # "dogcattreepony"
Initialiseur
Sans initialiseur, la fonction ` reduce() ` prend le premier élément de l'itérable comme valeur de départ. Si l'itérable est vide, l'opération d'reduce() ion renvoie une exception d'TypeError. Afin de garantir la robustesse du code, veuillez fournir un initialiseur pour définir le comportement en cas d'entrée vide.
from functools import reduce
words = []
# Error: reduce() of empty iterable with no initial value
str_concat = reduce(lambda x, y: x + y, words)
# Correct: use empty string initializer
str_concat = reduce(lambda x, y: x + y, words, "")
Les initialiseurs fournissent également un résultat avec un conteneur vide. Par exemple, vous pouvez concaténer des mots dans une liste plate de caractères.
from functools import reduce
words = ['reduce', 'is', 'fun']
chars_list = reduce(lambda acc, word: acc + list(word), words, [])
print(chars_list) # ['r', 'e', 'd', 'u', 'c', 'e', 'i', 's', 'f', 'u', 'n']
Pour approfondir vos connaissances en Python et en manipulation de données, voici quelques options que je recommande.
- Introduction au cours sur l'importation de données en Python
- Cursus de compétences « Manipulation de données en Python »
- Remodeler les données avec pandas dans Python - aide-mémoire
- Cours sur la réduction de dimensionnalité en Python
- Pré-traitement des données : Guide complet avec exemples Python Blog
Définition du réducteur
Jusqu'à présent, nous avons utilisé des fonctions lambda pour définir l'opérateur binaire.
Vous pouvez également utiliser les opérateurs du module operator. Le module operator contient des versions fonctionnelles d'opérateurs courants et d'appels de méthodes. Par exemple, au lieu de x + y, operator.add(x, y). Cela vous permet de transmettre des opérateurs prédéfinis (et efficaces) à l'reduce() sans avoir à écrire de lambda.
from functools import reduce
import operator as op
numbers = [2, 4, 6]
total = reduce(op.add, numbers, 0) # instead of reduce(lambda x,y: x + y, numbers)
Une troisième option consiste à créer une fonction personnalisée. Il s'agit d'une option appropriée lorsqu'il n'existe pas d'opérateur prédéfini ou lorsque la fonction est trop complexe pour un lambda.
Par exemple, supposons que vous souhaitiez supprimer les doublons d'une liste, mais en conservant l'ordre de leur première apparition. Vous pourriez définir un réducteur qui ajoute un élément à une liste uniquement s'il n'y figure pas déjà.
from functools import reduce
items = ['the', 'wild', 'wild', 'world', 'is', 'the', 'wide', 'world', 'is', 'the', 'world']
def dedup(acc, x):
if x not in acc: # O(n) membership test
acc.append(x)
return acc
unique = reduce(dedup, items, []) # ['the', 'wild', 'world', 'is', 'wide']
Pour plus d'informations sur la suppression des doublons, veuillez consulter ce tutoriel.
Performances de la fonction reduce() en Python
reduce() implique des problèmes de performance.
Surcoût lié à l'appel de fonction
Python reduce() appelle notre fonction une fois pour chaque élément. Dans une liste contenant un million d'éléments, cela implique un million d'appels de fonction, chacun créant une trame, traitant des arguments et mettant à jour les comptes de référence. Cela entraîne des frais supplémentaires importants.
En revanche, une fonction intégrée telle que sum effectue un seul appel à une fonction C et exécute les millions d'additions à l'intérieur de la boucle C. Cette différence peut accélérer considérablement les commandes intégrées.
Problèmes liés à la localisation du cache et à l'efficacité du processeur
La réduction est également affectée par la localité du cache et l'efficacité du processeur.
Un processeur moderne peut exécuter plusieurs milliards d'instructions par seconde, mais l'accès à la mémoire vive est considérablement plus lent. Pour compenser, les processeurs modernes sont équipés decaches d' s (L1, L2, L3) qui stockent les données pour un accès rapide.
Ces caches exploitent deux modèles.
- Localité temporelle Les données récemment utilisées sont susceptibles d'être réutilisées.
- La localisation spatiale Les données proches d'une adresse récemment utilisée sont susceptibles d'être utilisées prochainement.
En revanche, chaque étape de l'reduce() e implique la recherche du pointeur pour trouver l'élément suivant et des appels de fonctions Python, ce qui perturbe la localité et ralentit le CPU. Les fonctions intégrées et vectorisées permettent d'éviter ce problème en exécutant des boucles C serrées.
Pour rafraîchir vos connaissances sur la rédaction de code Python efficace et idiomatique, veuillez consulter :
- 5 conseils pour écrire du code Pandas idiomatique tutoriel
- Écrire du code Python efficace cours
- Écrire du code efficace avec pandas cours
Alternatives à la fonction Python reduce()
Éléments encastrés:
- Code C optimisé. Les fonctions intégrées exécutent leurs boucles dans un code C optimisé, et non en Python. Cela permet d'éviter les frais généraux liés à l'
reduce(). Cet avantage en termes de vitesse est encore plus marqué pour les entrées volumineuses. - Lisibilité. Les fonctions intégrées ont des noms descriptifs (
sum,min) afin que leur objectif soit clair. Un appel àreduce()vous permet d'analyser la fonction en cours de pliage.
Boucles :
- Performance. Les boucles s'exécutent généralement plus lentement que les fonctions intégrées, mais plus rapidement que l'
reduce(). - Lisibilité. Tout comme les fonctions intégrées, les boucles sont généralement plus lisibles que l'
reduce(). Un appel àreduce()vous oblige à analyser une instruction fonctionnelle, alors qu'une boucle est plus conforme au style Python.
itertools.accumulate()
La bibliothèque Python « itertools » fournit un ensemble d'itérateurs performants tels que « count() », « product() » et « combinations() ». Une fonction utile d'itertools s est itertools.accumulate(). À l'instar de reduce(), il applique une fonction à un itérable. Cependant, accumulate() stocke les valeurs intermédiaires du calcul, et pas seulement le résultat final.
Par exemple,
import itertools, operator
from functools import reduce
list(itertools.accumulate([1, 2, 3, 4], initial=0)) # [0, 1, 3, 6, 10]
reduce(operator.add, [1, 2, 3, 4], 0) # 10
La fonction Accumuler est utile lorsque vous avez besoin de totaux cumulés ou de minimums/maximums. Par exemple, vous pourriez souhaiter connaître la température maximale mois après mois.
Erreurs courantes lors de l'utilisation de reduce()
Lorsque vous utilisez reduce(), veuillez garder à l'esprit les points suivants.
- Veuillez privilégier des alternatives plus simples telles que les intégrations ou les boucles. Veuillez conserver
reduce()pour une utilisation ultérieure. - Gérer les itérables vides. Veuillez toujours utiliser un initialiseur approprié afin d'éviter toute erreur en cas de saisie vide.
- Veuillez prêter attention aux problèmes de mémoire. Veuillez ne pas appliquer de force l'
reduce()dans des situations où une approche par générateur ou streaming serait plus efficace. - Veuillez éviter les lambdas complexes. Veuillez utiliser les fonctions du module
operatorlorsque cela est possible. Les lambdas, en particulier avec des opérations non associatives, peuvent nuire à la clarté. - Privilégiez la clarté à l'ingéniosité.
Meilleures pratiques et directives pour la fonction reduce() en Python
Comme pour tout outil, il existe des pratiques exemplaires à respecter avec reduce(). Si vous avez déterminé que reduce() est l'outil approprié à utiliser, voici quelques directives pour son utilisation.
Veuillez concevoir le réducteur en premier lieu.
- Veuillez définir le contrat en une seule phrase. « Regroupez les clés du dictionnaire en additionnant les comptes par clé. »
- Veuillez conserver le caractère associatif si possible. Cela vous permet de paralléliser et de tester plus facilement.
- Veuillez identifier votre élément d'identité et l'utiliser comme initialiseur. Par exemple, pour la somme, l'identité est 0. Pour
min, l'adresse estmath.inf, et pourset union, l'adresse estset().TypeErrorsCela permet de garantir la robustesse du code et de le préserver des erreurs de programmation.
Veuillez privilégier la simplicité et la lisibilité.
- Une opération claire. Aucun effet secondaire.
- Veuillez attribuer un nom descriptif au réducteur.
Document
- Dans la chaîne de documentation, veuillez noter l'élément d'identité et le comportement en cas d'entrée vide, l'hypothèse d'associativité et la politique en matière d'erreurs.
Test
- Tests unitaires sur les cas limites : itérable vide, types mixtes, valeurs extrêmes.
- Teste l'associativité.
Surveiller les performances
- Veuillez comparer les petites et grandes entrées. Veuillez comparer les benchmarks aux fonctions intégrées et aux boucles.
- Si la vitesse est un facteur important, envisagez le prétraitement, le traitement par lots et le transfert des calculs mathématiques lourds vers NumPy ou pandas.
Applications avancées et cas d'utilisation concrets de la fonction Python reduce()
Compte tenu de ces inconvénients, on pourrait penser que l'reduce() e n'a aucune valeur réelle. Au contraire, reduce() présente de nombreux cas d'utilisation utiles.
- Traitement des structures imbriquées
- Opérations de type base de données
- Pipelines de traitement des données
- Applications MapReduce
Traitement des structures imbriquées
Reduce offre un moyen efficace de parcourir des structures de données imbriquées, telles que les objets JSON, en regroupant une séquence de clés en recherches successives.
import json
from functools import reduce
import operator
data = json.loads('''
{
"user": {
"id": "ABC123",
"name": "Alice",
"email": "alice@example.com",
"profile": {
"address": {
"city": "San Francisco",
"zip": "94103"
},
"age": 34,
"skills": ["Python", "Data Science", "Machine Learning"]
}
}
}
''')
# Example lookups with reduce + operator.getitem
city = reduce(operator.getitem, ["user", "profile", "address", "city"], data)
print(city) # "San Francisco"
user_id = reduce(operator.getitem, ["user", "id"], data)
print(user_id) # "ABC123"
age = reduce(operator.getitem, ["user", "profile", "age"], data)
print(age) # 34
Il est judicieux d'utiliser reduce() dans ce contexte. Le JSON est profondément imbriqué : user → profile → address → city. Au lieu d'enchaîner manuellement les recherches, veuillez représenter le chemin sous forme de liste de clés. Veuillez ensuite utiliser reduce(operator.getitem, path, data) pour le parcourir. Cela permet de conserver un code générique, lisible et réutilisable.
Pipelines de traitement des données
Reduce peut piloter des pipelines de traitement de données en faisant passer les données par une séquence de transformations. Chaque fonction traite une seule étape, et le pipeline résulte de leur application dans l'ordre. Voici un pipeline de démonstration qui prétraite une chaîne de texte avant de l'introduire dans un modèle NLP.
from functools import reduce
import re
# Define preprocessing steps
def strip_punctuation(s):
return re.sub(r"[^\w\s]", "", s)
def to_lower(s):
return s.lower()
def remove_stopwords(s):
stops = {"the", "is", "a", "of"}
return " ".join(word for word in s.split() if word not in stops)
def stem_words(s):
# trivial "stemmer": cut off 'ing'
return " ".join(word[:-3] if word.endswith("ing") else word for word in s.split())
pipeline = [
strip_punctuation,
to_lower,
remove_stopwords,
stem_words,
]
# Input data
text = "The quick brown fox is Jumping over a log."
# Apply pipeline with reduce
processed = reduce(lambda acc, f: f(acc), pipeline, text)
print(processed) # quick brown fox jump over log
Gestion des erreurs dans les applications complexes
Revenons à l'exemple JSON imbriqué. À l'heure actuelle, l'appel direct à reduce(operator.getitem, …) renvoie une exception KeyError ou TypeError si une clé est manquante ou s'il rencontre un élément qui n'est pas un dictionnaire. Pour renforcer la sécurité du code, veuillez définir une fonction d'aide qui encapsule operator.getitem dans un bloc try/except et renvoie une valeur par défaut en cas d'erreur.
Voici une possibilité pour la fonction d'aide.
def deep_get(data, keys, default=None):
"""Traverse nested dicts/lists safely with reduce."""
try:
return reduce(operator.getitem, keys, data)
except (KeyError, IndexError, TypeError):
return default
Maintenant, modifiez les exemples de recherche pour utiliser notre nouvelle fonction au lieu d'une fonction déballée. reduce()
# Example lookups
city = deep_get(data, ["user", "profile", "address", "city"], default="Unknown City")
print(city) # "San Francisco"
user_id = deep_get(data, ["user", "id"], default="N/A")
print(user_id) # "ABC123"
age = deep_get(data, ["user", "profile", "age"], default="N/A")
print(age) # 34
# Example with missing key
phone = deep_get(data, ["user", "profile", "phone"], default="No phone")
print(phone) # "No phone"
Transformations de données en plusieurs étapes avec map() et filter()
Vous pouvez associer reduce() à d'autres outils fonctionnels, tels que map() et filter(), afin de créer des transformations de données en plusieurs étapes. Voici notre pipeline de prétraitement NLP précédent, rédigé de manière fonctionnelle.
from functools import reduce
import re
# Define preprocessing steps
def strip_punctuation(s):
return re.sub(r"[^\w\s]", "", s)
def to_lower(s):
return " ".join(map(str.lower, s.split()))
def remove_stopwords(s):
stops = {"the", "is", "a", "of"}
return " ".join(filter(lambda w: w not in stops, s.split()))
def stem_words(s):
# trivial "stemmer": cut off 'ing'
return " ".join(map(lambda w: w[:-3] if w.endswith("ing") else w, s.split()))
# Pipeline of transformations
pipeline = [
strip_punctuation,
to_lower,
remove_stopwords,
stem_words,
]
# Input data
text = "The quick brown fox is Jumping over a log."
# Apply pipeline with reduce
processed = reduce(lambda acc, f: f(acc), pipeline, text)
print(processed) # quick brown fox jump over log
Les transformations sont les suivantes :
- Supprimer la ponctuation. « Le renard brun rapide saute par-dessus une bûche. » → « Le renard brun rapide saute par-dessus une bûche. »
- Minuscules
« Le renard brun rapide saute par-dessus une bûche » → « le renard brun rapide saute par-dessus une bûche »
- Supprimer les mots vides. « Le renard brun rapide saute par-dessus une bûche » → « Renard brun rapide sautant par-dessus une bûche »
- Mots racines
« rapide renard brun sautant sur une bûche » → « rapide renard brun saute sur une bûche »
Pour approfondir vos connaissances en matière de programmation fonctionnelle et de vectorisation, nous vous recommandons ces articles DataCamp.
- Fonction filter() en Python : Conservez ce dont vous avez besoin - tutoriel
- Groupby, split-apply-combine et pandas - tutoriel
- Tutoriel Pandas Apply - tutoriel
Intégration avec l'écosystème Python moderne
Pour utiliser efficacement l'reduce(), il est utile de comprendre comment il s'intègre dans l'écosystème Python moderne. Explorons comment il s'intègre à NumPy et pandas, comment il soutient les systèmes parallèles et distribués, et comment il interagit avec les outils modernes tels que les analyseurs statiques.
numpy et pandas
NumPy et pandas fonctionnent à partir d'un code C optimisé, il est donc déconseillé de reproduire leurs fonctionnalités avec reduce(). Cependant, reduce() constitue un choix judicieux pour les pipelines comportant des étapes dynamiques. Par exemple, vous pourriez composer de nombreuses transformations NumPy sur un tableau.
from functools import reduce
import numpy as np
def standardize(x):
return (x - x.mean()) / (x.std() + 1e-9)
def clip(x):
return np.clip(x, 0, 1)
def log(x):
return np.log1p(x)
x = np.array([1, 500, 40.5, 100, 250.45])
funcs = [standardize, clip, log]
y = reduce(lambda a, f: f(a), funcs, x) # x is ndarray
Cadres informatiques parallèles et distribués
La réduction est un élément central des cadres informatiques parallèles et distribués. Ces systèmes fonctionnent en divisant un ensemble de données en partitions, en traitant ces partitions en parallèle, puis en combinant ces résultats partiels en une seule réponse. L'étape de « combinaison » correspond à la réduction.
Pour que la réduction fonctionne correctement dans un cluster, veuillez vous assurer que les conditions suivantes sont remplies.
- Associativité. L'opération doit donner le même résultat quel que soit le regroupement. Cela permet de fusionner les résultats partiels dans n'importe quel ordre à travers le réseau.
- Élément identitaire. L'opération doit comporter un initialiseur qui n'affecte pas le résultat final. Cela garantit l'exactitude lorsque les partitions sont vides ou de taille inégale.
Si ces propriétés ne sont pas respectées, les réductions deviennent lentes (car elles ne peuvent pas être parallélisées en toute sécurité) ou incorrectes (car les résultats dépendent de l'ordre d'évaluation).
Ce processus nécessite une opération associative et une identité claire. Dans le cas contraire, vous obtiendrez un code lent ou des résultats incorrects.
Outils d'analyse statique
Un analyseur statique (tel que mypy, Pyright, ruff ou bandit) est un outil qui inspecte le code sans l'exécuter. Ces outils détectent les erreurs, appliquent les règles de style et vérifient l'exactitude des types.
Les analyseurs statiques rencontrent des difficultés avec l'reduce(). Dans le processus de pliage, le type d'accumulateur peut différer du type d'élément, et l'inférence de type devient confuse.
Veuillez examiner ce code.
from functools import reduce
def add_chars(acc, word):
acc.extend(word) # extend adds each character of the string
return acc
chars = reduce(add_chars, ["hi", "ok"], [])
print(chars) # ['h', 'i', 'o', 'k']
Même si ce code fonctionne correctement, un analyseur statique pourrait signaler quelques problèmes.
- Le type d'élément de l'initialiseur [] est ambigu.
- Les analyseurs peuvent supposer que les types d'accumulateur et d'élément correspondent.
Afin de rendre le code plus clair pour les humains et les analyseurs, veuillez ajouter des indications de type.
from functools import reduce
from typing import List
def add_chars(acc: List[str], word: str) -> List[str]:
acc.extend(word) # extend adds each character of the string
return acc
chars: List[str] = reduce(add_chars, ["hi", "ok"], [])
print(chars) # ['h', 'i', 'o', 'k']
Le réducteur affiche désormais explicitement
- L'accumulateur est un dispositif de stockage d'énergie (
List[str]). - Chaque élément est un
str. - Le type de retour est un objet de type «
list[str]».
Grâce à ces indications, les analyseurs statiques peuvent vérifier le code de manière rigoureuse.
Conclusion
La fonction « reduce » provient de la programmation fonctionnelle, où la consolidation de collections en un seul résultat constitue un concept central. Même s'il ne s'agit plus d'un outil de premier ordre en Python, il a toujours sa place. Lorsque vous avez besoin de pipelines flexibles, de pliages personnalisés ou d'opérations qui ne correspondent pas parfaitement aux fonctions existantes, reduce() est un outil puissant. Utilisé avec précaution, il s'intègre parfaitement à l'écosystème Python au sens large et reste un outil pratique dans les situations appropriées.
Voici quelques liens Python connexes qui pourraient vous être utiles.
Foire aux questions sur la fonction reduce() en Python
Que fait la fonction reduce() ?
Il applique de manière répétée une fonction à deux arguments à un itérable afin de le réduire à un seul résultat.
Où est définie la fonction reduce() ?
Avant la version 3, il s'agissait d'une fonction intégrée. Depuis lors, il se trouve dans le module functools.
Quels types de fonctions dois-je transmettre à reduce() ?
Fonctions simples, associatives et bien documentées. Veuillez éviter les effets secondaires et la logique non associative.
Comment se compare-t-il à itertools.accumulate() ?
reduce() ne renvoie que le résultat final, tandis que accumulate() fournit tous les résultats intermédiaires.
Quand est-il approprié d'utiliser un initialiseur ?
Veuillez utiliser un initialiseur lorsque l'itérable peut être vide.