排序问题是所有程序员一定会遇到的问题,Python内置的排序工具sort()和sorted()功能强大,可以实现自定义的复杂式排序。平时我们使用两个函数可能没有仔细研究过它们的区别,随想随用了。但实际上二者还是有很大的去别的,在一些场景中不同互换使用。


本篇将会介绍如何对不同数据结构中的各种类型的数据进行排序,自定义顺序,以及使用两种不同的Python排序方法。最后还会介绍如何区分sorted和sort,如何根据个性要求在代码中自定义复杂的排序顺序。


使用sorted()排序值


开始使用Python排序,首先要了解如何对数字数据和字符串数据进行排序。


1. 排序数字型数据


可以使用Python通过sorted()对列表进行排序。比如定义了一个整数列表,然后使用numbers变量作为参数调用sorted():


>>> numbers = [6, 9, 3, 1]
>>> sorted(numbers)
[1, 3, 6, 9]
>>> numbers
[6, 9, 3, 1]

输出是一个新的排序列表,如果打印原始变量时,原始数字变量numbers未改变,因为sorted()只提供已排序的输出,而不会更改原始值。这意味着sorted()可以在列表中使用,将输出立即分配给变量:


>>> numbers = [6, 9, 3, 1]
>>> numbers_sorted = sorted(numbers)
>>> numbers_sorted
[1, 3, 6, 9]
>>> numbers
[6, 9, 3, 1]

我们还可以通过调用sorted的help()来确认所有这些观察结果。可选参数key和reverse将在本教程后面介绍:


>>> # Python 3
>>> help(sorted)
Help on built-in function sorted in module builtins:

sorted(iterable, /, *, key=None, reverse=False)
   Return a new list containing all items from the iterable in ascending order.

   A custom key function can be supplied to customize the sort order, and the
   reverse flag can be set to request the result in descending order.

像操作列表一样,sorted()也可同样地用于元组和集合:


>>> numbers_tuple = (6, 9, 3, 1)
>>> numbers_set = {5, 5, 10, 1, 0}
>>> numbers_tuple_sorted = sorted(numbers_tuple)
>>> numbers_set_sorted = sorted(numbers_set)
>>> numbers_tuple_sorted
[1, 3, 6, 9]
>>> numbers_set_sorted
[0, 1, 5, 10]

注意到,即使输入一个集合和一个元组,输出也是一个列表,因为sorted()按定义返回一个新列表。如果需要匹配输入类型,则可以将返回的对象强制转换为新类型。如果尝试将结果列表强制转换回集合,结果将是无序的,因为集合是无序的,如下:


>>> numbers_tuple = (6, 9, 3, 1)
>>> numbers_set = {5, 5, 10, 1, 0}
>>> numbers_tuple_sorted = sorted(numbers_tuple)
>>> numbers_set_sorted = sorted(numbers_set)
>>> numbers_tuple_sorted
[1, 3, 6, 9]
>>> numbers_set_sorted
[0, 1, 5, 10]
>>> tuple(numbers_tuple_sorted)
(1, 3, 6, 9)
>>> set(numbers_set_sorted)
{0, 1, 10, 5}


2. 排序字符串型数据


字符串类型与其他可迭代对象类似,如列表和元组。下面的示例显示了sorted()如何将传递给它的字符串进行遍历,并在输出中对每个字符进行排序:


>>> string_number_value = '34521'
>>> string_value = 'I like to sort'
>>> sorted_string_number = sorted(string_number_value)
>>> sorted_string = sorted(string_value)
>>> sorted_string_number
['1', '2', '3', '4', '5']
>>> sorted_string
[' ', ' ', ' ', 'I', 'e', 'i', 'k', 'l', 'o', 'o', 'r', 's', 't', 't']

sorted()将字符串视为列表并遍历每个元素。在字符串中,每个元素表示字符串中的一个字符,sorted会以相同的方式处理一个字符串,对每个字符进行排序,包括空格。 .


我们通过使用split()可以改变输出是单个字符的结果,以空格为边界将原始字符串拆分成几个单词,再通过.join()将几个单词重新组合在一起成为新的字符串,具体如下:


>>> string_value = 'I like to sort'
>>> sorted_string = sorted(string_value.split())
>>> sorted_string
['I', 'like', 'sort', 'to']
>>> ' '.join(sorted_string)
'I like sort to'


Python排序的局限性和陷阱


当使用Python对整数值进行排序时,可能会出现一些限制和奇怪的现象。


1. 具有不能比较数据类型的列表无法进行排序


有些数据类型使用sorted是无法进行比较的,因为它们的类型不同。如果尝试在包含不可比较数据的列表上使用sorted(),Python将返回错误。在此示例中,由于不兼容性,无法对同一列表中的None和int进行排序:


>>> mixed_types = [None, 0]
>>> sorted(mixed_types)
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
TypeError: '<' not supported between instances of 'int' and 'NoneType'

此错误显示了为什么Python无法对给定的值进行排序。它试图通过使用小于运算符(<)来确定值,以确定排序顺序中哪个值较低。例如,数字1应该出现在苹果这个词之前吗?但是,如果迭代器包含所有数字的整数和字符串的组合,则可以使用列表推导将它们强制转换为可比较的数据类型:


>>> mixed_numbers = [5, "1", 100, "34"]
>>> sorted(mixed_numbers)
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
TypeError: '<' not supported between instances of 'str' and 'int'
>>> # List comprehension to convert all values to integers
>>> [int(x) for x in mixed_numbers]
[5, 1, 100, 34]
>>> sorted([int(x) for x in mixed_numbers])
[1, 5, 34, 100]

mixed_numbers中的每个元素都调用了int()来将任何str值转换为int值。然后调用sorted()并成功比较每个元素并提供排序的输出。


另外,Python还可以隐式地将值转换为另一种类型。在下面的示例中,1 <= 0的评估是false语句,因此评估的输出将为False。数字1可以转换为True作为bool类型,而0转换为False。


即使列表中的元素看起来不同,它们也可以全部转换为布尔值(True或False)并使用sorted()进行相互比较:


>>> similar_values = [False, 0, 1, 'A' == 'B', 1 <= 0]
>>> sorted(similar_values)
[False, 0, False, False, 1]

'A'=='B'和1 <= 0转换为False并在有序输出中返回。


此示例说明了排序的一个重要方面:排序稳定性。在Python中,当你对相等的值进行排序时,它们将在输出中保留其原始顺序。即使1移动,所有其他值都相等,它们保持相对于彼此的原始顺序。在下面的示例中,所有值都被视为相等,并将保留其原始位置:


>>> false_values = [False, 0, 0, 1 == 2, 0, False, False]
>>> sorted(false_values)
[False, 0, 0, False, 0, False, False]

如果检查原始顺序和排序输出,可以看到1 == 2转换为False,所有排序输出都是原始顺序。


2. 当排序字符串时,大小写很重要


sorted()可用于字符串列表,以按升序对值进行排序,默认情况下按字母顺序排列:


>>> names = ['Harry', 'Suzy', 'Al', 'Mark']
>>> sorted(names)
['Al', 'Harry', 'Mark', 'Suzy']

但是,Python使用每个字符串中第一个字母的Unicode代码点来确定升序排序。意思是sorted()不会将名称Al和al视为相同。此示例使用ord()返回每个字符串中第一个字母的Unicode代码点:


>>> names_with_case = ['harry', 'Suzy', 'al', 'Mark']
>>> sorted(names_with_case)
['Mark', 'Suzy', 'al', 'harry']
>>> # 每个word中第一个字母的unicode代码点列表推导式
>>> [(ord(name[0]), name[0]) for name in sorted(names_with_case)]
[(77, 'M'), (83, 'S'), (97, 'a'), (104, 'h')]

name [0]返回sorted(names_with_case)的每个元素中的第一个字符,ord()提供Unicode代码点。即使a在字母表中的M之前,M的代码点在a之前,因此排序的输出首先是M。如果第一个字母相同,则sorted()将使用第二个字符来确定顺序,第三个字符等,依此类推,一直到字符串的结尾:


>>> very_similar_strs = ['hhhhhd', 'hhhhha', 'hhhhhc','hhhhhb']
>>> sorted(very_similar_strs)
['hhhhha', 'hhhhhb', 'hhhhhc', 'hhhhhd']

除最后一个字符外,very_similar_strs的每个值都相同。 sorted()比较字符串,因为前五个字符相同,输出将基于第六个字符。


包含相同值的字符串将最终排序为最短到最长,因为较短的字符串没有要与较长字符串进行比较的元素:


>>> different_lengths = ['hhhh', 'hh', 'hhhhh','h']
>>> sorted(different_lengths)
['h', 'hh', 'hhhh', 'hhhhh']

最短的字符串h排序第一,最长的字符串hhhhh排序最后。


用reverse参数使用sorted()


如sorted()的help()文档所示,有一个名为reverse的可选关键字参数,它将根据分配给它的布尔值更改排序行为。如果将reverse指定为True,则排序将按降序排列:


>>> names = ['Harry', 'Suzy', 'Al', 'Mark']
>>> sorted(names)
['Al', 'Harry', 'Mark', 'Suzy']
>>> sorted(names, reverse=True)
['Suzy', 'Mark', 'Harry', 'Al']

排序逻辑保持不变,这意味着名称仍按其第一个字母排序。但是,如果reverse关键字设置为True,则输出反转。


如果指定了False,则排序将保持升序。可以使用前面的任何示例来使用True或False来查看reverse的行为:


>>> names_with_case = ['harry', 'Suzy', 'al', 'Mark']
>>> sorted(names_with_case, reverse=True)
['harry', 'al', 'Suzy', 'Mark']
>>> similar_values = [False, 1, 'A' == 'B', 1 <= 0]
>>> sorted(similar_values, reverse=True)
[1, False, False, False]
>>> numbers = [6, 9, 3, 1]
>>> sorted(numbers, reverse=False)
[1, 3, 6, 9]


sorted()使用key参数排序


sorted()最强大的功能之一是一个叫做key的关键字参数。此参数需要将函数传递给它,并且该函数将用于要排序的列表中的每个值,以确定生成的顺序。


我们假设排序一个特定列表的要求是列表中字符串的长度,最短到最长。返回字符串长度len()的函数将与key参数一起使用:


>>> word = 'paper'
>>> len(word)
5
>>> words = ['banana', 'pie', 'Washington', 'book']
>>> sorted(words, key=len)
['pie', 'book', 'banana', 'Washington']

生成的顺序是一个字符串顺序最短到最长的列表。列表中每个元素的长度由len确定,然后以升序返回。


回到前面的例子,当大小写不同时按第一个字母排序。key可以通过将整个字符串转换为小写来解决该问题:


>>> names_with_case = ['harry', 'Suzy', 'al', 'Mark']
>>> sorted(names_with_case)
['Mark', 'Suzy', 'al', 'harry']
>>> sorted(names_with_case, key=str.lower)
['al', 'harry', 'Mark', 'Suzy']

输出值尚未转换为小写,因为key不会处理原始列表中的数据。在排序期间,传递给key的函数将在每个元素上调用以确定排序顺序,但原始值仍将体现在输出中。使用带有key参数的函数时,有两个主要限制。


首先,传递给key的函数中参数的数量必须为1。


下面的示例显示了带有两个参数的加法函数的定义。当该函数用于数字列表中的键时,它会失败,因为它缺少第二个参数。每次在排序期间调用add()时,它一次只从列表中接收一个元素:


>>> def add(x, y):
...    return x + y
...
>>> values_to_add = [1, 2, 3]
>>> sorted(values_to_add, key=add)
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
TypeError: add() missing 1 required positional argument: 'y'

第二个限制是与key一起使用的函数必须能够处理iterable中的所有值。例如,有一个数字列表,表示为要在sorted中使用的字符串,而key将尝试将它们转换为使用int。如果iterable中的值不能转换为整数,则该函数将失败:


>>> values_to_cast = ['1', '2', '3', 'four']
>>> sorted(values_to_cast, key=int)
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 10: 'four'

作为字符串的每个数值都可以转换为int,但是four不能。这会导致引发ValueError并解释four无法转换为int,因为它无效。


key功能非常强大,因为几乎任何内置或用户定义的函数都可用于操作输出顺序。


如果排序要求是按每个字符串中的最后一个字母排序可迭代(如果字母相同,然后使用下一个字母),则可以定义函数,然后在排序中使用。下面的示例定义了一个函数,该函数反转传递给它的字符串,然后该函数用作键的参数:


>>> def reverse_word(word):
...    return word[::-1]
...
>>> words = ['banana', 'pie', 'Washington', 'book']
>>> sorted(words, key=reverse_word)
['banana', 'pie', 'book', 'Washington']

word[::-1]切片语法用于反转字符串。每个元素都会应用reverse_word(),排序顺序将基于后向单词中的字符。


当然,也可以使用key参数中定义的lambda函数,而不是编写独立函数。 lambda匿名函数:1)必须内联定义;2)没有名字;3)不能包含statement;4)像函数一样执行。


在下面的示例中,key被定义为没有名称的lambda,lambda采用的参数是x,然后x [:: -1]是将对参数执行的操作:


>>> words = ['banana', 'pie', 'Washington', 'book']
>>> sorted(words, key=lambda x: x[::-1])
['banana', 'pie', 'book', 'Washington']

在每个元素上调用x [::-1]并反转该单词。然后将反转的输出用于排序,但仍返回原始单词。 如果需求发生变化,要求顺序也应该反转,那么reverse关键字可以与key参数一起使用:


>>> words = ['banana', 'pie', 'Washington', 'book']
>>> sorted(words, key=lambda x: x[::-1], reverse=True)
['Washington', 'book', 'pie', 'banana']

当需要基于属性对类对象进行排序时,lambda函数也很有用。如果有一组学生并需要按最终成绩(从最高到最低)对其进行排序,则可以使用lambda从该课程中获取成绩属性:


>>> from collections import namedtuple

>>> StudentFinal = namedtuple('StudentFinal', 'name grade')
>>> bill = StudentFinal('Bill', 90)
>>> patty = StudentFinal('Patty', 94)
>>> bart = StudentFinal('Bart', 89)
>>> students = [bill, patty, bart]
>>> sorted(students, key=lambda x: getattr(x, 'grade'), reverse=True)
[StudentFinal(name='Patty', grade=94), StudentFinal(name='Bill', grade=90), StudentFinal(name='Bart', grade=89)]

此示例使用namedtuple生成具有name和grade属性的类。 lambda在每个元素上调用getattr()并返回grade的值。 reverse设置为True以使升序输出转为降序,以便首先排序最高等级。


当在sorted()上同时使用key和reverse关键字参数时,如何进行排序的可能性是无穷无尽的。当对一个小函数使用基本lambda时,代码可以保持干净和简短,或者可以编写一个全新的函数导入,并在key参数中使用它。


使用.sort()排序值


名称相似的.sort()与sorted()内置函数有着很大的不同。虽然它们或多或少都可以完成相同的事情,但list.sort()的help()文档突出显示了.sort()和sorted()之间最重要的两个区别:


>>> # Python2
Help on method_descriptor:

sort(...)
   L.sort(cmp=None, key=None, reverse=False) -- stable sort *IN PLACE*;
   cmp(x, y) -> -1, 0, 1

>>> # Python3
>>> help(list.sort)
Help on method_descriptor:

sort(...)
   L.sort(key=None, reverse=False) -> None -- stable sort *IN PLACE*

首先,sort是列表类的一种方法,只能与列表一起使用。它不是传递给它的迭代的内置函数。 其次,sort返回None并修改值。我们来看看代码中这两种差异的影响:


>>> values_to_sort = [5, 2, 6, 1]
>>> # 尝试调用像使用sorted()调用sort()
>>> sort(values_to_sort)
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
NameError: name 'sort' is not defined

>>> # 尝试在一个元组上使用 .sort()
>>> tuple_val = (5, 1, 3, 5)
>>> tuple_val.sort()
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
AttributeError: 'tuple' object has no attribute 'sort'

>>> # 排序列表并且赋值给新的变量
>>> sorted_values = values_to_sort.sort()
>>> print(sorted_values)
None

>>> # 打印原始变量
>>> print(values_to_sort)
[1, 2, 5, 6]

与此代码示例中的sorted()相比,sort()操作的方式有一些非常显着的差异:


1. sort()不产生有序输出,因此对新变量的赋值仅传递None类型。

2. values_to_sort列表已就地更改,并且不以任何方式维持原始顺序。


这些差异使得.sort()和sorted()绝对不能在代码中互换,如果以错误的方式使用它们,它们会产生意想不到的结果。


.sort()具有相同的key和reverse可选关键字参数,这些参数产生与sorted()相同的强大功能。在这里,可以按第三个单词的第二个字母对短语列表进行排序,然后反向返回列表:


>>> phrases = ['when in rome', 
...    'what goes around comes around',
...    'all is fair in love and war'
...    ]
>>> phrases.sort(key=lambda x: x.split()[2][1], reverse=True)
>>> phrases
['what goes around comes around', 'when in rome', 'all is fair in love and war']


何时使用sorted和.sort?


我们已经看到了sorted()和.sort()之间的区别,但我们什么时候使用?该使用哪个?


假设有一场5k比赛即将举行:第一届年度Python 5k。需要获取和分类来自比赛的数据,参赛者的号码和完成比赛所需的秒数:


>>> from collections import namedtuple

>>> Runner = namedtuple('Runner', 'bibnumber duration')

当参赛者越过终点线时,每个参赛者将被添加到名为参赛者的列表中。在5k比赛中,并非所有参赛者同时越过起跑线,所以第一个越过终点线的人可能实际上不是最快的人:


>>> runners = []
>>> runners.append(Runner('2528567', 1500))
>>> runners.append(Runner('7575234', 1420))
>>> runners.append(Runner('2666234', 1600))
>>> runners.append(Runner('2425234', 1490))
>>> runners.append(Runner('1235234', 1620))
>>> # Thousands and Thousands of entries later...
>>> runners.append(Runner('2526674', 1906))

每次参赛者越过终点线时,他们的号码号和他们的总持续时间(以秒为单位)都会添加到跑步者。


现在,负责处理结果数据的尽职程序员看到了这个列表,知道前5名最快的参与者是获得奖品的获胜者,剩下的参赛者将按最快的时间进行排序。


赛事中没有提到通过不同属性进行多类型的排序要求,也没有提到将列表在某处存储,只需按持续时间排序并获取持续时间最短的五个参与者:


>>> runners.sort(key=lambda x: getattr(x, 'duration'))
>>> top_five_runners = runners[:5]

程序员选择在key参数中使用lambda来获取每个参赛者的duration属性,并使用.sort()对运行程序进行排序。在对参赛者进行排序后,前5个元素存储在top_five_runners中。


比赛总监过来告诉程序员,由于目前发布的Python是3.7,他们决定每隔37位越过终点线的人将获得一个免费的健身包。这时候,程序员开始出汗,因为参赛者名单已被不可逆转地改变。没有办法按照他们完成的顺序恢复原始的参赛者名单,并找到这些人。


如果你正在处理重要数据,甚至可能需要恢复原始数据,那么.sort()不是最佳选择。相反,如果数据是副本,是不重要的工作数据,或者没有人会在意失不失去它,那么.sort()可以是一个很好的选择。


因此,可以用sorted(),使用相同的lambda对runners进行排序:


>>> runners_by_duration = sorted(runners, key=lambda x: getattr(x, 'duration'))
>>> top_five_runners = runners_by_duration[:5]

在这个带有sorted()的场景中,原始的参赛者列表仍然完好无损并且没有被覆盖,并且每三十七人越过终点线的即兴要求可以通过与原始值互动来完成:


>>> every_thirtyseventh_runners = runners[::37]


结论:如何在Python中排序


.sort()和sorted()都可以准确地提供排序功能,但在输出和修改时,两者都具有非常不同的特性,因此请确定是否要在应用程序中使用.sort(),因为它不可撤销地覆盖数据。


©著作权归作者所有:来自51CTO博客作者mb5fe18e9fef50b的原创作品,如需转载,请注明出处,否则将追究法律责任

更多相关文章

  1. SQL今日一题(11):窗口函数
  2. 为什么说 Python 内置函数并不是万能的?
  3. 给你的Excel增加正则处理函数,简直如虎添翼
  4. Python 之父为什么嫌弃 lambda 匿名函数?
  5. Python 函数为什么会默认返回 None?
  6. Python 为什么没有 main 函数?为什么我不推荐写 main 函数?
  7. 学编程这么久,还傻傻分不清什么是方法(method),什么是函数(function)?
  8. 秒懂!图解四个实用的Pandas函数!
  9. 一个真实问题,搞定三个冷门pandas函数

随机推荐

  1. Android入门教程四十二之GridView(网格视
  2. 7、Android中Bundle类的作用
  3. android TabHost 例子
  4. android junit (一)
  5. 自定义控件之View原理与使用
  6. android基础知识12:android自动化测试06—
  7. Android FrameLayout子view居中(左居中,右
  8. Android定义圆角背景方面的介绍
  9. 【Android】应用程序启动过程源码分析
  10. 【译】Google官方推出的Android架构组件