การดำเนินรายการเชื่อมโยงใน 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 = ...มอบหมายเพียงตั้งค่า iterator แต่ไม่มีอะไรได้รับการประมวลผล
จากนั้นnlargestกินplayersทีละอย่างโดยเก็บเฉพาะ 10 อันดับแรกที่เห็นจนถึงตอนนี้และส่งกลับมาเรียงลำดับ (เรียงจากมากไปหาน้อย) อาจมีประสิทธิภาพมากกว่าการเรียงลำดับทุกอย่างขึ้นอยู่กับจำนวนผู้เล่นที่มีสิทธิ์
ฉันพบว่าสองขั้นตอนแรกของคุณสับสนมากกว่าที่เป็นประโยชน์เนื่องจากคุณwith_teamsสร้างผลลัพธ์ / รูปแบบกลางเพื่อทำความเข้าใจ ฉันคิดว่ามันง่ายกว่าและง่ายกว่าในการอ่านเพียงแค่สร้างคำสั่งของผู้เล่นจากสถิติโดยตรง จากนั้นอีกครั้งฉันอาจจะเอนเอียงไปทางนี้และอยู่ห่างจากคุณเพราะฉันคุ้นเคยกับ Python ซึ่งอย่างที่คุณพูดไม่ค่อยผูกมัด
Btw นี่คือข้อความเก่าจาก 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}
เป็นไปได้ที่จะเขียนขั้นตอนเหล่านั้นด้วยความเข้าใจเพียงครั้งเดียว - ประเภทของอะนาล็อก 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)
แต่ฉันเพิ่มคำตอบนี้จะชี้ให้เห็นว่าถ้าชนิดของ / กึ่งทำงาน 'ผูกมัด' OP สไตล์แสดงให้เห็นผ่านทางจาวาสคริปต์ที่จะแนะนำนี้แล้วเป็นไปได้ทั้งหมดในหลามเช่นกัน (แม้ว่ามันอาจต้องกำหนดคู่ของพิเศษ ฟังก์ชันตัวช่วยเพื่อเปิดใช้งาน)
นี่คือตัวอย่างด้านล่าง อันดับแรกเนื่องจาก python ไม่มีตัวดำเนินการ 'chain' (aka 'pipe') ที่ต้องการเราจึงสร้างตัวดำเนินการที่ง่ายมาก (นำมาจากที่นี่ ):
def chain( Accumulant, *Functions_list ):
for f in Functions_list: Accumulant = f( Accumulant )
return Accumulant
นอกจากนี้เรายังสร้างreduceฟังก์ชันที่เรียบง่าย curried เพื่อให้เราสามารถดำเนินการ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
ในที่สุดเรามาสร้างฟังก์ชันมาตรฐานสองสามเวอร์ชันที่ใช้งานได้ซึ่งเราต้องการใช้ โปรดทราบว่าสิ่งนี้ไม่จำเป็นและ lambdas ที่กำหนดไว้ที่นี่อาจถูกทิ้งลงใน 'chain' โดยตรง แต่การกำหนดไว้ล่วงหน้าที่นี่ทำให้สิ่งต่าง ๆ ง่ายต่อการมองเห็นมากขึ้นและฉันได้เลือกชื่อ / ฟังก์ชั่นเหล่านี้เพื่อให้เทียบเคียงโดยตรงกับ การทำงานของรหัสจาวาสคริปต์ในคำถาม:
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 )
)