Verketten von Listenoperationen in Python
Ich habe das folgende erfundene Problem in einem vergleichenden Programmiersprachenkurs zugewiesen, um den Schülern das Üben mit "Streaming" zu ermöglichen:
Schreibfunktion, die die zehn besten Spieler nach Punkten pro Spiel unter den Spielern zurückgibt, die in 15 oder mehr Spielen gespielt haben. Die Eingabe für Ihre Funktion ist ein vom Team eingegebenes Objekt mit einer Liste der Spielerstatistiken. Jede Spielerstatistik ist ein Array mit dem Spielernamen, der Anzahl der gespielten Spiele und der Gesamtzahl der Punkte.
Ein Beispieldatensatz lautet wie folgt:
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],
],
}
In JavaScript gibt es jetzt einen "fließenden" oder methodisch verketteten Stil, der leicht erkennbar ist:
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)
}
Meine Python-Lösung (unten) erfüllt jedoch nicht die gleiche Art und Weise (ich bin eher ein JavaScript-Programmierer). Ich habe gehört, dass Python-Listenverständnisse mapund vorgezogen werden filter; Ich denke, dass Python nicht über eine integrierte Funktion verfügt flat_map, und obwohl Sie damit ausgefallene Dinge tun könnenitertools , sind Pythonic-Programme meiner Meinung nach für die Berechnung von Zwischenausdrücken günstiger als für die Verkettung. Also habe ich mir Folgendes ausgedacht:
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]
Ich würde gerne wissen, ob der Code im Stil der aktuellen Python-Best Practices ist. Ich weiß, dass Python bei Datenwissenschaftlern sehr beliebt ist, und dieses Problem ist für mich zwar datenwissenschaftlich, fühlt sich jedoch datenwissenschaftlich an. Daher dachte ich, dass eine Reihe von Best Practices entstanden wäre, die mein Code möglicherweise nicht erfüllt. Außerdem habe ich Probleme mit Namen für die Zwischenausdrücke und bin mir nicht sicher, ob die Aufteilung der Schritte zu grob oder zu fein ist. Ich bin mir nicht sicher, welchen Ansatz ich wählen soll, um es zu bereinigen.
Natürlich ist es nicht zwingend erforderlich, eine Streaming-Lösung zu finden. Am wichtigsten ist eine Lösung, die am besten zu den Zen of Python-Regeln passt. "Es sollte einen - und vorzugsweise nur einen - offensichtlichen Weg geben, dies zu tun. Obwohl dieser Weg zunächst möglicherweise nicht offensichtlich ist, es sei denn, Sie sind es Niederländisch."
Antworten
Dies ist in gewissem Sinne mehr "Streaming":
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'))
Ihre with_teamsund with_ppgsind vollständig berechnete Listen und sortederstellen dann eine weitere, die dann sortiert wird, und dann werfen Sie alle bis auf zehn Elemente weg.
My playersist ein Generator-Iterator, der nach Bedarf weitere Elemente im laufenden Betrieb berechnet. Die players = ...Zuweisung richtet nur den Iterator ein, es wird jedoch noch nichts verarbeitet.
Dann nlargestverbraucht playerseins nach dem anderen, nur die Top 10 bis jetzt gesehen zu halten und zurückkehr sie sortiert (in absteigender Reihenfolge). Könnte auch effizienter sein als alles zu sortieren, abhängig von der Anzahl der berechtigten Spieler.
Ich fand Ihre ersten beiden Schritte eher verwirrend als hilfreich, da Sie with_teamsein Zwischenergebnis / Format erstellen, um es zu verstehen. Ich denke, es ist einfacher und leichter zu lesen, die Spielerdiktate direkt aus den Statistiken zu erstellen. Andererseits könnte ich dazu voreingenommen sein und mich von Ihnen entfernen, weil ich an Python gewöhnt bin, das, wie Sie sagen, nicht viel mit Verkettung zu tun hat.
Übrigens, hier ist eine alte Nachricht von Guido über einige Formen der Verkettung. Ich bin mir nicht sicher, ob es sich auf das bezieht, was wir hier haben, aber vielleicht trotzdem interessant.
Ich habe es dict(...)nur der Kürze halber verwendet, aber es {...}ist schneller, daher möchten Sie vielleicht letzteres behalten:
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}
Es ist möglich, diese Schritte in einem einzigen Verständnis zu schreiben - einer Art Python-Analogon für die Verkettung in JavaScript oder Ruby. Es liest sich nicht schlecht, wenn Sie die Logik visuell vermitteln. Ohne diese Aufmerksamkeit für das Codelayout würden die Leser und Betreuer zu stark belastet.
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]
Ich würde es wahrscheinlich expliziter in die drei Schritte aufteilen: Daten organisieren; Bestell Es; Wählen Sie die Top 10.
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]
Ich werde von Anfang an feststellen, dass ich nicht unbedingt denke, dass ein solcher "semi-funktionaler" Stil "besser" ist als die verschachtelten Listenverständnisse in der akzeptierten Antwort, die auch eine gewisse nette "flüssige" / "Ketten" -Stimmung haben sie (gemäß den Worten von OP).
Ich füge diese Antwort jedoch hinzu, um darauf hinzuweisen, dass dies auch in Python durchaus möglich ist , wenn die über Javascript demonstrierte Art des semi-funktionalen OP / Verkettungsstils bevorzugt wird (obwohl möglicherweise einige zusätzliche definiert werden müssen) Hilfsfunktionen, um es zu aktivieren).
Hier ist ein Beispiel unten. Da Python keinen maßgeschneiderten 'Chain'-Operator (auch bekannt als' Pipe'-Operator) hat, erstellen wir zunächst einen sehr einfachen (von hier übernommen ):
def chain( Accumulant, *Functions_list ):
for f in Functions_list: Accumulant = f( Accumulant )
return Accumulant
Lassen Sie uns auch eine einfache Curry- reduceFunktion erstellen , damit wir map -> reducestatt 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
Lassen Sie uns abschließend funktionale, Curry-Versionen einiger Standardfunktionen erstellen, die wir verwenden möchten. Beachten Sie, dass dies nicht erforderlich ist und die hier definierten Lambdas möglicherweise direkt in der 'Kette' abgeladen wurden. Die Vordefinition hier erleichtert die Arbeit für das Auge erheblich. Ich habe diese Namen / Funktionen so gewählt, dass sie direkt mit denen vergleichbar sind die Javascript-Code-Funktionalität in der Frage:
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)]
Mit dem oben Gesagten können wir den äquivalenten "fließenden" Methodenkettenstil in Python neu erstellen. Es sieht fast identisch aus:
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 )
)