Operações de lista de encadeamento em Python

Sep 21 2020

Atribuí o seguinte problema inventado em um curso de linguagens de programação comparativa para dar aos alunos a prática de "streaming":

Função de gravação que retorna os dez melhores jogadores por pontos por jogo entre os jogadores que estiveram em 15 jogos ou mais. A entrada para a sua função será um objeto, digitado por equipe, com uma lista de estatísticas do jogador. Cada estatística de jogador é uma matriz com o nome do jogador, o número de jogos disputados e o número total de pontos.

Um conjunto de dados de amostra é o seguinte:

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

Agora, em JavaScript, há um estilo "fluente" ou de encadeamento de método prontamente aparente:

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

No entanto, minha solução Python (abaixo) simplesmente não satisfaz da mesma forma (sou mais um programador de JavaScript). Ouvi dizer que as compreensões de lista Python são preferidas para mape filter; Acho que o Python não tem um built-in flat_mape, bem, embora você possa fazer coisas sofisticadas com ele itertools, os programas Pythônicos tendem a, eu acho, ser mais favoráveis ​​a expressões intermediárias de computação do que ao encadeamento. Então eu vim com o seguinte:

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]

Eu adoraria saber se o código está no estilo das práticas recomendadas atuais do Python. Sei que o Python é muito querido pelos cientistas de dados e esse problema, embora muito artificial, parece científico para mim, então imaginei que surgisse um conjunto de práticas recomendadas que meu código poderia não atender. Além disso, estou tendo problemas com nomes para as expressões intermediárias e não tenho certeza se a divisão das etapas é muito grosseira ou muito fina. Não tenho certeza de qual abordagem tomar para limpá-lo.

Obviamente, não é obrigatório encontrar uma solução de streaming; o que é mais importante é uma solução que melhor se adapte à (s) regra (s) do Zen do Python "Deve haver uma - e de preferência apenas uma - maneira óbvia de fazer isso. Embora essa forma possa não ser óbvia no início, a menos que você Holandês."

Respostas

17 superbrain Sep 21 2020 at 13:50

Em certo sentido, isso é mais "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'))

Suas listas with_teamse with_ppgsão totalmente calculadas e, em seguida, sortedcria outra que classifica e, em seguida, joga fora todos, exceto dez elementos dela.

My playersé um gerador de iterador, computando mais elementos dinamicamente conforme solicitado. A players = ...atribuição apenas configura o iterador, mas nada é processado ainda.

Em seguida, nlargestconsome playersum a um, mantendo apenas os 10 primeiros vistos até agora e retornando-os ordenados (em ordem decrescente). Também pode ser mais eficiente do que classificar tudo, dependendo do número de jogadores elegíveis.

Na verdade, achei suas duas primeiras etapas mais confusas do que úteis, pois você with_teamscria um resultado / formato intermediário para entender. Acho que é mais simples e fácil de ler produzir diretamente os dados do jogador a partir das estatísticas. Então, novamente, eu posso estar inclinado a isso e longe do seu porque estou acostumado com Python, que, como você disse, não é muito para encadeamento.

Aliás, aqui está uma mensagem antiga de Guido sobre algumas formas de encadeamento. Não tenho certeza se se relaciona com o que temos aqui, mas talvez seja interessante de qualquer maneira.

Usei dict(...)apenas para abreviar, mas {...}é mais rápido, então você pode querer manter o último:

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

É possível escrever essas etapas em uma única compreensão - uma espécie de análogo do Python para encadeamento em JavaScript ou Ruby. A leitura não é muito ruim se você transmitir a lógica visualmente. Sem essa atenção ao layout do código, muita carga seria colocada sobre os leitores e mantenedores.

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]

Eu provavelmente iria dividi-lo mais explicitamente nas 3 etapas: organizar os dados; peça; selecione os 10 principais.

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

Direi desde o início que não acho necessariamente que tal estilo 'semifuncional' seja "melhor" do que as compreensões de lista aninhada na resposta aceita, que também tem uma certa vibração 'fluida' / 'corrente' agradável para eles (de acordo com as palavras de OP).

No entanto, estou adicionando esta resposta para apontar que se o tipo de OP semi-funcional / estilo 'encadeamento' demonstrado via Javascript for preferido , então isso também é inteiramente possível em Python (embora possa exigir a definição de alguns funções auxiliares para habilitá-lo).

Aqui está um exemplo abaixo. Primeiro, uma vez que o python não tem um operador 'cadeia' (também conhecido como 'tubo') personalizado, criamos um muito simples (retirado daqui ):

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

Vamos também criar uma função simples, curried reduce, para que possamos executar em map -> reducevez 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

Finalmente, vamos criar versões funcionais curry de algumas funções padrão que queremos usar. Observe que isso não é necessário, e os lambdas definidos aqui poderiam ter sido despejados diretamente na 'cadeia', mas predefini-los aqui torna as coisas muito mais fáceis de ver, e eu escolhi esses nomes / funções para que sejam diretamente comparáveis ​​a a funcionalidade do código javascript na questão:

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

Munidos do que foi dito acima, podemos recriar o estilo de encadeamento de métodos "fluente" equivalente em python. Parece quase idêntico:

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