Concatenare le operazioni sugli elenchi in Python

Sep 21 2020

Ho assegnato il seguente problema artificioso in un corso di linguaggi di programmazione comparativi per far fare pratica agli studenti con lo "streaming":

Funzione di scrittura che restituisce i primi dieci giocatori in base ai punti per partita tra i giocatori che hanno partecipato a 15 o più partite. L'input per la tua funzione sarà un oggetto, digitato dalla squadra, con un elenco di statistiche del giocatore. Ogni statistica del giocatore è un array con il nome del giocatore, il numero di partite giocate e il numero totale di punti.

Un set di dati di esempio è il seguente:

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],
    ],
}

Ora in JavaScript, è immediatamente evidente uno stile "fluente" o di concatenamento di metodi:

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)
}

Tuttavia, la mia soluzione Python (sotto) non soddisfa allo stesso modo (sono più un programmatore JavaScript). Ho sentito che le comprensioni di elenchi di Python sono preferite a mape filter; Penso che Python non abbia un built-in flat_mape, beh, anche se puoi fare cose fantasiose con itertools, i programmi Pythonic tendono, penso, ad essere più favorevoli al calcolo di espressioni intermedie che al concatenamento. Quindi ho pensato quanto segue:

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]

Mi piacerebbe sapere se il codice è nello stile delle attuali migliori pratiche di Python. So che Python è molto amato dai data scientist e questo problema, sebbene molto artificioso, mi sembra una scienza dei dati, quindi ho pensato che sarebbe nata una serie di best practice che il mio codice potrebbe non soddisfare. Inoltre, ho problemi con i nomi per le espressioni intermedie e non sono sicuro che la suddivisione dei passaggi sia troppo grossolana o troppo fine. Non sono sicuro di quale approccio adottare per ripulirlo.

Naturalmente, non è imperativo trovare una soluzione di streaming; la cosa più importante è una soluzione che si adatti meglio alle regole Zen of Python "Dovrebbe esserci un modo ovvio, e preferibilmente solo uno, per farlo. Anche se all'inizio potrebbe non essere ovvio a meno che tu non sia Olandese."

Risposte

17 superbrain Sep 21 2020 at 13:50

Questo è più "streaming" in un certo senso:

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'))

Il tuo with_teamse with_ppgsono elenchi completamente calcolati, quindi ne sortedcrea un altro che poi ordina, e poi butti via tutti gli elementi tranne dieci.

My playersè un generatore iteratore, che calcola più elementi al volo come richiesto. L' players = ...assegnazione imposta solo l'iteratore, ma non viene ancora elaborato nulla.

Quindi nlargestconsuma playersuno per uno, mantenendo solo i primi 10 visti finora e restituendoli ordinati (in ordine decrescente). Potrebbe anche essere più efficiente dell'ordinamento di tutto, a seconda del numero di giocatori idonei.

In realtà ho trovato i tuoi primi due passaggi più confusi che utili, poiché with_teamscrea un risultato / formato intermedio da comprendere. Penso che sia più semplice e più facile da leggere produrre direttamente i dettami del giocatore dalle statistiche. Poi di nuovo, potrei essere prevenuto verso questo e lontano dal tuo perché sono abituato a Python, che, come dici tu, non è molto legato al concatenamento.

A proposito, ecco un vecchio messaggio di Guido su alcune forme di concatenamento. Non sono sicuro che si riferisca a quello che abbiamo qui, ma forse è comunque interessante.

L'ho usato dict(...)solo per brevità, ma {...}è più veloce, quindi potresti voler mantenere quest'ultimo:

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}
6 FMc Sep 21 2020 at 09:49

È possibile scrivere questi passaggi in un'unica comprensione, una sorta di analogo di Python per il concatenamento in JavaScript o Ruby. Non si legge troppo male se trasmetti la logica visivamente. Senza quell'attenzione al layout del codice, troppo carico sarebbe posto sui lettori e sui manutentori.

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]

Probabilmente lo suddividerei più esplicitamente nei 3 passaggi: organizzare i dati; Ordinalo; seleziona i primi 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]
1 TasosPapastylianou Dec 19 2020 at 15:40

Affermerò dall'inizio che non penso necessariamente che un tale stile 'semifunzionale' sia "migliore" delle comprensioni di elenchi annidati nella risposta accettata, che hanno anche una certa atmosfera 'fluida' / 'catena' per loro (come da parole di OP).

Tuttavia, sto aggiungendo questa risposta per sottolineare che se si preferisce il tipo di OP in stile semi-funzionale / 'concatenato' dimostrato tramite Javascript , allora questo è del tutto possibile anche in python (sebbene potrebbe richiedere la definizione di un paio di extra funzioni di supporto per abilitarlo).

Di seguito è riportato un esempio. Innanzitutto, poiché python non ha un operatore "chain" (noto anche come "pipe"), ne creiamo uno molto semplice (preso da qui ):

def chain( Accumulant, *Functions_list ):
    for f in Functions_list: Accumulant = f( Accumulant )
    return Accumulant

Creiamo anche una funzione semplice, curata reduce, in modo da poter eseguire map -> reduceinvece di 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

Infine, creiamo versioni funzionali e curate di un paio di funzioni standard che vogliamo utilizzare. Si noti che questo non è necessario e gli lambda definiti qui potrebbero essere stati scaricati direttamente nella 'catena', ma predefinirli qui rende le cose molto più facili alla vista, e ho scelto questi nomi / funzioni in modo che siano direttamente comparabili a la funzionalità del codice javascript nella domanda:

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)]

Armati di quanto sopra, possiamo ricreare l'equivalente "fluente" stile di concatenamento di metodi in Python. Sembra quasi identico:

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 )
  )