一文带你解密Python可迭代对象的排序问题

假设有一个可迭代对象,现在想要对它内部的元素进行排序,我们一般会使用内置函数 sorted,举个例子:

data = (3, 4, 1, 2, 5)
print(
    sorted(data)
)  # [1, 2, 3, 4, 5]

data = (3.14, 2, 1.75)
print(
    sorted(data)
)  # [1.75, 2, 3.14]

data = ["satori", "koishi", "marisa"]
print(
    sorted(data)
)  # ['koishi', 'marisa', 'satori']

如果可迭代对象里面的元素是数值,那么会按照数值的大小进行排序;如果是字符串,那么会按照字符串的字典序进行排序,并且 sorted 函数会返回一个新的列表。

sorted 函数默认是升序排序,如果想要降序,那么可以传递一个关键字参数 reverse=True。

data = [(3, 4),
        (3, 1),
        (2, 3)]

print(
    sorted(data)
)  # [(2, 3), (3, 1), (3, 4)]

如果可迭代对象里面都是元组的话,也是可以的,元组在比较大小的时候,会先按照元组的第一个元素比较;第一个元素相等,再按照第二个元素比较,依次类推。

因此在使用 sorted 函数的时候,可迭代对象内部的元素,要满足彼此之间都是可比较的,否则报错。

data = [123, 456, "123"]

try:
    print(sorted(data))
except TypeError as e:
    print(e)
"""
'<' not supported between instances of 'str' and 'int'
"""

data = [{"a": 1}, {"b": 2}]
try:
    print(sorted(data))
except TypeError as e:
    print(e)
"""
'<' not supported between instances of 'dict' and 'dict'
"""

我们看到,由于 data 里面存在不可比较的元素,因此报错了。那么问题来了,假设有这样一个列表:

data = [{"name": "satori", "age": 17},
        {"name": "marisa", "age": 15},
        {"name": "koishi", "age": 16}]

# 字典是不可比较大小的,因此直接使用 sorted 会报错
# 我们希望按照字典内部的 "age" 字段进行排序
# 得到下面的结果

[{'name': 'marisa', 'age': 15},
 {'name': 'koishi', 'age': 16},
 {'name': 'satori', 'age': 17}]

如果是你的话,你会怎么做呢?很明显,我们将每个 "age" 字段的值取出来,和所在的字典拼接成一个元组(或列表)不就行了,然后对元组进行排序,举个例子:

data = [{"name": "satori", "age": 17},
        {"name": "marisa", "age": 15},
        {"name": "koishi", "age": 16}]

data = [(d["age"], d) for d in data]
print(data)
"""
[(17, {'name': 'satori', 'age': 17}), 
 (15, {'name': 'marisa', 'age': 15}), 
 (16, {'name': 'koishi', 'age': 16})]
"""

# 由于 data 内部的元素是一个元组
# 所以排序的时候会按照元组的第一个元素排序
sorted_data = sorted(data)
print(sorted_data)
"""
[(15, {'name': 'marisa', 'age': 15}), 
 (16, {'name': 'koishi', 'age': 16}), 
 (17, {'name': 'satori', 'age': 17})]
"""

# 此时顺序就排好了,然后再把字典取出来
sorted_data = [tpl[-1] for tpl in sorted_data]
print(sorted_data)
"""
[{'name': 'marisa', 'age': 15}, 
 {'name': 'koishi', 'age': 16}, 
 {'name': 'satori', 'age': 17}]
"""

显然这样就实现了基于字典内部某个字段的值,来对字典进行排序,只不过上面的代码还有一点点缺陷。我们说元组在比较的时候会先比较第一个元素,第一个元素相同的话,会比较第二个元素。

而我们上面 data 里面的元组,由于第一个元素都不相等,所以直接就比较出来了。但如果是下面这种情况呢?

data = [{"name": "satori", "age": 17},
        {"name": "marisa", "age": 16},
        {"name": "koishi", "age": 16}]

data = [(d["age"], d) for d in data]
print(data)
"""
[(17, {'name': 'satori', 'age': 17}), 
 (16, {'name': 'marisa', 'age': 16}), 
 (16, {'name': 'koishi', 'age': 16})]
"""
try:
    sorted_data = sorted(data)
except TypeError as e:
    print(e)
"""
'<' not supported between instances of 'dict' and 'dict'
"""

此时就报错了,因为第二个元组和第三个元组内部的第一个元素都是 16,所以第一个元素相等,那么会比较第二个元素。而第二个元素是字典,字典之间无法比较,所以报错了。

但我们只是希望让字典的 "age" 字段的值参与比较,如果相等的话,那么就不用再比较了,相对顺序就保持现状。所以我们可以这么做:

data = [{"name": "satori", "age": 17},
        {"name": "marisa", "age": 16},
        {"name": "koishi", "age": 16}]

# 我们将索引也加进去
data = [(d["age"], i, d) for i, d in enumerate(data)]
print(data)
"""
[(17, 0, {'name': 'satori', 'age': 17}), 
 (16, 1, {'name': 'marisa', 'age': 16}), 
 (16, 2, {'name': 'koishi', 'age': 16})]
"""

# 如果 "age" 字段的值、或者说元组的第一个元素相等
# 那么就按照索引比较,索引一定是不重复的
sorted_data = sorted(data)
print(sorted_data)
"""
[(16, 1, {'name': 'marisa', 'age': 16}), 
 (16, 2, {'name': 'koishi', 'age': 16}), 
 (17, 0, {'name': 'satori', 'age': 17})]
"""

# 此时就成功排好序了
# 并且 "age" 字段的值相等的字典之间的相对顺序
# 在排序之前和排序之后都保持一致
# 这正是我们想要的结果
sorted_data = [tpl[-1] for tpl in sorted_data]
print(sorted_data)
"""
[{'name': 'marisa', 'age': 16}, 
 {'name': 'koishi', 'age': 16}, 
 {'name': 'satori', 'age': 17}]
"""

再比如,我们想要对元组排序,但我们希望按照元组的第二个元素进行排序:

data = [("satori", 17),
        ("marisa", 15),
        ("koishi", 16)]

data = [(tpl[1], i, tpl) for i, tpl in enumerate(data)]
print(data)
"""
[(17, 0, ('satori', 17)), 
 (15, 1, ('marisa', 15)), 
 (16, 2, ('koishi', 16))]
"""

sorted_data = sorted(data)
print(sorted_data)
"""
[(15, 1, ('marisa', 15)), 
 (16, 2, ('koishi', 16)), 
 (17, 0, ('satori', 17))]
"""

sorted_data = [tpl[-1] for tpl in sorted_data]
print(sorted_data)
"""
[('marisa', 15), 
 ('koishi', 16), 
 ('satori', 17)]
"""

所以当可迭代对象内部的元素无法进行排序,或者说我们不希望基于整个元素进行排序,那么就可以使用上面这个方法。将用来排序的值、索引、原始值放在一个元组里面,然后对元组排序,排完了再把最后一个值(也就是原始值)筛出来即可。

或者我们还可以做的再复杂一些:

data = [-3, -2, 3, 2, -1, 1, 0]

"""
对 data 进行排序,排序规则如下
先按照内部元素的正负进行排序,排序之后正数在后面
如果符号一样,再按照绝对值的大小进行排序
也就是说,排完之后是下面这样一个结果

[-1, -2, -3, 0, 1, 2, 3]
"""

# 如果只按照正负排序
data1 = [(n >= 0, i, n) for i, n in enumerate(data)]
sorted_data = sorted(data1)
print(sorted_data)
"""
[(False, 0, -3), (False, 1, -2), (False, 4, -1), 
 (True, 2, 3), (True, 3, 2), (True, 5, 1), (True, 6, 0)]
"""
sorted_data = [tpl[-1] for tpl in sorted_data]
# 此时正数就排在了负数的后面
print(
    sorted_data
)  # [-3, -2, -1, 3, 2, 1, 0]

# 如果只按照绝对值排序
data2 = [(abs(n), i, n) for i, n in enumerate(data)]
sorted_data = sorted(data2)
print(sorted_data)
"""
[(0, 6, 0), (1, 4, -1), (1, 5, 1), 
 (2, 1, -2), (2, 3, 2), (3, 0, -3), (3, 2, 3)]
"""
sorted_data = [tpl[-1] for tpl in sorted_data]
# 此时绝对值大的数,就排在了绝对值小的数的后面
print(
    sorted_data
)  # [0, -1, 1, -2, 2, -3, 3]

# 同时按照正负和绝对值排序
data3 = [(n >= 0, abs(n), i, n) for i, n in enumerate(data)]
sorted_data = sorted(data3)
print(sorted_data)
"""
[(False, 1, 4, -1), (False, 2, 1, -2), 
 (False, 3, 0, -3), (True, 0, 6, 0), 
 (True, 1, 5, 1), (True, 2, 3, 2), (True, 3, 2, 3)]
"""
sorted_data = [tpl[-1] for tpl in sorted_data]
# 大功告成
print(
    sorted_data
)  # [-1, -2, -3, 0, 1, 2, 3]

那么接下来,我们就可以封装一个属于我们自己的 my_sorted 函数了。

def my_sorted(data, *, key=None, reverse=False):
    """
    :param data: 可迭代对象
    :param key: callable
    :param reverse: 是否逆序
    :return:
    """
    if key is not None:
        data = [(key(item), i, item)
                for i, item in enumerate(data)]
    sorted_data = sorted(data)
    if key is not None:
        sorted_data = [tpl[-1] for tpl in sorted_data]
    if reverse:
        sorted_data = sorted_data[:: -1]
    return sorted_data

# 下面来测试一下
data = [-3, -2, 3, 2, -1, 1, 0]
print(
    my_sorted(data, key=lambda x: (x >= 0, abs(x)))
)  # [-1, -2, -3, 0, 1, 2, 3]

data = [{"name": "satori", "age": 17},
        {"name": "marisa", "age": 16},
        {"name": "koishi", "age": 16}]
print(my_sorted(data, key=lambda x: x["age"]))
"""
[{'name': 'marisa', 'age': 16}, 
 {'name': 'koishi', 'age': 16}, 
 {'name': 'satori', 'age': 17}]
"""

结果一切正常,当然啦,实际工作中我们肯定不会专门封装一个 my_sorted 函数,因为内置的 sorted 已经包含了我们上面的所有功能。

data = [-3, -2, 3, 2, -1, 1, 0]
print(
    sorted(data, key=lambda x: (x >= 0, abs(x)))
)  # [-1, -2, -3, 0, 1, 2, 3]

print(
    sorted(data, key=lambda x: (x >= 0, abs(x)), reverse=True)
)  # [3, 2, 1, 0, -3, -2, -1]

内置函数 sorted 除了接收一个可迭代对象之外,还接收两个关键字参数 key 和 reverse,含义就是我们介绍的那样。在 sorted 的内部,它的处理方式和我们上面是一致的,如果指定了 key,也就是自定义排序规则,那么在底层会将可迭代对象内部的值封装成元组,然后对元组排序。排完序之后,再将元组的最后一个值、也就是原始值取出来,并返回。

所以这就是 sorted 函数的全部秘密,它里面的参数 key 赋予了 sorted 函数强大的能力,有了这个参数,我们想怎么排序,就怎么排序。

class A:

    def __init__(self, a):
        self.a = a

    def __repr__(self):
        return f"self.a = {self.a}"

    def __hash__(self):
        return self.a

a1 = A(1)
a2 = A(2)
a3 = A(3)
a4 = A(4)

data = [a2, a3, a1, a4]
print(data)
"""
[self.a = 2, self.a = 3, self.a = 1, self.a = 4]
"""

# A 的实例对象无法比较,我们希望按照内部的属性 a 进行比较
print(sorted(data, key=lambda x: x.a))
"""
[self.a = 1, self.a = 2, self.a = 3, self.a = 4]
"""

# 或者按照哈希值比较,此时仍相当于按照 self.a 比较
print(sorted(data, key=lambda x: hash(x), reverse=True))
"""
[self.a = 4, self.a = 3, self.a = 2, self.a = 1]
"""

因此我们想怎么比就怎么比,参数 key 赋予了我们极大的自由,key 接收一个函数(当然其它 callable 也可以,但大部分场景都是匿名函数),此函数接收一个参数,该参数会对应可迭代对象里面的每一个元素。而函数的返回值,决定了 sorted 的比较逻辑。

比如,我们不光可以对元组、列表排序,还可以对字典内部的键值对排序。

d = {"satori": 17, "marisa": 15, "koishi": 16}

# 对字典调用 sorted,针对的是字典里面的键
# 所以返回的也是键
print(sorted(d))  # ['koishi', 'marisa', 'satori']

# 匿名函数里面的参数 x 对应可迭代对象里面的每一个元素
# 这里就是字典的键,函数返回 d[x] 表示按照值来排序
# 但排序之后得到的仍然是键
print(
    sorted(d, key=lambda x: d[x])
)  # ['marisa', 'koishi', 'satori']

# 此时的 x 就是键值对组成的元组
# 这里按照值来排序
print(
    sorted(d.items(), key=lambda x: x[1])
)  # [('marisa', 15), ('koishi', 16), ('satori', 17)]

# 如果排序的时候,还希望键值对调换顺序,可以这么做
print(
    sorted(zip(d.values(), d.keys()), key=lambda x: x[0])
)  # [(15, 'marisa'), (16, 'koishi'), (17, 'satori')]

当然啦,还有很多其它排序方式,比如按照数量排序:

string = "a" * 4 + "b" * 3 + "c" * 5 + "d" * 2
data = ["a", "b", "c", "d"]

print(
    sorted(data, key=lambda x: string.count(x))
)  # ['d', 'b', 'a', 'c']

最后再来介绍一个知识点,sorted 在对可迭代对象内部的元素进行排序的时候,肯定要有大小比较的过程,这是肯定的。但问题是比较的时候,用的什么方式呢?举个例子,我想判断 a 和 b 的大小关系(假设不相等),无论是执行 a > b 还是 a < b,根据结果我都能得出它们谁大谁小。

而 sorted 在比较的时候是怎么做的呢,这里给出结论:每次在比较两个对象的时候,都会调用左边对象的 __lt__ 方法。其实关于 sorted 内部是怎么比的,我们无需太关注,但之所以说这一点,是因为在极端场景下可能会遇到。举个例子:

# 第一个元素表示 "商品名称"
# 第二个元素表示 "销量"
data = [("apple", 200),
        ("banana", 200),
        ("peach", 150),
        ("cherry", 150),
        ("orange", 150)]

# 我们需要先按照 "销量" 的大小降序排序
# 如果 "销量" 相同,则按照 "商品名称" 的字典序升序排序
# 该怎么做呢?

# 由于一部分升序,一部分降序
# 我们无法直接使用 reverse 参数,所以就默认按照升序排
# 虽然 "销量" 要求降序排,但可以对它取反
# 这样值越大,取反之后的值就越小,从而实现降序效果
print(
    sorted(data, key=lambda x: (~x[1], x[0]))
)
"""
[('apple', 200), 
 ('banana', 200), 
 ('cherry', 150), 
 ('orange', 150), 
 ('peach', 150)]
"""

可能有小伙伴觉得这也没什么难的,那么我们将问题稍微换一下。如果让你先按照 "销量" 升序排序,如果 "销量相同",再按照 "商品名称" 的字典序降序排序,你要怎么做呢?

显然这个问题的难点就在于字符串要怎么降序排,整数可以取反,但字符串是无法取反的。所以我们可以自定义一个类,实现它的 __lt__ 方法。

data = [("apple", 200),
        ("banana", 200),
        ("peach", 150),
        ("cherry", 150),
        ("orange", 150)]

class STR(str):

    def __lt__(self, other):
        # 调用 str 的 __lt__,得到布尔值
        # 然后再取反,当然,把 not 换成 ~ 也是可以的
        # 因此:"apple" < "banana" 为 True
        # 但是:STR("apple") < STR("banana") 为 False
        return not super().__lt__(other)

# 销量升序排,直接 x[1] 即可
# 但是商品名称降序排,需要使用类 STR 将 x[0] 包起来
print(
    sorted(data, key=lambda x: (x[1], STR(x[0])))
)
"""
[('peach', 150), 
 ('orange', 150), 
 ('cherry', 150), 
 ('banana', 200), 
 ('apple', 200)]
"""

# 事实上,如果你的思维够灵活,你会发现
# "销量"降序排、"商品名称"升序排,排完之后再整体取反
# 就是这里 "销量"升序排、"商品名称"将序排 的结果
print(
    sorted(data, key=lambda x: (~x[1], x[0]), reverse=True)
    ==
    sorted(data, key=lambda x: (x[1], STR(x[0])))
)  # True

# 当然这个思路也很巧妙

由于默认是调用 __lt__ 进行比较的,因此我们需要实现 __lt__。

以上就是一文带你解密Python可迭代对象的排序问题的详细内容,更多关于Python可迭代对象排序的资料请关注我们其它相关文章!

(0)

相关推荐

  • 基于python list对象中嵌套元组使用sort时的排序方法

    在list中嵌套元组,在进行sort排序的时候,产生的是原数组的副本,排序过程中,先根据第一个字段进行从小到大排序,如果第一个字段相同的话,再根据第二个字段进行排序,依次类推,当涉及到字母的时候,是按照字典序进行排序. 如下: a = [(1, 'B'), (1, 'A'), (1, 'C'), (1, 'AC'), (2, 'B'), (2, 'A'), (1, 'ABC')] a a.sort() a 输出结果为: [(1, 'B'), (1, 'A'), (1, 'C'), (1, 'AC

  • Python利用operator模块实现对象的多级排序详解

    前言 最近在工作中碰到一个小的排序问题,需要按嵌套对象的多个属性来排序,于是发现了Python里的operator模块和sorted函数组合可以实现这个功能.本文介绍了Python用operator模块实现对象的多级排序的相关内容,分享出来供大家参考学习,下面来看看详细的介绍: 比如我有如下的类关系,A对象引用了一个B对象, class A(object): def __init__(self, b): self.b = b def __str__(self): return "[%s, %s,

  • python 对给定可迭代集合统计出现频率,并排序的方法

    给定一个可迭代sequence,对其中的值进行出现次数统计: 方法1: def get_counts(sequence): counts = {} for x in sequence: if x in counts: counts[x] += 1 else: counts[x] = 1 return counts 方法2: 利用python中内置的collections from collections import defaultdict def get_counts2(sequence):

  • python实现对象列表根据某个属性排序的方法详解

    本文实例讲述了python实现对象列表根据某个属性排序的方法.分享给大家供大家参考,具体如下: 对于一个已有的python list, 里面的内容是一些对象,这些对象有一些相同的属性值, 在一些特定的情况下,需要自己选择特定的排序,也就是根据某一个具体的属性来排序,在网上找了下资料,一般来说有两种方法,但从根本上来说,还是调用了list.sort 方法来实现.下面是简单的测试代码片段: #coding:utf-8 class Person: def __init__(self,name,age,

  • 一文带你解密Python可迭代对象的排序问题

    假设有一个可迭代对象,现在想要对它内部的元素进行排序,我们一般会使用内置函数 sorted,举个例子: data = (3, 4, 1, 2, 5) print(     sorted(data) )  # [1, 2, 3, 4, 5] data = (3.14, 2, 1.75) print(     sorted(data) )  # [1.75, 2, 3.14] data = ["satori", "koishi", "marisa"]

  • 一文带你了解Python 四种常见基础爬虫方法介绍

    一.Urllib方法 Urllib是python内置的HTTP请求库 import urllib.request #1.定位抓取的url url='http://www.baidu.com/' #2.向目标url发送请求 response=urllib.request.urlopen(url) #3.读取数据 data=response.read() # print(data) #打印出来的数据有ASCII码 print(data.decode('utf-8')) #decode将相应编码格式的

  • 一文带你了解Python中的双下方法

    目录 前言 1. init方法 2. 运算符的双下方法 2.1 比较运算符 2.2 算术运算符 2.3 反向算术运算符 2.4 增量赋值运算符 2.4 位运算符 3.字符串表示 4.数值转换 5.集合相关的双下方法 6.迭代相关的双下方法 7.类相关的双下方法 7.1 实例的创建和销毁 7.2 属性管理 7.3 属性描述符 8.总结 前言 大家在写 Python 代码的时候有没有这样的疑问. 为什么数学中的+号,在字符串运算中却变成拼接功能,如'ab' + 'cd'结果为abcd:而*号变成了重

  • 一文搞懂​​​​​​​python可迭代对象,迭代器,生成器,协程

    目录 设计模式:迭代 python:可迭代对象和迭代器 为什么要有生成器? python的生成器实现 协程 设计模式:迭代 迭代是一种设计模式,解决有序便利序列的问题.通用的可迭代对象需要支持done和next方法. 伪代码如下: while not iterator.done(): item = iterator.next() ..... python:可迭代对象和迭代器 python的可迭代对象需要实现__iter__()方法,返回一个迭代器.for循环和顶级函数iter(obj)调用obj

  • 一文带你了解Python列表生成式应用的八重境界

    目录 1. 引言 2. Level1: 基础用法 3. Level2: 加入条件语句 4. Level3: 加入 enumerate() 5. Level4: 加入 zip() 6. Level5: 加入三目运算符 7. Level6: 嵌套循环 8. Level7: 嵌套列表生成式 9. Level8: 合并上述所有技巧 10. 应用栗子 11. 总结 1. 引言 在Python中有非常多且好用的技巧,其中使用最多的是列表生成式,往往可以将复杂的逻辑用简单的语言来实现,本文重点介绍列表生成式应

  • 一文带你了解Python枚举类enum的使用

    目录 简介 初试 遍历 可哈希 访问成员 唯一枚举值 自动枚举值 比较运算 功能性API IntEnum IntFlag Flag 简介 枚举是与多个唯一常量绑定的一组符号 因为枚举表示的是常量,建议枚举成员名用大写 IntEnum 便于进行系统交互 初试 from enum import Enum class Color(Enum): RED = 1 GREEN = 2 BLUE = 3 print(Color.RED) # Color.RED print(repr(Color.RED)) #

  • Python可迭代对象操作示例

    本文实例讲述了Python可迭代对象.分享给大家供大家参考,具体如下: 1.列表生成式 list = [result for x in range(m, n)] g1 = (i for i in range(101)) print(type(g1)) print(g1) print(g1.__next__()) 输出: <class 'generator'> <generator object <genexpr> at 0x0000024E6AC08F10> 0 g1

  • python可迭代对象去重实例

    可迭代对象去重(保持顺序不变) def filter_multi(items,key=None): """ 可迭代对象去重(保持顺序不变) [1,4,7,2,4,7,3,5] ==> [1,4,7,2,3,5] """ its = list() for x in items: val = x if key is None else key(x) if val not in its: yield val its.append(val) #如:

  • Python 可迭代对象 iterable的具体使用

    目录 前置知识 可迭代对象 如何判断一个对象是否是可迭代对象? enumerate 函数 多嵌套列表 总结 前置知识 如果给定一个 list 或 tuple,我们可以通过 for 循环来遍历这个 list 或 tuple,这种遍历我们称为迭代(Iteration) 在 Python 中,迭代是通过 for ... in 来完成的 lists = [1, 2, 3, 4, 5] for i in lists: print(i) 可迭代对象 for 循环不仅可以用在 list 或 tuple 上,还

  • 一文带你了解Python中的字符串是什么

    在< 详解Python拼接字符串的七种方式 >这篇文章里,我提到过,字符串是程序员离不开的事情.后来,我看到了一个英文版本的说法: There are few guarantees in life: death, taxes, and programmers needing to deal with strings. 它竟然把程序员处理字符串跟死亡大事并列了,可见这是多么命中注定-- 回头看其它文章,我发现这种说法得到了佐证,因为我在无意中已零零碎碎地提及了字符串的很多方面,例如:字符串读写文

随机推荐