Python 中 sorted 如何自定义比较逻辑


Posted in Python onFebruary 02, 2021

在 Python 中对一个可迭代对象进行排序是很常见的一个操作,一般会用到 sorted() 函数

num_list = [4, 2, 8, -9, 1, -3]
sorted_num_list = sorted(num_list)
print(sorted_num_list)

上面的代码是对整数列表 num_list 按从小到大的顺序进行排序,得到的结果如下

[-9, -3, 1, 2, 4, 8]

有时候不仅仅是对元素本身进行排序,而是在元素值的基础上进行一些计算之后再进行比较,比如将 num_list 中的元素按照其平方值的大小进行排序。

在 Python 2 中,可以通过 sorted() 函数中的 cmp 或 key 参数来实现这种自定义的比较逻辑。cmp 比较函数接收两个参数 x 和 y(x 和 y 都是列表中元素)并且返回一个数字,如果返回正数表示 x > y,返回 0 表示 x == y,返回负数表示 x < y。key 函数接收一个参数,重新计算出一个结果,然后用计算出的结果参与排序比较。因此在 Python 2 中按平方值大小排序可以有下面两种实现方式

num_list = [4, 2, 8, -9, 1, -3]
# cmp 参数只在 Python 2 中存在,Python 3 及之后的版本移除了 cmp 参数
sorted_num_list = sorted(num_list, cmp=lambda x, y: x ** 2 - y ** 2)
sorted_num_list = sorted(num_list, key=lambda x: x ** 2)

但是随着 Python 3.0 的发布,cmp 参数也随之被移除了,也就是说在 Python 3 中自定义比较逻辑就只能通过 key 参数来实现。至于为什么将 cmp 参数移除,在 Python 的 Issue tracker 中有一段很长的讨论,主要有以下两点原因

  • cmp 是一个冗余参数,所有使用 cmp 的场景都可以用 key 来代替
  • 使用 key 比使用 cmp 的性能更快,对于有 N 个元素的列表,在排序过程中如果调用 cmp 进行比较,那么 cmp 的调用次数为 Nlog(N) 量级(基于比较的排序的最快时间复杂度),如果使用 key 参数,那么只需要在每个元素上调用一次 key 函数,只有 N 次调用,虽然使用 key 参数也要进行 O(Nlog(N)) 量级比较次数,但这些比较是在 C 语言层,比调用用户自定义的函数快。

关于上面性能的问题,我做了一个实验,分别随机生成 1000、10000、100000 和 1000000 个整数,然后用 key 和 cmp 的方式分别进行排序并记录排序的时间消耗

import random
import time

counts = (1000, 10000, 100000, 1000000)

def custom_cmp(x, y):
  return x ** 2 - y ** 2

def custom_key(x):
  return x ** 2

print('%7s%20s%20s' % ('count', 'cmp_duration', 'key_duration'))
for count in counts:
  min_num = -count // 2
  max_num = count // 2
  nums = [random.randint(min_num, max_num) for _ in range(count)]
  start = time.time()
  sorted(nums, cmp=custom_cmp)
  cmp_duration = time.time() - start
  start = time.time()
  sorted(nums, key=custom_key)
  key_duration = time.time() - start
  print('%7d%20.2f%20.2f' % (count, cmp_duration, key_duration))

在我的笔记本上一次运行结果如下

count    cmp_duration    key_duration
  1000        0.00        0.00
 10000        0.02        0.01
 100000        0.34        0.11
1000000        4.75        1.85

可以看到,当列表中数字的数量超过 100000 的时候,使用 key 函数的性能优势就非常明显了,比 cmp 快了 2~3 倍。

对于熟悉 Java 或 C++ 等其他编程语言的同学来说,可能更熟悉 cmp 的比较方式。其实 Python 3 中也可以通过 functools 工具包中的 cmp_to_key() 函数来将 cmp 转换成 key,从而使用接收两个参数的自定义比较函数 cmp。

import functools

num_list = [4, 2, 8, -9, 1, -3]

def custom_cmp(x, y):
  return x ** 2 - y ** 2

sorted_num_list = sorted(num_list, key=functools.cmp_to_key(custom_cmp))
print(sorted_num_list)

那么,cmp_to_key() 函数是如何将 cmp 转换成 key 的呢,我们可以通过源码一探究竟

def cmp_to_key(mycmp):
  """Convert a cmp= function into a key= function"""
  class K(object):
    __slots__ = ['obj']
    def __init__(self, obj):
      self.obj = obj
    def __lt__(self, other):
      return mycmp(self.obj, other.obj) < 0
    def __gt__(self, other):
      return mycmp(self.obj, other.obj) > 0
    def __eq__(self, other):
      return mycmp(self.obj, other.obj) == 0
    def __le__(self, other):
      return mycmp(self.obj, other.obj) <= 0
    def __ge__(self, other):
      return mycmp(self.obj, other.obj) >= 0
    __hash__ = None
  return K

其实 cmp_to_key() 返回的是一个类 K,只不过在类 K 中重载了各种比较运算符,重载的过程中使用到了自定义的比较函数 mycmp,使得 K 的大小比较逻辑与 mycmp 一致。这样,对于 num_list 中的每个元素 num 都会执行一次 K(num) 生成一个类 K 的实例,然后通过比较不同 K 的实例的大小进行排序。

虽然通过 cmp_to_key() 可以调用自定义的 cmp 函数,但是还是要优先使用 key 函数,因为通过 cmp_to_key() 方式会在排序过程中创建很多类 K 的实例,对性能有很大影响,下面是 cmp_to_key() 和 key 的性能比较

count     cmp_to_key    key_duration
  1000        0.01        0.00
 10000        0.10        0.01
 100000        1.36        0.09
1000000        16.89        1.13

当 num_list 中的数量为 1000000 的时候 key 比 cmp_to_key 快了将近 15 倍。

本文主要介绍了如何在 sorted 函数中自定义比较逻辑,Python 2 中可以通过 cmp 或 key 来实现,cmp 接收 2 个参数,通过返回的数值来判断两个参数的大小,key 重新计算一个新的结果参与比较。在 Python 3 中,考虑到 cmp 的性能和冗余的原因,将其移除了。在 Python 3.2 中提供了 functools.cmp_to_key 这个函数来使用自定义的比较函数 cmp,但是出于性能的考虑,我们还是要优先使用 key 来进行排序。

以上就是Python 中 sorted 如何自定义比较逻辑的详细内容,更多关于python sorted自定义比较逻辑的资料请关注三水点靠木其它相关文章!

Python 相关文章推荐
python文件的md5加密方法
Apr 06 Python
python常用函数详解
Sep 13 Python
Python实现简单遗传算法(SGA)
Jan 29 Python
Python序列循环移位的3种方法推荐
Apr 09 Python
python学习笔记--将python源文件打包成exe文件(pyinstaller)
May 26 Python
python+POP3实现批量下载邮件附件
Jun 19 Python
Python爬虫常用库的安装及其环境配置
Sep 19 Python
python3 自动打印出最新版本执行的mysql2redis实例
Apr 09 Python
Python3将ipa包中的文件按大小排序
Apr 17 Python
Python批量删除mysql中千万级大量数据的脚本分享
Dec 03 Python
pandas:get_dummies()与pd.factorize()的用法及区别说明
May 21 Python
python办公自动化之excel的操作
May 23 Python
Python实现钉钉/企业微信自动打卡的示例代码
Feb 02 #Python
手把手教你配置JupyterLab 环境的实现
Feb 02 #Python
python 模块导入问题汇总
Feb 01 #Python
用python制作个视频下载器
Feb 01 #Python
python基于pexpect库自动获取日志信息
Feb 01 #Python
Python入门基础之数字字符串与列表
Feb 01 #Python
Pyecharts 中Geo函数常用参数的用法说明
Feb 01 #Python
You might like
php处理多图上传压缩代码功能
2018/06/13 PHP
PDO实现学生管理系统
2020/03/21 PHP
JS 建立对象的方法
2007/04/21 Javascript
网站导致浏览器崩溃的原因总结(多款浏览器) 推荐
2010/04/15 Javascript
showModalDialog在谷歌浏览器下会返回Null的解决方法
2013/11/27 Javascript
影响jQuery使用的14个方面
2014/09/01 Javascript
jQuery实现简单的日期输入格式化控件
2015/03/12 Javascript
AngularJS初始化静态模板详解
2016/01/14 Javascript
windows下vue.js开发环境搭建教程
2017/03/20 Javascript
vue中axios处理http发送请求的示例(Post和get)
2017/10/13 Javascript
分析JS中this引发的bug
2017/12/12 Javascript
vue之将echart封装为组件
2018/06/02 Javascript
vue.js内置组件之keep-alive组件使用
2018/07/10 Javascript
vue权限问题的完美解决方案
2019/05/08 Javascript
vue router 跳转时打开新页面的示例方法
2019/07/28 Javascript
js判断在哪个浏览器打开项目的方法
2020/01/21 Javascript
分享8个JavaScript库可更好地处理本地存储
2020/10/12 Javascript
Python字符串格式化
2015/06/15 Python
python多进程实现进程间通信实例
2017/11/24 Python
Python打印“菱形”星号代码方法
2018/02/05 Python
django中的HTML控件及参数传递方法
2018/03/20 Python
详解Django+Uwsgi+Nginx的生产环境部署
2018/06/25 Python
python使用response.read()接收json数据的实例
2018/12/19 Python
Pytorch在dataloader类中设置shuffle的随机数种子方式
2020/01/14 Python
css3实现背景图片拉伸效果像桌面壁纸一样
2013/08/19 HTML / CSS
年会活动策划方案
2014/01/23 职场文书
网上商城创业计划书范文
2014/01/31 职场文书
财务会计大学生自我评价
2014/04/09 职场文书
公司节能减排方案
2014/05/16 职场文书
会计专业毕业生求职信
2014/07/04 职场文书
2014年军人思想汇报范文
2014/10/12 职场文书
团日活动总结格式
2015/05/11 职场文书
2015年中秋晚会主持稿
2015/07/30 职场文书
篮球拉拉队口号
2015/12/25 职场文书
python读取pdf格式文档的实现代码
2021/04/01 Python
python 使用tkinter与messagebox写界面和弹窗
2022/03/20 Python