Объединение операций со списком в Python
Я поставил следующую надуманную задачу в курсе сравнительных языков программирования, чтобы дать студентам возможность попрактиковаться в «потоковой передаче»:
Функция записи, которая возвращает десять лучших игроков по очкам за игру среди игроков, сыгравших в 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 "Должен быть один - и желательно только один - очевидный способ сделать это. Хотя этот способ может быть неочевидным сначала, если только вы не Голландский."
Ответы
В каком-то смысле это больше «потоковое»:
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}
Эти шаги можно записать в одном понимании - что-то вроде аналога 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]
Я с самого начала заявлю, что я не обязательно думаю, что такой «полуфункциональный» стиль «лучше», чем понимание вложенных списков в принятом ответе, которые также имеют определенную приятную атмосферу «текучести» / «цепочки», чтобы их (по словам ОП).
Тем не менее, я добавляю этот ответ, чтобы указать, что если предпочтение отдается типу полуфункционального / цепочечного стиля 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 )
)