Chaînage des opérations de liste en Python
J'ai assigné le problème artificiel suivant dans un cours de langages de programmation comparatifs pour permettre aux étudiants de s'exercer au "streaming":
Fonction d'écriture qui renvoie les dix meilleurs joueurs par points par partie parmi les joueurs qui ont participé à 15 parties ou plus. L'entrée de votre fonction sera un objet, saisi par équipe, avec une liste de statistiques des joueurs. Chaque statistique de joueur est un tableau avec le nom du joueur, le nombre de parties jouées et le nombre total de points.
Un exemple d'ensemble de données est le suivant:
stats = {
'ATL': [
['Betnijah Laney', 16, 263],
['Courtney Williams', 14, 193],
],
'CHI': [
['Kahleah Copper', 17, 267],
['Allie Quigley', 17, 260],
['Courtney Vandersloot', 17, 225],
],
'CONN': [
['DeWanna Bonner', 16, 285],
['Alyssa Thomas', 16, 241],
],
'DAL': [
['Arike Ogunbowale', 16, 352],
['Satou Sabally', 12, 153],
],
'IND': [
['Kelsey Mitchell', 16, 280],
['Tiffany Mitchell', 13, 172],
['Candice Dupree', 16, 202],
],
'LA': [
['Nneka Ogwumike', 14, 172],
['Chelsea Gray', 16, 224],
['Candace Parker', 16, 211],
],
'LV': [
['A’ja Wilson', 15, 304],
['Dearica Hamby', 15, 188],
['Angel McCoughtry', 15, 220],
],
'MIN': [
['Napheesa Collier', 16, 262],
['Crystal Dangerfield', 16, 254],
],
'NY': [
['Layshia Clarendon', 15, 188]
],
'PHX': [
['Diana Taurasi', 13, 236],
['Brittney Griner', 12, 212],
['Skylar Diggins-Smith', 16, 261],
['Bria Hartley', 13, 190],
],
'SEA': [
['Breanna Stewart', 16, 317],
['Jewell Loyd', 16, 223],
],
'WSH': [
['Emma Meesseman', 13, 158],
['Ariel Atkins', 15, 212],
['Myisha Hines-Allen', 15, 236],
],
}
Désormais, en JavaScript, il existe un style "courant" ou d'enchaînement de méthodes clairement apparent:
function topTenScorers(stats) {
return Object.entries(stats)
.flatMap(([team, players]) => players.map(player => [...player, team]))
.filter(([, games, ,]) => games >= 15)
.map(([name, games, points, team]) => ({ name, ppg: points / games, team }))
.sort((p1, p2) => p2.ppg - p1.ppg)
.slice(0, 10)
}
Cependant, ma solution Python (ci-dessous) ne satisfait tout simplement pas de la même manière (je suis plus un programmeur JavaScript). J'ai entendu dire que les compréhensions de liste Python sont préférées à mapet filter; Je pense que Python n'a pas de fonction intégrée flat_mapet, bien que vous puissiez faire des choses fantaisistes avec itertools, les programmes Pythonic ont tendance, je pense, à être plus favorables au calcul d'expressions intermédiaires qu'au chaînage. J'ai donc proposé ce qui suit:
def top_ten_scorers(stats):
with_teams = [[*player, team]
for (team, players) in stats.items()
for player in players]
with_ppg = [{'name': name, 'ppg': points/games, 'team': team}
for [name, games, points, team] in with_teams
if games >= 15]
return sorted(with_ppg, key=lambda k: k['ppg'], reverse=True)[:10]
J'aimerais savoir si le code est dans le style des meilleures pratiques actuelles de Python. Je sais que Python est très apprécié des scientifiques des données, et ce problème, bien que très artificiel, me semble lié à la science des données.J'ai donc pensé qu'un ensemble de meilleures pratiques se serait produit que mon code pourrait ne pas répondre. De plus, j'ai des problèmes avec les noms des expressions intermédiaires et je ne sais pas si la répartition des étapes est trop grossière ou trop fine. Je ne sais pas quelle approche adopter pour le nettoyer.
Bien entendu, il n'est pas impératif de trouver une solution de streaming; ce qui est le plus important est une solution qui correspond le mieux aux règles Zen of Python "Il devrait y avoir une - et de préférence une seule - façon évidente de le faire. Bien que cette façon ne soit pas évidente au début, sauf si vous Néerlandais."
Réponses
C'est plus "streaming" dans un sens:
from heapq import nlargest
from operator import itemgetter
def top_ten_scorers(stats):
players = (dict(name=name, ppg=points/games, team=team)
for team, players in stats.items()
for name, games, points in players
if games >= 15)
return nlargest(10, players, key=itemgetter('ppg'))
Vos listes with_teamset with_ppgsont entièrement calculées, puis en sortedcrée une autre qu'il trie ensuite, puis vous en jetez tous les éléments sauf dix.
My playersest un itérateur de générateur, calculant plus d'éléments à la volée comme demandé. L' players = ...affectation ne configure que l'itérateur, mais rien n'est encore traité.
Puis nlargestconsomme playersun par un, en ne gardant que les 10 premiers vus jusqu'à présent et en les renvoyant triés (par ordre décroissant). Cela pourrait également être plus efficace que de tout trier, en fonction du nombre de joueurs éligibles.
En fait, j'ai trouvé vos deux premières étapes plus déroutantes qu'utiles, car vous with_teamscréez un résultat / format intermédiaire à comprendre. Je pense qu'il est plus simple et plus facile à lire de simplement produire directement les dicts du joueur à partir des statistiques. Là encore, je pourrais être biaisé vers cela et m'éloigner du vôtre parce que je suis habitué à Python, qui, comme vous le dites, n'aime pas beaucoup le chaînage.
Btw, voici un vieux message de Guido sur certaines formes de chaînage. Pas sûr que cela se rapporte à ce que nous avons ici, mais peut-être intéressant quand même.
J'ai utilisé dict(...)juste pour la brièveté, mais {...}c'est plus rapide, donc vous voudrez peut-être garder ce dernier:
Setup:
name, ppg, team = 'Betnijah Laney', 263/16, 'ATL'
Round 1:
347.041 ns dict(name=name, ppg=ppg, team=team)
128.325 ns {'name': name, 'ppg': ppg, 'team': team}
Round 2:
350.576 ns dict(name=name, ppg=ppg, team=team)
129.106 ns {'name': name, 'ppg': ppg, 'team': team}
Round 3:
347.753 ns dict(name=name, ppg=ppg, team=team)
130.734 ns {'name': name, 'ppg': ppg, 'team': team}
Il est possible d'écrire ces étapes en une seule compréhension - une sorte d'analogue Python pour le chaînage en JavaScript ou Ruby. Cela ne se lit pas trop mal si vous transmettez la logique visuellement. Sans cette attention à la mise en page du code, trop de fardeau serait placé sur les lecteurs et les responsables.
from operator import itemgetter
def top_ten_scorers(stats):
return sorted(
(
dict(
name = name,
team = team,
ppg = points / games,
)
for team, players in stats.items()
for name, games, points in players
if games >= 15
),
reverse = True,
key = itemgetter('ppg'),
)[:10]
Je le décomposerais probablement plus explicitement en 3 étapes: organiser les données; commande le; sélectionnez les 10 premiers.
def top_ten_scorers2(stats):
players = [
dict(
name = name,
team = team,
ppg = points / games,
)
for team, players in stats.items()
for name, games, points in players
if games >= 15
]
ranked = sorted(players, reverse = True, key = itemgetter('ppg'))
return ranked[:10]
Je dirai dès le début que je ne pense pas nécessairement qu'un tel style «semi-fonctionnel» soit «meilleur» que les compréhensions de liste imbriquées dans la réponse acceptée, qui ont aussi une certaine ambiance «fluide» / «chaîne» pour eux (selon les mots d'OP).
Cependant, j'ajoute cette réponse pour souligner que si le type d'OP de style semi-fonctionnel / `` chaînage '' démontré via Javascript est préféré , cela est tout à fait possible en python également (bien que cela puisse nécessiter de définir quelques fonctions d'assistance pour l'activer).
Voici un exemple ci-dessous. Premièrement, comme python n'a pas d'opérateur 'chain' (aka 'pipe') sur mesure, nous en créons un très simple (pris à partir d' ici ):
def chain( Accumulant, *Functions_list ):
for f in Functions_list: Accumulant = f( Accumulant )
return Accumulant
Créons également une fonction simple, curry reduce, afin que nous puissions effectuer à la map -> reduceplace de flatmap:
def reduce_f( Function ):
def reductor (List):
while len( List ) > 1: List.insert( 0, Function( List.pop(0), List.pop(0) ) )
return List[0]
return reductor
Enfin, créons des versions fonctionnelles et curry de quelques fonctions standard que nous voulons utiliser. Notez que ce n'est pas nécessaire, et les lambdas définis ici auraient pu être vidés directement dans la `` chaîne '', mais les prédéfinir ici rend les choses beaucoup plus faciles à regarder, et j'ai choisi ces noms / fonctions afin qu'ils soient directement comparables à la fonctionnalité de code javascript dans la question:
splat_f = lambda f: lambda t: f(*t) # explode a tuple and pass it as arguments to f
map_f = lambda f: lambda _: list( map( f, _ ) )
filter_f = lambda f: lambda _: list( filter( f, _ ) )
sorted_f = lambda f: lambda _: sorted(_, key=f )
slice_f = lambda start, stop, step=1: lambda l: l[slice(start, stop, step)]
Armés de ce qui précède, nous pouvons recréer le style de chaînage de méthodes "fluide" équivalent en python. Cela semble presque identique:
def topTenScores( stats ):
return chain( stats
, dict.items, list
, map_f( splat_f(lambda team, players: list(map(lambda player: [*player, team], players))))
, reduce_f( list.__add__ )
, filter_f( splat_f(lambda _1, games, _2, _3: games >= 15) )
, map_f( splat_f(lambda name, games, points, team:{'name':name,'ppg':points/games,'team':team}))
, sorted_f( lambda x : x['ppg'] )
, slice_f( 0, 10 )
)