Pythonでのリスト操作の連鎖
学生に「ストリーミング」の練習をさせるために、比較プログラミング言語コースで次の不自然な問題を割り当てました。
15ゲーム以上のプレイヤーのうち、上位10名をゲームあたりのポイントで返す書き込み機能。関数への入力は、プレーヤーの統計情報のリストを含む、チームによってキー設定されたオブジェクトになります。各プレーヤーの統計は、プレーヤー名、プレイしたゲームの数、および合計ポイント数の配列です。
サンプルデータセットは次のとおりです。
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、Pythonプログラムは、連鎖よりも中間式の計算に適している傾向があると思います。だから私は次のことを思いついた:
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のルールに最適なソリューションです。「それを行うための明白な方法は1つ、できれば1つだけである必要があります。ただし、最初はその方法は明らかではないかもしれませんが、オランダの。"
回答
これはある意味でより「ストリーミング」です。
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、それがその後、ソートすることを別のものを作成して、あなたはそれのすべてが、10個の要素を捨てます。
私playersはジェネレータイテレータであり、要求に応じてその場でより多くの要素を計算します。players = ...割り当ては唯一のイテレータを設定しますが、何もまだ処理されなかっます。
次にnlargest、players1つずつ消費し、これまでに表示された上位10のみを保持し、並べ替えて(降順で)返します。対象となるプレーヤーの数によっては、すべてを並べ替えるよりも効率的である可能性もあります。
あなたwith_teamsが理解するための中間の結果/フォーマットを作成するので、私は実際にあなたの最初の2つのステップが役に立つよりも混乱していると感じました。統計からプレイヤーの口述を直接生成する方が簡単で読みやすいと思います。繰り返しになりますが、私はPythonに慣れているので、これに偏ってあなたから離れているかもしれません。Pythonは、あなたが言うように、連鎖にはあまり興味がありません。
ところで、これはチェーンのいくつかの形式についてのGuidoからの古いメッセージです。それが私たちがここに持っているものに関連しているかどうかはわかりませんが、とにかく興味深いかもしれません。
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}
これらのステップを単一の理解で書くことができます-JavaScriptまたはRubyでチェーンするためのPythonアナログのようなものです。論理を視覚的に伝えれば、それほど悪くはありません。コードのレイアウトに注意を払わないと、リーダーとメンテナーに過度の負担がかかります。
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を介して示される半機能/「チェーン」スタイルのOPの種類が望ましい場合、これは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 )
)