深入研究Python 世界的规则,我的薪资直接翻倍

前言

编程,其实和玩电子游戏有一些相似之处。你在玩不同游戏前,需要先学习每个游戏的不同规则,只有熟悉和灵活运用游戏规则,才更有可能在游戏中获胜。

而编程也是一样,不同编程语言同样有着不一样的“规则”。大到是否支持面向对象,小到是否可以定义常量,编程语言的规则比绝大多数电子游戏要复杂的多。

当我们编程时,如果直接拿一种语言的经验套用到另外一种语言上,很多时候并不能取得最佳结果。这就好像一个 CS(反恐精英) 高手在不了解规则的情况下去玩 PUBG(绝地求生),虽然他的枪法可能万中无一,但是极有可能在发现第一个敌人前,他就会倒在某个窝在草丛里的敌人的伏击下。

Python 里的规则

Python 是一门初见简单、深入后愈觉复杂的语言。拿 Python 里最重要的“对象”概念来说,Python 为其定义了多到让你记不全的规则,比如:

  • 定义了 __str__ 方法的对象,就可以使用 str() 函数来返回可读名称

  • 定义了 __next__ 和 __iter__ 方法的对象,就可以被循环迭代

  • 定义了 __bool__ 方法的对象,在进行布尔判断时就会使用自定义的逻辑

  • ... ...

熟悉规则,并让自己的代码适应这些规则,可以帮助我们写出更地道的代码,事半功倍的完成工作。下面,让我们来看一个有关适应规则的故事。

案例:从两份旅游数据中获取人员名单

某日,在一个主打新西兰出境游的旅游公司里,商务同事突然兴冲冲的跑过来找到我,说他从某合作伙伴那里,要到了两份重要的数据:

  1. 所有去过“泰国普吉岛”的人员及联系方式

  2. 所有去过“新西兰”的人员及联系方式

数据采用了 JSON 格式,如下所示:

# 去过普吉岛的人员数据users_visited_phuket = [{"first_name": "Sirena", "last_name": "Gross", "phone_number": "650-568-0388",                                     "date_visited": "2018-03-14"},{"first_name": "James", "last_name": "Ashcraft", "phone_number": "412-334-4380", "date_visited": "2014-09-16"},... ...]# 去过新西兰的人员数据users_visited_nz = [{"first_name": "Justin", "last_name": "Malcom", "phone_number": "267-282-1964", "date_visited": "2011-03-13"},{"first_name": "Albert", "last_name": "Potter", "phone_number": "702-249-3714", "date_visited": "2013-09-11"},... ...]
 

每份数据里面都有着 手机号码旅游时间 四个字段。基于这份数据,商务同学提出了一个(听上去毫无道理)的假设:“去过普吉岛的人,应该对去新西兰旅游也很有兴趣。我们需要从这份数据里,找出那些去过普吉岛但没有去过新西兰的人,针对性的卖产品给他们。

第一次蛮力尝试

有了原始数据和明确的需求,接下来的问题就是如何写代码了。依靠蛮力,我很快就写出了第一个方案:

def find_potential_customers_v1():"""找到去过普吉岛但是没去过新西兰的人"""for phuket_record in users_visited_phuket:is_potential = Truefor nz_record in users_visited_nz:if phuket_record['first_name'] == nz_record['first_name'] and \phuket_record['last_name'] == nz_record['last_name'] and \phuket_record['phone_number'] == nz_record['phone_number']:is_potential = Falsebreakif is_potential:yield phuket_record
  1.  

因为原始数据里没有“用户 ID”之类的唯一标示,所以我们只能把“姓名和电话号码完全相同”作为判断是不是同一个人的标准。

find_potential_customers_v1 函数通过循环的方式,先遍历所有去过普吉岛的人,然后再遍历新西兰的人,如果在新西兰的记录中找不到完全匹配的记录,就把它当做“潜在客户”返回。

这个函数虽然可以完成任务,但是相信不用我说你也能发现。它有着非常严重的性能问题。对于每一条去过普吉岛的记录,我们都需要遍历所有新西兰访问记录,尝试找到匹配。整个算法的时间复杂度是可怕的 O(n*m),如果新西兰的访问条目数很多的话,那么执行它将耗费非常长的时间。

为了优化内层循环性能,我们需要减少线性查找匹配部分的开销。

尝试使用集合优化函数

如果你对 Python 有所了解的话,那么你肯定知道,Python 里的字典和集合对象都是基于 哈希表(Hash Table) 实现的。判断一个东西是不是在集合里的平均时间复杂度是 O(1),非常快。

所以,对于上面的函数,我们可以先尝试针对新西兰访问记录初始化一个集合,之后的查找匹配部分就可以变得很快,函数整体时间复杂度就能变为 O(n+m)

让我们看看新的函数:

def find_potential_customers_v2():"""找到去过普吉岛但是没去过新西兰的人,性能改进版"""# 首先,遍历所有新西兰访问记录,创建查找索引nz_records_idx = {(rec['first_name'], rec['last_name'], rec['phone_number'])for rec in users_visited_nz}for rec in users_visited_phuket:key = (rec['first_name'], rec['last_name'], rec['phone_number'])if key not in nz_records_idx:yield rec

使用了集合对象后,新函数在速度上相比旧版本有了飞跃性的突破。但是,对这个问题的优化并不是到此为止,不然文章标题就应该改成:“如何使用集合提高程序性能” 了。

对问题的重新思考

让我们来尝试重新抽象思考一下问题的本质。首先,我们有一份装了很多东西的容器 A(普吉岛访问记录),然后给我们另一个装了很多东西的容器 B(新西兰访问记录),之后定义相等规则:“姓名与电话一致”。最后基于这个相等规则,求 A 和 B 之间的“差集”

如果你对 Python 里的集合不是特别熟悉,我就稍微多介绍一点。假如我们拥有两个集合 A 和 B,那么我们可以直接使用 A-B 这样的数学运算表达式来计算二者之间的 差集

>>> a = {1, 3, 5, 7}>>> b = {3, 5, 8}# 产生新集合:所有在 a 但是不在 b 里的元素>>> a - b{1, 7}

所以,计算“所有去过普吉岛但没去过新西兰的人”,其实就是一次集合的求差值操作。那么要怎么做,才能把我们的问题套入到集合的游戏规则里去呢?

利用集合的游戏规则

在 Python 中,如果要把某个东西装到集合或字典里,一定要满足一个基本条件:“这个东西必须是可以被哈希(Hashable)的” 。什么是 “Hashable”?

举个例子,Python 里面的所有可变对象,比如字典,就 不是 Hashable 的。当你尝试把字典放入集合中时,会发生这样的错误:

>>> s = set()>>> s.add({'foo': 'bar'})Traceback (most recent call last):File "<stdin>", line 1, in <module>TypeError: unhashable type: 'dict'

所以,如果要利用集合解决我们的问题,就首先得定义我们自己的 “Hashable” 对象:VisitRecord。而要让一个自定义对象变得 Hashable,唯一要做的事情就是定义对象的 __hash__ 方法。

class VisitRecord:"""旅游记录"""def __init__(self, first_name, last_name, phone_number, date_visited):self.first_name = first_nameself.last_name = last_nameself.phone_number = phone_numberself.date_visited = date_visited

一个好的哈希算法,应该让不同对象之间的值尽可能的唯一,这样可以最大程度减少“哈希碰撞”发生的概率,默认情况下,所有 Python 对象的哈希值来自它的内存地址。

在这个问题里,我们需要自定义对象的 __hash__ 方法,让它利用 (姓,名,电话)元组作为 VisitRecord 类的哈希值来源。

def __hash__(self):return hash((self.first_name, self.last_name, self.phone_number))

自定义完 __hash__ 方法后, VisitRecord 实例就可以正常的被放入集合中了。但这还不够,为了让前面提到的求差值算法正常工作,我们还需要实现 __eq__ 特殊方法。

__eq__ 是 Python 在判断两个对象是否相等时调用的特殊方法。默认情况下,它只有在自己和另一个对象的内存地址完全一致时,才会返回 True。但是在这里,我们复用了 VisitRecord 对象的哈希值,当二者相等时,就认为它们一样。

def __eq__(self, other):# 当两条访问记录的名字与电话号相等时,判定二者相等。if isinstance(other, VisitRecord) and hash(other) == hash(self):return Truereturn False

完成了恰当的数据建模后,之后的求差值运算便算是水到渠成了。新版本的函数只需要一行代码就能完成操作:

def find_potential_customers_v3():return set(VisitRecord(**r) for r in users_visited_phuket) - \set(VisitRecord(**r) for r in users_visited_nz)

Hint:如果你使用的是 Python 2,那么除了 __eq__ 方法外,你还需要自定义类的 __ne__(判断不相等时使用) 方法。

使用 dataclass 简化代码

故事到这里并没有结束。在上面的代码里,我们手动定义了自己的 数据类 VisitRecord,实现了 __init____eq__ 等初始化方法。但其实还有更简单的做法。

因为定义数据类这种需求在 Python 中实在太常见了,所以在 3.7 版本中,标准库中新增了 dataclasses 模块,专门帮你简化这类工作。

如果使用 dataclasses 提供的特性,我们的代码可以最终简化成下面这样:

@dataclass(unsafe_hash=True)class VisitRecordDC:first_name: strlast_name: strphone_number: str# 跳过“访问时间”字段,不作为任何对比条件date_visited: str = field(hash=False, compare=False)def find_potential_customers_v4():return set(VisitRecordDC(**r) for r in users_visited_phuket) - \set(VisitRecordDC(**r) for r in users_visited_nz)

不用干任何脏活累活,只要不到十行代码就完成了工作。

案例总结

问题解决以后,让我们再做一点小小的总结。在处理这个问题时,我们一共使用了三种方案:

  1. 使用普通的两层循环筛选符合规则的结果集

  2. 利用哈希表结构(set 对象)创建索引,提升处理效率

  3. 将数据转换为自定义对象,利用规则,直接使用集合运算

为什么第三种方式会比前面两种好呢?

首先,第一个方案的性能问题过于明显,所以很快就会被放弃。那么第二个方案呢?仔细想想看,方案二其实并没有什么明显的缺点。甚至和第三个方案相比,因为少了自定义对象的过程,它在性能与内存占用上,甚至有可能会微微强于后者。

但请再思考一下,如果你把方案二的代码换成另外一种语言,比如 Java,它是不是基本可以做到 1:1 的完全翻译?换句话说,它虽然效率高、代码直接,但是它没有完全利用好 Python 世界提供的规则,最大化的从中受益。

如果要具体化这个问题里的“规则”,那就是 “Python 拥有内置结构集合,集合之间可以进行差值等四则运算” 这个事实本身。匹配规则后编写的方案三代码拥有下面这些优势:

  • 为数据建模后,可以更方便的定义其他方法

  • 如果需求变更,做反向差值运算、求交集运算都很简单

  • 理解集合与 dataclasses 逻辑后,代码远比其他版本更简洁清晰

  • 如果要修改相等规则,比如“只拥有相同姓的记录就算作一样”,只需要继承 VisitRecord 覆盖 __eq__ 方法即可

其他规则如何影响我们

在前面,我们花了很大的篇幅讲了如何利用“集合的规则”来编写事半功倍的代码。除此之外,Python 世界中还有着很多其他规则。如果能熟练掌握这些规则,就可以设计出符合 Python 惯例的 API,让代码更简洁精炼。

下面是两个具体的例子。

使用 __format__ 做对象字符串格式化

如果你的自定义对象需要定义多种字符串表示方式,就像下面这样:

class Student:def __init__(self, name, age):self.name = nameself.age = agedef get_simple_display(self):return f'{self.name}({self.age})'def get_long_display(self):return f'{self.name} is {self.age} years old.'piglei = Student('piglei', '18')# OUTPUT: piglei(18)print(piglei.get_simple_display())# OUTPUT: piglei is 18 years old.print(piglei.get_long_display())

那么除了增加这种 get_xxx_display() 额外方法外,你还可以尝试自定义 Student 类的 __format__ 方法,因为那才是将对象变为字符串的标准规则。

class Student:def __init__(self, name, age):self.name = nameself.age = agedef __format__(self, format_spec):if format_spec == 'long':return f'{self.name} is {self.age} years old.'elif format_spec == 'simple':return f'{self.name}({self.age})'raise ValueError('invalid format spec')piglei = Student('piglei', '18')print('{0:simple}'.format(piglei))print('{0:long}'.format(piglei))

使用 __getitem__ 定义对象切片操作

如果你要设计某个可以装东西的容器类型,那么你很可能会为它定义“是否为空”、“获取第 N 个对象”等方法:

class Events:def __init__(self, events):self.events = eventsdef is_empty(self):return not bool(self.events)def list_events_by_range(self, start, end):return self.events[start:end]events = Events(['computer started','os launched','docker started','os stopped',])# 判断是否有内容,打印第二个和第三个对象if not events.is_empty():print(events.list_events_by_range(1, 3))

但是,这样并非最好的做法。因为 Python 已经为我们提供了一套对象规则,所以我们不需要像写其他语言的 OO(面向对象) 代码那样去自己定义额外方法。我们有更好的选择:​​​​​​​

class Events:def __init__(self, events):self.events = eventsdef __len__(self):"""自定义长度,将会被用来做布尔判断"""return len(self.events)def __getitem__(self, index):"""自定义切片方法"""# 直接将 slice 切片对象透传给 events 处理return self.events[index]# 判断是否有内容,打印第二个和第三个对象if events:print(events[1:3])

新的写法相比旧代码,更能适配进 Python 世界的规则,API 也更为简洁。

关于如何适配规则、写出更好的 Python 代码。Raymond Hettinger 在 PyCon 2015 上有过一次非常精彩的演讲 “Beyond PEP8 - Best practices for beautiful intelligible code”。这次演讲长期排在我个人的 “PyCon 视频 TOP5” 名单上,如果你还没有看过,我强烈建议你现在就去看一遍 :)

Hint:更全面的 Python 对象模型规则可以在 官方文档 找到,有点难读,但值得一读。

总结

Python 世界有着一套非常复杂的规则,这些规则的涵盖范围包括“对象与对象是否相等“、”对象与对象谁大谁小”等等。它们大部分都需要通过重新定义“双下划线方法 __xxx__” 去实现。

如果熟悉这些规则,并在日常编码中活用它们,有助于我们更高效的解决问题、设计出更符合 Python 哲学的 API。下面是本文的一些要点总结:

  • 永远记得对原始需求做抽象分析,比如问题是否能用集合求差集解决

  • 如果要把对象放入集合,需要自定义对象的 __hash__ 与 __eq__ 方法

  • __hash__ 方法决定性能(碰撞出现概率), __eq__ 决定对象间相等逻辑

  • 使用 dataclasses 模块可以让你少写很多代码

  • 使用 __format__ 方法替代自己定义的字符串格式化方法

  • 在容器类对象上使用 __len__、 __getitem__ 方法,而不是自己实现

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/473304.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MyEclipse中代码提醒功能

一&#xff1a;最近仔细研究了下spring mvc中的代码&#xff0c;自己在配置文件哪里来时出现问题&#xff0c;没有提醒&#xff0c;只好自己搜了下有关的信息。如下 window--->preferences---->java---->Editor------>Content Assist如下图&#xff1a; 讲画圈部分…

天池 在线编程 木材加工(二分查找)

文章目录1. 题目2. 解题1. 题目 有一些原木&#xff0c;现在想把这些木头切割成一些长度相同的小段木头&#xff0c;需要得到的小段的数目至少为 k。 当然&#xff0c;我们希望得到的小段越长越好&#xff0c;你需要计算能够得到的小段木头的最大长度。 木头长度的单位是厘米…

利用numpy删除DataFrame某一行/列、多行内容

一、用法&#xff1a; DataFrame.drop(labelsNone,axis0, indexNone, columnsNone, inplaceFalse) 参数说明&#xff1a; labels&#xff1a;就是要删除的行列的名字&#xff0c;用列表给定axis&#xff1a; 默认为0&#xff0c;指删除行&#xff0c;因此删除columns时要指定…

天池 在线编程 最频繁出现的子串(字符串哈希)

文章目录1. 题目2. 解题1. 题目 给定一个字符串&#xff0c;我们想知道满足以下两个条件的子串最多出现了多少次&#xff1a; 子串的长度在之间 [minLength, maxLength] 子串的字符种类不超过 maxUnique 写一个函数 getMaxOccurrences &#xff0c;其返回满足条件的子串最多出…

关于Certificate、Provisioning Profile、App ID的介绍及其之间的关系

刚接触iOS开发的人难免会对苹果的各种证书、配置文件等不甚了解&#xff0c;可能你按照网上的教程一步一步的成功申请了真机调试&#xff0c;但是还是对其中的缘由一知半解。这篇文章就对Certificate、Provisioning Profile等做个总结。 1.概念介绍 如果你拥有一个开发者账户的…

12306 抢票项目霸榜 GitHub,标星即将破万

十一将至&#xff0c;你买到回家的火车票了吗&#xff1f;如果没有&#xff0c;你可以试着打开 GitHub&#xff0c;在搜索栏键入 12306 的关键词&#xff0c;我相信你会发现一个新大陆。没错&#xff0c;这里有 1572 个抢票项目。它们大多用 Python、JavaScript、Java 写成。其…

LeetCode 1700. 无法吃午餐的学生数量(队列模拟 / 不模拟)

文章目录1. 题目2. 解题1. 题目 学校的自助午餐提供圆形和方形的三明治&#xff0c;分别用数字 0 和 1 表示。 所有学生站在一个队列里&#xff0c;每个学生要么喜欢圆形的要么喜欢方形的。 餐厅里三明治的数量与学生的数量相同。 所有三明治都放在一个 栈 里&#xff0c;每一…

leetcode - Linked List Cycle

题目&#xff1a;Linked List Cycle Given a linked list, determine if it has a cycle in it. Follow up:Can you solve it without using extra space? 个人思路&#xff1a; 1、判断一个链表是否有环&#xff0c;标准做法是采取快慢指针&#xff0c;一个走一步&#xff0c…

高效程序员的 7 项技能

软件工程师把大量时间花在练习 LeetCode 问题获得面试技巧和完善简历上。一旦他们最终在一家初创公司、谷歌、亚马逊或其他公司找到工作&#xff0c;他们可能就会发现&#xff0c;他们获得这份工作所需的技能与他们日常工作所需的技能并不匹配。 受 TechLead 高效程序员的七项技…

想成为企业争抢的目标吗?你需要掌握的五大热门IT技能

在任何一个行业确保有最新的工作技能很重要&#xff0c;而在日新月异的IT界尤为重要&#xff0c;因为过去学到的技术和实践可能再也不是今天完成工作所需的技能和实践了。不管你何时或如何学得IT技能——无论通过正规的大学学位课程、强化培训班&#xff0c;还是完全通过自学并…

LeetCode 1701. 平均等待时间(模拟)

文章目录1. 题目2. 解题1. 题目 有一个餐厅&#xff0c;只有一位厨师。你有一个顾客数组 customers &#xff0c;其中 customers[i] [arrivali, timei] &#xff1a; arrivali 是第 i 位顾客到达的时间&#xff0c;到达时间按 非递减 顺序排列。timei 是给第 i 位顾客做菜需…

JS全选功能代码优化

JS全选功能代码优化 原文:JS全选功能代码优化JS全选功能代码优化 最近在看javascript MVC那本书&#xff0c;也感觉到自己写的代码也并不优雅&#xff0c;所以一直在想 用另一种模式来编写JS代码&#xff0c;所以针对之前的简单的JS全选功能来做个简单的demo&#xff0c;使用目…

Win 7 通过事件管理器查看计算机开机关机时间

控制面板-管理工具-事件查看器 视图中开机来源&#xff1a;Kernel-General 事件ID&#xff1a;13 关机来源&#xff1a;Kernel-General 事件ID&#xff1a;12 转载于:https://www.cnblogs.com/hyiam/p/3810499.html

怎么样才能更高效的学习区块链

一、为什么选择区块链 选择区块链作为实践学习的案例&#xff0c;原因有三&#xff1a; 第一&#xff0c;区块链是我最近两三个月刚学习的领域&#xff0c;对我来说也是一门从零开始学习的领域&#xff0c;这样的学习案例最具有指导作用。因为时间没有隔太久&#xff0c;很多…

LeetCode 1702. 修改后的最大二进制字符串(贪心)

文章目录1. 题目2. 解题1. 题目 给你一个二进制字符串 binary &#xff0c;它仅有 0 或者 1 组成。你可以使用下面的操作任意次对它进行修改&#xff1a; 操作 1 &#xff1a;如果二进制串包含子字符串 "00" &#xff0c;你可以用 "10" 将其替换。 比方说…

如何在python中精确地进行浮点数的四舍五入

在python试题中碰到这么一道题&#xff1a; 输入三个浮点数,求它们的平均值并保留 1 位小数,对小数后第二位数进行四舍五入,最后输出结果 错误示范 因为涉及到四舍五入&#xff0c;随便搜了一下&#xff0c;发现了好多博客都用round()&#xff0c;就直接拿来用了 round(1.55…

c语言中的位移位操作

先要了解一下C语言里全部的位运算都是指二进制数的位运算。即使输入的是十进制的数&#xff0c;在内存中也是存储为二进制形式。 “<<”使用方法&#xff1a; 格式是&#xff1a;a<<m&#xff0c;a和m必须是整型表达式&#xff0c;要求m>0。 功能&#xff1a…

LeetCode 1704. 判断字符串的两半是否相似

文章目录1. 题目2. 解题1. 题目 给你一个偶数长度的字符串 s 。将其拆分成长度相同的两半&#xff0c;前一半为 a &#xff0c;后一半为 b 。 两个字符串 相似 的前提是它们都含有相同数目的元音&#xff08;‘a’&#xff0c;‘e’&#xff0c;‘i’&#xff0c;‘o’&#…

怎样处理糟糕的代码?

在职业生涯中&#xff0c;程序员难免会遇到糟糕的代码&#xff08;bad code)——但是你并不需要成为一个打败这些糟糕代码的“恶人”。 从轻松的角度来讲&#xff0c;糟糕的代码可以创造大量的就业机会。比如&#xff1a; 需要从诸多优秀开发人员中找一个人来修复错误代码。需…

LeetCode 1706. 球会落何处(模拟)

文章目录1. 题目2. 解题1. 题目 用一个大小为 m x n 的二维网格 grid 表示一个箱子。 你有 n 颗球。箱子的顶部和底部都是开着的。 箱子中的每个单元格都有一个对角线挡板&#xff0c;跨过单元格的两个角&#xff0c;可以将球导向左侧或者右侧。 将球导向右侧的挡板跨过左上…