Chuỗi hoạt động danh sách trong Python

Sep 21 2020

Tôi đã giao bài toán có sẵn sau đây trong một khóa học ngôn ngữ lập trình so sánh để cho sinh viên thực hành với "truyền trực tuyến":

Viết hàm trả về mười người chơi hàng đầu theo điểm mỗi trò chơi trong số những người chơi đã tham gia 15 trò chơi trở lên. Đầu vào cho chức năng của bạn sẽ là một đối tượng, được khóa theo nhóm, với danh sách thống kê người chơi. Mỗi chỉ số người chơi là một mảng với tên người chơi, số trò chơi đã chơi và tổng số điểm.

Một tập dữ liệu mẫu như sau:

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

Bây giờ trong JavaScript, có một kiểu chuỗi phương pháp hoặc "thông thạo" rất rõ ràng:

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

Tuy nhiên, giải pháp Python của tôi (bên dưới) không đáp ứng theo cách tương tự (tôi là một lập trình viên JavaScript nhiều hơn). Tôi đã nghe nói rằng khả năng hiểu danh sách Python được ưu tiên hơn mapvà filter; Tôi nghĩ rằng Python không có tích hợp sẵn flat_map, và, mặc dù bạn có thể làm những điều thú vị itertools, nhưng các chương trình Pythonic có xu hướng thuận lợi hơn cho việc tính toán các biểu thức trung gian hơn là chuỗi. Vì vậy, tôi đã nghĩ ra những điều sau:

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]

Tôi muốn biết liệu mã có theo phong cách của các phương pháp hay nhất hiện tại của Python hay không. Tôi biết Python rất được yêu thích bởi các nhà khoa học dữ liệu và vấn đề này, mặc dù rất phức tạp, nhưng tôi cảm thấy dữ liệu khoa học, vì vậy tôi đã tìm ra một tập hợp các phương pháp hay nhất mà mã của tôi có thể không đáp ứng được. Ngoài ra, tôi đang gặp sự cố với tên cho các biểu thức trung gian và không chắc liệu việc phân tích các bước quá thô hay quá tốt. Tôi không chắc nên thực hiện cách tiếp cận nào để làm sạch nó.

Tất nhiên, không bắt buộc phải tìm ra giải pháp phát trực tuyến; điều quan trọng nhất là một giải pháp phù hợp nhất với (các) quy tắc Zen của Python "Nên có một-- và tốt nhất là chỉ có một - cách thực hiện trước đó. Mặc dù cách đó có thể không rõ ràng lúc đầu trừ khi bạn Tiếng Hà Lan. "

Trả lời

17 superbrain Sep 21 2020 at 13:50

Đây là "phát trực tuyến" theo nghĩa:

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

Danh sách của bạn with_teamsvà with_ppgđược tính toán đầy đủ, sau đó sortedtạo một danh sách khác mà nó sắp xếp, rồi bạn vứt bỏ tất cả trừ mười phần tử của nó.

My playerslà một trình lặp máy phát điện, tính toán nhanh hơn các phần tử theo yêu cầu. Việc players = ...chỉ định chỉ thiết lập trình lặp, nhưng không có gì được xử lý.

Sau đó, nlargesttiêu thụ playerstừng thứ một, chỉ giữ lại 10 người hàng đầu được nhìn thấy cho đến nay và trả lại chúng được sắp xếp (theo thứ tự giảm dần). Cũng có thể hiệu quả hơn việc sắp xếp mọi thứ, tùy thuộc vào số lượng người chơi đủ điều kiện.

Tôi thực sự thấy hai bước đầu tiên của bạn khó hiểu hơn là hữu ích, vì bạn with_teamstạo ra một kết quả / định dạng trung gian để hiểu. Tôi nghĩ sẽ đơn giản hơn và dễ đọc hơn nếu chỉ trực tiếp tạo ra các số liệu thống kê của người chơi. Sau đó, một lần nữa, tôi có thể thành kiến ​​với điều này và tránh xa bạn vì tôi đã quen với Python, như bạn nói, không nhiều về chuỗi.

Btw, đây là một tin nhắn cũ từ Guido về một số hình thức xâu chuỗi. Không chắc nó liên quan đến những gì chúng ta có ở đây, nhưng có lẽ thú vị.

Tôi dict(...)chỉ sử dụng cho ngắn gọn, nhưng {...}nhanh hơn, vì vậy bạn có thể muốn giữ lại phần sau:

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

Có thể viết các bước đó bằng một cách hiểu duy nhất - loại tương tự Python để xâu chuỗi trong JavaScript hoặc Ruby. Nó không quá tệ nếu bạn truyền đạt logic một cách trực quan. Nếu không có sự chú ý đến bố cục mã, quá nhiều gánh nặng sẽ được đặt lên người đọc và người bảo trì.

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]

Tôi có thể sẽ chia nó một cách rõ ràng hơn thành 3 bước: tổ chức dữ liệu; đặt nó; chọn top 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

Tôi sẽ nói rõ ngay từ đầu rằng tôi không nhất thiết nghĩ rằng kiểu 'bán chức năng' như vậy là "tốt hơn" so với cách hiểu danh sách lồng nhau trong câu trả lời được chấp nhận, cũng có một sự rung cảm 'linh hoạt' / 'chuỗi' nhất định đối với chúng (theo lời của OP).

Tuy nhiên, tôi thêm câu trả lời này để chỉ ra rằng nếu loại kiểu bán chức năng / 'chuỗi' OP được thể hiện qua Javascript được ưa thích hơn , thì điều này cũng hoàn toàn có thể xảy ra trong python (mặc dù nó có thể yêu cầu xác định thêm một số chức năng trợ giúp để kích hoạt nó).

Đây là một ví dụ dưới đây. Đầu tiên, vì python không có toán tử 'chain' (còn gọi là 'pipe') riêng, nên chúng tôi tạo một toán tử rất đơn giản (lấy từ đây ):

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

Chúng ta cũng hãy tạo một hàm đơn giản, đơn giản reduce, để chúng ta có thể thực hiện map -> reducethay vì 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

Cuối cùng, hãy tạo các phiên bản chức năng của một vài hàm tiêu chuẩn mà chúng ta muốn sử dụng. Lưu ý rằng điều này là không cần thiết và các lambdas được xác định ở đây có thể đã được đặt trực tiếp trong 'chuỗi', nhưng việc xác định trước chúng ở đây giúp mọi thứ dễ nhìn hơn nhiều và tôi đã chọn các tên / hàm này để chúng có thể so sánh trực tiếp với chức năng mã javascript trong câu hỏi:

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

Được trang bị ở trên, chúng ta có thể tạo lại kiểu chuỗi phương pháp tương đương "thông thạo" trong python. Nó trông gần như giống hệt nhau:

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