การดำเนินรายการเชื่อมโยงใน 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 = ...มอบหมายเพียงตั้งค่า 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}
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)

แต่ฉันเพิ่มคำตอบนี้จะชี้ให้เห็นว่าถ้าชนิดของ / กึ่งทำงาน 'ผูกมัด' 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 )
  )