Làm cách nào để sắp xếp các đối tượng theo nhiều khóa trong Python?


97

Hoặc, trên thực tế, làm thế nào tôi có thể sắp xếp danh sách các từ điển theo nhiều khóa?

Tôi có một danh sách các phái:

b = [{u'TOT_PTS_Misc': u'Utley, Alex', u'Total_Points': 96.0},
 {u'TOT_PTS_Misc': u'Russo, Brandon', u'Total_Points': 96.0},
 {u'TOT_PTS_Misc': u'Chappell, Justin', u'Total_Points': 96.0},
 {u'TOT_PTS_Misc': u'Foster, Toney', u'Total_Points': 80.0},
 {u'TOT_PTS_Misc': u'Lawson, Roman', u'Total_Points': 80.0},
 {u'TOT_PTS_Misc': u'Lempke, Sam', u'Total_Points': 80.0},
 {u'TOT_PTS_Misc': u'Gnezda, Alex', u'Total_Points': 78.0},
 {u'TOT_PTS_Misc': u'Kirks, Damien', u'Total_Points': 78.0},
 {u'TOT_PTS_Misc': u'Worden, Tom', u'Total_Points': 78.0},
 {u'TOT_PTS_Misc': u'Korecz, Mike', u'Total_Points': 78.0},
 {u'TOT_PTS_Misc': u'Swartz, Brian', u'Total_Points': 66.0},
 {u'TOT_PTS_Misc': u'Burgess, Randy', u'Total_Points': 66.0},
 {u'TOT_PTS_Misc': u'Smugala, Ryan', u'Total_Points': 66.0},
 {u'TOT_PTS_Misc': u'Harmon, Gary', u'Total_Points': 66.0},
 {u'TOT_PTS_Misc': u'Blasinsky, Scott', u'Total_Points': 60.0},
 {u'TOT_PTS_Misc': u'Carter III, Laymon', u'Total_Points': 60.0},
 {u'TOT_PTS_Misc': u'Coleman, Johnathan', u'Total_Points': 60.0},
 {u'TOT_PTS_Misc': u'Venditti, Nick', u'Total_Points': 60.0},
 {u'TOT_PTS_Misc': u'Blackwell, Devon', u'Total_Points': 60.0},
 {u'TOT_PTS_Misc': u'Kovach, Alex', u'Total_Points': 60.0},
 {u'TOT_PTS_Misc': u'Bolden, Antonio', u'Total_Points': 60.0},
 {u'TOT_PTS_Misc': u'Smith, Ryan', u'Total_Points': 60.0}]

và tôi cần sử dụng sắp xếp nhiều khóa được đảo ngược bởi Total_Points, sau đó không được đảo ngược bởi TOT_PTS_Misc.

Điều này có thể được thực hiện tại dấu nhắc lệnh như sau:

a = sorted(b, key=lambda d: (-d['Total_Points'], d['TOT_PTS_Misc']))

Nhưng tôi phải chạy điều này thông qua một hàm, nơi tôi chuyển vào danh sách và các phím sắp xếp. Ví dụ def multikeysort(dict_list, sortkeys):,.

Làm cách nào để sử dụng dòng lambda sẽ sắp xếp danh sách, cho một số khóa tùy ý được chuyển vào hàm multikeysort và lưu ý rằng các khóa sắp xếp có thể có bất kỳ số lượng khóa nào và những khóa cần sắp xếp ngược lại sẽ được xác định với dấu '-' trước nó?

Câu trả lời:


72

Câu trả lời này phù hợp với bất kỳ loại cột nào trong từ điển - cột bị phủ định không cần phải là một số.

def multikeysort(items, columns):
    from operator import itemgetter
    comparers = [((itemgetter(col[1:].strip()), -1) if col.startswith('-') else
                  (itemgetter(col.strip()), 1)) for col in columns]
    def comparer(left, right):
        for fn, mult in comparers:
            result = cmp(fn(left), fn(right))
            if result:
                return mult * result
        else:
            return 0
    return sorted(items, cmp=comparer)

Bạn có thể gọi nó như thế này:

b = [{u'TOT_PTS_Misc': u'Utley, Alex', u'Total_Points': 96.0},
     {u'TOT_PTS_Misc': u'Russo, Brandon', u'Total_Points': 96.0},
     {u'TOT_PTS_Misc': u'Chappell, Justin', u'Total_Points': 96.0},
     {u'TOT_PTS_Misc': u'Foster, Toney', u'Total_Points': 80.0},
     {u'TOT_PTS_Misc': u'Lawson, Roman', u'Total_Points': 80.0},
     {u'TOT_PTS_Misc': u'Lempke, Sam', u'Total_Points': 80.0},
     {u'TOT_PTS_Misc': u'Gnezda, Alex', u'Total_Points': 78.0},
     {u'TOT_PTS_Misc': u'Kirks, Damien', u'Total_Points': 78.0},
     {u'TOT_PTS_Misc': u'Worden, Tom', u'Total_Points': 78.0},
     {u'TOT_PTS_Misc': u'Korecz, Mike', u'Total_Points': 78.0},
     {u'TOT_PTS_Misc': u'Swartz, Brian', u'Total_Points': 66.0},
     {u'TOT_PTS_Misc': u'Burgess, Randy', u'Total_Points': 66.0},
     {u'TOT_PTS_Misc': u'Smugala, Ryan', u'Total_Points': 66.0},
     {u'TOT_PTS_Misc': u'Harmon, Gary', u'Total_Points': 66.0},
     {u'TOT_PTS_Misc': u'Blasinsky, Scott', u'Total_Points': 60.0},
     {u'TOT_PTS_Misc': u'Carter III, Laymon', u'Total_Points': 60.0},
     {u'TOT_PTS_Misc': u'Coleman, Johnathan', u'Total_Points': 60.0},
     {u'TOT_PTS_Misc': u'Venditti, Nick', u'Total_Points': 60.0},
     {u'TOT_PTS_Misc': u'Blackwell, Devon', u'Total_Points': 60.0},
     {u'TOT_PTS_Misc': u'Kovach, Alex', u'Total_Points': 60.0},
     {u'TOT_PTS_Misc': u'Bolden, Antonio', u'Total_Points': 60.0},
     {u'TOT_PTS_Misc': u'Smith, Ryan', u'Total_Points': 60.0}]

a = multikeysort(b, ['-Total_Points', 'TOT_PTS_Misc'])
for item in a:
    print item

Hãy thử nó với một trong hai cột bị phủ định. Bạn sẽ thấy thứ tự sắp xếp đảo ngược.

Tiếp theo: thay đổi nó để nó không sử dụng lớp học thêm….


2016-01-17

Lấy cảm hứng của tôi từ câu trả lời này Cách tốt nhất để lấy mục đầu tiên từ một mục có thể lặp lại phù hợp với một điều kiện là gì? , Tôi đã rút ngắn mã:

from operator import itemgetter as i

def multikeysort(items, columns):
    comparers = [
        ((i(col[1:].strip()), -1) if col.startswith('-') else (i(col.strip()), 1))
        for col in columns
    ]
    def comparer(left, right):
        comparer_iter = (
            cmp(fn(left), fn(right)) * mult
            for fn, mult in comparers
        )
        return next((result for result in comparer_iter if result), 0)
    return sorted(items, cmp=comparer)

Trong trường hợp bạn thích mã của bạn ngắn gọn.


Sau 2016-01-17

Điều này hoạt động với python3 (đã loại bỏ cmpđối số sort):

from operator import itemgetter as i
from functools import cmp_to_key

def cmp(x, y):
    """
    Replacement for built-in function cmp that was removed in Python 3

    Compare the two objects x and y and return an integer according to
    the outcome. The return value is negative if x < y, zero if x == y
    and strictly positive if x > y.

    https://portingguide.readthedocs.io/en/latest/comparisons.html#the-cmp-function
    """

    return (x > y) - (x < y)

def multikeysort(items, columns):
    comparers = [
        ((i(col[1:].strip()), -1) if col.startswith('-') else (i(col.strip()), 1))
        for col in columns
    ]
    def comparer(left, right):
        comparer_iter = (
            cmp(fn(left), fn(right)) * mult
            for fn, mult in comparers
        )
        return next((result for result in comparer_iter if result), 0)
    return sorted(items, key=cmp_to_key(comparer))

Lấy cảm hứng từ câu trả lời này Làm cách nào để thực hiện sắp xếp tùy chỉnh trong Python 3?


Điều này hoạt động tốt nhất vì tôi có thể sử dụng đảo ngược trên bất kỳ phím hoặc cột nào. Cảm ơn bạn!
simi

Vì vậy, điều này hoạt động tốt. Tôi gọi hàm của mình với danh sách và chuỗi làm tham số. Tôi chia chuỗi trước rồi gọi multikeysort với danh sách và danh sách các khóa từ chuỗi đã chia. Không quan trọng mục nào trong chuỗi có dấu '-' ở đầu tên cột, vì nó sẽ hoạt động với một trong hai mục hoặc tất cả các mục. Tuyệt vời. Cảm ơn bạn.
simi

2
Cảm ơn, bạn đã cứu ngày của tôi!
Sander van Leeuwen

4
cmp()không có sẵn cho Python3, vì vậy tôi phải tự xác định nó, như đã đề cập ở đây: stackoverflow.com/a/22490617/398514
pferate

8
@hughdbrown: Bạn đã bỏ cmptừ khóa nhưng cmp()hàm vẫn được sử dụng 4 dòng trên. Tôi đã thử nó với 3.2, 3.3, 3.4 và 3.5, tất cả chúng đều không thành công khi gọi hàm, vì cmp()không được xác định. Dấu đầu dòng thứ ba ở đây ( docs.python.org/3.0/whatsnew/3.0.html#ordering-comparisons ) đề cập đến việc coi cmp()như đã biến mất.
pferate

55

Bài viết này có một tóm tắt hay về các kỹ thuật khác nhau để thực hiện việc này. Nếu yêu cầu của bạn đơn giản hơn "khóa đa hướng hai chiều đầy đủ", hãy xem. Rõ ràng là câu trả lời được chấp nhận và bài đăng trên blog mà tôi vừa tham khảo ảnh hưởng lẫn nhau theo một cách nào đó, mặc dù tôi không biết thứ tự nào.

Trong trường hợp liên kết chết, đây là tóm tắt rất nhanh về các ví dụ không được đề cập ở trên:

mylist = sorted(mylist, key=itemgetter('name', 'age'))
mylist = sorted(mylist, key=lambda k: (k['name'].lower(), k['age']))
mylist = sorted(mylist, key=lambda k: (k['name'].lower(), -k['age']))

Theo như tôi có thể nói, stygianvision sử dụng mã của tôi và không đưa ra tín dụng nào. Google choresult = cmp(fn(left), fn(right))
hughdbrown

4
Cảm ơn về phần tóm tắt, Link thực sự đã chết. :)
Amyth

49

Tôi biết đây là một câu hỏi khá cũ, nhưng không có câu trả lời nào đề cập đến việc Python đảm bảo trật tự sắp xếp ổn định cho các quy trình sắp xếp của nó, chẳng hạn như list.sort()sorted(), có nghĩa là các mục so sánh bằng nhau vẫn giữ nguyên thứ tự ban đầu của chúng.

Điều này có nghĩa là tương đương với ORDER BY name ASC, age DESC(sử dụng ký hiệu SQL) cho danh sách các từ điển có thể được thực hiện như sau:

items.sort(key=operator.itemgetter('age'), reverse=True)
items.sort(key=operator.itemgetter('name'))

Lưu ý cách sắp xếp các mục đầu tiên theo thuộc tính "lesser" age(giảm dần), sau đó theo thuộc tính "major" name, dẫn đến thứ tự cuối cùng chính xác.

Đảo ngược / đảo ngược hoạt động cho tất cả các loại có thể xác định được, không chỉ các số mà bạn có thể phủ định bằng cách đặt dấu trừ ở phía trước.

Và vì thuật toán Timsort được sử dụng trong (ít nhất) CPython, điều này thực sự khá nhanh trong thực tế.


2
rất đẹp. đối với các tập dữ liệu vừa phải mà việc sắp xếp tập hợp nhiều lần không thành vấn đề, điều này thật tuyệt! Như bạn đã chỉ ra, bạn phải đảo ngược loại python so với loại sql. Cảm ơn.
Greg

Loại thứ hai sẽ phá vỡ kết quả của loại thứ nhất. Thật buồn cười là không ai trong số những người ủng hộ nhận ra điều đó.
núi lửa

9
buồn cười là bạn đã không nhận thấy rằng tiêu chí sắp xếp chính đi sau cùng, như được hiển thị trong ví dụ của tôi, và được đề cập rõ ràng trong nhận xét khác để làm cho nó rất rõ ràng trong trường hợp bạn không nhận thấy.
wouter bolsterlee

24
def sortkeypicker(keynames):
    negate = set()
    for i, k in enumerate(keynames):
        if k[:1] == '-':
            keynames[i] = k[1:]
            negate.add(k[1:])
    def getit(adict):
       composite = [adict[k] for k in keynames]
       for i, (k, v) in enumerate(zip(keynames, composite)):
           if k in negate:
               composite[i] = -v
       return composite
    return getit

a = sorted(b, key=sortkeypicker(['-Total_Points', 'TOT_PTS_Misc']))

Chà! Điều đó thật tuyệt vời. Nó hoạt động tuyệt vời. Tôi là một người mới đến nỗi tôi cảm thấy mình sẽ không bao giờ biết được tất cả những điều này. Đó là quá nhanh. Cảm ơn rât nhiều.
simi

Tuy nhiên, điều gì sẽ xảy ra nếu các khóa được gửi đến trình chọn phím sắp xếp là một chuỗi, chẳng hạn như '-Total_Points, TOT_PTS_Misc'?
simi

1
Sau đó, bạn có thể chia chuỗi thành một mảng đầu tiên bằng cách gọisome_string.split(",")
Jason Creighton

Cảm ơn bạn. Tôi nhận ra rằng tôi có thể thực hiện tách chuỗi, sau khi tôi đã nhận xét. DOH!
simi

2
Nhưng nếu bạn phủ định giá trị chuỗi thay vì giá trị số thì sao? Tôi không nghĩ rằng điều đó sẽ hiệu quả.
Nick Perkins

5

Tôi sử dụng phần sau để sắp xếp mảng 2d trên một số cột

def k(a,b):
    def _k(item):
        return (item[a],item[b])
    return _k

Điều này có thể được mở rộng để làm việc trên một số mục tùy ý. Tôi có xu hướng nghĩ rằng việc tìm kiếm một mẫu truy cập tốt hơn vào các khóa có thể sắp xếp của bạn tốt hơn là viết một trình so sánh ưa thích.

>>> data = [[0,1,2,3,4],[0,2,3,4,5],[1,0,2,3,4]]
>>> sorted(data, key=k(0,1))
[[0, 1, 2, 3, 4], [0, 2, 3, 4, 5], [1, 0, 2, 3, 4]]
>>> sorted(data, key=k(1,0))
[[1, 0, 2, 3, 4], [0, 1, 2, 3, 4], [0, 2, 3, 4, 5]]
>>> sorted(a, key=k(2,0))
[[0, 1, 2, 3, 4], [1, 0, 2, 3, 4], [0, 2, 3, 4, 5]]

4

Hôm nay tôi gặp sự cố tương tự - tôi phải sắp xếp các mục từ điển theo giá trị số giảm dần và theo giá trị chuỗi tăng dần. Để giải quyết vấn đề về các hướng xung đột, tôi đã phủ định các giá trị số nguyên.

Đây là một biến thể của giải pháp của tôi - áp dụng cho OP

sorted(b, key=lambda e: (-e['Total_Points'], e['TOT_PTS_Misc']))

Rất đơn giản - và hoạt động như một sự quyến rũ

[{'TOT_PTS_Misc': 'Chappell, Justin', 'Total_Points': 96.0},
 {'TOT_PTS_Misc': 'Russo, Brandon', 'Total_Points': 96.0},
 {'TOT_PTS_Misc': 'Utley, Alex', 'Total_Points': 96.0},
 {'TOT_PTS_Misc': 'Foster, Toney', 'Total_Points': 80.0},
 {'TOT_PTS_Misc': 'Lawson, Roman', 'Total_Points': 80.0},
 {'TOT_PTS_Misc': 'Lempke, Sam', 'Total_Points': 80.0},
 {'TOT_PTS_Misc': 'Gnezda, Alex', 'Total_Points': 78.0},
 {'TOT_PTS_Misc': 'Kirks, Damien', 'Total_Points': 78.0},
 {'TOT_PTS_Misc': 'Korecz, Mike', 'Total_Points': 78.0},
 {'TOT_PTS_Misc': 'Worden, Tom', 'Total_Points': 78.0},
 {'TOT_PTS_Misc': 'Burgess, Randy', 'Total_Points': 66.0},
 {'TOT_PTS_Misc': 'Harmon, Gary', 'Total_Points': 66.0},
 {'TOT_PTS_Misc': 'Smugala, Ryan', 'Total_Points': 66.0},
 {'TOT_PTS_Misc': 'Swartz, Brian', 'Total_Points': 66.0},
 {'TOT_PTS_Misc': 'Blackwell, Devon', 'Total_Points': 60.0},
 {'TOT_PTS_Misc': 'Blasinsky, Scott', 'Total_Points': 60.0},
 {'TOT_PTS_Misc': 'Bolden, Antonio', 'Total_Points': 60.0},
 {'TOT_PTS_Misc': 'Carter III, Laymon', 'Total_Points': 60.0},
 {'TOT_PTS_Misc': 'Coleman, Johnathan', 'Total_Points': 60.0},
 {'TOT_PTS_Misc': 'Kovach, Alex', 'Total_Points': 60.0},
 {'TOT_PTS_Misc': 'Smith, Ryan', 'Total_Points': 60.0},
 {'TOT_PTS_Misc': 'Venditti, Nick', 'Total_Points': 60.0}]

0
from operator import itemgetter
from functools import partial

def _neg_itemgetter(key, d):
    return -d[key]

def key_getter(key_expr):
    keys = key_expr.split(",")
    getters = []
    for k in keys:
        k = k.strip()
        if k.startswith("-"):
           getters.append(partial(_neg_itemgetter, k[1:]))
        else:
           getters.append(itemgetter(k))

    def keyfunc(dct):
        return [kg(dct) for kg in getters]

    return keyfunc

def multikeysort(dict_list, sortkeys):
    return sorted(dict_list, key = key_getter(sortkeys)

Trình diễn:

>>> multikeysort([{u'TOT_PTS_Misc': u'Utley, Alex', u'Total_Points': 60.0},
                 {u'TOT_PTS_Misc': u'Russo, Brandon', u'Total_Points': 96.0}, 
                 {u'TOT_PTS_Misc': u'Chappell, Justin', u'Total_Points': 96.0}],
                "-Total_Points,TOT_PTS_Misc")
[{u'Total_Points': 96.0, u'TOT_PTS_Misc': u'Chappell, Justin'}, 
 {u'Total_Points': 96.0, u'TOT_PTS_Misc': u'Russo, Brandon'}, 
 {u'Total_Points': 60.0, u'TOT_PTS_Misc': u'Utley, Alex'}]

Việc phân tích cú pháp hơi mong manh, nhưng ít nhất nó cho phép số lượng khoảng cách giữa các phím thay đổi.


Tuy nhiên, khi tôi có mục thứ hai trong chuỗi với dấu '-', nó sẽ mang lại cho tôi một loại toán hạng xấu đối với lỗi một ngôi.
simi

Bạn không thể lấy âm của một chuỗi.
Torsten Marek

Có, tôi biết, nhưng đây là cách các tham số được truyền vào. Ngay cả khi tôi thực hiện tách, cái này hay cái kia sẽ bắt đầu bằng '-'. Tôi nghĩ rằng các phím sắp xếp cần được tách ra trước khi gọi key_getter, theo cách đó mỗi mục trong danh sách khóa sẽ kiểm tra ký tự đầu tiên. Có phải tôi đang trên đường ray bên phải không?
simi

0

Vì bạn đã cảm thấy thoải mái với lambda, đây là một giải pháp ít dài dòng hơn.

>>> def itemgetter(*names):
    return lambda mapping: tuple(-mapping[name[1:]] if name.startswith('-') else mapping[name] for name in names)

>>> itemgetter('a', '-b')({'a': 1, 'b': 2})
(1, -2)

Điều này không hoạt động. Tôi có: giá trị = ['-Total_Points', 'TOT_PTS_Misc'] rồi đến b là danh sách các phần tử Khi tôi gọi g = itemgetter (giá trị) (b) Tôi nhận được AttributeError: đối tượng 'list' không có thuộc tính 'startwith'
simi

Nó có một số lượng tên thay đổi, không phải danh sách các tên. Gọi nó như thế này: itemgetter (* giá trị). Hãy xem xét operator.itemgetter nội trang tương tự để biết một ví dụ khác.
A. Coady
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.