Объединение операций со списком в Python

Sep 21 2020

Я поставил следующую надуманную задачу в курсе сравнительных языков программирования, чтобы дать студентам возможность попрактиковаться в «потоковой передаче»:

Функция записи, которая возвращает десять лучших игроков по очкам за игру среди игроков, сыгравших в 15 или более играх. Входными данными для вашей функции будет объект, управляемый командой, со списком характеристик игрока. Каждая статистика игрока представляет собой массив с именем игрока, количеством сыгранных игр и общим количеством очков.

Примерный набор данных выглядит следующим образом:

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

Теперь в JavaScript есть «беглый» стиль или стиль цепочки методов:

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

Однако мое решение Python (ниже) просто не удовлетворяет тому же самому (я больше программист на JavaScript). Я слышал, что понимание списков Python предпочтительнее mapи filter; Я думаю, что у Python нет встроенного flat_map, и, хотя вы можете делать что-то необычное itertools, программы Pythonic, как мне кажется, более предпочтительны для вычисления промежуточных выражений, чем для связывания. Итак, я придумал следующее:

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]

Я хотел бы знать, соответствует ли код текущим лучшим практикам Python. Я знаю, что Python очень нравится специалистам по данным, и эта проблема, хотя и очень надуманная, кажется мне наукой о данных, поэтому я решил, что возникнет набор лучших практик, которым мой код может не соответствовать. Кроме того, у меня проблемы с именами промежуточных выражений, и я не уверен, является ли разбивка шагов слишком грубой или слишком точной. Я не уверен, какой подход использовать, чтобы очистить его.

Конечно, решение для потоковой передачи не обязательно; что наиболее важно, так это решение, которое наилучшим образом соответствует правилу (ам) Zen of Python "Должен быть один - и желательно только один - очевидный способ сделать это. Хотя этот способ может быть неочевидным сначала, если только вы не Голландский."

Ответы

17 superbrain Sep 21 2020 at 13:50

В каком-то смысле это больше «потоковое»:

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

Ваши with_teamsи with_ppgявляются полностью вычисленными списками, а затем sortedсоздает еще один, который затем сортирует, а затем вы выбрасываете все, кроме десяти его элементов.

My players- итератор генератора, вычисляющий больше элементов на лету по запросу. players = ...Задание только устанавливает итератор, но ничего пока не получает обрабатываются.

Затем nlargestпотребляет playersодин за другим, оставляя только первые 10 видимых на данный момент и возвращая их отсортированными (в порядке убывания). Это также может быть более эффективным, чем сортировка всего, в зависимости от количества подходящих игроков.

На самом деле я нашел ваши первые два шага более запутанными, чем полезными, поскольку вы with_teamsсоздаете промежуточный результат / формат для понимания. Я думаю, что проще и легче читать, просто создавать диктовки игрока из статистики. С другой стороны, я могу быть предвзятым в отношении этого и от вашего, потому что я привык к Python, который, как вы говорите, не очень подходит для цепочки.

Кстати, вот старое сообщение от Гвидо о некоторых формах связывания. Не уверен, что это связано с тем, что у нас есть, но, возможно, все равно интересно.

Я использовал dict(...)только для краткости, но {...}он быстрее, поэтому вы можете оставить последнее:

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

Эти шаги можно записать в одном понимании - что-то вроде аналога Python для связывания в JavaScript или Ruby. Он читается не так уж плохо, если визуально передать логику. Без такого внимания к макету кода на читателей и сопровождающих будет возложена слишком большая нагрузка.

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]

Я бы, вероятно, разбил это более подробно на 3 этапа: организация данных; заказать; выберите топ-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

Я с самого начала заявлю, что я не обязательно думаю, что такой «полуфункциональный» стиль «лучше», чем понимание вложенных списков в принятом ответе, которые также имеют определенную приятную атмосферу «текучести» / «цепочки», чтобы их (по словам ОП).

Тем не менее, я добавляю этот ответ, чтобы указать, что если предпочтение отдается типу полуфункционального / цепочечного стиля OP, продемонстрированного через Javascript , то это вполне возможно и в python (хотя для этого может потребоваться определение нескольких дополнительных вспомогательные функции, чтобы включить его).

Вот пример ниже. Во-первых, поскольку в python нет специального оператора «цепочка» (он же «труба»), мы создаем очень простой (взятый отсюда ):

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

Давайте также создадим простую каррированную reduceфункцию, чтобы мы могли выполнять map -> reduceвместо 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

Наконец, давайте создадим функциональные каррированные версии пары стандартных функций, которые мы хотим использовать. Обратите внимание, что в этом нет необходимости, и определенные здесь лямбды могли быть сброшены непосредственно в `` цепочку '', но их предварительное определение здесь значительно упрощает работу, и я выбрал эти имена / функции, чтобы они были напрямую сопоставимы с функциональность кода javascript в вопросе:

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

Вооружившись вышесказанным, мы можем воссоздать эквивалентный «плавный» стиль цепочки методов в python. Выглядит почти идентично:

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