diff --git a/src/.vuepress/sidebar.ts b/src/.vuepress/sidebar.ts index 04b362a4f34..ec6a1246893 100644 --- a/src/.vuepress/sidebar.ts +++ b/src/.vuepress/sidebar.ts @@ -4,4 +4,35 @@ export default sidebar({ // "/HomeWork": "structure", // "/R-Course/": "structure", // "/note/": "structure", + "Python/Python-core-technology-and-practice": [ + { + text: "开篇词", + collapsible: true, + icon: "python", + children: [ + "00", + ], + }, + { + text: "基础篇", + icon: "python", + collapsible: true, + children: [ + "01", + "02", + "03", + "04", + "05", + "06", + "07", + "08", + "09", + "10", + "11", + "12", + "13", + "14", + ], + }, + ] }); diff --git a/src/.vuepress/theme.ts b/src/.vuepress/theme.ts index de85a2a033e..1379452450b 100644 --- a/src/.vuepress/theme.ts +++ b/src/.vuepress/theme.ts @@ -46,6 +46,7 @@ export default hopeTheme({ "/Python/Assignment/Assignment1.html": ["aiyc", "ydn", "Vector", "yh", "zy", "xxy", "lcy", "kai"], "/Python/Assignment/Assignment2.html": ["aiyc", "ydn", "Vector", "yh", "zy", "xxy", "lcy", "kai"], "/Python/basequestion/": ["aiyc", "ydn", "Vector", "yh", "zy", "xxy", "lcy", "kai"], + "Python/Python-core-technology-and-practice": ["aiyc", "kai"], }, }, diff --git a/src/Python/.DS_Store b/src/Python/.DS_Store index 214fbb3937b..92815b9fa13 100644 Binary files a/src/Python/.DS_Store and b/src/Python/.DS_Store differ diff --git a/src/Python/Python-core-technology-and-practice/.DS_Store b/src/Python/Python-core-technology-and-practice/.DS_Store new file mode 100755 index 00000000000..1204a7dab0e Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/.DS_Store differ diff --git a/src/Python/Python-core-technology-and-practice/00.assets/1619238527769-3c72765d-11fb-4a70-91d9-aadb6fddbe8f.png b/src/Python/Python-core-technology-and-practice/00.assets/1619238527769-3c72765d-11fb-4a70-91d9-aadb6fddbe8f.png new file mode 100755 index 00000000000..204cae8c776 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/00.assets/1619238527769-3c72765d-11fb-4a70-91d9-aadb6fddbe8f.png differ diff --git a/src/Python/Python-core-technology-and-practice/00.md b/src/Python/Python-core-technology-and-practice/00.md new file mode 100755 index 00000000000..d426bc27afc --- /dev/null +++ b/src/Python/Python-core-technology-and-practice/00.md @@ -0,0 +1,110 @@ +--- +title: 开篇词-从工程的角度深入理解 Python +icon: python +date: 2022-08-27 18:46:28 +author: AI悦创 +isOriginal: true +category: Python 进阶 +tag: + - Python 进阶 +sticky: false +star: false +article: true +timeline: true +image: false +navbar: true +sidebarIcon: true +headerDepth: 5 +comment: true +lastUpdated: true +editLink: false +backToTop: true +toc: true +--- + +![image.png](./00.assets/1619238527769-3c72765d-11fb-4a70-91d9-aadb6fddbe8f.png) + +你好,我是悦创。 + +我是一名全栈工程师,目前学习机器学习的知识,主要学习领域是人工智能的推荐排序系统与算法。 + +一听机器学习,很多人第一反应可能是“好难呀、厉害呀”。可事实上,我的编程之路并非一路高光。 + +不同于大城市长大或竞赛出身、十一二岁接触编程的人,在刚上大学时,我的编程基础几乎为零。大一上的 C 语言,便是我出生起学到的第一门编程语言。初识计算机语言的世界,很有趣也很吸引我,这也是我成为程序员的最初动力。 + +和很多对编程感兴趣的人一样,哪怕老师只是在讲台上,照本宣科地读着 N 年前的课件,我也会竖起耳朵认真听讲、认真做笔记。并且,私下里我还买了不少厚重的大块头书,在网上查了不少博客、帖子,照着上面的例子一行行地敲代码。很多内容我并不理解,比如指针、递归这类抽象的概念,查了一堆资料也没看明白。但靠着死记硬背,考试基本可以过关,虽然这个过程比较痛苦,也比较累。 + +后来,为了更深入了解计算机,又陆续学到不少新的编程语言,比如 Node.js、Python、PHP、Scala 等等。这个阶段,我边学习,边做项目,却发现轻松了很多。 + +这两个学习阶段,收获和感受天差地别,难道仅仅是因为“万事入门难”吗?我不止一次反思过这个问题,终于发现,问题出在了**资料本身**上。 + +**为什么这么说呢?** 一是因为书上或网上的很多东西,非常理论化,实例少之又少,单凭死记硬背很难真正掌握;二是这些内容中,原创的观点和经验更少,大多互相抄袭,内容雷同且不实用,远离实际工程,毫无借鉴价值。 + +但显然,市面上的资料问题,我们个人是很难解决的。我们能做的,便是克服常见资料的弊端,另辟蹊径来学习。这其中,最重要的一点就是,**从工程的角度思考学习,以实用为出发点,多练习、多阅读、多做项目,这样才能有质的提高。** + +接触编程这么多年,也验证了我的观点。我身边的新手,他们学习新的语言总是只会啃书练习,还难以上手; **而有经验的同事则不同,他们能花很短的时间看完基础语法,然后找行家去了解一些重难点、易错点,最后亲自动手完成一个项目,达到融会贯通的效果。** 这样下来,可能几周时间就掌握得差不多了。 + +这样的差距,确实让人心塞,而这也是我开这个专栏的最初动力——帮助更多入门级程序员迅速成长。至于专栏主题,我选择了 Python 这门编程语言,原因也很明了。 + +这首先来自于我个人的重要感悟。经过多年学习工作的积累,我深刻认识到, **牢牢掌握一门编程语言及其学习方法,是日后在所有领域深造的根基。** 而在实际工作和生活中,我更是见过不少反例,比如搞机器学习的工程师,算法、理论等极强,但是编程水平或是工程水平很一般,于是涉及到偏工程的工作或合作时,就显得力不从心,这样就非常可惜了。 + +另外,不可否认,Python 确实是这个时代最流行、也必须要掌握的编程语言。Python 可以运用在数据处理、Web 开发、人工智能等多个领域,它的语言简洁、开发效率高、可移植性强,并且可以和其他编程语言(比如 C++)轻松无缝衔接。现如今,不少学校的文科生甚至中学生也开设了此课程,可见其重要程度。 + +因此,我决定开设这么一个专栏,从工程的角度去讲解 Python 这门编程语言。我不是语言学专家,不会死抠一些很偏的知识点;相反,作为一名工程师,我会从实际出发,以工作中遇到的实例为主线,去讲解 Python 的核心技术和应用。 + +专栏的所有内容都基于 Python 的 3.7 版本,其中有大量独家解读、案例,以及不少我阅读源码后的发现和体会。同时,在层次划分上,我希望能难易兼顾,循序渐进。专栏中既有核心的基础知识,也有高级的进阶操作,尽量做到“老少皆宜”。 + +从内容上来说,专栏主要分为四大版块。 + + + +# 1. Python 基础篇 + +第一部分主要讲解 Python 的基础知识。当然,不同于其他基础教材,专栏的基础版块并不只有基础概念、操作,我同时加入了很多进阶难度的知识,或是一些重难点、易错点等需要注意的地方。如果你觉得自己基础的东西都会了,这部分不用学了,那你就大错特错了。比如, + +- 列表和元组存储结构的差异是怎样的?它们性能的详细比较又如何? +- 字符串相加的时间复杂度,你真的清楚吗? + +**基础不牢,地动山摇。** 更深刻、实质的基础理解,才是更牢固的知识大厦的根基。我希望这一版块,不仅可以让入门级的程序员查漏补缺、打牢基础,也能让有经验的程序员,重新从工程角度认识基础、升华理解。 + +# 2. Python 进阶篇 +这部分讲的是 Python 的一些进阶知识,比如装饰器、并发编程等等。如果你的工作只是写 100 行以下的脚本程序,可能不怎么会用得到。但如果你做的是大型程序的开发,则非常有必要。我希望通过这一版块,让你熟悉各种高级用法,真正理解 Python,理解这门编程语言的特点。 + +# 3. Python 规范篇 +这部分着重于教你把程序写得更加规范、更加稳定。我在实际工作中见过不少程序员,会写程序,但写得实在有点“惨不忍睹”,导致最后调试起来错误不断,修改非常费劲儿。因此,我觉得用单独一个版块讲解这个问题非常有必要。 + +当然,我不会用一些似是而非的规范来说教,而是会用具体的编程操作和技巧,教你提高代码质量。比如,如何合理地分解代码、运用 assert,如何写单元测试等等。 + +# 4. Python 实战篇 +没上过战场开过枪的人,不可能做主官;没有实战经验的语言学习者,不可能成为高手。**这部分,我会通过量化交易系统这个具体的实战案例,带你综合运用前面所学的 Python 知识。** + +真正要掌握一门编程语言,仅仅学会分散的知识点是不够的,还必须要把知识点串联起来,做一些中型的项目才能有更深的领悟与提高。 + +专栏篇幅只有 40 多篇,但是每篇绝对都是干货满满。我希望这个专栏,能帮助更多入门级和有一定项目基础的程序员,真正掌握 Python,并且给你一些学习上的启发。 + +100 天后,晋级为 Python 高手,让我们一起加油吧! + +**课程的练习代码:**[**https://gitee.com/huangjiabaoaiyc/PythonPractice**](https://gitee.com/huangjiabaoaiyc/PythonPractice) + +欢迎关注我公众号:AI悦创,有更多更好玩的等你发现! + +::: details 公众号:AI悦创【二维码】 + +![](/gzh.jpg) + +::: + +::: info AI悦创·编程一对一 + +AI悦创·推出辅导班啦,包括「Python 语言辅导班、C++ 辅导班、java 辅导班、算法/数据结构辅导班、少儿编程、pygame 游戏开发」,全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践等。当然,还有线下线上摄影课程、Photoshop、Premiere 一对一教学、QQ、微信在线,随时响应!微信:Jiabcdefh + +C++ 信息奥赛题解,长期更新!长期招收一对一中小学信息奥赛集训,莆田、厦门地区有机会线下上门,其他地区线上。微信:Jiabcdefh + +方法一:[QQ](http://wpa.qq.com/msgrd?v=3&uin=1432803776&site=qq&menu=yes) + +方法二:微信:Jiabcdefh + +::: + +![](/zsxq.jpg) + diff --git a/src/Python/Python-core-technology-and-practice/01.assets/2cfc18adf51b61ca8140561071d20c1d.png b/src/Python/Python-core-technology-and-practice/01.assets/2cfc18adf51b61ca8140561071d20c1d.png new file mode 100755 index 00000000000..0843510ac47 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/01.assets/2cfc18adf51b61ca8140561071d20c1d.png differ diff --git a/src/Python/Python-core-technology-and-practice/01.assets/6415e7ddb2a3b3d222b052569e8195c9.jpg b/src/Python/Python-core-technology-and-practice/01.assets/6415e7ddb2a3b3d222b052569e8195c9.jpg new file mode 100755 index 00000000000..107754241a3 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/01.assets/6415e7ddb2a3b3d222b052569e8195c9.jpg differ diff --git a/src/Python/Python-core-technology-and-practice/01.md b/src/Python/Python-core-technology-and-practice/01.md new file mode 100755 index 00000000000..2d7127f2609 --- /dev/null +++ b/src/Python/Python-core-technology-and-practice/01.md @@ -0,0 +1,130 @@ +--- +title: 01-如何逐步突破,成为Python高手? +icon: python +date: 2022-11-22 00:15:56 +author: AI悦创 +isOriginal: true +category: Python 进阶 +tag: + - Python 进阶 +sticky: false +star: false +article: true +timeline: true +image: false +navbar: true +sidebarIcon: true +headerDepth: 5 +comment: true +lastUpdated: true +editLink: false +backToTop: true +toc: true +--- + +![img](./01.assets/6415e7ddb2a3b3d222b052569e8195c9.jpg) + +你好,我是悦创。 + +工作中,我总听到很多程序员抱怨,说现在的计算机编程语言太多了,学不过来了。一些人 Java 用了很多年,但是最近的项目突然需要用 Python,就会不知所措,压力很大。 + +众所周知,Facebook 的主流语言是 Hack(PHP 的进化版本)。不过,我敢拍着胸脯说,就刚入职的工程师而言,100 个里至少有 95 个,以前都从未用过 Hack 或者 PHP。但是,这些人上手都特别快,基本上一两周后,日常编程便毫无压力了。 + +他们是怎么做到的呢? + +事实上,他们遵循的,正是我在开篇词中提到的方法,也是本专栏学习的中心观点:“从工程的角度去学习 Python”。那么具体来说,到底要怎么学,学习的过程中又要特别注意哪些地方呢? + +## 1. 不同语言,需融会贯通 + +其实,如果你在学一门语言的时候多阅读、多练习、多思考,你就会发现,不同语言都是类似的。编程语言本就是人类控制计算机的指令,语法规则等方面自然大同小异。 + +而在原有基础上,学习一门新的编程语言,其实也没有那么难,你首先要做到的是明确区分。比如,在学习 Python 的条件与循环语句时,多回忆一下其他语言的语法是怎样的。再如,遇到 Python 中的字符串相加时,你能分析出它的复杂度吗?再联想到其他语言,比如 Java 中字符串相加的复杂度,它们之间有什么相同点、又有什么区别呢? + +除了能够明确区分语言的不同点,我们还要能联系起来灵活运用。比如,最典型的“编程语言两问”: + +- 你了解你学过的每种编程语言的特点吗? +- 你能根据不同的产品需求,选用合适的编程语言吗? + +举个例子,Python 的优点之一是特别擅长数据分析,所以广泛应用于人工智能、机器学习等领域,如机器学习中 TensorFlow 的框架,就是用 Python 写的。但是涉及到底层的矩阵运算等等,还是要依赖于 C++ 完成,因为 C++ 的速度快,运行效率更高。 + +事实上,很多公司都是这样,服务器端开发基于 Python,但底层的基础架构依赖于 C++。这就是典型的“不同需求选用不同语言”。毕竟,你要明白,哪怕只是几十到几百毫秒的速度差距,对于公司、对于用户体验来说都是决定性的。 + +## 2. 唯一语言,可循序渐进 + +当然,如果 Python 是你学的第一门编程语言,那也不必担心。我们知道,虽然同为人机交互的桥梁,Python 语言比起 C++、Java 等主流语言,语法更简洁,也更接近英语,对编程世界的新人还是很友好的,这也是其显著优点。这种情况下,你要做的就是专注于 Python 这一门语言,明确学习的重点,把握好节奏循序渐进地学习。 + +根据我多年的学习工作经验,我把编程语言的学习重点,总结成了下面这三步,无论你是否有其他语言的基础,都可以对照来做,稳步进阶。 + +## 3. 第一步:大厦之基,勤加练习 + +任何一门编程语言,其覆盖范围都是相当广泛的,从基本的变量赋值、条件循环,到并发编程、Web 开发等等,我想市面上几乎没有任何一本书能够罗列完全。 + +所以,我建议你,在掌握必要的基础时,就得多上手操作了。千万不要等到把教材上所有东西都学完了才开始,因为到那时候你会发现,前面好不容易记住的一堆东西似乎又忘记了。计算机科学是一门十分讲究实战的学科,因此越早上手练习,练得越多越勤,就越好。 + +不过,到底什么叫做必要的基础呢?以 Python 为例,如果你能够理解变量间的赋值、基本的数据类型、条件与循环语句、函数的用法,那么你就达到了第一步的底线标准,应该开始在课下多多练习了。 + +比方说,你可以自己动手编程做一个简易的计算器,这应该也是大多数程序员实操的第一个小项目。用户输入数字和运算符后,你的程序能够检查输入是否合法并且返回正确的结果吗? + +在做这个小项目的过程中,你可能会遇到不少问题。我的建议是,遇到不懂的问题时,多去 [Stack Overflow](https://stackoverflow.com/) 上查询,这样你还能阅读别人优秀的代码,借鉴别人的思路,对于你的学习肯定大有帮助。当然,实在解决不了的问题,也可以写在留言区,我们一起来解决。 + +## 3. 第二步:代码规范,必不可少 + +诚然,学习编程讲究快和高效。但是,与此同时,请一定不要忽略每一种语言必要的编程规范。在你自己刚开始写代码练习时,你可以不写单元测试,但总不能几百行的代码却没有一个函数,而是从头顺序写到尾吧?你可以省略一些可有可无的注释,但总不能把很多行代码全部并到一行吧? + +比如,我们来看下面这行代码: + +```python +v.A(param1, param2, param3).B(param4, param5).C(param6, param7).D() +``` + +显然,这样写十分不科学,应该把它拆分成多行: + +```python +v.A(param1, param2, param3) \ # 字符'\'表示换行 + .B(param4, param5) \ + .C(param6, param7) \ + .D() +``` + +再比如,变量和函数的命名虽有一定的随意性,但一定要有意义。如果你图省事,直接把变量依次命名为 v1、v2、v3 等,把函数依次命名为 func1、func2、func3 等等,不仅让其他人难理解,就算是你自己,日后维护起来都费劲儿。 + +一名优秀的程序员,一定遵守编程语言的代码规范。像 Facebook 的工程师,每次写完代码都必须经过别人的 review 才能提交。如果有不遵守代码规范的例子,哪怕只是一个函数或是一个变量的命名,我们都会要求原作者加以修改,严格规范才能保证代码库的代码质量。 + +## 4. 第三步:开发经验,质的突破 + +想要真正熟练地掌握 Python 或者是任何一门其他的编程语言,拥有大中型产品的开发经验是必不可少的。因为实战经验才能让你站得更高,望得更远。 + +比如我们每天都在用搜索引擎,但你了解一个搜索引擎的服务器端实现吗?这是一个典型的面向对象设计,你需要定义一系列相关的类和函数,需要从产品需求、代码复杂度、效率以及可读性等多个方面考虑,同时,上线后还要进行各种优化等等。 + +当然,在专栏里我没办法让你完成一个上亿用户级的实践产品,但是我会把自己这些年的开发经验倾囊相授,并通过量化交易这个实战案例,带你踏入“高级战场”,帮你掌握必要的开发知识。 + +最后,我专门为你绘制了一张 Python 学习的知识图谱,里面涵盖了 Python 最高频的核心知识,大部分内容我在专栏中都会讲到。你可以保存或者打印出来,作为学习参考。 + +![img](./01.assets/2cfc18adf51b61ca8140561071d20c1d.png) + +今天,我跟你分享了 Python 的学习方法和注意事项,其实这些观点不只适用于 Python,也能帮助你学习任何一门其他计算机编程语言,希望你能牢记在心。在接下来的课程里,我会带你逐步突破,最终成为一名 Python 高手。 + +那么,对于学习 Python 或者是其他编程语言,你有什么困扰或是心得吗?欢迎在留言区与我交流! + +欢迎关注我公众号:AI悦创,有更多更好玩的等你发现! + +::: details 公众号:AI悦创【二维码】 + +![](/gzh.jpg) + +::: + +::: info AI悦创·编程一对一 + +AI悦创·推出辅导班啦,包括「Python 语言辅导班、C++ 辅导班、java 辅导班、算法/数据结构辅导班、少儿编程、pygame 游戏开发」,全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践等。当然,还有线下线上摄影课程、Photoshop、Premiere 一对一教学、QQ、微信在线,随时响应!微信:Jiabcdefh + +C++ 信息奥赛题解,长期更新!长期招收一对一中小学信息奥赛集训,莆田、厦门地区有机会线下上门,其他地区线上。微信:Jiabcdefh + +方法一:[QQ](http://wpa.qq.com/msgrd?v=3&uin=1432803776&site=qq&menu=yes) + +方法二:微信:Jiabcdefh + +::: + +![](/zsxq.jpg) + diff --git a/src/Python/Python-core-technology-and-practice/02.assets/5c3daf49453370c3aa7ddf3bb36cab2d.png b/src/Python/Python-core-technology-and-practice/02.assets/5c3daf49453370c3aa7ddf3bb36cab2d.png new file mode 100755 index 00000000000..8f98b34656f Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/02.assets/5c3daf49453370c3aa7ddf3bb36cab2d.png differ diff --git a/src/Python/Python-core-technology-and-practice/02.assets/dee40d0f591d3f5e2f43839dccc24471.png b/src/Python/Python-core-technology-and-practice/02.assets/dee40d0f591d3f5e2f43839dccc24471.png new file mode 100755 index 00000000000..7fb9d961a3d Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/02.assets/dee40d0f591d3f5e2f43839dccc24471.png differ diff --git a/src/Python/Python-core-technology-and-practice/02.assets/f81efe2538074a3385b9ba70aced2cc9.png b/src/Python/Python-core-technology-and-practice/02.assets/f81efe2538074a3385b9ba70aced2cc9.png new file mode 100755 index 00000000000..509c550ff21 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/02.assets/f81efe2538074a3385b9ba70aced2cc9.png differ diff --git a/src/Python/Python-core-technology-and-practice/02.assets/ff53754d5318668c1014674c3917d308.jpg b/src/Python/Python-core-technology-and-practice/02.assets/ff53754d5318668c1014674c3917d308.jpg new file mode 100755 index 00000000000..1a395d7bdd8 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/02.assets/ff53754d5318668c1014674c3917d308.jpg differ diff --git a/src/Python/Python-core-technology-and-practice/02.md b/src/Python/Python-core-technology-and-practice/02.md new file mode 100755 index 00000000000..2a5f217b0a1 --- /dev/null +++ b/src/Python/Python-core-technology-and-practice/02.md @@ -0,0 +1,140 @@ +--- +title: 02-Jupyter Notebook为什么是现代Python的必学技术? +icon: python +date: 2022-11-22 00:29:36 +author: AI悦创 +isOriginal: true +category: Python 进阶 +tag: + - Python 进阶 +sticky: false +star: false +article: true +timeline: true +image: false +navbar: true +sidebarIcon: true +headerDepth: 5 +comment: true +lastUpdated: true +editLink: false +backToTop: true +toc: true +--- + +![img](./02.assets/ff53754d5318668c1014674c3917d308.jpg) + +你好,我是悦创。 + +Stack Overflow 曾在 2017 年底,发布了在该站上各种语言的提问流量。其中,Python 已经超过了 JavaScript 成为了流量最高的语言,预测在 2020 年前会远远甩开 JavaScript。 + +![img](./02.assets/5c3daf49453370c3aa7ddf3bb36cab2d.png) + +可能你已经知道,Python 在 14 年后的“崛起”,得益于机器学习和数学统计应用的兴起。那为什么 Python 如此适合数学统计和机器学习呢?作为“老司机”的我可以肯定地告诉你,Jupyter Notebook ([https://jupyter.org/](https://jupyter.org/))功不可没。 + +毫不夸张地说,根据我对 Facebook 等硅谷一线大厂的了解,一个 Python 工程师如果现在还不会使用 Jupyter Notebook 的话,可能就真的太落伍了。 + +磨刀不误砍柴工,高效的工具让我们的编程事半功倍。这一节课,我就来带你学习一下 Jupyter Notebook,为后面的 Python 学习打下必备基础。 + +## 1. 什么是 Jupyter Notebook? + +说了这么多,到底什么是 Jupyter Notebook?按照 Jupyter 创始人 Fernando Pérez 的说法,他最初的梦想是做一个综合 Ju (Julia)、Py (Python)和 R 三种科学运算语言的计算工具平台,所以将其命名为 Ju-Py-te-R。发展到现在,Jupyter 已经成为一个几乎支持所有语言,能够把**软件代码、计算输出、解释文档、多媒体资源**整合在一起的多功能科学运算平台。 + +英文里说一图胜千言(A picture is worth a thousand words)。看下面这个图片,你就明白什么是 Jupyter Notebook 了。 + +![img](./02.assets/dee40d0f591d3f5e2f43839dccc24471.png) + +你在一个框框中直接输入代码,运行,它立马就在下面给你输出。怎么样,是不是很酷?你可能会纳闷儿,这样一个看起来“华而不实”的玩意儿,真的就成了 Python 社区的颠覆者吗?说实话放在几年前我也是不信的。所以 Jupyter Notebook 的影响究竟有多大呢? + +## 2. Jupyter Notebook 的影响力 + +我们衡量一个技术的影响力,或者说要用自己的技术去影响世界时,必定绕不开这个技术对教育界的影响力。 + +就拿微软的 Word 文本处理系统来说吧。从纯技术角度来讲,Word 的单机设计理念早已落后时代 20 年。但以 Google Doc 为代表的在线文档系统,却并没有像想象中那样,实现对 Word 的降维打击。 + +直观的原因是用户习惯,使用 Word 修改文档,那就来回发几十遍呗,用着也还可以。但更深刻来想,之所以养成这样的用户习惯,是因为我们的教育根源。教育系统从娃娃抓起,用小学中学大学十几年的时间,训练了用户 Word 的使用习惯。到工作中,老员工又会带着新员工继续使用 Word,如此行程技术影响力生生不息的正向反馈。 + +回到我们今天的主题,我们来看 Jupyter Notebook。从 2017 年开始,已有大量的北美顶尖计算机课程,开始完全使用 Jupyter Notebook 作为工具。比如李飞飞的 CS231N《计算机视觉与神经网络》课程,在 16 年时作业还是命令行 Python 的形式,但是 17 年的作业就全部在 Jupyter Notebook 上完成了。再如 UC Berkeley 的《数据科学基础》课程,从 17 年起,所有作业也全部用 Jupyter Notebook 完成。 + +而 Jupyter Notebook 在工业界的影响力更甚。在 Facebook,虽然大规模的后台开发仍然借助于功能齐全的 IDE,但是几乎所有的中小型程序,比如内部的一些线下分析软件,机器学习模块的训练都是借助于 Jupyter Notebook 完成的。据我了解,在别的硅谷一线大厂,例如 Google 的 AI Research 部门 Google Brain,也是清一色地全部使用 Jupyter Notebook,虽然用的是他们自己的改进定制版,叫 Google Colab。 + +看到这里,相信你已经认可了 Jupter Notebook 现如今的江湖地位。不过,说到技术的选择,有些人会说,这个技术流行,我们应该用;有些人认为,阿里已经在用这个技术了,这就是未来,我们也要用等等。不得不说,这些都是片面的认知。不管是阿里还是 Facebook 用的技术,其实不一定适用你的应用场景。 + +我经常会鼓励技术同行,对于技术选择要有独立的思考,不要人云亦云。最起码你要去思考,Facebook 为什么选择这个技术?这个技术解决了哪些问题?Facebook 为什么不选择别的技术?有哪些局限?单从选择结果而言,Facebook 选择的技术很可能是因为它有几百个产品线,几万个工程师。而同样的技术,在一个十人的团队里,反而成了拖累。 + +在这里,我不想忽悠你任何技术,我想教会你的是辩证分析技术的思考方法。接下来,我们就来看看,Jupyter 究竟解决了哪些别人没有解决的问题。 + +## 3. Jupyter 的优点 + +### 3.1 整合所有的资源 + +在真正的软件开发中,上下文切换占用了大量的时间。什么意思呢?举个例子你就很好理解了,比如你需要切换窗口去看一些文档,再切换窗口去用另一个工具画图等等。这些都是影响生产效率的因素。 + +正如我前面提到的,Jupyter 通过把所有和软件编写有关的资源全部放在一个地方,解决了这个问题。当你打开一个 Jupyter Notebook 时,就已经可以看到相应的文档、图表、视频和相应的代码。这样,你就不需要切换窗口去找资料,只要看一个文件,就可以获得项目的所有信息。 + +### 3.2 交互性编程体验 + +在机器学习和数学统计领域,Python 编程的实验性特别强,经常出现的情况是,一小块代码需要重写 100 遍,比如为了尝试 100 种不同的方法,但别的代码都不想动。这一点和传统的 Python 开发有很大不同。如果是在传统的 Python 开发流程中,每一次实验都要把所有代码重新跑一遍,会花费开发者很多时间。特别是在像 Facebook 这样千万行级别的代码库里,即使整个公司的底层架构已经足够优化,真要重新跑一遍,也需要几分钟的时间。 + +而 Jupyter Notebook 引进了 Cell 的概念,每次实验可以只跑一小个 Cell 里的代码;并且,所见即所得,在代码下面立刻就可以看到结果。这样强的互动性,让 Python 研究员可以专注于问题本身,不被繁杂的工具链所累,不用在命令行直接切换,所有科研工作都能在 Jupyter 上完成。 + +### 3.3 零成本重现结果 + +同样在机器学习和数学统计领域,Python 的使用是非常短平快的。常见的场景是,我在论文里看到别人的方法效果很好,可是当我去重现时,却发现需要 pip 重新安装一堆依赖软件。这些准备工作可能会消耗你 80% 的时间,却并不是真正的生产力。 + +Jupyter Notebook 如何解决这个问题呢? + +其实最初的 Jupyter Notebook 也是挺麻烦的,需要你先在本机上安装 IPython 引擎及其各种依赖软件。不过现在的技术趋势,则是彻底云端化了,例如 Jupyter 官方的 Binder 平台(介绍文档:[https://mybinder.readthedocs.io/en/latest/index.html](https://mybinder.readthedocs.io/en/latest/index.html))和 Google 提供的 Google Colab 环境(介绍:[https://colab.research.google.com/notebooks/welcome.ipynb](https://colab.research.google.com/notebooks/welcome.ipynb))。它们让 Jupyter Notebook 变得和石墨文档、Google Doc 在线文档一样,在浏览器点开链接就能运行。 + +所以,现在当你用 Binder 打开一份 GitHub 上的 Jupyter Notebook 时,你不需要安装任何软件,直接在浏览器打开一份代码,就能在云端运行。 + +## 4. Jupyter Notebook 初体验 + +学习技术的最好方法就是用技术。不过,在今天的篇幅里,我不可能带你完全学会 Jupyter Notebook 的所有技巧。我想先带你直接感受一下,使用 Jupyter Notebook 的工作体验。 + +比如这样一个 [GitHub 文件](https://github.com/binder-examples/python2_with_3/blob/master/index3.ipynb) 。在 [Binder](https://mybinder.org/) 中,你只要输入其对应的 GitHub Repository 的名字或者 URL,就能在云端打开整个 Repository,选择你需要的 [notebook](https://mybinder.org/v2/gh/binder-examples/python2_with_3/master?filepath=index3.ipynb) ,你就能看到下图这个界面。 + +![img](./02.assets/f81efe2538074a3385b9ba70aced2cc9.png) + +每一个 Jupyter 的运行单元都包含了 In、Out 的 Cell。如图所示,你可以使用 Run 按钮,运行单独的一个 Cell。当然,你也可以在此基础上加以修改,或者新建一个 notebook,写成自己想要的程序。赶紧打开链接试一试吧! + +另外,我还推荐下面这些 Jupyter Notebook,作为你实践的第一站。 + +- 第一个是 Jupyter 官方:[https://mybinder.org/v2/gh/binder-examples/matplotlib-versions/mpl-v2.0/?filepath=matplotlib_versions_demo.ipynb](https://mybinder.org/v2/gh/binder-examples/matplotlib-versions/mpl-v2.0/?filepath=matplotlib_versions_demo.ipynb) +- 第二个是 Google Research 提供的 Colab 环境,尤其适合机器学习的实践应用:[https://colab.research.google.com/notebooks/basic_features_overview.ipynb](https://colab.research.google.com/notebooks/basic_features_overview.ipynb) + +> 如果你想在本地或者远程的机器上安装 Jupyter Notebook,可以参考下面的两个文档。 + +- 安装:[https://jupyter.org/install.html](https://jupyter.org/install.html) +- 运行:[https://jupyter.readthedocs.io/en/latest/running.html#running](https://jupyter.readthedocs.io/en/latest/running.html#running) + +## 5. 总结 + +这节课,我为你介绍了 Jupyter Notebook,并告诉你它为什么日趋成为 Python 社区的必学技术。这主要是因为它的三大特点:**整合所有的资源、交互性编程体验和零成本重现结果。** 但还是那句话,学习技术必须动手实操。这节课后,希望你能自己动手试一试 Jupyter Notebook,后面我们的一些课程代码,我也会用 Jupyter Notebook 的形式分享给你。 + +## 6. 思考题 + +你尝试 Jupyter Notebook 了吗?欢迎在留言区和我分享你的使用体验。 + +欢迎关注我公众号:AI悦创,有更多更好玩的等你发现! + +::: details 公众号:AI悦创【二维码】 + +![](/gzh.jpg) + +::: + +::: info AI悦创·编程一对一 + +AI悦创·推出辅导班啦,包括「Python 语言辅导班、C++ 辅导班、java 辅导班、算法/数据结构辅导班、少儿编程、pygame 游戏开发」,全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践等。当然,还有线下线上摄影课程、Photoshop、Premiere 一对一教学、QQ、微信在线,随时响应!微信:Jiabcdefh + +C++ 信息奥赛题解,长期更新!长期招收一对一中小学信息奥赛集训,莆田、厦门地区有机会线下上门,其他地区线上。微信:Jiabcdefh + +方法一:[QQ](http://wpa.qq.com/msgrd?v=3&uin=1432803776&site=qq&menu=yes) + +方法二:微信:Jiabcdefh + +::: + +![](/zsxq.jpg) + diff --git a/src/Python/Python-core-technology-and-practice/03.assets/ba76d73736a5049aea77b7fa50e888fe.jpg b/src/Python/Python-core-technology-and-practice/03.assets/ba76d73736a5049aea77b7fa50e888fe.jpg new file mode 100755 index 00000000000..1e9e35cde2d Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/03.assets/ba76d73736a5049aea77b7fa50e888fe.jpg differ diff --git a/src/Python/Python-core-technology-and-practice/03.md b/src/Python/Python-core-technology-and-practice/03.md new file mode 100755 index 00000000000..ecf38077b96 --- /dev/null +++ b/src/Python/Python-core-technology-and-practice/03.md @@ -0,0 +1,394 @@ +--- +title: 03-列表和元组,到底用哪一个? +icon: python +date: 2022-11-22 00:48:51 +author: AI悦创 +isOriginal: true +category: Python 进阶 +tag: + - Python 进阶 +sticky: false +star: false +article: true +timeline: true +image: false +navbar: true +sidebarIcon: true +headerDepth: 5 +comment: true +lastUpdated: true +editLink: false +backToTop: true +toc: true +--- + +![img](./03.assets/ba76d73736a5049aea77b7fa50e888fe.jpg) + +你好,我是悦创。 + +前面的课程,我们讲解了 Python 语言的学习方法,并且带你了解了 Python 必知的常用工具——Jupyter。那么从这节课开始,我们将正式学习 Python 的具体知识。 + +对于每一门编程语言来说,数据结构都是其根基。了解掌握 Python 的基本数据结构,对于学好这门语言至关重要。今天我们就一起来学习,Python 中最常见的两种数据结构:列表(list)和元组(tuple)。 + +## 1. 列表和元组基础 + +首先,我们需要弄清楚最基本的概念,什么是列表和元组呢? + +实际上,列表和元组,都是**一个可以放置任意数据类型的有序集合。** + +在绝大多数编程语言中,集合的数据类型必须一致。不过,对于 Python 的列表和元组来说,并无此要求: + +```python +l = [1, 2, 'hello', 'world'] # 列表中同时含有 int 和 string 类型的元素 +l +[1, 2, 'hello', 'world'] + +tup = ('jason', 22) # 元组中同时含有 int 和 string 类型的元素 +tup +('jason', 22) +``` + +其次,我们必须掌握它们的区别。 + +- **列表是动态的**,长度大小不固定,可以随意地增加、删减或者改变元素(mutable)。 +- **而元组是静态的**,长度大小固定,无法增加删减或者改变(immutable)。 + +下面的例子中,我们分别创建了一个列表与元组。你可以看到,对于列表,我们可以很轻松地让其最后一个元素,由 4 变为 40;但是,如果你对元组采取相同的操作,Python 就会报错,原因就是元组是不可变的。 + +```python +l = [1, 2, 3, 4] +l[3] = 40 # 和很多语言类似,python 中索引同样从 0 开始,l[3] 表示访问列表的第四个元素 +l +[1, 2, 3, 40] + +tup = (1, 2, 3, 4) +tup[3] = 40 +Traceback (most recent call last): + File "", line 1, in +TypeError: 'tuple' object does not support item assignment +``` + +可是,如果你想对已有的元组做任何"改变",该怎么办呢?那就只能重新开辟一块内存,创建新的元组了。 + +比如下面的例子,我们想增加一个元素 5 给元组,实际上就是创建了一个新的元组,然后把原来两个元组的值依次填充进去。 + +而对于列表来说,由于其是动态的,我们只需简单地在列表末尾,加入对应元素就可以了。如下操作后,会修改原来列表中的元素,而不会创建新的列表。 + +```python +tup = (1, 2, 3, 4) +new_tup = tup + (5, ) # 创建新的元组 new_tup,并依次填充原元组的值 +new _tup +(1, 2, 3, 4, 5) + +l = [1, 2, 3, 4] +l.append(5) # 添加元素 5 到原列表的末尾 +l +[1, 2, 3, 4, 5] +``` + +通过上面的例子,相信你肯定掌握了列表和元组的基本概念。接下来我们来看一些列表和元组的基本操作和注意事项。 + +首先,和其他语言不同,**Python 中的列表和元组都支持负数索引**,`-1` 表示最后一个元素,`-2` 表示倒数第二个元素,以此类推。 + +```python +l = [1, 2, 3, 4] +l[-1] +4 + +tup = (1, 2, 3, 4) +tup[-1] +4 +``` + +除了基本的初始化,索引外,**列表和元组都支持切片操作:** + +```python +l = [1, 2, 3, 4] +l[1:3] # 返回列表中索引从 1 到 2 的子列表 +[2, 3] + +tup = (1, 2, 3, 4) +tup[1:3] # 返回元组中索引从 1 到 2 的子元组 +(2, 3) +``` + +另外,列表和元组都**可以随意嵌套**: + +```python +l = [[1, 2, 3], [4, 5]] # 列表的每一个元素也是一个列表 + +tup = ((1, 2, 3), (4, 5, 6)) # 元组的每一个元素也是一个元组 +``` + +当然,两者也可以通过 `list()` 和 `tuple()` 函数相互转换: + +```python +list((1, 2, 3)) +[1, 2, 3] + +tuple([1, 2, 3]) +(1, 2, 3) +``` + +最后,我们来看一些列表和元组常用的内置函数: + +```python +l = [3, 2, 3, 7, 8, 1] +l.count(3) +2 +l.index(7) +3 +l.reverse() +l +[1, 8, 7, 3, 2, 3] +l.sort() +l +[1, 2, 3, 3, 7, 8] + +tup = (3, 2, 3, 7, 8, 1) +tup.count(3) +2 +tup.index(7) +3 +list(reversed(tup)) +[1, 8, 7, 3, 2, 3] +sorted(tup) +[1, 2, 3, 3, 7, 8] +``` + +这里我简单解释一下这几个函数的含义。 + +- `count(item)` 表示统计列表 / 元组中 item 出现的次数。 +- `index(item)` 表示返回列表 / 元组中 item 第一次出现的索引。 +- `list.reverse()` 和 ` list.sort() ` 分别表示原地倒转列表和排序(注意,元组没有内置的这两个函数)。 +- `reversed()` 和 `sorted()` 同样表示对列表 / 元组进行倒转和排序,`reversed()` 返回一个倒转后的迭代器(上文例子使用 `list()` 函数再将其转换为列表);`sorted()` 返回排好序的新列表。 + +## 2. 列表和元组存储方式的差异 + +前面说了,列表和元组最重要的区别就是,列表是动态的、可变的,而元组是静态的、不可变的。这样的差异,势必会影响两者存储方式。我们可以来看下面的例子: + +```python +l = [1, 2, 3] +l.__sizeof__() +64 +tup = (1, 2, 3) +tup.__sizeof__() +48 +``` + +你可以看到,对列表和元组,我们放置了相同的元素,但是元组的存储空间,却比列表要少 16 字节。这是为什么呢? + +事实上,由于列表是动态的,所以它需要存储指针,来指向对应的元素(上述例子中,对于 int 型,8 字节)。另外,由于列表可变,所以需要额外存储已经分配的长度大小(8 字节),这样才可以实时追踪列表空间的使用情况,当空间不足时,及时分配额外空间。 + +```python +l = [] +l.__sizeof__() // 空列表的存储空间为40字节 +40 +l.append(1) +l.__sizeof__() +72 // 加入了元素1之后,列表为其分配了可以存储4个元素的空间 (72 - 40)/8 = 4 +l.append(2) +l.__sizeof__() +72 // 由于之前分配了空间,所以加入元素2,列表空间不变 +l.append(3) +l.__sizeof__() +72 // 同上 +l.append(4) +l.__sizeof__() +72 // 同上 +l.append(5) +l.__sizeof__() +104 // 加入元素5之后,列表的空间不足,所以又额外分配了可以存储4个元素的空间 +``` + +上面的例子,大概描述了列表空间分配的过程。我们可以看到,为了减小每次增加 / 删减操作时空间分配的开销,Python 每次分配空间时都会额外多分配一些,这样的机制(over-allocating)保证了其操作的高效性:增加 / 删除的时间复杂度均为 O(1)。 + +但是对于元组,情况就不同了。元组长度大小固定,元素不可变,所以存储空间固定。 + +看了前面的分析,你也许会觉得,这样的差异可以忽略不计。但是想象一下,如果列表和元组存储元素的个数是一亿,十亿甚至更大数量级时,你还能忽略这样的差异吗? + +## 3. 列表和元组的性能 + +通过学习列表和元组存储方式的差异,我们可以得出结论:元组要比列表更加轻量级一些,所以总体上来说,元组的性能速度要略优于列表。 + +另外,Python 会在后台,对静态数据做一些**资源缓存**(resource caching)。通常来说,因为垃圾回收机制的存在,如果一些变量不被使用了,Python 就会回收它们所占用的内存,返还给操作系统,以便其他变量或其他应用使用。 + +但是对于一些静态变量,比如元组,如果它不被使用并且占用空间不大时,Python 会暂时缓存这部分内存。这样,下次我们再创建同样大小的元组时,Python 就可以不用再向操作系统发出请求,去寻找内存,而是可以直接分配之前缓存的内存空间,这样就能大大加快程序的运行速度。 + +下面的例子,是计算**初始化**一个相同元素的列表和元组分别所需的时间。我们可以看到,元组的初始化速度,要比列表快 5 倍。 + +```python +python3 -m timeit 'x=(1,2,3,4,5,6)' +20000000 loops, best of 5: 9.97 nsec per loop +python3 -m timeit 'x=[1,2,3,4,5,6]' +5000000 loops, best of 5: 50.1 nsec per loop +``` + +但如果是**索引操作**的话,两者的速度差别非常小,几乎可以忽略不计。 + +```python +python3 -m timeit -s 'x=[1,2,3,4,5,6]' 'y=x[3]' +10000000 loops, best of 5: 22.2 nsec per loop +python3 -m timeit -s 'x=(1,2,3,4,5,6)' 'y=x[3]' +10000000 loops, best of 5: 21.9 nsec per loop +``` + +当然,如果你想要增加、删减或者改变元素,那么列表显然更优。原因你现在肯定知道了,那就是对于元组,你必须得通过新建一个元组来完成。 + +## 4. 列表和元组的使用场景 + +那么列表和元组到底用哪一个呢?根据上面所说的特性,我们具体情况具体分析。 + +1. 如果存储的数据和数量不变,比如你有一个函数,需要返回的是一个地点的经纬度,然后直接传给前端渲染,那么肯定选用元组更合适。 + +```python +def get_location(): + ..... + return (longitude, latitude) +``` + +2. 如果存储的数据或数量是可变的,比如社交平台上的一个日志功能,是统计一个用户在一周之内看了哪些用户的帖子,那么则用列表更合适。 + +```python +viewer_owner_id_list = [] # 里面的每个元素记录了这个 viewer 一周内看过的所有 owner 的 id +records = queryDB(viewer_id) # 索引数据库,拿到某个 viewer 一周内的日志 +for record in records: + viewer_owner_id_list.append(record.id) +``` + +## 5. 总结 + +关于列表和元组,我们今天聊了很多,最后一起总结一下你必须掌握的内容。 + +总的来说,列表和元组都是有序的,可以存储任意数据类型的集合,区别主要在于下面这两点。 + +- 列表是动态的,长度可变,可以随意的增加、删减或改变元素。列表的存储空间略大于元组,性能略逊于元组。 +- 元组是静态的,长度大小固定,不可以对元素进行增加、删减或者改变操作。元组相对于列表更加轻量级,性能稍优。 + +## 6. 思考题 + +1. 想创建一个空的列表,我们可以用下面的 A、B 两种方式,请问它们在效率上有什么区别吗?我们应该优先考虑使用哪种呢?可以说说你的理由。 + +```python +# 创建空列表 +# option A +empty_list = list() + +# option B +empty_list = [] +``` + +2. 你在平时的学习工作中,是在什么场景下使用列表或者元组呢?欢迎留言和我分享。 + +## 7. 私教学员问答 + +### 7.1 和你一起搬砖的胡大爷 + +老师能不能讲一下 list 和 tuple 的内部实现,里边是 linked list 还是 array,还是把 array linked 一下这种。 最后那个问题,类比 java,new 是在 heap,直接声明就可能在常量区了。老师能讲下 Python 的 vm 么,比如内存分配,gc 算法之类的。 + +::: tip 回复 + +1. list 和 tuple 的内部实现都是 array 的形式,list 因为可变,所以是一个 over-allocate 的 array,tuple 因为不可变,所以长度大小固定。具体可以参照源码 + - list: [https://github.com/python/cpython/blob/master/Objects/listobject.c](https://github.com/python/cpython/blob/master/Objects/listobject.c) + - tuple: [https://github.com/python/cpython/blob/master/Objects/tupleobject.c](https://github.com/python/cpython/blob/master/Objects/tupleobject.c ) +2. 最后的思考题: 区别主要在于 `list()` 是一个 function call,Python 的 function call 会创建 stack,并且进行一系列参数检查的操作,比较 expensive,反观[]是一个内置的 C 函数,可以直接被调用,因此效率高。 内存分配,GC 等等知识会在第二章进阶里面专门讲到。 + +::: + +### 7.2 布霆 + +老师请问一下,为什么 `l = [1, 2, 3]` 消耗的空间为 64 字节,而 `l.append(1)` , `l.append(2)` , `l.append(3)` 消耗的空间为 72 字节,这不是相同的列表吗? + +::: tip 回复 + +列表的 over-allocate 是在你加入了新元素之后解释器判断得出当前存储空间不够,给你分配额外的空间,因此 `l=[]`, `l.append(1)`, `l.append(2),` `l.append(3)` 实际分配了4个元素的空间。但是 `l=[1, 2, 3]` 直接初始化列表,并没有增加元素的操作,因此只会分配 3 个元素的空间 + +::: + +### 7.3 kevinsu + +可以这样比较吗?老师 + +```python +import time +time1 = time.clock() +empty_list = list() +time2 = time.clock() +diff_time = time2 - time1 +print (diff_time) + +import time +time1 = time.clock() +empty_list = [] +time2 = time.clock() +diff_time = time2 - time1 +print (diff_time) +``` + +::: tip 回复 + +这样可以,但是不是很准确,尤其对于简单并且运行速度很快的代码块,建议用 timeit。 因为程序中还有很多因素会影响计算的时间,比如垃圾回收机制。使用 timeit 会自动关掉垃圾回收机制,让程序的运行更加独立,时间计算更加准确。 + +::: + +### 7.4 Mr.Chen + +老师,“有序”应该怎么理解。 + +::: tip 回复 + +内部的排列是有序的,比如你遍历一遍并打印,其顺序应该和你插入元素的顺序一样 + +::: + +### 7.5 不瘦到140不改名 + +`print([].__sizeof__())` # 40 + +`print(().__sizeof__())` # 24 + +老师 我想问一下,列表比元组多了16个字节,由于列表是可变的,所以需要分配8字节来存储已经分配的长度大小,那剩余的8字节干什么了呢? + +::: tip 回复 + +文中有提到。元祖是直接存储的元素,但是列表存储的是指向元素的指针,这就是你说的剩余的8字节。可以参考源码: + +- 列表:[https://github.com/python/cpython/blob/3.7/Include/listobject.h](https://github.com/python/cpython/blob/3.7/Include/listobject.h) +- 元祖:[https://github.com/python/cpython/blob/3.7/Include/tupleobject.h](https://github.com/python/cpython/blob/3.7/Include/tupleobject.h) + +::: + +### 7.6 武林秀才 + +`reversed()` 返回的是一个反转的迭代器,不是返回倒排的列表或元组。 + +::: tip 回复 + +是的。但是我这里只是以列表和元祖为例,迭代器的概念第二大章才讲到 + +::: + + + +欢迎关注我公众号:AI悦创,有更多更好玩的等你发现! + +::: details 公众号:AI悦创【二维码】 + +![](/gzh.jpg) + +::: + +::: info AI悦创·编程一对一 + +AI悦创·推出辅导班啦,包括「Python 语言辅导班、C++ 辅导班、java 辅导班、算法/数据结构辅导班、少儿编程、pygame 游戏开发」,全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践等。当然,还有线下线上摄影课程、Photoshop、Premiere 一对一教学、QQ、微信在线,随时响应!微信:Jiabcdefh + +C++ 信息奥赛题解,长期更新!长期招收一对一中小学信息奥赛集训,莆田、厦门地区有机会线下上门,其他地区线上。微信:Jiabcdefh + +方法一:[QQ](http://wpa.qq.com/msgrd?v=3&uin=1432803776&site=qq&menu=yes) + +方法二:微信:Jiabcdefh + +::: + +![](/zsxq.jpg) + diff --git a/src/Python/Python-core-technology-and-practice/04.assets/4ed5dc4e2bb43d1b23bc8fdf0456ff5b.jpg b/src/Python/Python-core-technology-and-practice/04.assets/4ed5dc4e2bb43d1b23bc8fdf0456ff5b.jpg new file mode 100755 index 00000000000..967fd834942 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/04.assets/4ed5dc4e2bb43d1b23bc8fdf0456ff5b.jpg differ diff --git a/src/Python/Python-core-technology-and-practice/04.md b/src/Python/Python-core-technology-and-practice/04.md new file mode 100755 index 00000000000..1a7954b37f8 --- /dev/null +++ b/src/Python/Python-core-technology-and-practice/04.md @@ -0,0 +1,475 @@ +--- +title: 04-字典、集合,你真的了解吗? +icon: python +date: 2022-11-22 16:50:09 +author: AI悦创 +isOriginal: true +category: Python 进阶 +tag: + - Python 进阶 +sticky: false +star: false +article: true +timeline: true +image: false +navbar: true +sidebarIcon: true +headerDepth: 5 +comment: true +lastUpdated: true +editLink: false +backToTop: true +toc: true +--- + +![img](./04.assets/4ed5dc4e2bb43d1b23bc8fdf0456ff5b.jpg) + +你好,我是悦创。 + +前面的课程,我们学习了 Python 中的列表和元组,了解了他们的基本操作和性能比较。这节课,我们再来学习两个同样很常见并且很有用的数据结构:字典(dict)和集合(set)。字典和集合在 Python 被广泛使用,并且性能进行了高度优化,其重要性不言而喻。 + +## 1. 字典和集合基础 + +那究竟什么是字典,什么是集合呢?字典是一系列由键(key)和值(value)配对组成的元素的集合,在 Python3.7+,字典被确定为有序(注意:在 3.6 中,字典有序是一个 implementation detail,在 3.7 才正式成为语言特性,因此 3.6 中无法 100% 确保其有序性),而 3.6 之前是无序的,其长度大小可变,元素可以任意地删减和改变。 + +相比于列表和元组,字典的性能更优,特别是对于查找、添加和删除操作,字典都能在常数时间复杂度内完成。 + +而集合和字典基本相同,唯一的区别,就是集合没有键和值的配对,是一系列无序的、唯一的元素组合。 + +首先我们来看字典和集合的创建,通常有下面这几种方式: + +```python +d1 = {'name': 'jason', 'age': 20, 'gender': 'male'} +d2 = dict({'name': 'jason', 'age': 20, 'gender': 'male'}) +d3 = dict([('name', 'jason'), ('age', 20), ('gender', 'male')]) +d4 = dict(name='jason', age=20, gender='male') +d1 == d2 == d3 ==d4 +True + +s1 = {1, 2, 3} +s2 = set([1, 2, 3]) +s1 == s2 +True +``` + +这里注意,Python 中字典和集合,无论是键还是值,都可以是混合类型。比如下面这个例子,我创建了一个元素为 `1,'hello',5.0` 的集合: + +```python +s = {1, 'hello', 5.0} +``` + +再来看元素访问的问题。字典访问可以直接索引键,如果不存在,就会抛出异常: + +```python +d = {'name': 'jason', 'age': 20} +d['name'] +'jason' +d['location'] +Traceback (most recent call last): + File "", line 1, in +KeyError: 'location' +``` + +也可以使用 `get(key, default)` 函数来进行索引。如果键不存在,调用 `get()` 函数可以返回一个默认值。比如下面这个示例,返回了 `'null'`。 + +```python +d = {'name': 'jason', 'age': 20} +d.get('name') +'jason' +d.get('location', 'null') +'null' +``` + +说完了字典的访问,我们再来看集合。 + +首先我要强调的是,**集合并不支持索引操作,因为集合本质上是一个哈希表,和列表不一样。** 所以,下面这样的操作是错误的,Python 会抛出异常: + +```python +s = {1, 2, 3} +s[0] +Traceback (most recent call last): + File "", line 1, in +TypeError: 'set' object does not support indexing +``` + +想要判断一个元素在不在字典或集合内,我们可以用 `value in dict/set` 来判断。 + +```python +s = {1, 2, 3} +1 in s +True +10 in s +False + +d = {'name': 'jason', 'age': 20} +'name' in d +True +'location' in d +False +``` + +当然,除了创建和访问,字典和集合也同样支持增加、删除、更新等操作。 + +```python +d = {'name': 'jason', 'age': 20} +d['gender'] = 'male' # 增加元素对'gender': 'male' +d['dob'] = '1999-02-01' # 增加元素对'dob': '1999-02-01' +d +{'name': 'jason', 'age': 20, 'gender': 'male', 'dob': '1999-02-01'} +d['dob'] = '1998-01-01' # 更新键'dob'对应的值 +d.pop('dob') # 删除键为'dob'的元素对 +'1998-01-01' +d +{'name': 'jason', 'age': 20, 'gender': 'male'} + +s = {1, 2, 3} +s.add(4) # 增加元素4到集合 +s +{1, 2, 3, 4} +s.remove(4) # 从集合中删除元素4 +s +{1, 2, 3} +``` + +不过要注意,集合的 `pop()` 操作是删除集合中最后一个元素,可是集合本身是无序的,你无法知道会删除哪个元素,因此这个操作得谨慎使用。 + +实际应用中,很多情况下,我们需要对字典或集合进行排序,比如,取出值最大的 50 对。 + +对于字典,我们通常会根据键或值,进行升序或降序排序: + +```python +d = {'b': 1, 'a': 2, 'c': 10} +d_sorted_by_key = sorted(d.items(), key=lambda x: x[0]) # 根据字典键的升序排序 +d_sorted_by_value = sorted(d.items(), key=lambda x: x[1]) # 根据字典值的升序排序 +d_sorted_by_key +[('a', 2), ('b', 1), ('c', 10)] +d_sorted_by_value +[('b', 1), ('a', 2), ('c', 10)] +``` + +这里返回了一个列表。列表中的每个元素,是由原字典的键和值组成的元组。 + +而对于集合,其排序和前面讲过的列表、元组很类似,直接调用 `sorted(set)` 即可,结果会返回一个排好序的列表。 + +```python +s = {3, 4, 2, 1} +sorted(s) # 对集合的元素进行升序排序 +[1, 2, 3, 4] +``` + +## 2. 字典和集合性能 + +文章开头我就说到了,字典和集合是进行过性能高度优化的数据结构,特别是对于查找、添加和删除操作。那接下来,我们就来看看,它们在具体场景下的性能表现,以及与列表等其他数据结构的对比。 + +比如电商企业的后台,存储了每件产品的 ID、名称和价格。现在的需求是,给定某件商品的 ID,我们要找出其价格。 + +如果我们用列表来存储这些数据结构,并进行查找,相应的代码如下: + +```python +def find_product_price(products, product_id): + for id, price in products: + if id == product_id: + return price + return None + +products = [ + (143121312, 100), + (432314553, 30), + (32421912367, 150) +] + +print('The price of product 432314553 is {}'.format(find_product_price(products, 432314553))) + +# 输出 +The price of product 432314553 is 30 +``` + +假设列表有 n 个元素,而查找的过程要遍历列表,那么时间复杂度就为 O(n)。即使我们先对列表进行排序,然后使用二分查找,也会需要 O(logn) 的时间复杂度,更何况,列表的排序还需要 O(nlogn) 的时间。 + +但如果我们用字典来存储这些数据,那么查找就会非常便捷高效,只需 O(1) 的时间复杂度就可以完成。原因也很简单,刚刚提到过的,字典的内部组成是一张哈希表,你可以直接通过键的哈希值,找到其对应的值。 + +```python +products = { + 143121312: 100, + 432314553: 30, + 32421912367: 150 +} +print('The price of product 432314553 is {}'.format(products[432314553])) + +# 输出 +The price of product 432314553 is 30 +``` + +类似的,现在需求变成,要找出这些商品有多少种不同的价格。我们还用同样的方法来比较一下。 + +如果还是选择使用列表,对应的代码如下,其中,A 和 B 是两层循环。同样假设原始列表有 n 个元素,那么,在最差情况下,需要 `O(n^2)` 的时间复杂度。 + +```python +# list version +def find_unique_price_using_list(products): + unique_price_list = [] + for _, price in products: # A + if price not in unique_price_list: #B + unique_price_list.append(price) + return len(unique_price_list) + +products = [ + (143121312, 100), + (432314553, 30), + (32421912367, 150), + (937153201, 30) +] +print('number of unique price is: {}'.format(find_unique_price_using_list(products))) + +# 输出 +number of unique price is: 3 +``` + +但如果我们选择使用集合这个数据结构,由于集合是高度优化的哈希表,里面元素不能重复,并且其添加和查找操作只需 O(1) 的复杂度,那么,总的时间复杂度就只有 O(n)。 + +```python +# set version +def find_unique_price_using_set(products): + unique_price_set = set() + for _, price in products: + unique_price_set.add(price) + return len(unique_price_set) + +products = [ + (143121312, 100), + (432314553, 30), + (32421912367, 150), + (937153201, 30) +] +print('number of unique price is: {}'.format(find_unique_price_using_set(products))) + +# 输出 +number of unique price is: 3 +``` + +可能你对这些时间复杂度没有直观的认识,我可以举一个实际工作场景中的例子,让你来感受一下。 + +下面的代码,初始化了含有 100,000 个元素的产品,并分别计算了使用列表和集合来统计产品价格数量的运行时间: + +```python +import time +id = [x for x in range(0, 100000)] +price = [x for x in range(200000, 300000)] +products = list(zip(id, price)) + +# 计算列表版本的时间 +start_using_list = time.perf_counter() +find_unique_price_using_list(products) +end_using_list = time.perf_counter() +print("time elapse using list: {}".format(end_using_list - start_using_list)) +## 输出 +time elapse using list: 41.61519479751587 + +# 计算集合版本的时间 +start_using_set = time.perf_counter() +find_unique_price_using_set(products) +end_using_set = time.perf_counter() +print("time elapse using set: {}".format(end_using_set - start_using_set)) +# 输出 +time elapse using set: 0.008238077163696289 +``` + +你可以看到,仅仅十万的数据量,两者的速度差异就如此之大。事实上,大型企业的后台数据往往有上亿乃至十亿数量级,如果使用了不合适的数据结构,就很容易造成服务器的崩溃,不但影响用户体验,并且会给公司带来巨大的财产损失。 + +## 3. 字典和集合的工作原理 + +我们通过举例以及与列表的对比,看到了字典和集合操作的高效性。不过,字典和集合为什么能够如此高效,特别是查找、插入和删除操作? + +这当然和字典、集合内部的数据结构密不可分。不同于其他数据结构,字典和集合的内部结构都是一张哈希表。 + +- 对于字典而言,这张表存储了哈希值(hash)、键和值这 3 个元素。 +- 而对集合来说,区别就是哈希表内没有键和值的配对,只有单一的元素了。 + +我们来看,老版本 Python 的哈希表结构如下所示: + +```python +--+-------------------------------+ + | 哈希值(hash) 键(key) 值(value) +--+-------------------------------+ +0 | hash0 key0 value0 +--+-------------------------------+ +1 | hash1 key1 value1 +--+-------------------------------+ +2 | hash2 key2 value2 +--+-------------------------------+ +. | ... +__+_______________________________+ +``` + +不难想象,随着哈希表的扩张,它会变得越来越稀疏。举个例子,比如我有这样一个字典: + +```python +{'name': 'mike', 'dob': '1999-01-01', 'gender': 'male'} +``` + +那么它会存储为类似下面的形式: + +```python +entries = [ +['--', '--', '--'] +[-230273521, 'dob', '1999-01-01'], +['--', '--', '--'], +['--', '--', '--'], +[1231236123, 'name', 'mike'], +['--', '--', '--'], +[9371539127, 'gender', 'male'] +] +``` + +这样的设计结构显然非常浪费存储空间。为了提高存储空间的利用率,现在的哈希表除了字典本身的结构,会把索引和哈希值、键、值单独分开,也就是下面这样新的结构: + +```python +Indices +---------------------------------------------------- +None | index | None | None | index | None | index ... +---------------------------------------------------- + +Entries +-------------------- +hash0 key0 value0 +--------------------- +hash1 key1 value1 +--------------------- +hash2 key2 value2 +--------------------- + ... +--------------------- +``` + +那么,刚刚的这个例子,在新的哈希表结构下的存储形式,就会变成下面这样: + +```python +indices = [None, 1, None, None, 0, None, 2] +entries = [ +[1231236123, 'name', 'mike'], +[-230273521, 'dob', '1999-01-01'], +[9371539127, 'gender', 'male'] +] +``` + +我们可以很清晰地看到,空间利用率得到很大的提高。 + +清楚了具体的设计结构,我们接着来看这几个操作的工作原理。 + +## 4. 插入操作 + +每次向字典或集合插入一个元素时,Python 会首先计算键的哈希值(`hash(key)`),再和 `mask = PyDicMinSize - 1` 做与操作,计算这个元素应该插入哈希表的位置 `index = hash(key) & mask`。如果哈希表中此位置是空的,那么这个元素就会被插入其中。 + +而如果此位置已被占用,Python 便会比较两个元素的哈希值和键是否相等。 + +- 若两者都相等,则表明这个元素已经存在,如果值不同,则更新值。 +- 若两者中有一个不相等,这种情况我们通常称为哈希冲突(hash collision),意思是两个元素的键不相等,但是哈希值相等。这种情况下,Python 便会继续寻找表中空余的位置,直到找到位置为止。 + +值得一提的是,通常来说,遇到这种情况,最简单的方式是线性寻找,即从这个位置开始,挨个往后寻找空位。当然,Python 内部对此进行了优化(这一点无需深入了解,你有兴趣可以查看源码,我就不再赘述),让这个步骤更加高效。 + +## 5. 查找操作 + +和前面的插入操作类似,Python 会根据哈希值,找到其应该处于的位置;然后,比较哈希表这个位置中元素的哈希值和键,与需要查找的元素是否相等。如果相等,则直接返回;如果不等,则继续查找,直到找到空位或者抛出异常为止。 + +## 6. 删除操作 + +对于删除操作,Python 会暂时对这个位置的元素,赋于一个特殊的值,等到重新调整哈希表的大小时,再将其删除。 + +不难理解,哈希冲突的发生,往往会降低字典和集合操作的速度。因此,为了保证其高效性,字典和集合内的哈希表,通常会保证其至少留有 1/3 的剩余空间。随着元素的不停插入,当剩余空间小于 1/3 时,Python 会重新获取更大的内存空间,扩充哈希表。不过,这种情况下,表内所有的元素位置都会被重新排放。 + +虽然哈希冲突和哈希表大小的调整,都会导致速度减缓,但是这种情况发生的次数极少。所以,平均情况下,这仍能保证插入、查找和删除的时间复杂度为 O(1)。 + +## 7. 总结 + +这节课,我们一起学习了字典和集合的基本操作,并对它们的高性能和内部存储结构进行了讲解。 + +字典在 Python3.7+ 是有序的数据结构,而集合是无序的,其内部的哈希表存储结构,保证了其查找、插入、删除操作的高效性。所以,字典和集合通常运用在对元素的高效查找、去重等场景。 + +## 8. 思考题 + +1. 下面初始化字典的方式,哪一种更高效? + +```python +# Option A +d = {'name': 'jason', 'age': 20, 'gender': 'male'} + +# Option B +d = dict({'name': 'jason', 'age': 20, 'gender': 'male'}) +``` + +2. 字典的键可以是一个列表吗?下面这段代码中,字典的初始化是否正确呢?如果不正确,可以说出你的原因吗? + +```python +d = {'name': 'jason', ['education']: ['Tsinghua University', 'Stanford University']} +``` + +欢迎留言和我分享,也欢迎你把这篇文章分享给你的同事、朋友。 + +## 9. 评论 + +思考题 1: 第一种方法更快,原因感觉上是和之前一样,就是不需要去调用相关的函数,而且像老师说的那样 {} 应该是关键字,内部会去直接调用底层C写好的代码 + +思考题 2: 用列表作为 Key 在这里是不被允许的,因为列表是一个动态变化的数据结构,字典当中的 key 要求是不可变的,原因也很好理解,key 首先是不重复的,如果 Key 是可以变化的话,那么随着 Key 的变化,这里就有可能就会有重复的 Key,那么这就和字典的定义相违背;如果把这里的列表换成之前我们讲过的元组是可以的,因为元组不可变 + +作者回复: 正解 + +--- + +1. 直接{}的方式,更高效。可以使用 dis 分析其字节码 + +2. 字典的键值,需要不可变,而列表是动态的,可变的。可以改为元组 + +作者回复: 使用 dis 分析其字节码很赞 + +--- + +文中提到的新的哈希表结构有点不太明白 None 1 None None 0 None 2 是什么意思? index是索引的话 为什么中间会出现两个None + +作者回复: 这只是一种表示。None 表示 indices 这个 array 上对应的位置没有元素,index 表示有元素,并且对应 entries 这个 array index 位置上的元素。你看那个具体的例子就能看懂了 + +--- + +```python +--+-------------------------------+ + | 哈希值 (hash) 键 (key) 值 (value) +--+-------------------------------+ +0 | hash0 key0 value0 +--+-------------------------------+ +1 | hash1 key1 value1 +--+-------------------------------+ +2 | hash2 key2 value2 +--+-------------------------------+ +. | ... +__+_______________________________+ +``` + +第一种数据结构,如何可以o(1)的查找一个key? + +没有索引啊 这篇文章感觉写的不好,例子没有讲透 + +稀疏一定浪费吗,里面没有值的话能占用多少空间 我理解耗费空间的应该是k v的存储吧 + +作者回复: 根据 key 计算hash值后直接就可以找到对应的 value 啊,所以是O(1),他并不是列表需要遍历,他是哈希表 稀疏肯定浪费空间啊,里面没有值也是会有一定的存储损耗的 你自己去看看市面上 Python 教材里讲字典集合的,有几个能讲到像我这样深入。 + +欢迎关注我公众号:AI悦创,有更多更好玩的等你发现! + +::: details 公众号:AI悦创【二维码】 + +![](/gzh.jpg) + +::: + +::: info AI悦创·编程一对一 + +AI悦创·推出辅导班啦,包括「Python 语言辅导班、C++ 辅导班、java 辅导班、算法/数据结构辅导班、少儿编程、pygame 游戏开发」,全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践等。当然,还有线下线上摄影课程、Photoshop、Premiere 一对一教学、QQ、微信在线,随时响应!微信:Jiabcdefh + +C++ 信息奥赛题解,长期更新!长期招收一对一中小学信息奥赛集训,莆田、厦门地区有机会线下上门,其他地区线上。微信:Jiabcdefh + +方法一:[QQ](http://wpa.qq.com/msgrd?v=3&uin=1432803776&site=qq&menu=yes) + +方法二:微信:Jiabcdefh + +::: + +![](/zsxq.jpg) + diff --git a/src/Python/Python-core-technology-and-practice/05.assets/71d67729e2af1491f6fce3d41315419e-5583796.jpg b/src/Python/Python-core-technology-and-practice/05.assets/71d67729e2af1491f6fce3d41315419e-5583796.jpg new file mode 100644 index 00000000000..d3cf3bcb776 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/05.assets/71d67729e2af1491f6fce3d41315419e-5583796.jpg differ diff --git a/src/Python/Python-core-technology-and-practice/05.assets/71d67729e2af1491f6fce3d41315419e.jpg b/src/Python/Python-core-technology-and-practice/05.assets/71d67729e2af1491f6fce3d41315419e.jpg new file mode 100755 index 00000000000..d3cf3bcb776 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/05.assets/71d67729e2af1491f6fce3d41315419e.jpg differ diff --git a/src/Python/Python-core-technology-and-practice/05.assets/b7a296ab8d26664e03a076fa50d5b152-5583992.png b/src/Python/Python-core-technology-and-practice/05.assets/b7a296ab8d26664e03a076fa50d5b152-5583992.png new file mode 100644 index 00000000000..ce8fad22ea6 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/05.assets/b7a296ab8d26664e03a076fa50d5b152-5583992.png differ diff --git a/src/Python/Python-core-technology-and-practice/05.assets/b7a296ab8d26664e03a076fa50d5b152.png b/src/Python/Python-core-technology-and-practice/05.assets/b7a296ab8d26664e03a076fa50d5b152.png new file mode 100755 index 00000000000..3d144a9be63 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/05.assets/b7a296ab8d26664e03a076fa50d5b152.png differ diff --git a/src/Python/Python-core-technology-and-practice/05.md b/src/Python/Python-core-technology-and-practice/05.md new file mode 100755 index 00000000000..d5357ee2bef --- /dev/null +++ b/src/Python/Python-core-technology-and-practice/05.md @@ -0,0 +1,307 @@ +--- +title: 05-深入浅出字符串 +icon: python +date: 2022-12-15 17:37:35 +author: AI悦创 +isOriginal: true +category: Python 进阶 +tag: + - Python 进阶 +sticky: false +star: false +article: true +timeline: true +image: false +navbar: true +sidebarIcon: true +headerDepth: 5 +comment: true +lastUpdated: true +editLink: false +backToTop: true +toc: true +--- + +![](./05.assets/71d67729e2af1491f6fce3d41315419e-5583796.jpg) + +你好,我是悦创。 + +Python 的程序中充满了字符串(string),在平常阅读代码时也屡见不鲜。字符串同样是 Python 中很常见的一种数据类型,比如日志的打印、程序中函数的注释、数据库的访问、变量的基本操作等等,都用到了字符串。 + +当然,我相信你本身对字符串已经有所了解。今天这节课,我主要带你回顾一下字符串的常用操作,并对其中的一些小 tricks 详细地加以解释。 + +## 1. 字符串基础 + +什么是字符串呢?字符串是由独立字符组成的一个序列,通常包含在单引号(`''`)双引号(`""`)或者三引号之中(`''' '''`或`""" """`,两者一样),比如下面几种写法。 + +```python +name = 'jason' +city = 'beijing' +text = "welcome to jike shijian" +``` + +这里定义了 name、city 和 text 三个变量,都是字符串类型。我们知道,Python 中单引号、双引号和三引号的字符串是一模一样的,没有区别,比如下面这个例子中的 s1、s2、s3 完全一样。 + +```python +s1 = 'hello' +s2 = "hello" +s3 = """hello""" +s1 == s2 == s3 +True +``` + +Python 同时支持这三种表达方式,很重要的一个原因就是,这样方便你在字符串中,内嵌带引号的字符串。比如: + +```python +"I'm a student" +``` + +Python 的三引号字符串,则主要应用于多行字符串的情境,比如函数的注释等等。 + +```python +def calculate_similarity(item1, item2): + """ + Calculate similarity between two items + Args: + item1: 1st item + item2: 2nd item + Returns: + similarity score between item1 and item2 + """ +``` + +同时,Python 也支持转义字符。所谓的转义字符,就是用反斜杠开头的字符串,来表示一些特定意义的字符。我把常见的的转义字符,总结成了下面这张表格。 + +![](./05.assets/b7a296ab8d26664e03a076fa50d5b152-5583992.png) + +为了方便你理解,我举一个例子来说明。 + +```python +s = 'a\nb\tc' +print(s) +a +b c +``` + +这段代码中的 `'\n'` ,表示一个字符——换行符;`'\t'` 也表示一个字符——横向制表符。所以,最后打印出来的输出,就是字符 a,换行,字符 b,然后制表符,最后打印字符 c。不过要注意,虽然最后打印的输出横跨了两行,但是整个字符串 s 仍然只有 5 个元素。 + +```python +len(s) +5 +``` + +在转义字符的应用中,最常见的就是换行符 `'\n'` 的使用。比如文件读取,如果我们一行行地读取,那么每一行字符串的末尾,都会包含换行符 `'\n'` 。而最后做数据处理时,我们往往会丢掉每一行的换行符。 + +## 2. 字符串的常用操作 + +讲完了字符串的基本原理,下面我们一起来看看字符串的常用操作。你可以把字符串想象成一个由单个字符组成的数组,所以,Python 的字符串同样支持索引,切片和遍历等等操作。 + +```python +name = 'jason' +name[0] +'j' +name[1:3] +'as' +``` + +和其他数据结构,如列表、元组一样,字符串的索引同样从 0 开始,`index=0` 表示第一个元素(字符),`[index:index+2]` 则表示第 index 个元素到 index+1 个元素组成的子字符串。 + +遍历字符串同样很简单,相当于遍历字符串中的每个字符。 + +```python +for char in name: + print(char) +j +a +s +o +n +``` + +特别要注意,Python 的字符串是不可变的(immutable)。因此,用下面的操作,来改变一个字符串内部的字符是错误的,不允许的。 + +```python +s = 'hello' +s[0] = 'H' +Traceback (most recent call last): + File "", line 1, in +TypeError: 'str' object does not support item assignment +``` + +Python 中字符串的改变,通常只能通过创建新的字符串来完成。比如上述例子中,想把 `'hello'` 的第一个字符 `'h'`,改为大写的 `'H'`,我们可以采用下面的做法: + +```python +s = 'H' + s[1:] +s = s.replace('h', 'H') +``` + +- 第一种方法,是直接用大写的 `'H'`,通过加号 `'+'` 操作符,与原字符串切片操作的子字符串拼接而成新的字符串。 +- 第二种方法,是直接扫描原字符串,把小写的 `'h'` 替换成大写的 `'H'` ,得到新的字符串。 + +你可能了解到,在其他语言中,如 Java,有可变的字符串类型,比如 StringBuilder,每次添加、改变或删除字符(串),无需创建新的字符串,时间复杂度仅为 O(1)。这样就大大提高了程序的运行效率。 + +但可惜的是,Python 中并没有相关的数据类型,我们还是得老老实实创建新的字符串。因此,每次想要改变字符串,往往需要 O(n) 的时间复杂度,其中,n 为新字符串的长度。 + +你可能注意到了,上述例子的说明中,我用的是“往往”、“通常”这样的字眼,并没有说“一定”。这是为什么呢?显然,随着版本的更新,Python 也越来越聪明,性能优化得越来越好了。 + +这里,我着重讲解一下,使用加法操作符 `'+='` 的字符串拼接方法。因为它是一个例外,打破了字符串不可变的特性。 + +操作方法如下所示: + +```python +str1 += str2 # 表示str1 = str1 + str2 +``` + +我们来看下面这个例子: + +```python +s = '' +for n in range(0, 100000): + s += str(n) +``` + +你觉得这个例子的时间复杂度是多少呢? + +每次循环,似乎都得创建一个新的字符串;而每次创建一个新的字符串,都需要 O(n) 的时间复杂度。因此,总的时间复杂度就为 `O(1) + O(2) + … + O(n) = O(n^2)`。这样到底对不对呢? + +乍一看,这样分析确实很有道理,但是必须说明,这个结论只适用于老版本的 Python 了。自从 Python2.5 开始,每次处理字符串的拼接操作时(`str1 += str2`),Python 首先会检测 str1 还有没有其他的引用。如果没有的话,就会尝试原地扩充字符串 buffer 的大小,而不是重新分配一块内存来创建新的字符串并拷贝。这样的话,上述例子中的时间复杂度就仅为 O(n) 了。 + +因此,以后你在写程序遇到字符串拼接时,如果使用 `'+='` 更方便,就放心地去用吧,不用过分担心效率问题了。 + +另外,对于字符串拼接问题,除了使用加法操作符,我们还可以使用字符串内置的 join 函数。`string.join(iterable)`,表示把每个元素都按照指定的格式连接起来。 + +```python +l = [] +for n in range(0, 100000): + l.append(str(n)) +l = ' '.join(l) +``` + +由于列表的 append 操作是 O(1) 复杂度,字符串同理。因此,这个含有 for 循环例子的时间复杂度为 `n*O(1)=O(n)`。 + +接下来,我们看一下字符串的分割函数 `split()`。`string.split(separator)`,表示把字符串按照 separator 分割成子字符串,并返回一个分割后子字符串组合的列表。它常常应用于对数据的解析处理,比如我们读取了某个文件的路径,想要调用数据库的 API,去读取对应的数据,我们通常会写成下面这样: + +```python +def query_data(namespace, table): + """ + given namespace and table, query database to get corresponding + data + """ + +path = 'hive://ads/training_table' +namespace = path.split('//')[1].split('/')[0] # 返回'ads' +table = path.split('//')[1].split('/')[1] # 返回 'training_table' +data = query_data(namespace, table) +``` + +此外,常见的函数还有: + +- `string.strip(str)`,表示去掉首尾的 str 字符串; +- `string.lstrip(str)`,表示只去掉开头的 str 字符串; +- `string.rstrip(str)`,表示只去掉尾部的 str 字符串。 + +这些在数据的解析处理中同样很常见。比如很多时候,从文件读进来的字符串中,开头和结尾都含有空字符,我们需要去掉它们,就可以用 `strip()` 函数: + +```python +s = ' my name is jason ' +s.strip() +'my name is jason' +``` + +当然,Python 中字符串还有很多常用操作,比如,`string.find(sub, start, end)`,表示从 start 到 end 查找字符串中子字符串 sub 的位置等等。这里,我只强调了最常用并且容易出错的几个函数,其他内容你可以自行查找相应的文档、范例加以了解,我就不一一赘述了。 + +## 3. 字符串的格式化 + +最后,我们一起来看看字符串的格式化。什么是字符串的格式化呢? + +通常,我们使用一个字符串作为模板,模板中会有格式符。这些格式符为后续真实值预留位置,以呈现出真实值应该呈现的格式。字符串的格式化,通常会用在程序的输出、logging 等场景。 + +举一个常见的例子。比如我们有一个任务,给定一个用户的 userid,要去数据库中查询该用户的一些信息,并返回。而如果数据库中没有此人的信息,我们通常会记录下来,这样有利于往后的日志分析,或者是线上 bug 的调试等等。 + +我们通常会用下面的方法来表示: + +```python +print('no data available for person with id: {}, name: {}'.format(id, name)) +``` + +其中的 `string.format()`,就是所谓的格式化函数;而大括号 `{}` 就是所谓的格式符,用来为后面的真实值——变量 name 预留位置。如果 `id = '123'`、`name='jason'`,那么输出便是: + +```python +'no data available for person with id: 123, name: jason' +``` + +这样看来,是不是非常简单呢? + +不过要注意,`string.format()` 是最新的字符串格式函数与规范。自然,我们还有其他的表示方法,比如在 Python 之前版本中,字符串格式化通常用 `%` 来表示,那么上述的例子,就可以写成下面这样: + +```python +print('no data available for person with id: %s, name: %s' % (id, name)) +``` + +其中 `%s` 表示字符串型,`%d` 表示整型等等,这些属于常识,你应该都了解。 + +当然,现在你写程序时,我还是推荐使用 format 函数,毕竟这是最新规范,也是官方文档推荐的规范。 + +也许有人会问,为什么非要使用格式化函数,上述例子用字符串的拼接不也能完成吗?没错,在很多情况下,字符串拼接确实能满足格式化函数的需求。但是使用格式化函数,更加清晰、易读,并且更加规范,不易出错。 + +## 4. 总结 + +这节课,我们主要学习了 Python 字符串的一些基本知识和常用操作,并且结合具体的例子与场景加以说明,特别需要注意下面几点。 + +- Python 中字符串使用单引号、双引号或三引号表示,三者意义相同,并没有什么区别。其中,三引号的字符串通常用在多行字符串的场景。 +- Python 中字符串是不可变的(前面所讲的新版本 Python 中拼接操作 `’+='` 是个例外)。因此,随意改变字符串中字符的值,是不被允许的。 +- Python 新版本(2.5+)中,字符串的拼接变得比以前高效了许多,你可以放心使用。 +- Python 中字符串的格式化(string.format)常常用在输出、日志的记录等场景。 + +## 5. 思考题 + +最后,给你留一道思考题。在新版本的 Python(2.5+)中,下面的两个字符串拼接操作,你觉得哪个更优呢?欢迎留言和我分享你的观点,也欢迎你把这篇文章分享给你的同事、朋友。 + +```python +s = '' +for n in range(0, 100000): + s += str(n) +``` + +```python +l = [] +for n in range(0, 100000): + l.append(str(n)) + +s = ' '.join(l) +``` + +> 关于思考题,如果字符串拼接的次数较少,比如 range(100),那么方法一更优,因为时间复杂度精确的来说第一种是 O(n),第二种是 O(2n),如果拼接的次数较多,比如 range(1000000),方法二稍快一些,虽然方法二会遍历两次,但是 join 的速度其实很快,列表 append 和 join 的开销要比字符串+=小一些。 + +## 6. 杂谈 + +%format 形式在东西多了以后比较费事,结构冗长,会导致错误,比如不能正确显示元组或字典。幸运的是,未来有更光明的日子。 + +str.format格式相对好一些,但参数多了或者处理更长字符串时还是冗长。 + +f-string 这种方式可以更加简化表达过程。还支持大小写(f.或者F.) + + + +欢迎关注我公众号:AI悦创,有更多更好玩的等你发现! + +::: details 公众号:AI悦创【二维码】 + +![](/gzh.jpg) + +::: + +::: info AI悦创·编程一对一 + +AI悦创·推出辅导班啦,包括「Python 语言辅导班、C++ 辅导班、java 辅导班、算法/数据结构辅导班、少儿编程、pygame 游戏开发」,全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践等。当然,还有线下线上摄影课程、Photoshop、Premiere 一对一教学、QQ、微信在线,随时响应!微信:Jiabcdefh + +C++ 信息奥赛题解,长期更新!长期招收一对一中小学信息奥赛集训,莆田、厦门地区有机会线下上门,其他地区线上。微信:Jiabcdefh + +方法一:[QQ](http://wpa.qq.com/msgrd?v=3&uin=1432803776&site=qq&menu=yes) + +方法二:微信:Jiabcdefh + +::: + +![](/zsxq.jpg) + diff --git a/src/Python/Python-core-technology-and-practice/06.assets/379c3eda07d781fd5be94e880e023e53.jpg b/src/Python/Python-core-technology-and-practice/06.assets/379c3eda07d781fd5be94e880e023e53.jpg new file mode 100644 index 00000000000..69d8d0d5b0d Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/06.assets/379c3eda07d781fd5be94e880e023e53.jpg differ diff --git a/src/Python/Python-core-technology-and-practice/06.md b/src/Python/Python-core-technology-and-practice/06.md new file mode 100755 index 00000000000..a97d2d29bdf --- /dev/null +++ b/src/Python/Python-core-technology-and-practice/06.md @@ -0,0 +1,444 @@ +--- +title: 06-Python “黑箱”:输入与输出 +icon: python +date: 2023-06-01 10:07:28 +author: AI悦创 +isOriginal: true +category: Python 进阶 +tag: + - Python 进阶 +sticky: false +star: false +article: true +timeline: true +image: false +navbar: true +sidebarIcon: true +headerDepth: 5 +comment: true +lastUpdated: true +editLink: false +backToTop: true +toc: true +--- + +![](./06.assets/379c3eda07d781fd5be94e880e023e53.jpg) + +你好,我是悦创。 + +世纪之交的论坛上曾有一句流行语:在互联网上,没人知道你是一条狗。互联网刚刚兴起时,一根网线链接到你家,信息通过这条高速线缆直达你的屏幕,你通过键盘飞速回应朋友的消息,信息再次通过网线飞入错综复杂的虚拟世界,再进入朋友家。抽象来看,一台台的电脑就是一个个黑箱,黑箱有了输入和输出,就拥有了图灵机运作的必要条件。 + +Python 程序也是一个黑箱:通过输入流将数据送达,通过输出流将处理后的数据送出,可能 Python 解释器后面藏了一个人,还是一个史莱哲林?No one cares。 + +好了废话不多说,今天我们就由浅及深讲讲 Python 的输入和输出。 + +## 1. 输入输出基础 + +最简单直接的输入来自键盘操作,比如下面这个例子。 + +```python +name = input('your name:') +gender = input('you are a boy?(y/n)') + +###### 输入 ###### +your name:Jack +you are a boy? + +welcome_str = 'Welcome to the matrix {prefix} {name}.' +welcome_dic = { + 'prefix': 'Mr.' if gender == 'y' else 'Mrs', + 'name': name +} + +print('authorizing...') +print(welcome_str.format(**welcome_dic)) + +########## 输出 ########## +authorizing... +Welcome to the matrix Mr. Jack. +``` + +`input()` 函数暂停程序运行,同时等待键盘输入;直到回车被按下,函数的参数即为提示语,输入的类型永远是字符串型(str)。注意,初学者在这里很容易犯错,下面的例子我会讲到。`print()` 函数则接受字符串、数字、字典、列表甚至一些自定义类的输出。 + +我们再来看下面这个例子。 + +```python +a = input() +1 +b = input() +2 + +print('a + b = {}'.format(a + b)) +########## 输出 ############## +a + b = 12 +print('type of a is {}, type of b is {}'.format(type(a), type(b))) +########## 输出 ############## +type of a is , type of b is +print('a + b = {}'.format(int(a) + int(b))) +########## 输出 ############## +a + b = 3 +``` + +这里注意,把 `str` 强制转换为 `int` 请用 `int()`,转为浮点数请用 `float()`。而在生产环境中使用强制转换时,请记得加上 `try except`(即错误和异常处理,专栏后面文章会讲到)。 + +Python 对 `int` 类型没有最大限制(相比之下, C++ 的 int 最大为 2147483647,超过这个数字会产生溢出),但是对 `float` 类型依然有精度限制。这些特点,除了在一些算法竞赛中要注意,在生产环境中也要时刻提防,避免因为对边界条件判断不清而造成 bug 甚至 `0day`(危重安全漏洞)。 + +我们回望一下币圈。2018 年 4 月 23 日中午 11 点 30 分左右,BEC 代币智能合约被黑客攻击。黑客利用数据溢出的漏洞,攻击与美图合作的公司美链 BEC 的智能合约,成功地向两个地址转出了天量级别的 BEC 代币,导致市场上的海量 BEC 被抛售,该数字货币的价值也几近归零,给 BEC 市场交易带来了毁灭性的打击。 + +由此可见,虽然输入输出和类型处理事情简单,但我们一定要慎之又慎。毕竟相当比例的安全漏洞,都来自随意的 I/O 处理。 + +## 2. 文件输入输出 + +命令行的输入输出,只是 Python 交互的最基本方式,适用一些简单小程序的交互。而生产级别的 Python 代码,大部分 I/O 则来自于文件、网络、其他进程的消息等等。 + +接下来,我们来详细分析一个文本文件读写。假设我们有一个文本文件 `in.txt`,内容如下: + +``` +I have a dream that my four little children will one day live in a nation where they will not be judged by the color of their skin but by the content of their character. I have a dream today. + +I have a dream that one day down in Alabama, with its vicious racists, . . . one day right there in Alabama little black boys and black girls will be able to join hands with little white boys and white girls as sisters and brothers. I have a dream today. + +I have a dream that one day every valley shall be exalted, every hill and mountain shall be made low, the rough places will be made plain, and the crooked places will be made straight, and the glory of the Lord shall be revealed, and all flesh shall see it together. + +This is our hope. . . With this faith we will be able to hew out of the mountain of despair a stone of hope. With this faith we will be able to transform the jangling discords of our nation into a beautiful symphony of brotherhood. With this faith we will be able to work together, to pray together, to struggle together, to go to jail together, to stand up for freedom together, knowing that we will be free one day. . . . + +And when this happens, and when we allow freedom ring, when we let it ring from every village and every hamlet, from every state and every city, we will be able to speed up that day when all of God's children, black men and white men, Jews and Gentiles, Protestants and Catholics, will be able to join hands and sing in the words of the old Negro spiritual: "Free at last! Free at last! Thank God Almighty, we are free at last!" +``` + +好,让我们来做一个简单的 NLP(自然语言处理)任务。如果你对此不太了解也没有影响,我会带你一步步完成这个任务。 + +首先,我们要清楚 NLP 任务的基本步骤,也就是下面的四步: + +1. 读取文件; +2. 去除所有标点符号和换行符,并把所有大写变成小写; +3. 合并相同的词,统计每个词出现的频率,并按照词频从大到小排序; +4. 将结果按行输出到文件 `out.txt`。 +5. 你可以自己先思考一下,用 Python 如何解决这个问题。这里,我也给出了我的代码,并附有详细的注释。我们一起来看下这段代码。 + +```python +import re + +# 你不用太关心这个函数 +def parse(text): + # 使用正则表达式去除标点符号和换行符 + text = re.sub(r'[^\w ]', ' ', text) + + # 转为小写 + text = text.lower() + + # 生成所有单词的列表 + word_list = text.split(' ') + + # 去除空白单词 + word_list = filter(None, word_list) + + # 生成单词和词频的字典 + word_cnt = {} + for word in word_list: + if word not in word_cnt: + word_cnt[word] = 0 + word_cnt[word] += 1 + + # 按照词频排序 + sorted_word_cnt = sorted(word_cnt.items(), key=lambda kv: kv[1], reverse=True) + + return sorted_word_cnt + +with open('in.txt', 'r') as fin: + text = fin.read() + +word_and_freq = parse(text) + +with open('out.txt', 'w') as fout: + for word, freq in word_and_freq: + fout.write('{} {}\n'.format(word, freq)) + +########## 输出(省略较长的中间结果) ########## + +and 15 +be 13 +will 11 +to 11 +the 10 +of 10 +a 8 +we 8 +day 6 + +... + +old 1 +negro 1 +spiritual 1 +thank 1 +god 1 +almighty 1 +are 1 +``` + +你不用太关心 `parse()` 函数的具体实现,你只需要知道,它做的事情是把输入的 text 字符串,转化为我们需要的排序后的词频统计。而 `sorted_word_cnt` 则是一个二元组的列表(list of tuples)。 + +首先我们需要先了解一下,计算机中文件访问的基础知识。事实上,计算机内核(kernel)对文件的处理相对比较复杂,涉及到内核模式、虚拟文件系统、锁和指针等一系列概念,这些内容我不会深入讲解,我只说一些基础但足够使用的知识。 + +我们先要用 `open()` 函数拿到文件的指针。其中,第一个参数指定文件位置(相对位置或者绝对位置);第二个参数,如果是 `'r'` 表示读取,如果是 `'w'` 则表示写入,当然也可以用 `'rw'` ,表示读写都要。a 则是一个不太常用(但也很有用)的参数,表示追加(append),这样打开的文件,如果需要写入,会从原始文件的最末尾开始写入。 + +这里我插一句,在 Facebook 的工作中,代码权限管理非常重要。如果你只需要读取文件,就不要请求写入权限。这样在某种程度上可以降低 bug 对整个系统带来的风险。 + +好,回到我们的话题。在拿到指针后,我们可以通过 `read()` 函数,来读取文件的全部内容。代码 `text = fin.read()` ,即表示把文件所有内容读取到内存中,并赋值给变量 text。这么做自然也是有利有弊: + +- 优点是方便,接下来我们可以很方便地调用 parse 函数进行分析; +- 缺点是如果文件过大,一次性读取可能造成内存崩溃。 + +这时,我们可以给 read 指定参数 size ,用来表示读取的最大长度。还可以通过 `readline()` 函数,每次读取一行,这种做法常用于数据挖掘(Data Mining)中的数据清洗,在写一些小的程序时非常轻便。如果每行之间没有关联,这种做法也可以降低内存的压力。而 `write()` 函数,可以把参数中的字符串输出到文件中,也很容易理解。 + +这里我需要简单提一下 with 语句(后文会详细讲到)。`open()` 函数对应于 `close()` 函数,也就是说,如果你打开了文件,在完成读取任务后,就应该立刻关掉它。而如果你使用了 with 语句,就不需要显式调用 `close()`。在 with 的语境下任务执行完毕后,`close()` 函数会被自动调用,代码也简洁很多。 + +最后需要注意的是,所有 I/O 都应该进行错误处理。因为 I/O 操作可能会有各种各样的情况出现,而一个健壮(robust)的程序,需要能应对各种情况的发生,而不应该崩溃(故意设计的情况除外)。 + +## 3. JSON 序列化与实战 + +最后,我来讲一个和实际应用很贴近的知识点。 + +JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,它的设计意图是把所有事情都用设计的字符串来表示,这样既方便在互联网上传递信息,也方便人进行阅读(相比一些 binary 的协议)。JSON 在当今互联网中应用非常广泛,也是每一个用 Python 程序员应当熟练掌握的技能点。 + +设想一个情景,你要向交易所购买一定数额的股票。那么,你需要提交股票代码、方向(买入 / 卖出)、订单类型(市价 / 限价)、价格(如果是限价单)、数量等一系列参数,而这些数据里,有字符串,有整数,有浮点数,甚至还有布尔型变量,全部混在一起并不方便交易所解包。 + +那该怎么办呢? + +其实,我们要讲的 JSON ,正能解决这个场景。你可以把它简单地理解为两种黑箱: + +- 第一种,输入这些杂七杂八的信息,比如 Python 字典,输出一个字符串; +- 第二种,输入这个字符串,可以输出包含原始信息的 Python 字典。 + +具体代码如下: + +```python +import json + +params = { + 'symbol': '123456', + 'type': 'limit', + 'price': 123.4, + 'amount': 23 +} + +params_str = json.dumps(params) + +print('after json serialization') +print('type of params_str = {}, params_str = {}'.format(type(params_str), params)) + +original_params = json.loads(params_str) + +print('after json deserialization') +print('type of original_params = {}, original_params = {}'.format(type(original_params), original_params)) + +########## 输出 ########## + +after json serialization +type of params_str = , params_str = {'symbol': '123456', 'type': 'limit', 'price': 123.4, 'amount': 23} +after json deserialization +type of original_params = , original_params = {'symbol': '123456', 'type': 'limit', 'price': 123.4, 'amount': 23} +``` + +其中, + +- `json.dumps()` 这个函数,接受 Python 的基本数据类型,然后将其序列化为 string; +- 而 `json.loads()` 这个函数,接受一个合法字符串,然后将其反序列化为 Python 的基本数据类型。 + +是不是很简单呢? + +不过还是那句话,请记得加上错误处理。不然,哪怕只是给 `json.loads()` 发送了一个非法字符串,而你没有 catch 到,程序就会崩溃了。 + +到这一步,你可能会想,如果我要输出字符串到文件,或者从文件中读取 JSON 字符串,又该怎么办呢? + +是的,你仍然可以使用上面提到的 `open()` 和 `read()/write()` ,先将字符串读取 / 输出到内存,再进行 JSON 编码 / 解码,当然这有点麻烦。 + +```python +import json + +params = { + 'symbol': '123456', + 'type': 'limit', + 'price': 123.4, + 'amount': 23 +} + +with open('params.json', 'w') as fout: + params_str = json.dump(params, fout) + +with open('params.json', 'r') as fin: + original_params = json.load(fin) + +print('after json deserialization') +print('type of original_params = {}, original_params = {}'.format(type(original_params), original_params)) + +########## 输出 ########## + +after json deserialization +type of original_params = , original_params = {'symbol': '123456', 'type': 'limit', 'price': 123.4, 'amount': 23} +``` + +这样,我们就简单清晰地实现了读写 JSON 字符串的过程。当开发一个第三方应用程序时,你可以通过 JSON 将用户的个人配置输出到文件,方便下次程序启动时自动读取。这也是现在普遍运用的成熟做法。 + +那么 JSON 是唯一的选择吗?显然不是,它只是轻量级应用中最方便的选择之一。据我所知,在 Google,有类似的工具叫做 Protocol Buffer,当然,Google 已经完全开源了这个工具,你可以自己了解一下使用方法。 + +相比于 JSON,它的优点是生成优化后的二进制文件,因此性能更好。但与此同时,生成的二进制序列,是不能直接阅读的。它在 TensorFlow 等很多对性能有要求的系统中都有广泛的应用。 + +## 4. 总结 + +这节课,我们主要学习了 Python 的普通 I/O 和文件 I/O,同时了解了 JSON 序列化的基本知识,并通过具体的例子进一步掌握。再次强调一下需要注意的几点: + +- I/O 操作需谨慎,一定要进行充分的错误处理,并细心编码,防止出现编码漏洞; +- 编码时,对内存占用和磁盘占用要有充分的估计,这样在出错时可以更容易找到原因; +- JSON 序列化是很方便的工具,要结合实战多多练习; +- 代码尽量简洁、清晰,哪怕是初学阶段,也要有一颗当元帅的心。 + +## 5. 思考题 + +最后,我给你留了两道思考题。 + +第一问:你能否把 NLP 例子中的 word count 实现一遍?不过这次,`in.txt` 可能非常非常大(意味着你不能一次读取到内存中),而 `output.txt` 不会很大(意味着重复的单词数量很多)。 + +提示:你可能需要每次读取一定长度的字符串,进行处理,然后再读取下一次的。但是如果单纯按照长度划分,你可能会把一个单词隔断开,所以需要细心处理这种边界情况。 + +第二问:你应该使用过类似百度网盘、Dropbox 等网盘,但是它们可能空间有限(比如 5GB)。如果有一天,你计划把家里的 100GB 数据传送到公司,可惜你没带 U 盘,于是你想了一个主意: + +每次从家里向 Dropbox 网盘写入不超过 5GB 的数据,而公司电脑一旦侦测到新数据,就立即拷贝到本地,然后删除网盘上的数据。等家里电脑侦测到本次数据全部传入公司电脑后,再进行下一次写入,直到所有数据都传输过去。 + +根据这个想法,你计划在家写一个 `server.py`,在公司写一个 `client.py` 来实现这个需求。 + +提示:我们假设每个文件都不超过 5GB。 + +- 你可以通过写入一个控制文件(`config.json`)来同步状态。不过,要小心设计状态,这里有可能产生 race condition。 +- 你也可以通过直接侦测文件是否产生,或者是否被删除来同步状态,这是最简单的做法。 + +不要担心难度问题,尽情写下你的思考,最终代码我也会为你准备好。 + +欢迎在留言区写下你的答案,也欢迎你把这篇文章转给你的同事、朋友,一起在思考中学习。 + +## 6. Answer + +思考题第一题: + +```python +import re + +CHUNK_SIZE = 100 # 这个数表示一次最多读取的字符长度 + +# 这个函数每次会接收上一次得到的 last_word,然后和这次的 text 合并起来处理。 +# 合并后判断最后一个词有没有可能连续,并分离出来,然后返回。 +# 这里的代码没有 if 语句,但是仍然是正确的,可以想一想为什么。 +def parse_to_word_list(text, last_word, word_list): + text = re.sub(r'[^\w ]', ' ', last_word + text) + text = text.lower() + cur_word_list = text.split(' ') + cur_word_list, last_word = cur_word_list[:-1], cur_word_list[-1] + word_list += filter(None, cur_word_list) + return last_word + +def solve(): + with open('in.txt', 'r') as fin: + word_list, last_word = [], '' + while True: + text = fin.read(CHUNK_SIZE) + if not text: + break # 读取完毕,中断循环 + last_word = parse_to_word_list(text, last_word, word_list) + + word_cnt = {} + for word in word_list: + if word not in word_cnt: + word_cnt[word] = 0 + word_cnt[word] += 1 + + sorted_word_cnt = sorted(word_cnt.items(), key=lambda kv: kv[1], reverse=True) + return sorted_word_cnt + +print(solve()) +``` + +思考题第二题:(省略了一些异常处理,后面会讲到) + +```python +# filename:server.py +# 我们假设 server 电脑上的所有的文件都在 BASR_DIR 中,为了简化不考虑文件夹结构,网盘的路径在 NET_DIR + +import os +from shutil import copyfile +import time + +BASE_DIR = 'server/' +NET_DIR = 'net/' + +def main(): + filenames = os.listdir(BASE_DIR) + for i, filename in enumerate(filenames): + print('copying {} into net drive... {}/{}'.format(filename, i + 1, len(filenames))) + copyfile(BASE_DIR + filename, NET_DIR + filename) + print('copied {} into net drive, waiting client complete... {}/{}'.format(filename, i + 1, len(filenames))) + + while os.path.exists(NET_DIR + filename): + time.sleep(3) + + print('transferred {} into client. {}/{}'.format(filename, i + 1, len(filenames))) + +if __name__ == "__main__": + main() + +++++++++++++++++++++++ +client.py +# 我们假设 client 电脑上要输出的文件夹在 BASR_DIR ,网盘的路径在 NET_DIR + +import os +from shutil import copyfile +import time + +BASE_DIR = 'client/' +NET_DIR = 'net/' + +def main(): + while True: + filenames = os.listdir(NET_DIR) + for filename in filenames: + print('downloading {} into local disk...'.format(filename)) + copyfile(NET_DIR + filename, BASE_DIR + filename) + os.remove(NET_DIR + filename) # 我们需要删除这个文件,网盘会提我们同步这个操作,从而 server 知晓已完成 + print('downloaded {} into local disk.'.format(filename)) + time.sleep(3) + +if __name__ == "__main__": + main() +``` + +--- + +> 老师,为什么filter(none,list)可以过滤空值,不是保留空值嘛 +> +> 作者回复: filter(None, Iterable) 是一种容易出错的用法,这里不止过滤空字符串,还能过滤 0,None,空列表等值。这里的 None,严格意义上等于 lambda x: x, 是一个 callable。 + + + + + +欢迎关注我公众号:AI悦创,有更多更好玩的等你发现! + +::: details 公众号:AI悦创【二维码】 + +![](/gzh.jpg) + +::: + +::: info AI悦创·编程一对一 + +AI悦创·推出辅导班啦,包括「Python 语言辅导班、C++ 辅导班、java 辅导班、算法/数据结构辅导班、少儿编程、pygame 游戏开发」,全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践等。当然,还有线下线上摄影课程、Photoshop、Premiere 一对一教学、QQ、微信在线,随时响应!微信:Jiabcdefh + +C++ 信息奥赛题解,长期更新!长期招收一对一中小学信息奥赛集训,莆田、厦门地区有机会线下上门,其他地区线上。微信:Jiabcdefh + +方法一:[QQ](http://wpa.qq.com/msgrd?v=3&uin=1432803776&site=qq&menu=yes) + +方法二:微信:Jiabcdefh + +::: + +![](/zsxq.jpg) + diff --git a/src/Python/Python-core-technology-and-practice/07.assets/1e6630cb78925eb8fa3a13b9c659492e.jpg b/src/Python/Python-core-technology-and-practice/07.assets/1e6630cb78925eb8fa3a13b9c659492e.jpg new file mode 100644 index 00000000000..296f61e8a61 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/07.assets/1e6630cb78925eb8fa3a13b9c659492e.jpg differ diff --git a/src/Python/Python-core-technology-and-practice/07.assets/949742df36600c086c31e399ce515f45.png b/src/Python/Python-core-technology-and-practice/07.assets/949742df36600c086c31e399ce515f45.png new file mode 100644 index 00000000000..18e2d949c97 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/07.assets/949742df36600c086c31e399ce515f45.png differ diff --git a/src/Python/Python-core-technology-and-practice/07.md b/src/Python/Python-core-technology-and-practice/07.md new file mode 100755 index 00000000000..31309beec90 --- /dev/null +++ b/src/Python/Python-core-technology-and-practice/07.md @@ -0,0 +1,409 @@ +--- +title: 07-修炼基本功:条件与循环 +icon: python +date: 2023-06-01 10:27:40 +author: AI悦创 +isOriginal: true +category: Python 进阶 +tag: + - Python 进阶 +sticky: false +star: false +article: true +timeline: true +image: false +navbar: true +sidebarIcon: true +headerDepth: 5 +comment: true +lastUpdated: true +editLink: false +backToTop: true +toc: true +--- + +![](./07.assets/1e6630cb78925eb8fa3a13b9c659492e.jpg) + +你好,我是悦创。 + +前面几节,我们一起学习了列表、元组、字典、集合和字符串等一系列 Python 的基本数据类型。但是,如何把这一个个基本的数据结构类型串接起来,组成一手漂亮的代码呢?这就是我们今天所要讨论的“条件与循环”。 + +我习惯把“条件与循环”,叫做编程中的基本功。为什么称它为基本功呢?因为它控制着代码的逻辑,可以说是程序的中枢系统。如果把写程序比作盖楼房,那么条件与循环就是楼房的根基,其他所有东西都是在此基础上构建而成。 + +毫不夸张地说,写一手简洁易读的条件与循环代码,对提高程序整体的质量至关重要。 + +## 1. 条件语句 + +首先,我们一起来看一下 Python 的条件语句,用法很简单。比如,我想要表示 `y=|x|`这个函数,那么相应的代码便是: + +```python +# y = |x| +if x < 0: + y = -x +else: + y = x +``` + +和其他语言不一样,我们不能在条件语句中加括号,写成下面这样的格式。 + +```python +if (x < 0) +``` + +但需要注意的是,在条件语句的末尾必须加上冒号(`:`),这是 Python 特定的语法规范。 + +由于 Python 不支持 switch 语句,因此,当存在多个条件判断时,我们需要用 else if 来实现,这在 Python 中的表达是 **elif**。语法如下: + +```python +if condition_1: + statement_1 +elif condition_2: + statement_2 +... +elif condition_i: + statement_i +else: + statement_n +``` + +整个条件语句是顺序执行的,如果遇到一个条件满足,比如 `condition_i` 满足时,在执行完 `statement_i` 后,便会退出整个 if、elif、else 条件语句,而不会继续向下执行。这个语句在工作中很常用,比如下面的这个例子。 + +实际工作中,我们经常用 ID 表示一个事物的属性,然后进行条件判断并且输出。比如,在 integrity 的工作中,通常用 0、1、2 分别表示一部电影的色情暴力程度。其中,0 的程度最高,是 red 级别;1 其次,是 yellow 级别;2 代表没有质量问题,属于 green。 + +如果给定一个 ID,要求输出某部电影的质量评级,则代码如下: + +```python +if id == 0: + print('red') +elif id == 1: + print('yellow') +else: + print('green') +``` + +不过要注意,if 语句是可以单独使用的,但 elif、else 都必须和 if 成对使用。 + +另外,在我们进行条件判断时, 不少人喜欢省略判断的条件,比如写成下面这样: + +```python +if s: # s is a string + ... +if l: # l is a list + ... +if i: # i is an int + ... +... +``` + +关于省略判断条件的常见用法,我大概总结了一下: + +![](./07.assets/949742df36600c086c31e399ce515f45.png) + +不过,切记,在实际写代码时,我们鼓励,除了 boolean 类型的数据,条件判断最好是显性的。比如,在判断一个整型数是否为 0 时,我们最好写出判断的条件: + +```python +if i != 0: + ... +``` + +而不是只写出变量名: + +```python +if i: + ... +``` + +## 2. 循环语句 + +讲完了条件语句,我们接着来看循环语句。所谓循环,顾名思义,本质上就是遍历集合中的元素。和其他语言一样,Python 中的循环一般通过 for 循环和 while 循环实现。 + +比如,我们有一个列表,需要遍历列表中的所有元素并打印输出,代码如下: + +```python +l = [1, 2, 3, 4] +for item in l: + print(item) +1 +2 +3 +4 +``` + +你看,是不是很简单呢? + +其实,Python 中的数据结构只要是可迭代的(iterable),比如列表、集合等等,那么都可以通过下面这种方式遍历: + +```python +for item in : + ... +``` + +这里需要单独强调一下字典。字典本身只有键是可迭代的,如果我们要遍历它的值或者是键值对,就需要通过其内置的函数 `values()` 或者 `items()` 实现。其中,`values()` 返回字典的值的集合,`items()` 返回键值对的集合。 + +```python +d = {'name': 'jason', 'dob': '2000-01-01', 'gender': 'male'} +for k in d: # 遍历字典的键 + print(k) +name +dob +gender + +for v in d.values(): # 遍历字典的值 + print(v) +jason +2000-01-01 +male + +for k, v in d.items(): # 遍历字典的键值对 + print('key: {}, value: {}'.format(k, v)) +key: name, value: jason +key: dob, value: 2000-01-01 +key: gender, value: male +``` + +看到这里你也许会问,有没有办法通过集合中的索引来遍历元素呢?当然可以,其实这种情况在实际工作中还是很常见的,甚至很多时候,我们还得根据索引来做一些条件判断。 + +我们通常通过 `range()` 这个函数,拿到索引,再去遍历访问集合中的元素。比如下面的代码,遍历一个列表中的元素,当索引小于 5 时,打印输出: + +```python +l = [1, 2, 3, 4, 5, 6, 7] +for index in range(0, len(l)): + if index < 5: + print(l[index]) + +1 +2 +3 +4 +5 +``` + +当我们同时需要索引和元素时,还有一种更简洁的方式,那就是通过 Python 内置的函数 `enumerate()`。用它来遍历集合,不仅返回每个元素,并且还返回其对应的索引,这样一来,上面的例子就可以写成: + +```python +l = [1, 2, 3, 4, 5, 6, 7] +for index, item in enumerate(l): + if index < 5: + print(item) + +1 +2 +3 +4 +5 +``` + +在循环语句中,我们还常常搭配 continue 和 break 一起使用。所谓 continue,就是让程序跳过当前这层循环,继续执行下面的循环;而 break 则是指完全跳出所在的整个循环体。在循环中适当加入 continue 和 break,往往能使程序更加简洁、易读。 + +比如,给定两个字典,分别是产品名称到价格的映射,和产品名称到颜色列表的映射。我们要找出价格小于 1000,并且颜色不是红色的所有产品名称和颜色的组合。如果不用 continue,代码应该是下面这样的: + +```python +# name_price: 产品名称(str)到价格(int)的映射字典 +# name_color: 产品名字(str)到颜色(list of str)的映射字典 +for name, price in name_price.items(): + if price < 1000: + if name in name_color: + for color in name_color[name]: + if color != 'red': + print('name: {}, color: {}'.format(name, color)) + else: + print('name: {}, color: {}'.format(name, 'None')) +``` + +而加入 continue 后,代码显然清晰了很多: + +```python +# name_price: 产品名称(str)到价格(int)的映射字典 +# name_color: 产品名字(str)到颜色(list of str)的映射字典 +for name, price in name_price.items(): + if price >= 1000: + continue + if name not in name_color: + print('name: {}, color: {}'.format(name, 'None')) + continue + for color in name_color[name]: + if color == 'red': + continue + print('name: {}, color: {}'.format(name, color)) +``` + +我们可以看到,按照第一个版本的写法,从开始一直到打印输出符合条件的产品名称和颜色,共有 5 层 for 或者 if 的嵌套;但第二个版本加入了 continue 后,只有 3 层嵌套。 + +显然,如果代码中出现嵌套里还有嵌套的情况,代码便会变得非常冗余、难读,也不利于后续的调试、修改。因此,我们要尽量避免这种多层嵌套的情况。 + +前面讲了 for 循环,对于 while 循环,原理也是一样的。它表示当 condition 满足时,一直重复循环内部的操作,直到 condition 不再满足,就跳出循环体。 + +```python +while condition: + .... +``` + +很多时候,for 循环和 while 循环可以互相转换,比如要遍历一个列表,我们用 while 循环同样可以完成: + +```python +l = [1, 2, 3, 4] +index = 0 +while index < len(l): + print(l[index]) + index += 1 +``` + +那么,两者的使用场景又有什么区别呢? + +通常来说,如果你只是遍历一个已知的集合,找出满足条件的元素,并进行相应的操作,那么使用 for 循环更加简洁。但如果你需要在满足某个条件前,不停地重复某些操作,并且没有特定的集合需要去遍历,那么一般则会使用 while 循环。 + +比如,某个交互式问答系统,用户输入文字,系统会根据内容做出相应的回答。为了实现这个功能,我们一般会使用 while 循环,大致代码如下: + +```python +while True: + try: + text = input('Please enter your questions, enter "q" to exit') + if text == 'q': + print('Exit system') + break + ... + ... + print(response) + except Exception as err: + print('Encountered error: {}'.format(err)) + break +``` + +同时需要注意的是,for 循环和 while 循环的效率问题。比如下面的 while 循环: + +```python +i = 0 +while i < 1000000: + i += 1 +``` + +和等价的 for 循环: + +```python +for i in range(0, 1000000): + pass +``` + +究竟哪个效率高呢? + +要知道,`range()` 函数是直接由 C 语言写的,调用它速度非常快。而 while 循环中的“`i += 1`”这个操作,得通过 Python 的解释器间接调用底层的 C 语言;并且这个简单的操作,又涉及到了对象的创建和删除(因为 i 是整型,是 immutable,`i += 1` 相当于 `i = new int(i + 1)`)。所以,显然,for 循环的效率更胜一筹。 + +## 3. 条件与循环的复用 + +前面两部分讲了条件与循环的一些基本操作,接下来,我们重点来看它们的进阶操作,让程序变得更简洁高效。 + +在阅读代码的时候,你应该常常会发现,有很多将条件与循环并做一行的操作,例如: + +```python +expression1 if condition else expression2 for item in iterable +``` + +将这个表达式分解开来,其实就等同于下面这样的嵌套结构: + +```python +for item in iterable: + if condition: + expression1 + else: + expression2 +``` + +而如果没有 else 语句,则需要写成: + +```python +expression for item in iterable if condition +``` + +举个例子,比如我们要绘制 `y = 2*|x| + 5` 的函数图像,给定集合 x 的数据点,需要计算出 y 的数据集合,那么只用一行代码,就可以很轻松地解决问题了: + +```python +y = [value * 2 + 5 if value > 0 else -value * 2 + 5 for value in x] +``` + +再比如我们在处理文件中的字符串时,常常遇到的一个场景:将文件中逐行读取的一个完整语句,按逗号分割单词,去掉首位的空字符,并过滤掉长度小于等于 3 的单词,最后返回由单词组成的列表。这同样可以简洁地表达成一行: + +```python +text = ' Today, is, Sunday' +text_list = [s.strip() for s in text.split(',') if len(s.strip()) > 3] +print(text_list) +['Today', 'Sunday'] +``` + +当然,这样的复用并不仅仅局限于一个循环。比如,给定两个列表 x、y,要求返回 x、y 中所有元素对组成的元组,相等情况除外。那么,你也可以很容易表示出来: + +```python +[(xx, yy) for xx in x for yy in y if xx != yy] +``` + +这样的写法就等价于: + +```python +l = [] +for xx in x: + for yy in y: + if xx != yy: + l.append((xx, yy)) +``` + +熟练之后,你会发现这种写法非常方便。当然,如果遇到逻辑很复杂的复用,你可能会觉得写成一行难以理解、容易出错。那种情况下,用正常的形式表达,也不失为一种好的规范和选择。 + +## 4. 总结 + +今天这节课,我们一起学习了条件与循环的基本概念、进阶用法以及相应的应用。这里,我重点强调几个易错的地方。 + +- 在条件语句中,if 可以单独使用,但是 elif 和 else 必须和 if 同时搭配使用;而 If 条件语句的判断,除了 boolean 类型外,其他的最好显示出来。 +- 在 for 循环中,如果需要同时访问索引和元素,你可以使用 `enumerate()` 函数来简化代码。 +- 写条件与循环时,合理利用 continue 或者 break 来避免复杂的嵌套,是十分重要的。 +- 要注意条件与循环的复用,简单功能往往可以用一行直接完成,极大地提高代码质量与效率。 + +## 5. 思考题 + +最后给你留一个思考题。给定下面两个列表 attributes 和 values,要求针对 values 中每一组子列表 value,输出其和 attributes 中的键对应后的字典,最后返回字典组成的列表。 + +```python +attributes = ['name', 'dob', 'gender'] +values = [['jason', '2000-01-01', 'male'], +['mike', '1999-01-01', 'male'], +['nancy', '2001-02-01', 'female'] +] + +# expected output: +[{'name': 'jason', 'dob': '2000-01-01', 'gender': 'male'}, +{'name': 'mike', 'dob': '1999-01-01', 'gender': 'male'}, +{'name': 'nancy', 'dob': '2001-02-01', 'gender': 'female'}] +``` + +你能分别用一行和多行条件循环语句,来实现这个功能吗? + +欢迎在留言区写下你的答案,还有你今天学习的心得和疑惑,也欢迎你把这篇文章分享给你的同事、朋友。 + + + + + + + + + +欢迎关注我公众号:AI悦创,有更多更好玩的等你发现! + +::: details 公众号:AI悦创【二维码】 + +![](/gzh.jpg) + +::: + +::: info AI悦创·编程一对一 + +AI悦创·推出辅导班啦,包括「Python 语言辅导班、C++ 辅导班、java 辅导班、算法/数据结构辅导班、少儿编程、pygame 游戏开发」,全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践等。当然,还有线下线上摄影课程、Photoshop、Premiere 一对一教学、QQ、微信在线,随时响应!微信:Jiabcdefh + +C++ 信息奥赛题解,长期更新!长期招收一对一中小学信息奥赛集训,莆田、厦门地区有机会线下上门,其他地区线上。微信:Jiabcdefh + +方法一:[QQ](http://wpa.qq.com/msgrd?v=3&uin=1432803776&site=qq&menu=yes) + +方法二:微信:Jiabcdefh + +::: + +![](/zsxq.jpg) + diff --git a/src/Python/Python-core-technology-and-practice/08.assets/8e0dd8db8228b383ec0e0f22bac1540c.jpg b/src/Python/Python-core-technology-and-practice/08.assets/8e0dd8db8228b383ec0e0f22bac1540c.jpg new file mode 100755 index 00000000000..8f8e70f6745 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/08.assets/8e0dd8db8228b383ec0e0f22bac1540c.jpg differ diff --git a/src/Python/Python-core-technology-and-practice/08.md b/src/Python/Python-core-technology-and-practice/08.md new file mode 100755 index 00000000000..3cbcacb8045 --- /dev/null +++ b/src/Python/Python-core-technology-and-practice/08.md @@ -0,0 +1,397 @@ +--- +title: 08-异常处理:如何提高程序的稳定性? +icon: python +date: 2022-12-16 19:17:10 +author: AI悦创 +isOriginal: true +category: Python 进阶 +tag: + - Python 进阶 +sticky: false +star: false +article: true +timeline: true +image: false +navbar: true +sidebarIcon: true +headerDepth: 5 +comment: true +lastUpdated: true +editLink: false +backToTop: true +toc: true +--- + +![img](./08.assets/8e0dd8db8228b383ec0e0f22bac1540c.jpg) + +你好,我是悦创。 + +今天这节课,我想和你聊聊 Python 的异常处理。和其他语言一样,异常处理是 Python 中一种很常见,并且很重要的机制与代码规范。 + +我在实际工作中,见过很多次这样的情况:一位工程师提交了代码,不过代码某处忘记了异常处理。碰巧这种异常发生的频率不低,所以在代码 push 到线上后没多久,就会收到紧急通知——服务器崩溃了。 + +如果事情严重,对用户的影响也很大,这位工程师还得去专门的会议上做自我检讨,可以说是很惨了。这类事件层出不穷,也告诉我们,正确理解和处理程序中的异常尤为关键。 + +## 1. 错误与异常 + +首先要了解,Python 中的错误和异常是什么?两者之间又有什么联系和区别呢? + +通常来说,程序中的错误至少包括两种,一种是语法错误,另一种则是异常。 + +所谓语法错误,你应该很清楚,也就是你写的代码不符合编程规范,无法被识别与执行,比如下面这个例子: + +```python +if name is not None + print(name) +``` + +If 语句漏掉了冒号,不符合 Python 的语法规范,所以程序就会报错 invalid syntax。 + +而异常则是指程序的语法正确,也可以被执行,但在执行过程中遇到了错误,抛出了异常,比如下面的 3 个例子: + +```python +10 / 0 +Traceback (most recent call last): + File "", line 1, in +ZeroDivisionError: integer division or modulo by zero + +order * 2 +Traceback (most recent call last): + File "", line 1, in +NameError: name 'order' is not defined + +1 + [1, 2] +Traceback (most recent call last): + File "", line 1, in +TypeError: unsupported operand type(s) for +: 'int' and 'list' +``` + +它们语法完全正确,但显然,我们不能做除法时让分母为 0;也不能使用未定义的变量做运算;而让一个整型和一个列表相加也是不可取的。 + +于是,当程序运行到这些地方时,就抛出了异常,并且终止运行。例子中的 `ZeroDivisionError` `NameError` 和 `TypeError`,就是三种常见的异常类型。 + +当然,Python 中还有很多其他异常类型,比如 `KeyError` 是指字典中的键找不到;`FileNotFoundError` 是指发送了读取文件的请求,但相应的文件不存在等等,我在此不一一赘述,你可以自行参考[相应文档](https://docs.python.org/3/library/exceptions.html#bltin-exceptions)。 + +## 2. 如何处理异常 + +刚刚讲到,如果执行到程序中某处抛出了异常,程序就会被终止并退出。你可能会问,那有没有什么办法可以不终止程序,让其照样运行下去呢?答案当然是肯定的,这也就是我们所说的异常处理,通常使用 try 和 except 来解决,比如: + +```python +try: + s = input('please enter two numbers separated by comma: ') + num1 = int(s.split(',')[0].strip()) + num2 = int(s.split(',')[1].strip()) + ... +except ValueError as err: + print('Value Error: {}'.format(err)) + +print('continue') +... +``` + +这里默认用户输入以逗号相隔的两个整形数字,将其提取后,做后续的操作(注意 input 函数会将输入转换为字符串类型)。如果我们输入 a,b,程序便会抛出异常 `invalid literal for int() with base 10: 'a'`,然后跳出 try 这个 block。 + +由于程序抛出的异常类型是 ValueError,和 except block 所 catch 的异常类型相匹配,所以 except block 便会被执行,最终输出 `Value Error: invalid literal for int() with base 10: 'a'`,并打印出 continue。 + +```python +please enter two numbers separated by comma: a,b +Value Error: invalid literal for int() with base 10: 'a' +continue +``` + +我们知道,except block 只接受与它相匹配的异常类型并执行,如果程序抛出的异常并不匹配,那么程序照样会终止并退出。 + +所以,还是刚刚这个例子,如果我们只输入1,程序抛出的异常就是 `IndexError: list index out of range`,与 ValueError 不匹配,那么 except block 就不会被执行,程序便会终止并退出(continue 不会被打印)。 + +```python +please enter two numbers separated by comma: 1 +IndexError Traceback (most recent call last) +IndexError: list index out of range +``` + +不过,很显然,这样强调一种类型的写法有很大的局限性。那么,该怎么解决这个问题呢? + +其中一种解决方案,是在 except block 中加入多种异常的类型,比如下面这样的写法: + +```python +try: + s = input('please enter two numbers separated by comma: ') + num1 = int(s.split(',')[0].strip()) + num2 = int(s.split(',')[1].strip()) + ... +except (ValueError, IndexError) as err: + print('Error: {}'.format(err)) + +print('continue') +... +``` + +或者第二种写法: + +```python +try: + s = input('please enter two numbers separated by comma: ') + num1 = int(s.split(',')[0].strip()) + num2 = int(s.split(',')[1].strip()) + ... +except ValueError as err: + print('Value Error: {}'.format(err)) +except IndexError as err: + print('Index Error: {}'.format(err)) + +print('continue') +... +``` + +这样,每次程序执行时,except block 中只要有一个 exception 类型与实际匹配即可。 + +不过,很多时候,我们很难保证程序覆盖所有的异常类型,所以,更通常的做法,是在最后一个 except block,声明其处理的异常类型是 Exception。Exception 是其他所有非系统异常的基类,能够匹配任意非系统异常。那么这段代码就可以写成下面这样: + +```python +try: + s = input('please enter two numbers separated by comma: ') + num1 = int(s.split(',')[0].strip()) + num2 = int(s.split(',')[1].strip()) + ... +except ValueError as err: + print('Value Error: {}'.format(err)) +except IndexError as err: + print('Index Error: {}'.format(err)) +except Exception as err: + print('Other error: {}'.format(err)) + +print('continue') +... +``` + +或者,你也可以在 except 后面省略异常类型,这表示与任意异常相匹配(包括系统异常等): + +```python +try: + s = input('please enter two numbers separated by comma: ') + num1 = int(s.split(',')[0].strip()) + num2 = int(s.split(',')[1].strip()) + ... +except ValueError as err: + print('Value Error: {}'.format(err)) +except IndexError as err: + print('Index Error: {}'.format(err)) +except: + print('Other error') + +print('continue') +... +``` + +需要注意,当程序中存在多个 except block 时,最多只有一个 except block 会被执行。换句话说,如果多个 except 声明的异常类型都与实际相匹配,那么只有最前面的 except block 会被执行,其他则被忽略。 + +异常处理中,还有一个很常见的用法是 finally,经常和 try、except 放在一起来用。无论发生什么情况,finally block 中的语句都会被执行,哪怕前面的 try 和 excep block 中使用了 return 语句。 + +一个常见的应用场景,便是文件的读取: + +```python +import sys +try: + f = open('file.txt', 'r') + .... # some data processing +except OSError as err: + print('OS error: {}'.format(err)) +except: + print('Unexpected error:', sys.exc_info()[0]) +finally: + f.close() +``` + +这段代码中,try block 尝试读取 `file.txt` 这个文件,并对其中的数据进行一系列的处理,到最后,无论是读取成功还是读取失败,程序都会执行 finally 中的语句——关闭这个文件流,确保文件的完整性。因此,在 finally 中,我们通常会放一些**无论如何都要执行**的语句。 + +值得一提的是,对于文件的读取,我们也常常使用 with open,你也许在前面的例子中已经看到过,with open 会在最后自动关闭文件,让语句更加简洁。 + +## 3. 用户自定义异常 + +前面的例子里充斥了很多 Python 内置的异常类型,你可能会问,我可以创建自己的异常类型吗? + +答案是肯定是,Python 当然允许我们这么做。下面这个例子,我们创建了自定义的异常类型 MyInputError,定义并实现了初始化函数和 str 函数(直接 print 时调用): + +```python +class MyInputError(Exception): + """Exception raised when there're errors in input""" + def __init__(self, value): # 自定义异常类型的初始化 + self.value = value + def __str__(self): # 自定义异常类型的string表达形式 + return ("{} is invalid input".format(repr(self.value))) + +try: + raise MyInputError(1) # 抛出MyInputError这个异常 +except MyInputError as err: + print('error: {}'.format(err)) +``` + +如果你执行上述代码块并输出,便会得到下面的结果: + +```python +error: 1 is invalid input +``` + +实际工作中,如果内置的异常类型无法满足我们的需求,或者为了让异常更加详细、可读,想增加一些异常类型的其他功能,我们可以自定义所需异常类型。不过,大多数情况下,Python 内置的异常类型就足够好了。 + +## 4. 异常的使用场景与注意点 + +学完了前面的基础知识,接下来我们着重谈一下,异常的使用场景与注意点。 + +通常来说,在程序中,如果我们不确定某段代码能否成功执行,往往这个地方就需要使用异常处理。除了上述文件读取的例子,我可以再举一个例子来说明。 + +大型社交网站的后台,需要针对用户发送的请求返回相应记录。用户记录往往储存在 `key-value` 结构的数据库中,每次有请求过来后,我们拿到用户的 ID,并用 ID 查询数据库中此人的记录,就能返回相应的结果。 + +而数据库返回的原始数据,往往是 json string 的形式,这就需要我们首先对 json string 进行 decode(解码),你可能很容易想到下面的方法: + +```python +import json +raw_data = queryDB(uid) # 根据用户的id,返回相应的信息 +data = json.loads(raw_data) +``` + +这样的代码是不是就足够了呢? + +要知道,在 `json.loads()` 函数中,输入的字符串如果不符合其规范,那么便无法解码,就会抛出异常,因此加上异常处理十分必要。 + +```python +try: + data = json.loads(raw_data) + .... +except JSONDecodeError as err: + print('JSONDecodeError: {}'.format(err)) +``` + +不过,有一点切记,我们不能走向另一个极端——滥用异常处理。 + +比如,当你想要查找字典中某个键对应的值时,绝不能写成下面这种形式: + +```python +d = {'name': 'jason', 'age': 20} +try: + value = d['dob'] + ... +except KeyError as err: + print('KeyError: {}'.format(err)) +``` + +诚然,这样的代码并没有 bug,但是让人看了摸不着头脑,也显得很冗余。如果你的代码中充斥着这种写法,无疑对阅读、协作来说都是障碍。因此,对于 `flow-control`(流程控制)的代码逻辑,我们一般不用异常处理。 + +字典这个例子,写成下面这样就很好。 + +```python +if 'dob' in d: + value = d['dob'] + ... +``` + +## 5. 总结 + +这节课, 我们一起学习了 Python 的异常处理及其使用场景,你需要重点掌握下面几点。 + +- 异常,通常是指程序运行的过程中遇到了错误,终止并退出。我们通常使用 try except 语句去处理异常,这样程序就不会被终止,仍能继续执行。 +- 处理异常时,如果有必须执行的语句,比如文件打开后必须关闭等等,则可以放在 finally block 中。 +- 异常处理,通常用在你不确定某段代码能否成功执行,也无法轻易判断的情况下,比如数据库的连接、读取等等。正常的 flow-control 逻辑,不要使用异常处理,直接用条件语句解决就可以了。 + +## 6. 思考题 + +最后,给你留一个思考题。在异常处理时,如果 try block 中有多处抛出异常,需要我们使用多个 try except block 吗?以数据库的连接、读取为例,下面两种写法,你觉得哪种更好呢? + +### 6.1 第一种: + +```python +try: + db = DB.connect('') # 可能会抛出异常 + raw_data = DB.queryData('') # 可能会抛出异常 +except (DBConnectionError, DBQueryDataError) err: + print('Error: {}'.format(err)) +``` + +### 6.2 第二种: + +```python +try: + db = DB.connect('') # 可能会抛出异常 + try: + raw_data = DB.queryData('') + except DBQueryDataError as err: + print('DB query data error: {}'.format(err)) +except DBConnectionError as err: + print('DB connection error: {}'.format(err)) +``` + +欢迎在留言区写下你的答案,还有你今天学习的心得和疑惑,也欢迎你把这篇文章分享给你的同事、朋友。 + +## 7. 评论 + +老师,看到异常这一讲,忽然想起了一个问题,一直困扰着我 + +```python +e = 1 +try: + 1 / 0 +except ZeroDivisionError as e: + pass print(e) # NameError: name 'e' is not defined +``` + +这里为什么会显示 e 没有被定义呢? + +作者回复: 你可以阅读官方文档:https://docs.python.org/3/reference/compound_stmts.html#the-try-statement + +"When an exception has been assigned using as target, it is cleared at the end of the except clause." + +比如下面这个 code block: + +```python +except E as N: + foo +``` + +就等于 + +```python +except E as N: + try: + foo + finally: + del N +``` + +因此你例子中的 e 最后被 delete 了,所以会抛出 NameError + +--- + +第一种写法更加简洁,易于阅读。而且 except 后面的错误类型先抛出数据库连接错误,之后才抛出查询错误,实现的异常处理和第二种一样。 + +作者回复: 正解 + +--- + +想请问老师,在 facebook 里面开发,对于异常处理有什么规范需要遵循吗?自定义异常、抛异常、捕获异常,粒度一般怎么把控呢? 与此相应的,我对日志输出也有同样的疑问,希望老师能结合您在大公司里的实战经验多讲讲。 + +作者回复: 我会在最后一章里对大公司开发的规范,流程做一个详细的介绍。通常来说,异常能用内置的 exception 就用,如果需要自定义就自定义,看实际的需求。一般来说异常抛出,我们都会对其进行 Log(一般每 1000 次log一次),输出到 real time 的 table 和 dashboard 里,这样有利于之后的分析和改进。 + +--- + +欢迎关注我公众号:AI悦创,有更多更好玩的等你发现! + +::: details 公众号:AI悦创【二维码】 + +![](/gzh.jpg) + +::: + +::: info AI悦创·编程一对一 + +AI悦创·推出辅导班啦,包括「Python 语言辅导班、C++ 辅导班、java 辅导班、算法/数据结构辅导班、少儿编程、pygame 游戏开发」,全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践等。当然,还有线下线上摄影课程、Photoshop、Premiere 一对一教学、QQ、微信在线,随时响应!微信:Jiabcdefh + +C++ 信息奥赛题解,长期更新!长期招收一对一中小学信息奥赛集训,莆田、厦门地区有机会线下上门,其他地区线上。微信:Jiabcdefh + +方法一:[QQ](http://wpa.qq.com/msgrd?v=3&uin=1432803776&site=qq&menu=yes) + +方法二:微信:Jiabcdefh + +::: + +![](/zsxq.jpg) + diff --git a/src/Python/Python-core-technology-and-practice/09.assets/8c7b36a39f71260b16132ad9615fb31b.jpg b/src/Python/Python-core-technology-and-practice/09.assets/8c7b36a39f71260b16132ad9615fb31b.jpg new file mode 100755 index 00000000000..5042de90909 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/09.assets/8c7b36a39f71260b16132ad9615fb31b.jpg differ diff --git a/src/Python/Python-core-technology-and-practice/09.md b/src/Python/Python-core-technology-and-practice/09.md new file mode 100755 index 00000000000..54077997295 --- /dev/null +++ b/src/Python/Python-core-technology-and-practice/09.md @@ -0,0 +1,578 @@ +--- +title: 09-不可或缺的自定义函数 +icon: python +date: 2023-02-07 09:05:46 +author: AI悦创 +isOriginal: true +category: Python 进阶 +tag: + - Python 进阶 +sticky: false +star: false +article: true +timeline: true +image: false +navbar: true +sidebarIcon: true +headerDepth: 5 +comment: true +lastUpdated: true +editLink: false +backToTop: true +toc: true +--- + +![](./09.assets/8c7b36a39f71260b16132ad9615fb31b.jpg) + +你好,我是悦创。 + +实际工作生活中,我曾见到不少初学者编写的 Python 程序,他们长达几百行的代码中,却没有一个函数,通通按顺序堆到一块儿,不仅让人读起来费时费力,往往也是错误连连。 + +一个规范的值得借鉴的 Python 程序,除非代码量很少(比如 10 行、20 行以下),基本都应该由多个函数组成,这样的代码才更加模块化、规范化。 + +函数是 Python 程序中不可或缺的一部分。事实上,在前面的学习中,我们已经用到了很多 Python 的内置函数,比如 `sorted()` 表示对一个集合序列排序,`len()` 表示返回一个集合序列的长度大小等等。这节课,我们主要来学习 Python 的自定义函数。 + +## 1. 函数基础 + +那么,到底什么是函数,如何在 Python 程序中定义函数呢? + +说白了,函数就是为了实现某一功能的代码段,只要写好以后,就可以重复利用。我们先来看下面一个简单的例子: + +```python +def my_func(message): + print('Got a message: {}'.format(message)) + +# 调用函数 my_func() +my_func('Hello World') +# 输出 +Got a message: Hello World +``` + +其中: + +- def 是函数的声明; +- `my_func` 是函数的名称; +- 括号里面的 message 则是函数的参数; +- 而 print 那行则是函数的主体部分,可以执行相应的语句; +- 在函数最后,你可以返回调用结果(return 或 yield),也可以不返回。 + +总结一下,大概是下面的这种形式: + +```python +def name(param1, param2, ..., paramN): + statements + return/yield value # optional +``` + +和其他需要编译的语言(比如 C 语言)不一样的是,def 是可执行语句,这意味着函数直到被调用前,都是不存在的。当程序调用函数时,def 语句才会创建一个新的函数对象,并赋予其名字。 + +我们一起来看几个例子,加深你对函数的印象: + +```python +def my_sum(a, b): + return a + b + +result = my_sum(3, 5) +print(result) + +# 输出 +8 +``` + +这里,我们定义了 `my_sum()` 这个函数,它有两个参数 a 和 b,作用是相加;随后,调用 `my_sum()` 函数,分别把 3 和 5 赋于 a 和 b;最后,返回其相加的值,赋于变量 result,并输出得到 8。 + +再来看一个例子: + +```python +def find_largest_element(l): + if not isinstance(l, list): + print('input is not type of list') + return + if len(l) == 0: + print('empty input') + return + largest_element = l[0] + for item in l: + if item > largest_element: + largest_element = item + print('largest element is: {}'.format(largest_element)) + +find_largest_element([8, 1,-3, 2, 0]) + +# 输出 +largest element is: 8 +``` + +这个例子中,我们定义了函数 `find_largest_element`,作用是遍历输入的列表,找出最大的值并打印。因此,当我们调用它,并传递列表 `[8, 1, -3, 2, 0]` 作为参数时,程序就会输出 `largest element is: 8`。 + +需要注意,主程序调用函数时,必须保证这个函数此前已经定义过,不然就会报错,比如: + +```python +my_func('hello world') +def my_func(message): + print('Got a message: {}'.format(message)) + +# 输出 +NameError: name 'my_func' is not defined +``` + +但是,如果我们在函数内部调用其他函数,函数间哪个声明在前、哪个在后就无所谓,因为 def 是可执行语句,函数在调用之前都不存在,我们只需保证调用时,所需的函数都已经声明定义: + +```python +def my_func(message): + my_sub_func(message) # 调用 my_sub_func() 在其声明之前不影响程序执行 + +def my_sub_func(message): + print('Got a message: {}'.format(message)) + +my_func('hello world') + +# 输出 +Got a message: hello world +``` + +另外,Python 函数的参数可以设定默认值,比如下面这样的写法: + +```python +def func(param = 0): + ... +``` + +这样,在调用函数 `func()` 时,如果参数 param 没有传入,则参数默认为 0;而如果传入了参数 param,其就会覆盖默认值。 + +前面说过,Python 和其他语言相比的一大特点是,Python 是 dynamically typed 的,可以接受任何数据类型(整型,浮点,字符串等等)。对函数参数来说,这一点同样适用。比如还是刚刚的 `my_sum` 函数,我们也可以把列表作为参数来传递,表示将两个列表相连接: + +```python +print(my_sum([1, 2], [3, 4])) + +# 输出 +[1, 2, 3, 4] +``` + +同样,也可以把字符串作为参数传递,表示字符串的合并拼接: + +```python +print(my_sum('hello ', 'world')) + +# 输出 +hello world +``` + +当然,如果两个参数的数据类型不同,比如一个是列表、一个是字符串,两者无法相加,那就会报错: + +```python +print(my_sum([1, 2], 'hello')) +TypeError: can only concatenate list (not "str") to list +``` + +我们可以看到,Python 不用考虑输入的数据类型,而是将其交给具体的代码去判断执行,同样的一个函数(比如这边的相加函数 `my_sum()`),可以同时应用在整型、列表、字符串等等的操作中。 + +在编程语言中,我们把这种行为称为**多态**。这也是 Python 和其他语言,比如 Java、C 等很大的一个不同点。当然,Python 这种方便的特性,在实际使用中也会带来诸多问题。因此,必要时请你在开头加上数据的类型检查。 + +Python 函数的另一大特性,是 Python 支持函数的嵌套。所谓的函数嵌套,就是指函数里面又有函数,比如: + +```python +def f1(): + print('hello') + def f2(): + print('world') + f2() +f1() + +# 输出 +hello +world +``` + +这里函数 `f1()` 的内部,又定义了函数 `f2()`。在调用函数 `f1()` 时,会先打印字符串 `'hello'`,然后 `f1()` 内部再调用 `f2()`,打印字符串 `'world'`。你也许会问,为什么需要函数嵌套?这样做有什么好处呢? + +其实,函数的嵌套,主要有下面两个方面的作用。 + +第一,函数的嵌套能够保证内部函数的隐私。内部函数只能被外部函数所调用和访问,不会暴露在全局作用域,因此,如果你的函数内部有一些隐私数据(比如数据库的用户、密码等),不想暴露在外,那你就可以使用函数的的嵌套,将其封装在内部函数中,只通过外部函数来访问。比如: + +```python +def connect_DB(): + def get_DB_configuration(): + ... + return host, username, password + conn = connector.connect(get_DB_configuration()) + return conn +``` + +这里的函数 `get_DB_configuration`,便是内部函数,它无法在` connect_DB()` 函数以外被单独调用。也就是说,下面这样的外部直接调用是错误的: + +```python +get_DB_configuration() + +# 输出 +NameError: name 'get_DB_configuration' is not defined +``` + +我们只能通过调用外部函数 `connect_DB()` 来访问它,这样一来,程序的安全性便有了很大的提高。 + +第二,合理的使用函数嵌套,能够提高程序的运行效率。我们来看下面这个例子: + +```python +def factorial(input): + # validation check + if not isinstance(input, int): + raise Exception('input must be an integer.') + if input < 0: + raise Exception('input must be greater or equal to 0' ) + ... + + def inner_factorial(input): + if input <= 1: + return 1 + return input * inner_factorial(input-1) + return inner_factorial(input) + + +print(factorial(5)) +``` + +这里,我们使用递归的方式计算一个数的阶乘。因为在计算之前,需要检查输入是否合法,所以我写成了函数嵌套的形式,这样一来,输入是否合法就只用检查一次。而如果我们不使用函数嵌套,那么每调用一次递归便会检查一次,这是没有必要的,也会降低程序的运行效率。 + +实际工作中,如果你遇到相似的情况,输入检查不是很快,还会耗费一定的资源,那么运用函数的嵌套就十分必要了。 + +## 2. 函数变量作用域 + +Python 函数中变量的作用域和其他语言类似。如果变量是在函数内部定义的,就称为局部变量,只在函数内部有效。一旦函数执行完毕,局部变量就会被回收,无法访问,比如下面的例子: + +```python +def read_text_from_file(file_path): + with open(file_path) as file: + ... +``` + +我们在函数内部定义了 file 这个变量,这个变量只在 `read_text_from_file` 这个函数里有效,在函数外部则无法访问。 + +相对应的,全局变量则是定义在整个文件层次上的,比如下面这段代码: + +```python +MIN_VALUE = 1 +MAX_VALUE = 10 +def validation_check(value): + if value < MIN_VALUE or value > MAX_VALUE: + raise Exception('validation check fails') +``` + +这里的 `MIN_VALUE` 和 `MAX_VALUE` 就是全局变量,可以在文件内的任何地方被访问,当然在函数内部也是可以的。不过,我们**不能在函数内部随意改变全局变量的值**。比如,下面的写法就是错误的: + +```python +MIN_VALUE = 1 +MAX_VALUE = 10 +def validation_check(value): + ... + MIN_VALUE += 1 + ... +validation_check(5) +``` + +如果运行这段代码,程序便会报错: + +```python +UnboundLocalError: local variable 'MIN_VALUE' referenced before assignment +``` + +这是因为,Python 的解释器会默认函数内部的变量为局部变量,但是又发现局部变量 `MIN_VALUE` 并没有声明,因此就无法执行相关操作。所以,如果我们一定要在函数内部改变全局变量的值,就必须加上 global 这个声明: + +```python +MIN_VALUE = 1 +MAX_VALUE = 10 +def validation_check(value): + global MIN_VALUE + ... + MIN_VALUE += 1 + ... +validation_check(5) +``` + +这里的 global 关键字,并不表示重新创建了一个全局变量 `MIN_VALUE`,而是告诉 Python 解释器,函数内部的变量 `MIN_VALUE`,就是之前定义的全局变量,并不是新的全局变量,也不是局部变量。这样,程序就可以在函数内部访问全局变量,并修改它的值了。 + +另外,如果遇到函数内部局部变量和全局变量同名的情况,那么在函数内部,局部变量会覆盖全局变量,比如下面这种: + +```python +MIN_VALUE = 1 +MAX_VALUE = 10 +def validation_check(value): + MIN_VALUE = 3 + ... +``` + +在函数 `validation_check()` 内部,我们定义了和全局变量同名的局部变量 `MIN_VALUE`,那么,`MIN_VALUE` 在函数内部的值,就应该是 3 而不是 1 了。 + +类似的,对于嵌套函数来说,内部函数可以访问外部函数定义的变量,但是无法修改,若要修改,必须加上 nonlocal 这个关键字: + +```python +def outer(): + x = "local" + def inner(): + nonlocal x # nonlocal 关键字表示这里的 x 就是外部函数 outer 定义的变量 x + x = 'nonlocal' + print("inner:", x) + inner() + print("outer:", x) +outer() +# 输出 +inner: nonlocal +outer: nonlocal +``` + +如果不加上 nonlocal 这个关键字,而内部函数的变量又和外部函数变量同名,那么同样的,内部函数变量会覆盖外部函数的变量。 + +```python +def outer(): + x = "local" + def inner(): + x = 'nonlocal' # 这里的 x 是 inner 这个函数的局部变量 + print("inner:", x) + inner() + print("outer:", x) +outer() +# 输出 +inner: nonlocal +outer: local +``` + +## 3. 闭包 + +这节课的第三个重点,我想再来介绍一下闭包(closure)。闭包其实和刚刚讲的嵌套函数类似,不同的是,这里外部函数返回的是一个函数,而不是一个具体的值。返回的函数通常赋于一个变量,这个变量可以在后面被继续执行调用。 + +举个例子你就更容易理解了。比如,我们想计算一个数的 n 次幂,用闭包可以写成下面的代码: + +```python +def nth_power(exponent): + def exponent_of(base): + return base ** exponent + return exponent_of # 返回值是 exponent_of 函数 + +square = nth_power(2) # 计算一个数的平方 +cube = nth_power(3) # 计算一个数的立方 +square +# 输出 +.exponent(base)> + +cube +# 输出 +.exponent(base)> + +print(square(2)) # 计算2的平方 +print(cube(2)) # 计算2的立方 +# 输出 +4 # 2^2 +8 # 2^3 +``` + +这里外部函数 `nth_power()` 返回值,是函数 `exponent_of()`,而不是一个具体的数值。需要注意的是,在执行完 `square = nth_power(2)` 和 `cube = nth_power(3)` 后,外部函数 `nth_power()` 的参数 exponent,仍然会被内部函数 `exponent_of()` 记住。这样,之后我们调用 `square(2)` 或者 `cube(2)` 时,程序就能顺利地输出结果,而不会报错说参数 exponent 没有定义了。 + +看到这里,你也许会思考,为什么要闭包呢?上面的程序,我也可以写成下面的形式啊! + +```python +def nth_power_rewrite(base, exponent): + return base ** exponent +``` + +确实可以,不过,要知道,使用闭包的一个原因,是让程序变得更简洁易读。设想一下,比如你需要计算很多个数的平方,那么你觉得写成下面哪一种形式更好呢? + +```python +# 不使用闭包 +res1 = nth_power_rewrite(base1, 2) +res2 = nth_power_rewrite(base2, 2) +res3 = nth_power_rewrite(base3, 2) +... + +# 使用闭包 +square = nth_power(2) +res1 = square(base1) +res2 = square(base2) +res3 = square(base3) +... +``` + +显然是第二种,是不是?首先直观来看,第二种形式,让你每次调用函数都可以少输入一个参数,表达更为简洁。 + +其次,和上面讲到的嵌套函数优点类似,函数开头需要做一些额外工作,而你又需要多次调用这个函数时,将那些额外工作的代码放在外部函数,就可以减少多次调用导致的不必要的开销,提高程序的运行效率。 + +另外还有一点,我们后面会讲到,闭包常常和装饰器(decorator)一起使用。 + +## 4. 总结 + +这节课,我们一起学习了 Python 函数的概念及其应用,有这么几点你需要注意: + +1. Python 中函数的参数可以接受任意的数据类型,使用起来需要注意,必要时请在函数开头加入数据类型的检查; +2. 和其他语言不同,Python 中函数的参数可以设定默认值; +3. 嵌套函数的使用,能保证数据的隐私性,提高程序运行效率; +4. 合理地使用闭包,则可以简化程序的复杂度,提高可读性。 + +## 5. 思考题 + +最后给你留一道思考题。在实际的学习工作中,你遇到过哪些使用嵌套函数或者是闭包的例子呢?欢迎在下方留言,与我讨论,也欢迎你把这篇文章分享给你的同事、朋友。 + + + +## 6. 评论 + +### 6.1 Geek_7777 + +闭包,调用 `square(2)`,这个参数2为啥能传给 base,不太懂请教下 + +> 作者回复: 因为这里 `square=nth_power(2)` 已经是一个函数了,这个函数有两个参数,已经接受了 exponent,因此如果你调用了`square(2)`,这个参数会再传给 base,这样就能输出结果了 + + + +### 6.2 Vincent + +关于嵌套函数:“我们只能通过调用外部函数 `connect_DB()` 来访问它,这样一来,程序的安全性便有了很大的提高。” 这个怎么就安全了呢?这个安全指的是什么安全呢? + +> 作者回复: 数据库的用户名密码等一些信息不会暴露在外部的 API 中 + +### 6.3 Gfcn + +没想到连闭包都讲,真的是干货满满,32个赞 + +> 作者回复: 谢谢支持 + +### 6.4 路伴友行 + +顺便我想多问一句,在 Python 里是不推荐使用递归的,是因为 Python 没有对递归做优化,那使用 yield from 来代替递归会不会好些呢? 其实我上一个例子就是一个尝试,我之前只尝试过打印栈信息,只看到有 2 层,就是不清楚有些其他什么弊端。 + +> 作者回复: 你说的没错 + + + +### 6.5 乔克 + +老师,您说的“函数的调用和声明哪个在前哪个在后是无所谓的。”请问这句话怎么理解呢? 如下是会报异常 NameError: name 'f' is not defined: + +```python +f() +def f(): + print("test") +``` + +> 作者回复: 文中已经更新了。可能之前表达的不准确,意思是主程序调用函数时,必须保证这个函数此前已经定义过,但是,如果我们在函数内部调用其他函数,函数间哪个声明在前、哪个在后就无所谓,因为 def 是可执行语句,函数调用前都不存在,我们只需保证调用时,所需的函数都已经声明定义 + +### 6.6. third + +```python +1.Python中...是啥意思?发现在代码中运行没有错误。也没有百度到 + +2.#不是说全局变量可以在文件的任意地方都可以被访问吗?,我试了下,去掉x的赋值,就可以访问了。这是什么原因呢? +#x=10 +def outer(): + print(x) + x = "local" + def inner(): + nonlocal x # nonlocal 关键字表示这里的 x 就是外部函数 outer 定义的变量 x + x = 'nonlocal' + print("inner:", x) + inner() + print("outer:", x) +x=10 +outer() + +#报错Traceback (most recent call last): +# File "D:/软件/Python/Lib/idlelib/新p/学习分析/写着玩.py", line 11, in +# outer() +# File "D:/软件/Python/Lib/idlelib/新p/学习分析/写着玩.py", line 2, in outer +# print(x) +# UnboundLocalError: local variable 'x' referenced before assignment +``` + +> 作者回复: 1. 我只是用‘...’表示省略 2. 全局变量在任何地方都可以访问,但是访问之前你必须得定义赋值他啊 + +### 6.7 SCAR + +老师函数嵌套的作用二的例子,如果是在大量的调用函数时,可能还是分开检查和递归比较好,因为嵌套内函数是函数的一个 local 变量,在大量调用函数的时候,local 变量是不断产生和销毁的,这会非常费时间,它可能会反噬掉一次类型检查节省下来的时间。看下面我贴出的计算1百万次 100 阶乘的时间,所以还是要根据具体情况来定,当然大部分时候函数不会这么大量调用。 + +```python +def factorial(input): + # validation check + if not isinstance(input, int): + raise Exception('input must be an integer.') + if input < 0: + raise Exception('input must be greater or equal to 0' ) + ... + + def inner_factorial(input): + if input <= 1: + return 1 + return input * inner_factorial(input-1) + return inner_factorial(input) + +def factorial_1(input): + # validation check + if not isinstance(input, int): + raise Exception('input must be an integer.') + if input < 0: + raise Exception('input must be greater or equal to 0' ) + +def inner_factorial_1(input): + if input <= 1: + return 1 + return input*inner_factorial_1(input-1) + +%%time +for i in range(1000000): + factorial(100) +CPU times: user 21.6 s, sys: 11.6 ms, total: 21.6 s +Wall time: 21.7 s + + +%%time +for i in range(1000000): + factorial_1(100) + inner_factorial_1(100) +CPU times: user 19.7 s, sys: 12 ms, total: 19.7 s +Wall time: 19.7 s +``` + +> 作者回复: 这个case by case,需要注意的是有些时候一些 validation check 的 cost 很高,比如机器学习里面我们会对训练数据(>= 1000 million 的样本)做一些统计等等 + + + +### 6.8 MickeyW + +python 里的闭包也会跟 javaScript 里的闭包一样,有内存得不到释放的问题么? + +> 作者回复: 有可能,stackoverflow上有相关的讨论:[https://stackoverflow.com/questions/2017381/is-it-possible-to-have-an-actual-memory-leak-in-python-because-of-your-code](https://stackoverflow.com/questions/2017381/is-it-possible-to-have-an-actual-memory-leak-in-python-because-of-your-code) + +### 6.9 rogerr + +连接数据库的密码信息虽然在嵌套的函数里,但对于脚本来说还是明文的 + +> 作者回复: 我这里只是举例说明。实际会做 hash,或者通过 token之类的其他方式访问 + + + +### 6.10 徐辰伟 + +文章中说函数的声明和调用哪个在前,哪个在后都无所谓。可是实际试了下先调用再声明会报错? + +> 作者回复: 文中已经更新了。可能之前表达的不准确,意思是主程序调用函数时,必须保证这个函数此前已经定义过,但是,如果我们在函数内部调用其他函数,函数间哪个声明在前、哪个在后就无所谓,因为 def 是可执行语句,函数调用前都不存在,我们只需保证调用时,所需的函数都已经声明定义 + + + +欢迎关注我公众号:AI悦创,有更多更好玩的等你发现! + +::: details 公众号:AI悦创【二维码】 + +![](/gzh.jpg) + +::: + +::: info AI悦创·编程一对一 + +AI悦创·推出辅导班啦,包括「Python 语言辅导班、C++ 辅导班、java 辅导班、算法/数据结构辅导班、少儿编程、pygame 游戏开发」,全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践等。当然,还有线下线上摄影课程、Photoshop、Premiere 一对一教学、QQ、微信在线,随时响应!微信:Jiabcdefh + +C++ 信息奥赛题解,长期更新!长期招收一对一中小学信息奥赛集训,莆田、厦门地区有机会线下上门,其他地区线上。微信:Jiabcdefh + +方法一:[QQ](http://wpa.qq.com/msgrd?v=3&uin=1432803776&site=qq&menu=yes) + +方法二:微信:Jiabcdefh + +::: + +![](/zsxq.jpg) + diff --git a/src/Python/Python-core-technology-and-practice/10.assets/58154038eb26ff83d72f993821002b0f.jpg b/src/Python/Python-core-technology-and-practice/10.assets/58154038eb26ff83d72f993821002b0f.jpg new file mode 100755 index 00000000000..049a6cb8406 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/10.assets/58154038eb26ff83d72f993821002b0f.jpg differ diff --git a/src/Python/Python-core-technology-and-practice/10.md b/src/Python/Python-core-technology-and-practice/10.md new file mode 100755 index 00000000000..8c0fbb7850d --- /dev/null +++ b/src/Python/Python-core-technology-and-practice/10.md @@ -0,0 +1,319 @@ +--- +title: 10-简约不简单的匿名函数 +icon: python +date: 2023-02-04 14:08:34 +author: AI悦创 +isOriginal: true +category: Python 进阶 +tag: + - Python 进阶 +sticky: false +star: false +article: true +timeline: true +image: false +navbar: true +sidebarIcon: true +headerDepth: 5 +comment: true +lastUpdated: true +editLink: false +backToTop: true +toc: true +--- + +![](./10.assets/58154038eb26ff83d72f993821002b0f.jpg) + +你好,我是悦创。 + +上一节,我们一起学习了 Python 中的“常规”函数,用途十分广泛。不过,除了常规函数,你应该也会在代码中见到一些“非常规”函数,它们往往很简短,就一行,并且有个很酷炫的名字——lambda,没错,这就是匿名函数。 + +匿名函数在实际工作中同样举足轻重,正确地运用匿名函数,能让我们的代码更简洁、易读。这节课,我们继续 Python 的函数之旅,一起来学习这个简约而不简单的匿名函数。 + +## 1. 匿名函数基础 + +首先,什么是匿名函数呢?以下是匿名函数的格式: + +```python +lambda argument1, argument2,... argumentN : expression +``` + +我们可以看到,匿名函数的关键字是 lambda,之后是一系列的参数,然后用冒号隔开,最后则是由这些参数组成的表达式。我们通过几个例子看一下它的用法: + +```python +square = lambda x: x**2 +square(3) + +9 +``` + +这里的匿名函数只输入一个参数 x,输出则是输入 x 的平方。因此当输入是 3 时,输出便是 9。如果把这个匿名函数写成常规函数的形式,则是下面这样: + +```python +def square(x): + return x**2 +square(3) + +9 +``` + +可以看到,匿名函数 lambda 和常规函数一样,返回的都是一个函数对象(function object),它们的用法也极其相似,不过还是有下面几点区别。 + +**第一,lambda 是一个表达式(expression),并不是一个语句(statement)。** + +- 所谓的表达式,就是用一系列“公式”去表达一个东西,比如 `x + 2`、 `x**2`等等; +- 而所谓的语句,则一定是完成了某些功能,比如赋值语句 `x = 1` 完成了赋值,print 语句 `print(x)` 完成了打印,条件语句 `if x < 0:` 完成了选择功能等等。 + +因此,lambda 可以用在一些常规函数 def 不能用的地方,比如,lambda 可以用在列表内部,而常规函数却不能: + +```python +[(lambda x: x*x)(x) for x in range(10)] +# 输出 +[0, 1, 4, 9, 16, 25, 36, 49, 64, 81] +``` + +再比如,lambda 可以被用作某些函数的参数,而常规函数 def 也不能: + +```python +l = [(1, 20), (3, 0), (9, 10), (2, -1)] +l.sort(key=lambda x: x[1]) # 按列表中元组的第 二个元素排序 +print(l) +# 输出 +[(2, -1), (3, 0), (9, 10), (1, 20)] +``` + +常规函数 def 必须通过其函数名被调用,因此必须首先被定义。但是作为一个表达式的 lambda,返回的函数对象就不需要名字了。 + +**第二,lambda 的主体是只有一行的简单表达式,并不能扩展成一个多行的代码块。** + +这其实是出于设计的考虑。Python 之所以发明 lambda,就是为了让它和常规函数各司其职:lambda 专注于简单的任务,而常规函数则负责更复杂的多行逻辑。关于这点,Python 之父 Guido van Rossum 曾发了一篇[文章](https://www.artima.com/weblogs/viewpost.jsp?thread=147358)解释,你有兴趣的话可以自己阅读。 + +## 2. 为什么要使用匿名函数? + +理论上来说,Python 中有匿名函数的地方,都可以被替换成等价的其他表达形式。一个 Python 程序是可以不用任何匿名函数的。不过,在一些情况下,使用匿名函数 lambda,可以帮助我们大大简化代码的复杂度,提高代码的可读性。 + +通常,我们用函数的目的无非是这么几点: + +1. 减少代码的重复性; +2. 模块化代码。 + +对于第一点,如果你的程序在不同地方包含了相同的代码,那么我们就会把这部分相同的代码写成一个函数,并为它取一个名字,方便在相对应的不同地方调用。 + +对于第二点,如果你的一块儿代码是为了实现一个功能,但内容非常多,写在一起降低了代码的可读性,那么通常我们也会把这部分代码单独写成一个函数,然后加以调用。 + +不过,再试想一下这样的情况。你需要一个函数,但它非常简短,只需要一行就能完成;同时它在程序中只被调用一次而已。那么请问,你还需要像常规函数一样,给它一个定义和名字吗? + +答案当然是否定的。这种情况下,函数就可以是匿名的,你只需要在适当的地方定义并使用,就能让匿名函数发挥作用了。 + +举个例子,如果你想对一个列表中的所有元素做平方操作,而这个操作在你的程序中只需要进行一次,用 lambda 函数可以表示成下面这样: + +```python +squared = map(lambda x: x**2, [1, 2, 3, 4, 5]) +``` + +如果用常规函数,则表示为这几行代码: + +```python +def square(x): + return x**2 + +squared = map(square, [1, 2, 3, 4, 5]) +``` + +这里我简单解释一下。函数 `map(function, iterable)` 的第一个参数是函数对象,第二个参数是一个可以遍历的集合,它表示对 iterable 的每一个元素,都运用 function 这个函数。两者一对比,我们很明显地发现,lambda 函数让代码更加简洁明了。 + +再举一个例子,在 Python 的 Tkinter GUI 应用中,我们想实现这样一个简单的功能:创建显示一个按钮,每当用户点击时,就打印出一段文字。如果使用 lambda 函数可以表示成下面这样: + +```python +from tkinter import Button, mainloop +button = Button( + text='This is a button', + command=lambda: print('being pressed')) # 点击时调用 lambda 函数 +button.pack() +mainloop() +``` + +而如果我们用常规函数 def,那么需要写更多的代码: + +```python +from tkinter import Button, mainloop + +def print_message(): + print('being pressed') + +button = Button( + text='This is a button', + command=print_message) # 点击时调用lambda函数 +button.pack() +mainloop() +``` + +显然,运用匿名函数的代码简洁很多,也更加符合 Python 的编程习惯。 + +## 3. Python 函数式编程 + +最后,我们一起来看一下,Python 的函数式编程特性,这与我们今天所讲的匿名函数 lambda,有着密切的联系。 + +所谓函数式编程,是指代码中每一块都是不可变的(immutable),都由纯函数(pure function)的形式组成。这里的纯函数,是指函数本身相互独立、互不影响,对于相同的输入,总会有相同的输出,没有任何副作用。 + +举个很简单的例子,比如对于一个列表,我想让列表中的元素值都变为原来的两倍,我们可以写成下面的形式: + +```python +def multiply_2(l): + for index in range(0, len(l)): + l[index] *= 2 + return l +``` + +这段代码就不是一个纯函数的形式,因为列表中元素的值被改变了,如果我多次调用 `multiply_2()` 这个函数,那么每次得到的结果都不一样。要想让它成为一个纯函数的形式,就得写成下面这种形式,重新创建一个新的列表并返回。 + +```python +def multiply_2_pure(l): + new_list = [] + for item in l: + new_list.append(item * 2) + return new_list +``` + +函数式编程的优点,主要在于其纯函数和不可变的特性使程序更加健壮,易于调试(debug)和测试;缺点主要在于限制多,难写。当然,Python 不同于一些语言(比如 Scala),它并不是一门函数式编程语言,不过,Python 也提供了一些函数式编程的特性,值得我们了解和学习。 + +Python 主要提供了这么几个函数:`map()`、`filter()` 和 `reduce()`,通常结合匿名函数 lambda 一起使用。这些都是你需要掌握的东西,接下来我逐一介绍。 + +首先是 `map(function, iterable)` 函数,前面的例子提到过,它表示,对 iterable 中的每个元素,都运用 function 这个函数,最后返回一个新的可遍历的集合。比如刚才列表的例子,要对列表中的每个元素乘以 2,那么用 map 就可以表示为下面这样: + +```python +l = [1, 2, 3, 4, 5] +new_list = map(lambda x: x * 2, l) # [2, 4, 6, 8, 10] +``` + +我们可以以 `map()` 函数为例,看一下 Python 提供的函数式编程接口的性能。还是同样的列表例子,它还可以用 for 循环和 list comprehension(目前没有统一中文叫法,你也可以直译为列表理解等)实现,我们来比较一下它们的速度: + +```python +python3 -mtimeit -s'xs=range(1000000)' 'map(lambda x: x*2, xs)' +2000000 loops, best of 5: 171 nsec per loop + +python3 -mtimeit -s'xs=range(1000000)' '[x * 2 for x in xs]' +5 loops, best of 5: 62.9 msec per loop + +python3 -mtimeit -s'xs=range(1000000)' 'l = []' 'for i in xs: l.append(i * 2)' +5 loops, best of 5: 92.7 msec per loop +``` + +你可以看到,`map()` 是最快的。因为 `map()` 函数直接由 C 语言写的,运行时不需要通过 Python 解释器间接调用,并且内部做了诸多优化,所以运行速度最快。 + +接下来来看 `filter(function, iterable)` 函数,它和 map 函数类似,function 同样表示一个函数对象。`filter()` 函数表示对 iterable 中的每个元素,都使用 function 判断,并返回 True 或者 False,最后将返回 True 的元素组成一个新的可遍历的集合。 + +举个例子,比如我要返回一个列表中的所有偶数,可以写成下面这样: + +```python +l = [1, 2, 3, 4, 5] +new_list = filter(lambda x: x % 2 == 0, l) # [2, 4] +``` + +最后我们来看 `reduce(function, iterable)` 函数,它通常用来对一个集合做一些累积操作。 + +function 同样是一个函数对象,规定它有两个参数,表示对 iterable 中的每个元素以及上一次调用后的结果,运用 function 进行计算,所以最后返回的是一个单独的数值。 + +举个例子,我想要计算某个列表元素的乘积,就可以用 `reduce()` 函数来表示: + +```python +l = [1, 2, 3, 4, 5] +product = reduce(lambda x, y: x * y, l) # 1*2*3*4*5 = 120 +``` + +当然,类似的,`filter()` 和 `reduce()` 的功能,也可以用 for 循环或者 list comprehension 来实现。 + +通常来说,在我们想对集合中的元素进行一些操作时,如果操作非常简单,比如相加、累积这种,那么我们优先考虑 `map()`、`filter()`、`reduce()` 这类或者 list comprehension 的形式。至于这两种方式的选择: + +- 在数据量非常多的情况下,比如机器学习的应用,那我们一般更倾向于函数式编程的表示,因为效率更高; +- 在数据量不多的情况下,并且你想要程序更加 Pythonic 的话,那么 list comprehension 也不失为一个好选择。 + +不过,如果你要对集合中的元素,做一些比较复杂的操作,那么,考虑到代码的可读性,我们通常会使用 for 循环,这样更加清晰明了。 + +## 4. 总结 + +这节课,我们一起学习了 Python 中的匿名函数 lambda,它的主要用途是减少代码的复杂度。需要注意的是 lambda 是一个表达式,并不是一个语句;它只能写成一行的表达形式,语法上并不支持多行。匿名函数通常的使用场景是:程序中需要使用一个函数完成一个简单的功能,并且该函数只调用一次。 + +其次,我们也入门了 Python 的函数式编程,主要了解了常见的 `map()`,`fiilter()` 和 `reduce()` 三个函数,并比较了它们与其他形式(for 循环,comprehension)的性能,显然,它们的性能效率是最优的。 + +## 5. 思考题 + +最后,我想给你留下两道思考题。 + +第一问:如果让你对一个字典,根据值进行由高到底的排序,该怎么做呢?以下面这段代码为例,你可以思考一下。 + +```python +d = {'mike': 10, 'lucy': 2, 'ben': 30} +``` + +第二问:在实际工作学习中,你遇到过哪些使用匿名函数的场景呢? + +欢迎在留言区写下你的答案想法,与我讨论,也欢迎你把这篇文章分享给你的同事、朋友。 + +## 6. 评论 + +### 6.1 Hoo-Ah + +- 第一问:`sorted(d.items(), key=lambda x: x[1], reverse=True)`; + +- 第二问:最开始接触 lambda 匿名函数的时候觉得蛮不理解的,觉得这个函数没有灵魂,用完一次就扔掉。后来在和高阶函数、列表生成式搭配使用以及一些小功能的使用上觉得很好用,这样代码即简洁又易于阅读。 +- 注:匿名函数最难理解的地方就是要传入的参数是一个可迭代的对象,lambda 内部会调用可迭代对象的 `__next__` 方法取值当作参数传入 lambda 函数冒号前面的值,然后把表达式计算的结果进行返回。 + +> 作者回复: 你说的对。关于迭代器生成器后面会讲到,所以这篇文章没有提及。 + +--- + +### 6.2 lmingzhi + +`python3 -mtimeit -s'xs=range(1000000)' 'map(lambda x: x*2, xs)` 这个地方 map 生成的是生成器,与后面的 2 个做比较感觉不大合适,是否更改为测试 `list(map(lambda x: x*2, xs))` 更恰当? + +> 作者回复: 实际情况中,Map返回的对象依然可以直接遍历,所以直接比较从实用的角度上来说也是可以的,Map在Python3中变为Lazy了以后,速度得到了很大的提升。当然,如果以返回的类型一致为标准,你的建议也是可以的 + +--- + +### 6.3 向南 + +```python +sorted(d.items(), key=lambda x: x[1], reverse=True) +``` + +lambda 函数在数据清洗的时候,作用很大 + +> 作者回复: 必须的 + + + +下面代码,`print(new_list)` 报错,而改成 `print(list(new_list))` 可以输出所有偶数,python3.8 版本 和之前版本 不同? + +```python +l = [1, 2, 3, 4, 5] + +new_list = filter(lambda x: x % 2 == 0, l) # [2, 4] +``` + +> 作者回复: `filter/map`都是返回一个 iterator,我注释写成 [2, 4] 只是为了更直观的表示这个函数的功能哈 + + + +欢迎关注我公众号:AI悦创,有更多更好玩的等你发现! + +::: details 公众号:AI悦创【二维码】 + +![](/gzh.jpg) + +::: + +::: info AI悦创·编程一对一 + +AI悦创·推出辅导班啦,包括「Python 语言辅导班、C++ 辅导班、java 辅导班、算法/数据结构辅导班、少儿编程、pygame 游戏开发」,全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践等。当然,还有线下线上摄影课程、Photoshop、Premiere 一对一教学、QQ、微信在线,随时响应!微信:Jiabcdefh + +C++ 信息奥赛题解,长期更新!长期招收一对一中小学信息奥赛集训,莆田、厦门地区有机会线下上门,其他地区线上。微信:Jiabcdefh + +方法一:[QQ](http://wpa.qq.com/msgrd?v=3&uin=1432803776&site=qq&menu=yes) + +方法二:微信:Jiabcdefh + +::: + +![](/zsxq.jpg) + diff --git a/src/Python/Python-core-technology-and-practice/11.assets/067b42c7b534922ff89ec677c310db1c.jpg b/src/Python/Python-core-technology-and-practice/11.assets/067b42c7b534922ff89ec677c310db1c.jpg new file mode 100644 index 00000000000..46c32a1f048 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/11.assets/067b42c7b534922ff89ec677c310db1c.jpg differ diff --git a/src/Python/Python-core-technology-and-practice/11.md b/src/Python/Python-core-technology-and-practice/11.md new file mode 100755 index 00000000000..79c313a2351 --- /dev/null +++ b/src/Python/Python-core-technology-and-practice/11.md @@ -0,0 +1,461 @@ +--- +title: 11-面向对象(上):从生活中的类比说起 +icon: python +date: 2023-04-29 06:09:50 +author: AI悦创 +isOriginal: true +category: Python 进阶 +tag: + - Python 进阶 +sticky: false +star: false +article: true +timeline: true +image: false +navbar: true +sidebarIcon: true +headerDepth: 5 +comment: true +lastUpdated: true +editLink: false +backToTop: true +toc: true +--- + +![](./11.assets/067b42c7b534922ff89ec677c310db1c.jpg) + +你好,我是悦创。 + +很多朋友最开始学编程的时候,是从 C++ 或者 JAVA 语言入手的。他们好不容易磕磕绊绊地搞懂了最基本的数据类型、赋值判断和循环,却又迎面撞上了 OOP (object oriented programming) 的大墙,一头扎进公有私有保护、多重继承、多态派生、纯函数、抽象类、友元函数等一堆专有名词的汪洋大海中找不到彼岸,于是就放弃了进阶之路。 + +相比之下,Python 是一门相对友好的语言,它在创立之初就鼓励命令交互式的轻量级编程。理论上,Python 的命令式语言是[图灵完备](https://zh.wikipedia.org/wiki/%E5%9C%96%E9%9D%88%E5%AE%8C%E5%82%99%E6%80%A7)的, 也就是说命令式语言,理论上可以做到其他任何语言能够做到的所有的事情,甚至进一步,仅仅依靠汇编语言的 MOV 指令,就能实现[图灵完备编程](http://stedolan.net/research/mov.pdf)。 + +那么为什么不这样做呢?其实,“上古时代”的程序员就是这么做的,可是随着程序功能复杂性的逐步提升,以及需求的不断迭代,很多老旧的代码修改起来麻烦无比,牵一发而动全身,根本无法迭代和维护,甚至只能推倒重来,这也是很多古老的代码被称为“屎山”的原因。 + +传统的命令式语言有无数重复性代码,虽然函数的诞生减缓了许多重复性,但随着计算机的发展,只有函数依然不够,需要把更加抽象的概念引入计算机才能缓解(而不是解决)这个问题,于是 OOP 应运而生。 + +Python 在 1989 年被一位程序员打发时间创立之后,一步步攻城掠地飞速发展,从最基础的脚本程序,到后来可以编写系统程序、大型工程、数据科学运算、人工智能,早已脱离了当初的设计,因此一些其他语言的优秀设计之处依然需要引入。我们必须花费一定的代价掌握面向对象编程,才能跨越学习道路中的瓶颈期,走向下一步。 + +接下来,我将用两节课来讲解面向对象编程,从基础到实战。第一讲,我将带你快速但清晰地疏通最基础的知识,确保你能够迅速领略面向对象的基本思想;第二讲,我们从零开始写一个搜索引擎,将前面所学知识融会贯通。 + +这些内容可能和你以往看到的所有教程都不太一样,我会尽可能从一个初学者的角度来审视这些难点。同时我们面向实战、面向工程,不求大而全,但是对最核心的思想会有足够的勾勒。我可以保证内容清晰易懂,但想要真正掌握,仍要求你能用心去阅读和思考。真正的提高,永远要靠自己才能做到。 + +## 1. 对象,你找到了吗? + +我们先来学习,面向对象编程中最基本的概念。 + +为了方便你理解其中的抽象概念,我先打个比方带你感受一下。生物课上,我们学过“界门纲目科属种”的概念,核心思想是科学家们根据各种动植物、微生物的相似之处,将其分化为不同的类型方便研究。生活中我们也是如此,习惯对身边的事物进行分类: + +- 猫和狗都是动物; +- 直线和圆都是平面几何的图形; +- 《哈利波特》和《冰与火之歌》(即《权力的游戏》)都是小说。 + +自然,同一类事物便会有着相似的特性: + +- 动物会动; +- 平面图形有面积和周长; +- 小说也都有相应的作者和大致情节等各种元素。 + +那回到我们的 Python 上,又对应哪些内容呢?这里,我们先来看一段最基本的 Python 面向对象的应用代码,不要被它的长度吓到,你无需立刻看懂所有代码,跟着节奏来,我会一点点为你剖析。 + +```python +class Document(): + def __init__(self, title, author, context): + print('init function called') + self.title = title + self.author = author + self.__context = context # __开头的属性是私有属性 + + def get_context_length(self): + return len(self.__context) + + def intercept_context(self, length): + self.__context = self.__context[:length] + +harry_potter_book = Document('Harry Potter', 'J. K. Rowling', '... Forever Do not believe any thing is capable of thinking independently ...') + +print(harry_potter_book.title) +print(harry_potter_book.author) +print(harry_potter_book.get_context_length()) + +harry_potter_book.intercept_context(10) + +print(harry_potter_book.get_context_length()) + +print(harry_potter_book.__context) + +########## 输出 ########## + +init function called +Harry Potter +J. K. Rowling +77 +10 + +--------------------------------------------------------------------------- +AttributeError Traceback (most recent call last) + in () + 22 print(harry_potter_book.get_context_length()) + 23 +---> 24 print(harry_potter_book.__context) + +AttributeError: 'Document' object has no attribute '__context' +``` + +参照着这段代码,我先来简单解释几个概念。 + +- 类:一群有着相似性的事物的集合,这里对应 Python 的 class。 +- 对象:集合中的一个事物,这里对应由 class 生成的某一个 object,比如代码中的 `harry_potter_book`。 +- 属性:对象的某个静态特征,比如上述代码中的 title、author 和 `__context`。 +- 函数:对象的某个动态能力,比如上述代码中的 `intercept_context()` 函数。 + +当然,这样的说法既不严谨,也不充分,但如果你对面向对象编程完全不了解,它们可以让你迅速有一个直观的了解。 + +这里我想多说两句。回想起当年参加数学竞赛时,我曾和一个大佬交流数学的学习,我清楚记得我们对数学有着相似的观点:很多数学概念非常抽象,如果纯粹从数理逻辑而不是更高的角度去解题,很容易陷入僵局;而具体、直观的想象和类比,才是迅速打开数学大门的钥匙。虽然这些想象和类比不严谨也不充分,很多时候甚至是错误或者异想天开的,但它们确实能帮我们快速找到正确的大门。 + +就像很多人都有过的一个疑惑,“学霸是怎样想到这个答案的?”。德国数学家克莱因曾说过,“推进数学的,主要是那些有卓越直觉的人,而不是以严格的证明方法见长的人。”编程世界同样如此,如果你不满足于只做一个 CRUD“码农”,而是想成为一个优秀的工程师,那就一定要积极锻炼直觉思考和快速类比的能力,尤其是在找不到 bug 的时候。这才是编程学习中能给人最快进步的方法和路径。 + +言归正传,继续回到我们的主题,还是通过刚刚那段代码,我想再给类下一个更为严谨的定义。 + +**类,一群有着相同属性和函数的对象的集合。** + +虽然有循环论证之嫌(lol),但是反复强调,还是希望你能对面向对象的最基础的思想,有更真实的了解。清楚记住这一点后,接下来,我们来具体解读刚刚这段代码。为了方便你的阅读学习,我把它重新放在了这段文字下方。 + +```python +class Document(): + def __init__(self, title, author, context): + print('init function called') + self.title = title + self.author = author + self.__context = context # __开头的属性是私有属性 + + def get_context_length(self): + return len(self.__context) + + def intercept_context(self, length): + self.__context = self.__context[:length] + +harry_potter_book = Document('Harry Potter', 'J. K. Rowling', '... Forever Do not believe any thing is capable of thinking independently ...') + +print(harry_potter_book.title) +print(harry_potter_book.author) +print(harry_potter_book.get_context_length()) + +harry_potter_book.intercept_context(10) + +print(harry_potter_book.get_context_length()) + +print(harry_potter_book.__context) + +########## 输出 ########## + +init function called +Harry Potter +J. K. Rowling +77 +10 + +--------------------------------------------------------------------------- +AttributeError Traceback (most recent call last) + in () + 22 print(harry_potter_book.get_context_length()) + 23 +---> 24 print(harry_potter_book.__context) + +AttributeError: 'Document' object has no attribute '__context' +``` + +可以看到,`class Document` 定义了 Document 类,再往下能看到它有三个函数,这三个函数即为 Document 类的三个函数。 + +其中,`__init__` 表示构造函数,意即一个对象生成时会被自动调用的函数。我们能看到, `harry_potter_book = Document(...)` 这一行代码被执行的时候,`'init function called'` 字符串会被打印出来。而 `get_context_length()` 和 `intercept_context()` 则为类的普通函数,我们调用它们来对对象的属性做一些事情。 + +`class Document` 还有三个属性,`title`、`author` 和 `__context` 分别表示标题、作者和内容,通过构造函数传入。这里代码很直观,我们可以看到, `intercept_context` 能修改对象 `harry_potter_book` 的 `__context` 属性。 + +这里唯一需要强调的一点是,如果一个属性以 `__` (注意,此处有两个 `_`) 开头,我们就默认这个属性是私有属性。私有属性,是指不希望在类的函数之外的地方被访问和修改的属性。所以,你可以看到,`title` 和 `author` 能够很自由地被打印出来,但是 `print(harry_potter_book.__context)` 就会报错。 + +## 2. 老师,能不能再给力点? + +掌握了最基础的概念,其实我们已经能做很多很多的事情了。不过,在工程实践中,随着复杂度继续提升,你可能会想到一些问题: + +- 如何在一个类中定义一些常量,每个对象都可以方便访问这些常量而不用重新构造? +- 如果一个函数不涉及到访问修改这个类的属性,而放到类外面有点不恰当,怎么做才能更优雅呢? +- 既然类是一群相似的对象的集合,那么可不可以是一群相似的类的集合呢? + +前两个问题很好解决,不过,它们涉及到一些常用的代码规范,这里我放了一段代码示例。同样的,你无需一口气读完这段代码,跟着我的节奏慢慢学习即可。 + +```python +class Document(): + + WELCOME_STR = 'Welcome! The context for this book is {}.' + + def __init__(self, title, author, context): + print('init function called') + self.title = title + self.author = author + self.__context = context + + # 类函数 + @classmethod + def create_empty_book(cls, title, author): + return cls(title=title, author=author, context='nothing') + + # 成员函数 + def get_context_length(self): + return len(self.__context) + + # 静态函数 + @staticmethod + def get_welcome(context): + return Document.WELCOME_STR.format(context) + + +empty_book = Document.create_empty_book('What Every Man Thinks About Apart from Sex', 'Professor Sheridan Simove') + + +print(empty_book.get_context_length()) +print(empty_book.get_welcome('indeed nothing')) + +########## 输出 ########## + +init function called +7 +Welcome! The context for this book is indeed nothing. +``` + +第一个问题,在 Python 的类里,你只需要和函数并列地声明并赋值,就可以实现这一点,例如这段代码中的 `WELCOME_STR`。一种很常规的做法,是用全大写来表示常量,因此我们可以在类中使用 `self.WELCOME_STR` ,或者在类外使用 `Entity.WELCOME_STR` ,来表达这个字符串。 + +而针对第二个问题,我们提出了类函数、成员函数和静态函数三个概念。它们其实很好理解,前两者产生的影响是动态的,能够访问或者修改对象的属性;而静态函数则与类没有什么关联,最明显的特征便是,静态函数的第一个参数没有任何特殊性。 + +具体来看这几种函数。一般而言,静态函数可以用来做一些简单独立的任务,既方便测试,也能优化代码结构。静态函数还可以通过在函数前一行加上 `@staticmethod` 来表示,代码中也有相应的示例。这其实使用了装饰器的概念,我们会在后面的章节中详细讲解。 + +而类函数的第一个参数一般为 cls,表示必须传一个类进来。类函数最常用的功能是实现不同的 `__init__` 构造函数,比如上文代码中,我们使用 `create_empty_book` 类函数,来创造新的书籍对象,其 context 一定为 `'nothing'`。这样的代码,就比你直接构造要清晰一些。类似的,类函数需要装饰器 `@classmethod` 来声明。 + +成员函数则是我们最正常的类的函数,它不需要任何装饰器声明,第一个参数 self 代表当前对象的引用,可以通过此函数,来实现想要的查询 / 修改类的属性等功能。 + +## 3. 继承,是每个富二代的梦想 + +接下来,我们来看第三个问题,既然类是一群相似的对象的集合,那么可不可以是一群相似的类的集合呢? + +答案是,当然可以。只要抽象得好,类可以描述成任何事物的集合。当然你要小心、严谨地去定义它,不然一不小心就会引起[第三次数学危机](https://en.wikipedia.org/wiki/Russell%27s_paradox) XD。 + +类的继承,顾名思义,指的是一个类既拥有另一个类的特征,也拥有不同于另一个类的独特特征。在这里的第一个类叫做子类,另一个叫做父类,特征其实就是类的属性和函数。 + +```python +class Entity(): + def __init__(self, object_type): + print('parent class init called') + self.object_type = object_type + + def get_context_length(self): + raise Exception('get_context_length not implemented') + + def print_title(self): + print(self.title) + +class Document(Entity): + def __init__(self, title, author, context): + print('Document class init called') + Entity.__init__(self, 'document') + self.title = title + self.author = author + self.__context = context + + def get_context_length(self): + return len(self.__context) + +class Video(Entity): + def __init__(self, title, author, video_length): + print('Video class init called') + Entity.__init__(self, 'video') + self.title = title + self.author = author + self.__video_length = video_length + + def get_context_length(self): + return self.__video_length + +harry_potter_book = Document('Harry Potter(Book)', 'J. K. Rowling', '... Forever Do not believe any thing is capable of thinking independently ...') +harry_potter_movie = Video('Harry Potter(Movie)', 'J. K. Rowling', 120) + +print(harry_potter_book.object_type) +print(harry_potter_movie.object_type) + +harry_potter_book.print_title() +harry_potter_movie.print_title() + +print(harry_potter_book.get_context_length()) +print(harry_potter_movie.get_context_length()) + +########## 输出 ########## + +Document class init called +parent class init called +Video class init called +parent class init called +document +video +Harry Potter(Book) +Harry Potter(Movie) +77 +120 +``` + +我们同样结合代码来学习这些概念。在这段代码中,Document 和 Video 它们有相似的地方,都有相应的标题、作者和内容等属性。我们可以从中抽象出一个叫做 Entity 的类,来作为它俩的父类。 + +首先需要注意的是构造函数。每个类都有构造函数,继承类在生成对象的时候,是不会自动调用父类的构造函数的,因此你必须在 `__init__()` 函数中显式调用父类的构造函数。它们的执行顺序是 子类的构造函数 -> 父类的构造函数。 + +其次需要注意父类 `get_context_length()` 函数。如果使用 Entity 直接生成对象,调用 `get_context_length()` 函数,就会 raise error 中断程序的执行。这其实是一种很好的写法,叫做函数重写,可以使子类必须重新写一遍 `get_context_length()` 函数,来覆盖掉原有函数。 + +最后需要注意到 `print_title()` 函数,这个函数定义在父类中,但是子类的对象可以毫无阻力地使用它来打印 title,这也就体现了继承的优势:减少重复的代码,降低系统的熵值(即复杂度)。 + +到这里,你对继承就有了比较详细的了解了,面向对象编程也可以说已经入门了。当然,如果你想达到更高的层次,大量练习编程,学习更多的细节知识,都是必不可少的。 + +最后,我想再为你扩展一下抽象函数和抽象类,我同样会用一段代码来辅助讲解。 + +```python +from abc import ABCMeta, abstractmethod + +class Entity(metaclass=ABCMeta): + @abstractmethod + def get_title(self): + pass + + @abstractmethod + def set_title(self, title): + pass + +class Document(Entity): + def get_title(self): + return self.title + + def set_title(self, title): + self.title = title + +document = Document() +document.set_title('Harry Potter') +print(document.get_title()) + +entity = Entity() + +########## 输出 ########## + +Harry Potter + +--------------------------------------------------------------------------- +TypeError Traceback (most recent call last) + in () + 21 print(document.get_title()) + 22 +---> 23 entity = Entity() + 24 entity.set_title('Test') + +TypeError: Can't instantiate abstract class Entity with abstract methods get_title, set_title +``` + +你应该发现了,Entity 本身是没有什么用的,只需拿来定义 Document 和 Video 的一些基本元素就够了。不过,万一你不小心生成 Entity 的对象该怎么办呢?为了防止这样的手误,必须要介绍一下抽象类。 + +抽象类是一种特殊的类,它生下来就是作为父类存在的,一旦对象化就会报错。同样,抽象函数定义在抽象类之中,子类必须重写该函数才能使用。相应的抽象函数,则是使用装饰器 `@abstractmethod` 来表示。 + +我们可以看到,代码中 `entity = Entity()` 直接报错,只有通过 Document 继承 Entity 才能正常使用。 + +这其实正是软件工程中一个很重要的概念,定义接口。大型工程往往需要很多人合作开发,比如在 Facebook 中,在 idea 提出之后,开发组和产品组首先会召开产品设计会,PM(Product Manager,产品经理) 写出产品需求文档,然后迭代;TL(Team Leader,项目经理)编写开发文档,开发文档中会定义不同模块的大致功能和接口、每个模块之间如何协作、单元测试和集成测试、线上灰度测试、监测和日志等等一系列开发流程。 + +抽象类就是这么一种存在,它是一种自上而下的设计风范,你只需要用少量的代码描述清楚要做的事情,定义好接口,然后就可以交给不同开发人员去开发和对接。 + +## 4. 总结 + +到目前为止,我们一直在强调一件事情:面向对象编程是软件工程中重要的思想。正如动态规划是算法中的重要思想一样,它不是某一种非常具体的技术,而是一种综合能力的体现,是将大型工程解耦化、模块化的重要方法。在实践中要多想,尤其是抽象地想,才能更快掌握这个技巧。 + +回顾一下今天的内容,我希望你能自己回答下面两个问题,作为今天内容的总结,写在留言区里。 + +第一个问题,面向对象编程四要素是什么?它们的关系又是什么? + +第二个问题,讲了这么久的继承,继承究竟是什么呢?你能用三个字表达出来吗? + +> 这里不开玩笑,Facebook 很多 Launch Doc (上线文档)中要求用五个单词总结你的文档,因为你的文档不仅仅是你的团队要看,往上走甚至会到 VP 或者 CTO 那里,你需要言简意赅,让他们快速理解你想要表达的意思。 + +## 5. 思考题 + +最后,再给你留一道思考题。既然你能通过继承一个类,来获得父类的函数和属性,那么你能继承两个吗?答案自是能的,这就叫做多重继承。那么问题来了。 + +我们使用单一继承的时候,构造函数的执行顺序很好确定,即子类 -> 父类 -> 爷类 ->… 的链式关系。不过,多重继承的时候呢?比如下面这个例子。 + +```python + --->B--- +A- -->D + --->C--- +``` + +这种继承方式,叫做菱形继承,BC 继承了 A,然后 D 继承了 BC,创造一个 D 的对象。那么,构造函数调用顺序又是怎样的呢? + +欢迎在留言区写下你的答案想法,与我讨论,也欢迎你把这篇文章分享给你的同事、朋友。 + +## 6. 思考题答案 + +思考题答案:庄小P 同学的写法很好,非常明确的表明了菱形继承潜在的问题:一个基类的初始化函数可能被调用两次。在一般的工程中,这显然不是我们所希望的。正确的做法应该是使用 super 来召唤父类的构造函数,而且 python 使用一种叫做方法解析顺序的算法(具体实现算法叫做 C3),来保证一个类只会被初始化一次。 + +```python +class A(): + def __init__(self): + print('enter A') + print('leave A') + +class B(A): + def __init__(self): + print('enter B') + super().__init__() + print('leave B') + +class C(A): + def __init__(self): + print('enter C') + super().__init__() + print('leave C') + +class D(B, C): + def __init__(self): + print('enter D') + super().__init__() + print('leave D') + +D() + +enter D +enter B +enter C +enter A +leave A +leave C +leave B +leave D + +``` + + + + + + + +欢迎关注我公众号:AI悦创,有更多更好玩的等你发现! + +::: details 公众号:AI悦创【二维码】 + +![](/gzh.jpg) + +::: + +::: info AI悦创·编程一对一 + +AI悦创·推出辅导班啦,包括「Python 语言辅导班、C++ 辅导班、java 辅导班、算法/数据结构辅导班、少儿编程、pygame 游戏开发」,全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践等。当然,还有线下线上摄影课程、Photoshop、Premiere 一对一教学、QQ、微信在线,随时响应!微信:Jiabcdefh + +C++ 信息奥赛题解,长期更新!长期招收一对一中小学信息奥赛集训,莆田、厦门地区有机会线下上门,其他地区线上。微信:Jiabcdefh + +方法一:[QQ](http://wpa.qq.com/msgrd?v=3&uin=1432803776&site=qq&menu=yes) + +方法二:微信:Jiabcdefh + +::: + +![](/zsxq.jpg) + diff --git a/src/Python/Python-core-technology-and-practice/12.assets/976f82f257e4f86857fe0561316c82df.jpg b/src/Python/Python-core-technology-and-practice/12.assets/976f82f257e4f86857fe0561316c82df.jpg new file mode 100755 index 00000000000..a475cd8c27d Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/12.assets/976f82f257e4f86857fe0561316c82df.jpg differ diff --git a/src/Python/Python-core-technology-and-practice/12.assets/image-20221008110128108.png b/src/Python/Python-core-technology-and-practice/12.assets/image-20221008110128108.png new file mode 100755 index 00000000000..85a08540ecb Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/12.assets/image-20221008110128108.png differ diff --git a/src/Python/Python-core-technology-and-practice/12.md b/src/Python/Python-core-technology-and-practice/12.md new file mode 100755 index 00000000000..8e709eec561 --- /dev/null +++ b/src/Python/Python-core-technology-and-practice/12.md @@ -0,0 +1,492 @@ +--- +title: 12-面向对象(下):如何实现一个搜索引擎? +icon: python +date: 2022-10-02 13:39:18 +author: AI悦创 +isOriginal: true +category: Python 进阶 +tag: + - Python 进阶 +sticky: false +star: false +article: true +timeline: true +image: false +navbar: true +sidebarIcon: true +headerDepth: 5 +comment: true +lastUpdated: true +editLink: false +backToTop: true +toc: true +--- + +![img](./12.assets/976f82f257e4f86857fe0561316c82df.jpg) + +你好,我是悦创。这节课,我们来实现一个 Python 的搜索引擎(search engine)。 + +承接上文,今天这节课的主要目的是,带你模拟敏捷开发过程中的迭代开发流程,巩固面向对象的程序设计思想。 + +我们将从最简单最直接的搜索做起,一步步优化,这其中,我不会涉及到过多的超纲算法,但不可避免会介绍一些现代搜索引擎中的基础概念,例如语料(corpus)、倒序索引(inverted index)等。 + +如果你对这方面本身有些了解,自然可以轻松理解;即使你之前完全没接触过搜索引擎,也不用过分担心,我会力求简洁清晰,降低学习难度。同时,我希望你把更多的精力放在面向对象的建模思路上。 + +## 1. “高大上”的搜索引擎 + +引擎一词尤如其名,听起来非常酷炫。搜索引擎,则是新世纪初期互联网发展最重要的入口之一,依托搜索引擎,中国和美国分别诞生了百度、谷歌等巨型公司。 + +搜索引擎极大地方便了互联网生活,也成为上网必不可少的刚需工具。依托搜索引擎发展起来的互联网广告,则成了硅谷和中国巨头的核心商业模式;而搜索本身,也在持续进步着, Facebook 和微信也一直有意向在自家社交产品架设搜索平台。 + +关于搜索引擎的价值我不必多说了,今天我们主要来看一下搜索引擎的核心构成。 + +听 Google 的朋友说,他们入职培训的时候,有一门课程叫做 The life of a query,内容是讲用户在浏览器中键入一串文字,按下回车后发生了什么。今天我也按照这个思路,来简单介绍下。 + +我们知道,**一个搜索引擎由搜索器、索引器、检索器和用户接口四个部分组成。** + +搜索器,通俗来讲就是我们常提到的爬虫(scrawler),它能在互联网上大量爬取各类网站的内容,送给索引器。索引器拿到网页和内容后,会对内容进行处理,形成索引(index),存储于内部的数据库等待检索。 + +最后的用户接口很好理解,是指网页和 App 前端界面,例如百度和谷歌的搜索页面。用户通过用户接口,向搜索引擎发出询问(query),询问解析后送达检索器;检索器高效检索后,再将结果返回给用户。 + +![](./12.assets/image-20221008110128108.png) + +爬虫知识不是我们今天学习的重点,这里我就不做深入介绍了。我们假设搜索样本存在于本地磁盘上。 + +为了方便,我们只提供五个文件的检索,内容我放在了下面这段代码中: + +```txt +# 1.txt +I have a dream that my four little children will one day live in a nation where they will not be judged by the color of their skin but by the content of their character. I have a dream today. + +# 2.txt +I have a dream that one day down in Alabama, with its vicious racists, . . . one day right there in Alabama little black boys and black girls will be able to join hands with little white boys and white girls as sisters and brothers. I have a dream today. + +# 3.txt +I have a dream that one day every valley shall be exalted, every hill and mountain shall be made low, the rough places will be made plain, and the crooked places will be made straight, and the glory of the Lord shall be revealed, and all flesh shall see it together. + +# 4.txt +This is our hope. . . With this faith we will be able to hew out of the mountain of despair a stone of hope. With this faith we will be able to transform the jangling discords of our nation into a beautiful symphony of brotherhood. With this faith we will be able to work together, to pray together, to struggle together, to go to jail together, to stand up for freedom together, knowing that we will be free one day. . . . + +# 5.txt +And when this happens, and when we allow freedom ring, when we let it ring from every village and every hamlet, from every state and every city, we will be able to speed up that day when all of God's children, black men and white men, Jews and Gentiles, Protestants and Catholics, will be able to join hands and sing in the words of the old Negro spiritual: "Free at last! Free at last! Thank God Almighty, we are free at last!" +``` + +我们先来定义 SearchEngineBase 基类。这里我先给出了具体的代码,你不必着急操作,还是那句话,跟着节奏慢慢学,再难的东西也可以啃得下来。 + +```python +class SearchEngineBase(object): + def __init__(self): + pass + + def add_corpus(self, file_path): + with open(file_path, 'r') as fin: + text = fin.read() + self.process_corpus(file_path, text) + + def process_corpus(self, id, text): + raise Exception('process_corpus not implemented.') + + def search(self, query): + raise Exception('search not implemented.') + +def main(search_engine): + for file_path in ['1.txt', '2.txt', '3.txt', '4.txt', '5.txt']: + search_engine.add_corpus(file_path) + + while True: + query = input() + results = search_engine.search(query) + print('found {} result(s):'.format(len(results))) + for result in results: + print(result) +``` + +SearchEngineBase 可以被继承,继承的类分别代表不同的算法引擎。每一个引擎都应该实现 `process_corpus()` 和 `search()` 两个函数,对应我们刚刚提到的索引器和检索器。`main()` 函数提供搜索器和用户接口,于是一个简单的包装界面就有了。 + +具体来看这段代码,其中, + +- `add_corpus()` 函数负责读取文件内容,将文件路径作为 ID,连同内容一起送到 `process_corpus` 中。 +- `process_corpus` 需要对内容进行处理,然后文件路径为 ID ,将处理后的内容存下来。处理后的内容,就叫做索引(index)。 +- search 则给定一个询问,处理询问,再通过索引检索,然后返回。 + +好,理解这些概念后,接下来,我们实现一个最基本的可以工作的搜索引擎,代码如下: + +```python +class SimpleEngine(SearchEngineBase): + def __init__(self): + super(SimpleEngine, self).__init__() + self.__id_to_texts = {} + + def process_corpus(self, id, text): + self.__id_to_texts[id] = text + + def search(self, query): + results = [] + for id, text in self.__id_to_texts.items(): + if query in text: + results.append(id) + return results + +search_engine = SimpleEngine() +main(search_engine) + + +########## 输出 ########## + + +simple +found 0 result(s): +little +found 2 result(s): +1.txt +2.txt +``` + +你可能很惊讶,只需要短短十来行代码居然就可以了吗? + +没错,正是如此,这段代码我们拆开来看一下: + +SimpleEngine 实现了一个继承 SearchEngineBase 的子类,继承并实现了 `process_corpus` 和 search 接口,同时,也顺手继承了 `add_corpus` 函数(当然你想重写也是可行的),因此我们可以在`main()` 函数中直接调取。 + +在我们新的构造函数中,`self.__id_to_texts = {}` 初始化了自己的私有变量,也就是这个用来存储文件名到文件内容的字典。 + +`process_corpus()` 函数则非常直白地将文件内容插入到字典中。这里注意,ID 需要是唯一的,不然相同 ID 的新内容会覆盖掉旧的内容。 + +search 直接枚举字典,从中找到要搜索的字符串。如果能够找到,则将 ID 放到结果列表中,最后返回。 + +你看,是不是非常简单呢?这个过程始终贯穿着面向对象的思想,这里我为你梳理成了几个问题,你可以自己思考一下,当成是一个小复习。 + +- 现在你对父类子类的构造函数调用顺序和方法应该更清楚了吧? +- 继承的时候,函数是如何重写的? +- 基类是如何充当接口作用的(你可以自行删掉子类中的重写函数,抑或是修改一下函数的参数,看一下会报什么错)? +- 方法和变量之间又如何衔接起来的呢? + +好的,我们重新回到搜索引擎这个话题。 + +相信你也能看得出来,这种实现方式简单,但显然是一种很低效的方式:每次索引后需要占用大量空间,因为索引函数并没有做任何事情;每次检索需要占用大量时间,因为所有索引库的文件都要被重新搜索一遍。如果把语料的信息量视为 n,那么这里的时间复杂度和空间复杂度都应该是 `O(n)` 级别的。 + +而且,还有一个问题:这里的 query 只能是一个词,或者是连起来的几个词。如果你想要搜索多个词,它们又分散在文章的不同位置,我们的简单引擎就无能为力了。 + +这时应该怎么优化呢? + +最直接的一个想法,就是把语料分词,看成一个个的词汇,这样就只需要对每篇文章存储它所有词汇的 set 即可。根据齐夫定律(Zipf’s law,[https://en.wikipedia.org/wiki/Zipf%27s_law](https://en.wikipedia.org/wiki/Zipf%27s_law)),在自然语言的语料库里,一个单词出现的频率与它在频率表里的排名成反比,呈现幂律分布。因此,语料分词的做法可以大大提升我们的存储和搜索效率。 + +那具体该如何实现呢? + +## 2. Bag of Words 和 Inverted Index + +我们先来实现一个名叫 Bag of Words 的搜索模型。请看下面的代码: + +```python +import re + +class BOWEngine(SearchEngineBase): + def __init__(self): + super(BOWEngine, self).__init__() + self.__id_to_words = {} + + def process_corpus(self, id, text): + self.__id_to_words[id] = self.parse_text_to_words(text) + + def search(self, query): + query_words = self.parse_text_to_words(query) + results = [] + for id, words in self.__id_to_words.items(): + if self.query_match(query_words, words): + results.append(id) + return results + + @staticmethod + def query_match(query_words, words): + for query_word in query_words: + if query_word not in words: + return False + return True + + @staticmethod + def parse_text_to_words(text): + # 使用正则表达式去除标点符号和换行符 + text = re.sub(r'[^\w ]', ' ', text) + # 转为小写 + text = text.lower() + # 生成所有单词的列表 + word_list = text.split(' ') + # 去除空白单词 + word_list = filter(None, word_list) + # 返回单词的 set + return set(word_list) + +search_engine = BOWEngine() +main(search_engine) + + +########## 输出 ########## + + +i have a dream +found 3 result(s): +1.txt +2.txt +3.txt +freedom children +found 1 result(s): +5.txt +``` + +你应该发现,代码开始变得稍微复杂些了。 + +这里我们先来理解一个概念,BOW Model,即 [Bag of Words Model](https://en.wikipedia.org/wiki/Bag-of-words_model),中文叫做词袋模型。这是 NLP 领域最常见最简单的模型之一。 + +假设一个文本,不考虑语法、句法、段落,也不考虑词汇出现的顺序,只将这个文本看成这些词汇的集合。于是相应的,我们把 `id_to_texts` 替换成 `id_to_words`,这样就只需要存这些单词,而不是全部文章,也不需要考虑顺序。 + +其中,`process_corpus()` 函数调用类静态函数 `parse_text_to_words`,将文章打碎形成词袋,放入 set 之后再放到字典中。 + +`search()` 函数则稍微复杂一些。这里我们假设,想得到的结果,是所有的搜索关键词都要出现在同一篇文章中。那么,我们需要同样打碎 query 得到一个 set,然后把 set 中的每一个词,和我们的索引中每一篇文章进行核对,看一下要找的词是否在其中。而这个过程由静态函数 `query_match` 负责。 + +你可以回顾一下上节课学到的静态函数,我们看到,这两个函数都是没有状态的,它们不涉及对象的私有变量(没有 self 作为参数),相同的输入能够得到完全相同的输出结果。因此设置为静态,可以方便其他的类来使用。 + +可是,即使这样做,每次查询时依然需要遍历所有 ID,虽然比起 Simple 模型已经节约了大量时间,但是互联网上有上亿个页面,每次都全部遍历的代价还是太大了。到这时,又该如何优化呢? + +你可能想到了,我们每次查询的 query 的单词量不会很多,一般也就几个、最多十几个的样子。那可不可以从这里下手呢? + +再有,词袋模型并不考虑单词间的顺序,但有些人希望单词按顺序出现,或者希望搜索的单词在文中离得近一些,这种情况下词袋模型现任就无能为力了。 + +针对这两点,我们还能做得更好吗?显然是可以的,请看接下来的这段代码。 + +```python +import re + +class BOWInvertedIndexEngine(SearchEngineBase): + def __init__(self): + super(BOWInvertedIndexEngine, self).__init__() + self.inverted_index = {} + + def process_corpus(self, id, text): + words = self.parse_text_to_words(text) + for word in words: + if word not in self.inverted_index: + self.inverted_index[word] = [] + self.inverted_index[word].append(id) + + def search(self, query): + query_words = list(self.parse_text_to_words(query)) + query_words_index = list() + for query_word in query_words: + query_words_index.append(0) + + # 如果某一个查询单词的倒序索引为空,我们就立刻返回 + for query_word in query_words: + if query_word not in self.inverted_index: + return [] + + result = [] + while True: + + # 首先,获得当前状态下所有倒序索引的 index + current_ids = [] + + for idx, query_word in enumerate(query_words): + current_index = query_words_index[idx] + current_inverted_list = self.inverted_index[query_word] + + # 已经遍历到了某一个倒序索引的末尾,结束 search + if current_index >= len(current_inverted_list): + return result + + current_ids.append(current_inverted_list[current_index]) + + # 然后,如果 current_ids 的所有元素都一样,那么表明这个单词在这个元素对应的文档中都出现了 + if all(x == current_ids[0] for x in current_ids): + result.append(current_ids[0]) + query_words_index = [x + 1 for x in query_words_index] + continue + + # 如果不是,我们就把最小的元素加一 + min_val = min(current_ids) + min_val_pos = current_ids.index(min_val) + query_words_index[min_val_pos] += 1 + + @staticmethod + def parse_text_to_words(text): + # 使用正则表达式去除标点符号和换行符 + text = re.sub(r'[^\w ]', ' ', text) + # 转为小写 + text = text.lower() + # 生成所有单词的列表 + word_list = text.split(' ') + # 去除空白单词 + word_list = filter(None, word_list) + # 返回单词的 set + return set(word_list) + +search_engine = BOWInvertedIndexEngine() +main(search_engine) + + +########## 输出 ########## + + +little +found 2 result(s): +1.txt +2.txt +little vicious +found 1 result(s): +2.txt +``` + +首先我要强调一下,**这次的算法并不需要你完全理解**,这里的实现有一些超出了本章知识点。但希望你不要因此退缩,这个例子会告诉你,面向对象编程是如何把算法复杂性隔离开来,而保留接口和其他的代码不变。 + +我们接着来看这段代码。你可以看到,新模型继续使用之前的接口,仍然只在 `__init__()`、`process_corpus()` 和 `search()` 三个函数进行修改。 + +这其实也是大公司里团队协作的一种方式,**在合理的分层设计后,每一层的逻辑只需要处理好分内的事情即可**。在迭代升级我们的搜索引擎内核时, main 函数、用户接口没有任何改变。当然,如果公司招了新的前端工程师,要对用户接口部分进行修改,新人也不需要过分担心后台的事情,只要做好数据交互就可以了。 + +继续看代码,你可能注意到了开头的 Inverted Index。Inverted Index Model,即倒序索引,是非常有名的搜索引擎方法,接下来我简单介绍一下。 + +倒序索引,一如其名,也就是说这次反过来,我们保留的是 word -> id 的字典。于是情况就豁然开朗了,在 search 时,我们只需要把想要的 `query_word` 的几个倒序索引单独拎出来,然后从这几个列表中找共有的元素,那些共有的元素,即 ID,就是我们想要的查询结果。这样,我们就避免了将所有的 index 过一遍的尴尬。 + +`process_corpus` 建立倒序索引。注意,这里的代码都是非常精简的。在工业界领域,需要一个 unique ID 生成器,来对每一篇文章标记上不同的 ID,倒序索引也应该按照这个 `unique_id` 来进行排序。 + +至于 `search()` 函数,你大概了解它做的事情即可。它会根据 `query_words` 拿到所有的倒序索引,如果拿不到,就表示有的 query word 不存在于任何文章中,直接返回空;拿到之后,运行一个“合并 K 个有序数组”的算法,从中拿到我们想要的 ID,并返回。 + +> 注意,这里用到的算法并不是最优的,最优的写法需要用最小堆来存储 index。这是一道有名的 leetcode hard 题,有兴趣请参考:[https://blog.csdn.net/qqxx6661/article/details/77814794](https://blog.csdn.net/qqxx6661/article/details/77814794)) + +遍历的问题解决了,那第二个问题,如果我们想要实现搜索单词按顺序出现,或者希望搜索的单词在文中离得近一些呢? + +我们需要在 Inverted Index 上,对于每篇文章也保留单词的位置信息,这样一来,在合并操作的时候处理一下就可以了。 + +倒序索引我就介绍到这里了,如果你感兴趣可以自行查阅资料。还是那句话,我们的重点是面向对象的抽象,别忘了体会这一思想。 + +## 3. LRU 和多重继承 + +到这一步,终于,你的搜索引擎上线了,有了越来越多的访问量(QPS)。欣喜骄傲的同时,你却发现服务器有些“不堪重负”了。经过一段时间的调研,你发现大量重复性搜索占据了 90% 以上的流量,于是,你想到了一个大杀器——给搜索引擎加一个缓存。 + +所以,最后这部分,我就来讲讲缓存和多重继承的内容。 + +```python +import pylru + +class LRUCache(object): + def __init__(self, size=32): + self.cache = pylru.lrucache(size) + + def has(self, key): + return key in self.cache + + def get(self, key): + return self.cache[key] + + def set(self, key, value): + self.cache[key] = value + +class BOWInvertedIndexEngineWithCache(BOWInvertedIndexEngine, LRUCache): + def __init__(self): + super(BOWInvertedIndexEngineWithCache, self).__init__() + LRUCache.__init__(self) + + def search(self, query): + if self.has(query): + print('cache hit!') + return self.get(query) + + result = super(BOWInvertedIndexEngineWithCache, self).search(query) + self.set(query, result) + + return result + +search_engine = BOWInvertedIndexEngineWithCache() +main(search_engine) + + +########## 输出 ########## + + +little +found 2 result(s): +1.txt +2.txt +little +cache hit! +found 2 result(s): +1.txt +2.txt +``` + +它的代码很简单,LRUCache 定义了一个缓存类,你可以通过继承这个类来调用其方法。LRU 缓存是一种很经典的缓存(同时,LRU 的实现也是硅谷大厂常考的算法面试题,这里为了简单,我直接使用 pylru 这个包),它符合自然界的局部性原理,可以保留最近使用过的对象,而逐渐淘汰掉很久没有被用过的对象。 + +因此,这里的缓存使用起来也很简单,调用 `has()` 函数判断是否在缓存中,如果在,调用 get 函数直接返回结果;如果不在,送入后台计算结果,然后再塞入缓存。 + +我们可以看到,`BOWInvertedIndexEngineWithCache` 类,多重继承了两个类。首先,你需要注意的是构造函数(上节课的思考题,你思考了吗?)。多重继承有两种初始化方法,我们分别来看一下。 + +第一种方法,用下面这行代码,直接初始化该类的第一个父类: + +```python +super(BOWInvertedIndexEngineWithCache, self).__init__() +``` + +不过使用这种方法时,要求继承链的最顶层父类必须要继承 object。 + +第二种方法,对于多重继承,如果有多个构造函数需要调用, 我们必须用传统的方法 `LRUCache.__init__(self)` 。 + +其次,你应该注意,`search()` 函数被子类 `BOWInvertedIndexEngineWithCache` 再次重载,但是我还需要调用 `BOWInvertedIndexEngine` 的 `search()` 函数,这时该怎么办呢?请看下面这行代码: + +```python +super(BOWInvertedIndexEngineWithCache, self).search(query) +``` + +我们可以强行调用被覆盖的父类的函数。 + +这样一来,我们就简洁地实现了缓存,而且还是在不影响 `· 代码的情况下。这部分内容希望你多读几遍,`自己揣摩清楚,通过这个例子多多体会继承的优势。 + +## 4. 总结 + +今天这节课是面向对象的实战应用,相比起前面的理论知识,内容其实不那么友好。不过,若你能静下心来,仔细学习,理清楚整个过程的要点,对你理解面向对象必将有所裨益。比如,你可以根据下面两个问题,来检验今天这节课的收获。 + +- 你能把这节课所有的类的属性和函数抽取出来,自己在纸上画一遍继承关系吗? +- 迭代开发流程是怎样的? + +其实于我而言,通过构造搜索引擎这么一个例子来讲面向对象,也是颇费了一番功夫。这其中虽然涉及一些搜索引擎的专业知识和算法,但篇幅有限,也只能算是抛砖引玉,你若有所收获,我便欣然满足。 + +## 5. 思考题 + +最后给你留一道思考题。私有变量能被继承吗?如果不能,你想继承应该怎么去做呢?欢迎留言与我分享、讨论,也欢迎你把这篇文章分享给你的同事、朋友,一起交流与进步。 + +--- + +John Si 的评论说的很对,如果想要强行访问父类的私有类型,做法是 `self._ParentClass__var`,这是非常不推荐的 hacky method。以下是示范代码: + +```python +class A: + __a = 1 + +class B(A): + pass + +b = B() +print(b._A__a) +``` + + + +欢迎关注我公众号:AI悦创,有更多更好玩的等你发现! + +::: details 公众号:AI悦创【二维码】 + +![](/gzh.jpg) + +::: + +::: info AI悦创·编程一对一 + +AI悦创·推出辅导班啦,包括「Python 语言辅导班、C++ 辅导班、java 辅导班、算法/数据结构辅导班、少儿编程、pygame 游戏开发」,全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践等。当然,还有线下线上摄影课程、Photoshop、Premiere 一对一教学、QQ、微信在线,随时响应!微信:Jiabcdefh + +C++ 信息奥赛题解,长期更新!长期招收一对一中小学信息奥赛集训,莆田、厦门地区有机会线下上门,其他地区线上。微信:Jiabcdefh + +方法一:[QQ](http://wpa.qq.com/msgrd?v=3&uin=1432803776&site=qq&menu=yes) + +方法二:微信:Jiabcdefh + +::: + +![](/zsxq.jpg) + diff --git a/src/Python/Python-core-technology-and-practice/13.assets/cf3ddb72a523f2d2b8557a65487dbe2d.jpg b/src/Python/Python-core-technology-and-practice/13.assets/cf3ddb72a523f2d2b8557a65487dbe2d.jpg new file mode 100644 index 00000000000..71af91e3424 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/13.assets/cf3ddb72a523f2d2b8557a65487dbe2d.jpg differ diff --git a/src/Python/Python-core-technology-and-practice/13.md b/src/Python/Python-core-technology-and-practice/13.md new file mode 100755 index 00000000000..336dc5316fb --- /dev/null +++ b/src/Python/Python-core-technology-and-practice/13.md @@ -0,0 +1,439 @@ +--- +title: 13-搭建积木:Python 模块化 +icon: python +date: 2023-07-31 14:15:20 +author: AI悦创 +isOriginal: true +category: Python 进阶 +tag: + - Python 进阶 +sticky: false +star: false +article: true +timeline: true +image: false +navbar: true +sidebarIcon: true +headerDepth: 5 +comment: true +lastUpdated: true +editLink: false +backToTop: true +toc: true +--- + +![](./13.assets/cf3ddb72a523f2d2b8557a65487dbe2d.jpg) + +你好,我是悦创。 + +这是基础版块的最后一节。到目前为止,你已经掌握了 Python 这一门当代武功的基本招式和套路,走出了新手村,看到了更远的世界,有了和这个世界过过招的冲动。 + +于是,你可能开始尝试写一些不那么简单的系统性工程,或者代码量较大的应用程序。这时候,简单的一个 py 文件已经过于臃肿,无法承担一个重量级软件开发的重任。 + +今天这节课的主要目的,就是化繁为简,将功能模块化、文件化,从而可以像搭积木一样,将不同的功能,组件在大型工程中搭建起来。 + +## 1. 简单模块化 + +说到最简单的模块化方式,你可以把函数、类、常量拆分到不同的文件,把它们放在同一个文件夹,然后使用 `from your_file import function_name, class_name` 的方式调用。之后,这些函数和类就可以在文件内直接使用了。 + +::: code-tabs + +@tab utils.py + +```python +# utils.py + +def get_sum(a, b): + return a + b +``` + +@tab class_utils.py + +```python +# class_utils.py + +class Encoder(object): + def encode(self, s): + return s[::-1] + +class Decoder(object): + def decode(self, s): + return ''.join(reversed(list(s))) +``` + +@tab main.py + +```python +# main.py + +from utils import get_sum +from class_utils import * + +print(get_sum(1, 2)) + +encoder = Encoder() +decoder = Decoder() + +print(encoder.encode('abcde')) +print(decoder.decode('edcba')) + +########## 输出 ########## + +3 +edcba +abcde +``` + +::: + +我们来看这种方式的代码:`get_sum()` 函数定义在 `utils.py`,Encoder 和 Decoder 类则在 `class_utils.py`,我们在 main 函数直接调用 `from import` ,就可以将我们需要的东西 import 过来。 + +非常简单。 + +但是这就足够了吗?当然不,慢慢地,你会发现,所有文件都堆在一个文件夹下也并不是办法。 + +于是,我们试着建一些子文件夹: + +::: code-tabs + +@tab utils/utils.py + +```python +# utils/utils.py + +def get_sum(a, b): + return a + b +``` + +@tab utils/class_utils.py + +```python +# utils/class_utils.py + +class Encoder(object): + def encode(self, s): + return s[::-1] + +class Decoder(object): + def decode(self, s): + return ''.join(reversed(list(s))) +``` + +@tab src/sub_main.py + +```python +# src/sub_main.py + +import sys +sys.path.append("..") + +from utils.class_utils import * + +encoder = Encoder() +decoder = Decoder() + +print(encoder.encode('abcde')) +print(decoder.decode('edcba')) + +########## 输出 ########## + +edcba +abcde +``` + +::: + +而这一次,我们的文件结构是下面这样的: + +```text +. +├── utils +│ ├── utils.py +│ └── class_utils.py +├── src +│ └── sub_main.py +└── main.py +``` + +很容易看出,`main.py` 调用子目录的模块时,只需要使用 `.` 代替 ` /` 来表示子目录,`utils.utils` 表示 utils 子文件夹下的 `utils.py` 模块就行。 + +那如果我们想调用上层目录呢?注意,`sys.path.append("..")` 表示将当前程序所在位置**向上**提了一级,之后就能调用 utils 的模块了。 + +同时要注意一点,import 同一个模块只会被执行一次,这样就可以防止重复导入模块出现问题。当然,良好的编程习惯应该杜绝代码多次导入的情况。**在 Facebook 的编程规范中,除了一些极其特殊的情况,import 必须位于程序的最前端。** + +最后我想再提一下版本区别。你可能在许多教程中看到过这样的要求:我们还需要在模块所在的文件夹新建一个 `__init__.py`,内容可以为空,也可以用来表述包对外暴露的模块接口。不过,事实上,这是 Python 2 的规范。在 Python 3 规范中,`__init__.py` 并不是必须的,很多教程里没提过这一点,或者没讲明白,我希望你还是能注意到这个地方。 + +整体而言,这就是最简单的模块调用方式了。在我初用 Python 时,这种方式已经足够我完成大学期间的项目了,毕竟,很多学校项目的文件数只有个位数,每个文件代码也只有几百行,这种组织方式能帮我顺利完成任务。 + +但是在我去和 Facebook 的朋友对话后,我发现,一个项目组的 workspace 可能有上千个文件,有几十万到几百万行代码。这种调用方式已经完全不够用了,学会新的组织方式迫在眉睫。 + +接下来,我们就系统学习下,模块化的科学组织方式。 + +## 2. 项目模块化 + +我们先来回顾下相对路径和绝对路径的概念。 + +在 Linux 系统中,每个文件都有一个绝对路径,以 `/` 开头,来表示从根目录到叶子节点的路径,例如 `/home/ubuntu/Desktop/my_project/test.py`,这种表示方法叫作绝对路径。 + +另外,对于任意两个文件,我们都有一条通路可以从一个文件走到另一个文件,例如 `/home/ubuntu/Downloads/example.json`。再如,我们从 `test.py` 访问到 `example.json`,需要写成 `'../../Downloads/example.json'`,其中 `..` 表示上一层目录。这种表示方法,叫作相对路径。 + +通常,一个 Python 文件在运行的时候,都会有一个运行时位置,最开始时即为这个文件所在的文件夹。当然,这个运行路径以后可以被改变。运行 `sys.path.append("..")` ,则可以改变当前 Python 解释器的位置。不过,一般而言我并不推荐,固定一个确定路径对大型工程来说是非常必要的。 + +理清楚这些概念后,我们就很容易搞懂,项目中如何设置模块的路径。 + +首先,你会发现,相对位置是一种很不好的选择。因为代码可能会迁移,相对位置会使得重构既不雅观,也易出错。因此,在大型工程中尽可能使用绝对位置是第一要义。对于一个独立的项目,所有的模块的追寻方式,最好从项目的根目录开始追溯,这叫做相对的绝对路径。 + +事实上,在 Facebook 和 Google,整个公司都只有一个代码仓库,全公司的代码都放在这个库里。我当时刚了解到时,对此感到很困惑,也很新奇,难免会有些担心: + +- 这样做似乎会增大项目管理的复杂度吧? +- 是不是也会有不同组代码隐私泄露的风险呢? + +后来,随着工作的深入,我才发现了这种代码仓库独有的几个优点。 + +第一个优点,简化依赖管理。整个公司的代码模块,都可以被你写的任何程序所调用,而你写的库和模块也会被其他人调用。调用的方式,都是从代码的根目录开始索引,也就是前面提到过的相对的绝对路径。这样极大地提高了代码的分享共用能力,你不需要重复造轮子,只需要在写之前,去搜一下有没有已经实现好的包或者框架就可以了。 + +第二个优点,版本统一。不存在使用了一个新模块,却导致一系列函数崩溃的情况;并且所有的升级都需要通过单元测试才可以继续。 + +第三个优点,代码追溯。你可以很容易追溯,一个 API 是从哪里被调用的,它的历史版本是怎样迭代开发,产生变化的。 + +> 如果你有兴趣,可以参考这篇论文:[https://cacm.acm.org/magazines/2016/7/204032-why-google-stores-billions-of-lines-of-code-in-a-single-repository/fulltext](https://cacm.acm.org/magazines/2016/7/204032-why-google-stores-billions-of-lines-of-code-in-a-single-repository/fulltext) + +在做项目的时候,虽然你不可能把全世界的代码都放到一个文件夹下,但是类似模块化的思想还是要有的——那就是以项目的根目录作为最基本的目录,所有的模块调用,都要通过根目录一层层向下索引的方式来 import。 + +明白了这一点后,这次我们使用 PyCharm 来创建一个项目。这个项目结构如下所示: + +::: code-tabs + +@tab path tree + +```text +. +├── proto +│ ├── mat.py +├── utils +│ └── mat_mul.py +└── src + └── main.py +``` + +@tab proto/mat.py + +```python +# proto/mat.py + +class Matrix(object): + def __init__(self, data): + self.data = data + self.n = len(data) + self.m = len(data[0]) +``` + +@tab utils/mat_mul.py + +```python +# utils/mat_mul.py + +from proto.mat import Matrix + +def mat_mul(matrix_1: Matrix, matrix_2: Matrix): + assert matrix_1.m == matrix_2.n + n, m, s = matrix_1.n, matrix_1.m, matrix_2.m + result = [[0 for _ in range(n)] for _ in range(s)] + for i in range(n): + for j in range(s): + for k in range(m): + result[i][k] += matrix_1.data[i][j] * matrix_2.data[j][k] + + return Matrix(result) +``` + +@tab src/main.py + +```python +# src/main.py + +from proto.mat import Matrix +from utils.mat_mul import mat_mul + + +a = Matrix([[1, 2], [3, 4]]) +b = Matrix([[5, 6], [7, 8]]) + +print(mat_mul(a, b).data) + +########## 输出 ########## + +[[19, 22], [43, 50]] +``` + +::: + +这个例子和前面的例子长得很像,但请注意 `utils/mat_mul.py`,你会发现,它 `import Matrix` 的方式是 `from proto.mat` 。这种做法,直接从项目根目录中导入,并依次向下导入模块 `mat.py` 中的 `Matrix`,而不是使用 `..` 导入上一级文件夹。 + +是不是很简单呢?对于接下来的所有项目,你都能直接使用 Pycharm 来构建。把不同模块放在不同子文件夹里,跨模块调用则是从顶层直接索引,一步到位,非常方便。 + +我猜,这时你的好奇心来了。你尝试使用命令行进入 src 文件夹,直接输入 `Python main.py`,报错,找不到 proto。你不甘心,退回到上一级目录,输入`Python src/main.py`,继续报错,找不到 proto。 + +Pycharm 用了什么黑魔法呢? + +实际上,Python 解释器在遇到 import 的时候,它会在一个特定的列表中寻找模块。这个特定的列表,可以用下面的方式拿到: + +```python +import sys + +print(sys.path) + +########## 输出 ########## + +['', '/usr/lib/python36.zip', '/usr/lib/python3.6', '/usr/lib/python3.6/lib-dynload', '/usr/local/lib/python3.6/dist-packages', '/usr/lib/python3/dist-packages'] +``` + +请注意,它的第一项为空。其实,Pycharm 做的一件事,就是将第一项设置为项目根目录的绝对地址。这样,每次你无论怎么运行 `main.py`,import 函数在执行的时候,都会去项目根目录中找相应的包。 + +你说,你想修改下,使得普通的 Python 运行环境也能做到?这里有两种方法可以做到: + +```python +import sys + +sys.path[0] = '/home/ubuntu/workspace/your_projects' +``` + +第一种方法,“大力出奇迹”,我们可以强行修改这个位置,这样,你的 import 接下来肯定就畅通无阻了。但这显然不是最佳解决方案,把绝对路径写到代码里,是我非常不推荐的方式(你可以写到配置文件中,但找配置文件也需要路径寻找,于是就会进入无解的死循环)。 + +第二种方法,是修改 PYTHONHOME。这里我稍微提一下 Python 的 Virtual Environment(虚拟运行环境)。Python 可以通过 Virtualenv 工具,非常方便地创建一个全新的 Python 运行环境。 + +事实上,我们提倡,对于每一个项目来说,最好要有一个独立的运行环境来保持包和模块的纯净性。更深的内容超出了今天的范围,你可以自己查资料了解。 + +回到第二种修改方法上。在一个 Virtual Environment 里,你能找到一个文件叫 activate,在这个文件的末尾,填上下面的内容: + +```bash +export PYTHONPATH="/home/ubuntu/workspace/your_projects" +``` + +这样,每次你通过 activate 激活这个运行时环境的时候,它就会自动将项目的根目录添加到搜索路径中去。 + +## 3. 神奇的 if \_\_name\_\_ == '\_\_main\_\_' + +最后一部分,我们再来讲讲 `if __name__ == '__main__'` ,这个我们经常看到的写法。 + +Python 是脚本语言,和 C++、Java 最大的不同在于,不需要显式提供 `main()` 函数入口。如果你有 C++、Java 等语言经验,应该对 `main() {}` 这样的结构很熟悉吧? + +不过,既然 Python 可以直接写代码,`if __name__ == '__main__'` 这样的写法,除了能让 Python 代码更好看(更像 C++ )外,还有什么好处吗? + +项目结构如下: + +::: code-tabs + +@tab path tree + +```bash +. +├── utils.py +├── utils_with_main.py +├── main.py +└── main_2.py +``` + +@tab utils.py + +```python +# utils.py + +def get_sum(a, b): + return a + b + +print('testing') +print('{} + {} = {}'.format(1, 2, get_sum(1, 2))) +``` + +@tab utils_with_main.py + +```python +# utils_with_main.py + +def get_sum(a, b): + return a + b + +if __name__ == '__main__': + print('testing') + print('{} + {} = {}'.format(1, 2, get_sum(1, 2))) +``` + +@tab main.py + +```python +# main.py + +from utils import get_sum + +print('get_sum: ', get_sum(1, 2)) + +########## 输出 ########## + +testing +1 + 2 = 3 +get_sum: 3 +``` + +@tab main_2.py + +```python +# main_2.py + +from utils_with_main import get_sum + +print('get_sum: ', get_sum(1, 2)) + +########## 输出 ########## + +get_sum_2: 3 +``` + +::: + +看到这个项目结构,你就很清晰了吧。 + +import 在导入文件的时候,会自动把所有暴露在外面的代码全都执行一遍。因此,如果你要把一个东西封装成模块,又想让它可以执行的话,你必须将要执行的代码放在 `if __name__ == '__main__'`下面。 + +为什么呢?其实,`__name__` 作为 Python 的魔术内置参数,本质上是模块对象的一个属性。我们使用 import 语句时,`__name__` 就会被赋值为该模块的名字,自然就不等于 `__main__`了。更深的原理我就不做过多介绍了,你只需要明白这个知识点即可。 + +## 4. 总结 + +今天这节课,我为你讲述了如何使用 Python 来构建模块化和大型工程。这里需要强调几点: + +1. 通过绝对路径和相对路径,我们可以 import 模块; +2. 在大型工程中模块化非常重要,模块的索引要通过绝对路径来做,而绝对路径从程序的根目录开始; +3. 记着巧用 `if __name__ == '__main__'` 来避开 import 时执行。 + +## 5. 思考题 + +最后,我想为你留一道思考题。`from module_name import *` 和 `import module_name` 有什么区别呢?欢迎留言和我分享,也欢迎你把这篇文章分享给你的同事、朋友。 + +--- + +思考题答案: 很多回复说的很对,`from module_name import *` 会把 module 中所有的函数和类全拿过来,如果和其他函数名类名有冲突就会出问题;`import model_name` 也会导入所有函数和类,但是调用的时候必须使用 `model_name.func` 的方法来调用,等于增加了一层 layer,有效避免冲突。 + + + +欢迎关注我公众号:AI悦创,有更多更好玩的等你发现! + +::: details 公众号:AI悦创【二维码】 + +![](/gzh.jpg) + +::: + +::: info AI悦创·编程一对一 + +AI悦创·推出辅导班啦,包括「Python 语言辅导班、C++ 辅导班、java 辅导班、算法/数据结构辅导班、少儿编程、pygame 游戏开发」,全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践等。当然,还有线下线上摄影课程、Photoshop、Premiere 一对一教学、QQ、微信在线,随时响应!微信:Jiabcdefh + +C++ 信息奥赛题解,长期更新!长期招收一对一中小学信息奥赛集训,莆田、厦门地区有机会线下上门,其他地区线上。微信:Jiabcdefh + +方法一:[QQ](http://wpa.qq.com/msgrd?v=3&uin=1432803776&site=qq&menu=yes) + +方法二:微信:Jiabcdefh + +::: + +![](/zsxq.jpg) + diff --git a/src/Python/Python-core-technology-and-practice/14.assets/48c46d4a66e5c002ce392d79deee436e.png b/src/Python/Python-core-technology-and-practice/14.assets/48c46d4a66e5c002ce392d79deee436e.png new file mode 100644 index 00000000000..519b4155411 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/14.assets/48c46d4a66e5c002ce392d79deee436e.png differ diff --git a/src/Python/Python-core-technology-and-practice/14.assets/5b038b1819ee122b6309b5c5bae456d2.png b/src/Python/Python-core-technology-and-practice/14.assets/5b038b1819ee122b6309b5c5bae456d2.png new file mode 100644 index 00000000000..f5a80816f02 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/14.assets/5b038b1819ee122b6309b5c5bae456d2.png differ diff --git a/src/Python/Python-core-technology-and-practice/14.assets/7c67c59ef443a713d7b5181dcece55dd.jpg b/src/Python/Python-core-technology-and-practice/14.assets/7c67c59ef443a713d7b5181dcece55dd.jpg new file mode 100644 index 00000000000..88370629d46 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/14.assets/7c67c59ef443a713d7b5181dcece55dd.jpg differ diff --git a/src/Python/Python-core-technology-and-practice/14.assets/8fb9cf6bf14357104c88454eefaaeca2.png b/src/Python/Python-core-technology-and-practice/14.assets/8fb9cf6bf14357104c88454eefaaeca2.png new file mode 100644 index 00000000000..6f1410f4a3f Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/14.assets/8fb9cf6bf14357104c88454eefaaeca2.png differ diff --git a/src/Python/Python-core-technology-and-practice/14.assets/99e356ee9b00e645004879b9837c3ee0.png b/src/Python/Python-core-technology-and-practice/14.assets/99e356ee9b00e645004879b9837c3ee0.png new file mode 100644 index 00000000000..ace43dc48e0 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/14.assets/99e356ee9b00e645004879b9837c3ee0.png differ diff --git a/src/Python/Python-core-technology-and-practice/14.assets/aa20a535ce703ef0fe0f1291877f960c.png b/src/Python/Python-core-technology-and-practice/14.assets/aa20a535ce703ef0fe0f1291877f960c.png new file mode 100644 index 00000000000..a50233859f7 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/14.assets/aa20a535ce703ef0fe0f1291877f960c.png differ diff --git a/src/Python/Python-core-technology-and-practice/14.assets/cf241621f373b0e3712f3e0fcc71896b.png b/src/Python/Python-core-technology-and-practice/14.assets/cf241621f373b0e3712f3e0fcc71896b.png new file mode 100644 index 00000000000..936d5a614af Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/14.assets/cf241621f373b0e3712f3e0fcc71896b.png differ diff --git a/src/Python/Python-core-technology-and-practice/14.md b/src/Python/Python-core-technology-and-practice/14.md new file mode 100755 index 00000000000..94aa88dd8ea --- /dev/null +++ b/src/Python/Python-core-technology-and-practice/14.md @@ -0,0 +1,259 @@ +--- +title: 14-答疑(一):列表和元组的内部实现是怎样的? +icon: python +date: 2023-07-31 22:18:31 +author: AI悦创 +isOriginal: true +category: Python 进阶 +tag: + - Python 进阶 +sticky: false +star: false +article: true +timeline: true +image: false +navbar: true +sidebarIcon: true +headerDepth: 5 +comment: true +lastUpdated: true +editLink: false +backToTop: true +toc: true +--- + +![](./14.assets/7c67c59ef443a713d7b5181dcece55dd.jpg) + +你好,我是悦创。 + +转眼间,专栏上线已经三年了😂,而我们也在不知不觉中完成了第一大章基础篇的学习。我非常高兴看到很多同学一直在坚持积极地学习,并且留下了很多高质量的留言,值得我们互相思考交流。也有一些同学反复推敲,指出了文章中一些表达不严谨或是不当的地方,我也表示十分感谢。 + +大部分留言,我都在相对应的文章中回复过了。而一些手机上不方便回复,或是很有价值很典型的问题,我专门摘录了出来,作为今天的答疑内容,集中回复。 + +## 1. 问题一:列表和元组的内部实现 + +第一个问题,是胡峣同学提出的,有关列表(list)和元组(tuple)的内部实现,想知道里边是 linked list 或 array,还是把 array linked 一下这样的方式? + +![](./14.assets/8fb9cf6bf14357104c88454eefaaeca2.png) + + + +关于这个问题,我们可以分别从源码来看。 + +先来看 Python 3.7 的 list 源码。你可以先自己阅读下面两个链接里的内容。 + +- listobject.h:[https://github.com/python/cpython/blob/949fe976d5c62ae63ed505ecf729f815d0baccfc/Include/listobject.h#L23](https://github.com/python/cpython/blob/949fe976d5c62ae63ed505ecf729f815d0baccfc/Include/listobject.h#L23) + +- listobject.c: [https://github.com/python/cpython/blob/3d75bd15ac82575967db367c517d7e6e703a6de3/Objects/listobject.c#L33](https://github.com/python/cpython/blob/3d75bd15ac82575967db367c517d7e6e703a6de3/Objects/listobject.c#L33) + +我把 list 的具体结构放在了下面: + +![](./14.assets/99e356ee9b00e645004879b9837c3ee0.png) + +可以看到,list 本质上是一个 `over-allocate` 的 array。其中,`ob_item` 是一个指针列表,里面的每一个指针都指向列表的元素。而 allocated 则存储了这个列表已经被分配的空间大小。 + +需要注意的是,allocated 与列表实际空间大小的区别。列表实际空间大小,是指 `len(list)` 返回的结果,即上述代码注释中的 `ob_size`,表示这个列表总共存储了多少个元素。实际情况下,为了优化存储结构,避免每次增加元素都要重新分配内存,列表预分配的空间 allocated 往往会大于 `ob_size`(详见正文中的例子)。 + +所以,它们的关系为:`allocated >= len(list) = ob_size`。 + +如果当前列表分配的空间已满(即 `allocated == len(list)`),则会向系统请求更大的内存空间,并把原来的元素全部拷贝过去。列表每次分配空间的大小,遵循下面的模式: + +```python +0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ... +``` + +我们再来分析元组。下面是 Python 3.7 的 tuple 源码,同样的,你可以先自己阅读一下。 + +- tupleobject.h: [https://github.com/python/cpython/blob/3d75bd15ac82575967db367c517d7e6e703a6de3/Include/tupleobject.h#L25](https://github.com/python/cpython/blob/3d75bd15ac82575967db367c517d7e6e703a6de3/Include/tupleobject.h#L25) +- tupleobject.c:[https://github.com/python/cpython/blob/3d75bd15ac82575967db367c517d7e6e703a6de3/Objects/tupleobject.c#L16](https://github.com/python/cpython/blob/3d75bd15ac82575967db367c517d7e6e703a6de3/Objects/tupleobject.c#L16) + +同样的,下面为 tuple 的具体结构: + +![](./14.assets/5b038b1819ee122b6309b5c5bae456d2.png) + +你可以看到,它和 list 相似,本质也是一个 array,但是空间大小固定。不同于一般 array,Python 的 tuple 做了许多优化,来提升在程序中的效率。 + +举个例子,当 tuple 的大小不超过 20 时,Python 就会把它缓存在内部的一个 free list 中。这样,如果你以后需要再去创建同样的 tuple,Python 就可以直接从缓存中载入,提高了程序运行效率。 + +## 2. 问题二:为什么在旧哈希表中,元素会越来越稀疏? + +第二个问题,是 Hoo 同学提出的,为什么在旧哈希表中,元素会越来越稀疏? + +![](./14.assets/cf241621f373b0e3712f3e0fcc71896b.png) + +我们可以先来看旧哈希表的示意图: + +```python +--+-------------------------------+ + | 哈希值 (hash) 键 (key) 值 (value) +--+-------------------------------+ +0 | hash0 key0 value0 +--+-------------------------------+ +1 | hash1 key1 value1 +--+-------------------------------+ +2 | hash2 key2 value2 +--+-------------------------------+ +. | ... +__+_______________________________+ +``` + +你会发现,它是一个 `over-allocate` 的 array,根据元素键(key)的哈希值,来计算其应该被插入位置的索引。 + +因此,假设我有下面这样一个字典: + +```python +{'name': 'mike', 'dob': '1999-01-01', 'gender': 'male'} +``` + +那么这个字典便会存储为类似下面的形式: + +```python +entries = [ +['--', '--', '--'] +[-230273521, 'dob', '1999-01-01'], +['--', '--', '--'], +['--', '--', '--'], +[1231236123, 'name', 'mike'], +['--', '--', '--'], +[9371539127, 'gender', 'male'] +] +``` + +这里的’`---`‘,表示这个位置没有元素,但是已经分配了内存。 + +我们知道,当哈希表剩余空间小于 1/3 时,为了保证相关操作的高效性并避免哈希冲突,就会重新分配更大的内存。所以,当哈希表中的元素越来越多时,分配了内存但里面没有元素的位置,也会变得越来越多。这样一来,哈希表便会越来越稀疏。 + +而新哈希表的结构,改变了这一点,也大大提高了空间的利用率。新哈希表的结构如下所示: + +```python +Indices +---------------------------------------------------- +None | index | None | None | index | None | index ... +---------------------------------------------------- + + +Entries +-------------------- +hash0 key0 value0 +--------------------- +hash1 key1 value1 +--------------------- +hash2 key2 value2 +--------------------- + ... +--------------------- +``` + +你可以看到,它把存储结构分成了 Indices 和 Entries 这两个 array,而 `None` 代表这个位置分配了内存但没有元素。 + +我们同样还用上面这个例子,它在新哈希表中的存储模式,就会变为下面这样: + +```python +indices = [None, 1, None, None, 0, None, 2] +entries = [ +[1231236123, 'name', 'mike'], +[-230273521, 'dob', '1999-01-01'], +[9371539127, 'gender', 'male'] +] +``` + +其中,Indices 中元素的值,对应 entries 中相应的索引。比如 indices 中的 1,就对应着 `entries[1]` ,即 `'dob': '1999-01-01'`。 + +对比之下,我们会清晰感受到,新哈希表中的空间利用率,相比于旧哈希表有大大的提升。 + +## 3. 问题三:有关异常的困扰 + +第三个问题,是“不瘦到 140 不改名”同学提出的,对“NameError”异常的困惑。这是很常见的一个错误,我在这里也解释一下。 + +![](./14.assets/48c46d4a66e5c002ce392d79deee436e.png) + +这个问题其实有点 tricky,如果你查阅[官方文档](https://docs.python.org/3/reference/compound_stmts.html#the-try-statement),会看到这么一句话”When an exception has been assigned using as target, it is cleared at the end of the except clause. ” + +这句话意思是,如果你在异常处理的 except block 中,把异常赋予了一个变量,那么这个变量会在 except block 执行结束时被删除,相当于下面这样的表示: + +```python +e = 1 +try: + 1 / 0 +except ZeroDivisionError as e: + try: + pass + finally: + del e +``` + +这里的 e 一开始指向整数 1,但是在 except block 结束时被删除了(`del e`),所以程序执行就会抛出“NameError”的异常。 + +因此,这里提醒我们,在平时写代码时,一定要保证 except 中异常赋予的变量,在之后的语句中不再被用到。 + +## 4. 问题四:关于多态和全局变量的修改 + +最后的问题来自于 farFlight 同学,他提了两个问题: + +1. Python 自己判断类型的多态和子类继承的多态 Polymorphism 是否相同? +2. 函数内部不能直接用 `+=` 等修改全局变量,但是对于 list 全局变量,却可以使用 append、extend 之类修改,这是为什么呢? + +![](./14.assets/aa20a535ce703ef0fe0f1291877f960c.png) + + + +我们分别来看这两个问题。对于第一个问题,要搞清楚多态的概念,多态是指有多种不同的形式。因此,判断类型的多态和子类继承的多态,在本质上都是一样的,只不过你可以把它们理解为多态的两种不同表现。 + +再来看第二个问题。当全局变量指向的对象不可变时,比如是整型、字符串等等,如果你尝试在函数内部改变它的值,却不加关键字 global,就会抛出异常: + +```python +x = 1 + +def func(): + x += 1 +func() +x + +## 输出 +UnboundLocalError: local variable 'x' referenced before assignment +``` + +这是因为,程序默认函数内部的 x 是局部变量,而你没有为其赋值就直接引用,显然是不可行。 + +不过,如果全局变量指向的对象是可变的,比如是列表、字典等等,你就可以在函数内部修改它了: + +```python +x = [1] + +def func(): + x.append(2) +func() +x + +## 输出 +[1, 2] +``` + +当然,需要注意的是,这里的 `x.append(2)` ,并没有改变变量 x,x 依然指向原来的列表。事实上,这句话的意思是,访问 x 指向的列表,并在这个列表的末尾增加 2。 + +今天主要回答这些问题,同时也欢迎你继续在留言区写下疑问和感想,我会持续不断地解答。希望每一次的留言和答疑,都能给你带来新的收获和价值。 + + + +欢迎关注我公众号:AI悦创,有更多更好玩的等你发现! + +::: details 公众号:AI悦创【二维码】 + +![](/gzh.jpg) + +::: + +::: info AI悦创·编程一对一 + +AI悦创·推出辅导班啦,包括「Python 语言辅导班、C++ 辅导班、java 辅导班、算法/数据结构辅导班、少儿编程、pygame 游戏开发」,全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践等。当然,还有线下线上摄影课程、Photoshop、Premiere 一对一教学、QQ、微信在线,随时响应!微信:Jiabcdefh + +C++ 信息奥赛题解,长期更新!长期招收一对一中小学信息奥赛集训,莆田、厦门地区有机会线下上门,其他地区线上。微信:Jiabcdefh + +方法一:[QQ](http://wpa.qq.com/msgrd?v=3&uin=1432803776&site=qq&menu=yes) + +方法二:微信:Jiabcdefh + +::: + +![](/zsxq.jpg) + diff --git a/src/Python/Python-core-technology-and-practice/15.assets/09e9ece6db52e36bbc9c4b7ba825d7d3.jpg b/src/Python/Python-core-technology-and-practice/15.assets/09e9ece6db52e36bbc9c4b7ba825d7d3.jpg new file mode 100644 index 00000000000..5876311c53c Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/15.assets/09e9ece6db52e36bbc9c4b7ba825d7d3.jpg differ diff --git a/src/Python/Python-core-technology-and-practice/15.md b/src/Python/Python-core-technology-and-practice/15.md new file mode 100755 index 00000000000..850942da701 --- /dev/null +++ b/src/Python/Python-core-technology-and-practice/15.md @@ -0,0 +1,368 @@ +--- +title: 15-Python 对象的比较、拷贝 +icon: python +date: 2023-08-01 09:19:36 +author: AI悦创 +isOriginal: true +category: Python 进阶 +tag: + - Python 进阶 +sticky: false +star: false +article: true +timeline: true +image: false +navbar: true +sidebarIcon: true +headerDepth: 5 +comment: true +lastUpdated: true +editLink: false +backToTop: true +toc: true +--- + +![](./15.assets/09e9ece6db52e36bbc9c4b7ba825d7d3.jpg) + +你好,我是悦创。 + +在前面的学习中,我们其实已经接触到了很多 Python 对象比较和复制的例子,比如下面这个,判断 a 和 b 是否相等的 if 语句: + +```python +if a == b: + ... +``` + +再比如第二个例子,这里 l2 就是 l1 的拷贝。 + +```python +l1 = [1, 2, 3] +l2 = list(l1) +``` + +但你可能并不清楚,这些语句的背后发生了什么。比如, + +- l2 是 l1 的浅拷贝(shallow copy)还是深度拷贝(deep copy)呢? +- `a == b` 是比较两个对象的值相等,还是两个对象完全相等呢? + +关于这些的种种知识,我希望通过这节课的学习,让你有个全面的了解。 + +## 1. '==' VS 'is' + +等于(`==`)和 is 是 Python 中对象比较常用的两种方式。简单来说,`'=='` 操作符比较对象之间的值是否相等,比如下面的例子,表示比较变量 a 和 b 所指向的值是否相等。 + +```python +a == b +``` + +而 `'is'` 操作符比较的是对象的身份标识是否相等,即它们是否是同一个对象,是否指向同一个内存地址。 + +在 Python 中,每个对象的身份标识,都能通过函数 `id(object)` 获得。因此,`'is'` 操作符,相当于比较对象之间的 ID 是否相等,我们来看下面的例子: + +```python +a = 10 +b = 10 + +a == b +True + +id(a) +4427562448 + +id(b) +4427562448 + +a is b +True +``` + +这里,首先 Python 会为 10 这个值开辟一块内存,然后变量 a 和 b 同时指向这块内存区域,即 a 和 b 都是指向 10 这个变量,因此 a 和 b 的值相等,id 也相等,`a == b` 和 `a is b` 都返回 True。 + +不过,需要注意,对于整型数字来说,以上 `a is b` 为 True 的结论,只适用于 -5 到 256 范围内的数字。比如下面这个例子: + +```python +a = 257 +b = 257 + +a == b +True + +id(a) +4473417552 + +id(b) +4473417584 + +a is b +False +``` + +这里我们把 257 同时赋值给了 a 和 b,可以看到 `a == b` 仍然返回 True,因为 a 和 b 指向的值相等。但奇怪的是,`a is b` 返回了 false,并且我们发现,a 和 b 的 ID 不一样了,这是为什么呢? + +事实上,出于对性能优化的考虑,Python 内部会对 -5 到 256 的整型维持一个数组,起到一个缓存的作用。这样,每次你试图创建一个 -5 到 256 范围内的整型数字时,Python 都会从这个数组中返回相对应的引用,而不是重新开辟一块新的内存空间。 + +但是,如果整型数字超过了这个范围,比如上述例子中的 257,Python 则会为两个 257 开辟两块内存区域,因此 a 和 b 的 ID 不一样,`a is b` 就会返回 False 了。 + +通常来说,在实际工作中,当我们比较变量时,使用 `'=='` 的次数会比 `'is'` 多得多,因为我们一般更关心两个变量的值,而不是它们内部的存储地址。但是,当我们比较一个变量与一个单例(singleton)时,通常会使用 `'is'`。一个典型的例子,就是检查一个变量是否为 None: + +```python +if a is None: + ... + +if a is not None: + ... +``` + +这里注意,比较操作符 `'is'` 的速度效率,通常要优于 `'=='` 。因为 `'is'` 操作符不能被重载,这样,Python 就不需要去寻找,程序中是否有其他地方重载了比较操作符,并去调用。执行比较操作符 `'is'`,就仅仅是比较两个变量的 ID 而已。 + +但是 `'=='` 操作符却不同,执行 `a == b` 相当于是去执行 `a.__eq__(b)` ,而 Python 大部分的数据类型都会去重载 `__eq__` 这个函数,其内部的处理通常会复杂一些。比如,对于列表,`__eq__` 函数会去遍历列表中的元素,比较它们的顺序和值是否相等。 + +不过,对于不可变(immutable)的变量,如果我们之前用 `'=='` 或者 `'is'` 比较过,结果是不是就一直不变了呢? + +答案自然是否定的。我们来看下面一个例子: + +```python +t1 = (1, 2, [3, 4]) +t2 = (1, 2, [3, 4]) +t1 == t2 +True + +t1[-1].append(5) +t1 == t2 +False +``` + +我们知道元组是不可变的,但元组可以嵌套,它里面的元素可以是列表类型,列表是可变的,所以如果我们修改了元组中的某个可变元素,那么元组本身也就改变了,之前用 `'is'` 或者 `'=='` 操作符取得的结果,可能就不适用了。 + +这一点,你在日常写程序时一定要注意,在必要的地方请不要省略条件检查。 + +## 2. 浅拷贝和深度拷贝 + +接下来,我们一起来看看 Python 中的浅拷贝(shallow copy)和深度拷贝(deep copy)。 + +对于这两个熟悉的操作,我并不想一上来先抛概念让你死记硬背来区分,我们不妨先从它们的操作方法说起,通过代码来理解两者的不同。 + +先来看浅拷贝。常见的浅拷贝的方法,是使用数据类型本身的构造器,比如下面两个例子: + +```python +l1 = [1, 2, 3] +l2 = list(l1) + +l2 +[1, 2, 3] + +l1 == l2 +True + +l1 is l2 +False + +s1 = set([1, 2, 3]) +s2 = set(s1) + +s2 +{1, 2, 3} + +s1 == s2 +True + +s1 is s2 +False +``` + +这里,l2 就是 l1 的浅拷贝,s2 是 s1 的浅拷贝。当然,对于可变的序列,我们还可以通过切片操作符 `':'` 完成浅拷贝,比如下面这个列表的例子: + +```python +l1 = [1, 2, 3] +l2 = l1[:] + +l1 == l2 +True + +l1 is l2 +False +``` + +当然,Python 中也提供了相对应的函数 `copy.copy()`,适用于任何数据类型: + +```python +import copy +l1 = [1, 2, 3] +l2 = copy.copy(l1) +``` + +不过,需要注意的是,对于元组,使用 `tuple()` 或者切片操作符 `':'` 不会创建一份浅拷贝,相反,它会返回一个指向相同元组的引用: + +```python +t1 = (1, 2, 3) +t2 = tuple(t1) + +t1 == t2 +True + +t1 is t2 +True +``` + +这里,元组 `(1, 2, 3)` 只被创建一次,t1 和 t2 同时指向这个元组。 + +到这里,对于浅拷贝你应该很清楚了。浅拷贝,是指重新分配一块内存,创建一个新的对象,里面的元素是原对象中子对象的引用。因此,如果原对象中的元素不可变,那倒无所谓;但如果元素可变,浅拷贝通常会带来一些副作用,尤其需要注意。我们来看下面的例子: + +```python +l1 = [[1, 2], (30, 40)] +l2 = list(l1) +l1.append(100) +l1[0].append(3) + +l1 +[[1, 2, 3], (30, 40), 100] + +l2 +[[1, 2, 3], (30, 40)] + +l1[1] += (50, 60) +l1 +[[1, 2, 3], (30, 40, 50, 60), 100] + +l2 +[[1, 2, 3], (30, 40)] +``` + +这个例子中,我们首先初始化了一个列表 l1,里面的元素是一个列表和一个元组;然后对 l1 执行浅拷贝,赋予 l2。因为浅拷贝里的元素是对原对象元素的引用,因此 l2 中的元素和 l1 指向同一个列表和元组对象。 + +接着往下看。`l1.append(100)`,表示对 l1 的列表新增元素 100。这个操作不会对 l2 产生任何影响,因为 l2 和 l1 作为整体是两个不同的对象,并不共享内存地址。操作过后 l2 不变,l1 会发生改变: + +```python +[[1, 2, 3], (30, 40), 100] +``` + +再来看,`l1[0].append(3)`,这里表示对 l1 中的第一个列表新增元素 3。因为 l2 是 l1 的浅拷贝,l2 中的第一个元素和 l1 中的第一个元素,共同指向同一个列表,因此 l2 中的第一个列表也会相对应的新增元素 3。操作后 l1 和 l2 都会改变: + +```python +l1: [[1, 2, 3], (30, 40), 100] +l2: [[1, 2, 3], (30, 40)] +``` + +最后是 `l1[1] += (50, 60)` ,因为元组是不可变的,这里表示对 l1 中的第二个元组拼接,然后重新创建了一个新元组作为 l1 中的第二个元素,而 l2 中没有引用新元组,因此 l2 并不受影响。操作后 l2 不变,l1 发生改变: + +```python +l1: [[1, 2, 3], (30, 40, 50, 60), 100] +``` + +通过这个例子,你可以很清楚地看到使用浅拷贝可能带来的副作用。因此,如果我们想避免这种副作用,完整地拷贝一个对象,你就得使用深度拷贝。 + +所谓深度拷贝,是指重新分配一块内存,创建一个新的对象,并且将原对象中的元素,以递归的方式,通过创建新的子对象拷贝到新对象中。因此,新对象和原对象没有任何关联。 + +Python 中以 `copy.deepcopy()` 来实现对象的深度拷贝。比如上述例子写成下面的形式,就是深度拷贝: + +```python +import copy +l1 = [[1, 2], (30, 40)] +l2 = copy.deepcopy(l1) +l1.append(100) +l1[0].append(3) + +l1 +[[1, 2, 3], (30, 40), 100] + +l2 +[[1, 2], (30, 40)] +``` + +我们可以看到,无论 l1 如何变化,l2 都不变。因为此时的 l1 和 l2 完全独立,没有任何联系。 + +不过,深度拷贝也不是完美的,往往也会带来一系列问题。如果被拷贝对象中存在指向自身的引用,那么程序很容易陷入无限循环: + +```python +import copy +x = [1] +x.append(x) + +x +[1, [...]] + +y = copy.deepcopy(x) +y +[1, [...]] +``` + +上面这个例子,列表 x 中有指向自身的引用,因此 x 是一个无限嵌套的列表。但是我们发现深度拷贝 x 到 y 后,程序并没有出现 stack overflow 的现象。这是为什么呢? + +其实,这是因为深度拷贝函数 deepcopy 中会维护一个字典,记录已经拷贝的对象与其 ID。拷贝过程中,如果字典里已经存储了将要拷贝的对象,则会从字典直接返回,我们来看相对应的源码就能明白: + +```python +def deepcopy(x, memo=None, _nil=[]): + """Deep copy operation on arbitrary Python objects. + + See the module's __doc__ string for more info. + """ + + if memo is None: + memo = {} + d = id(x) # 查询被拷贝对象x的id + y = memo.get(d, _nil) # 查询字典里是否已经存储了该对象 + if y is not _nil: + return y # 如果字典里已经存储了将要拷贝的对象,则直接返回 + ... +``` + +## 3. 总结 + +今天这节课,我们一起学习了 Python 中对象的比较和拷贝,主要有下面几个重点内容。 + +- 比较操作符 `'=='` 表示比较对象间的值是否相等,而 `'is'` 表示比较对象的标识是否相等,即它们是否指向同一个内存地址。 +- 比较操作符 `'is'` 效率优于`'=='`,因为 `'is'` 操作符无法被重载,执行 `'is'` 操作只是简单的获取对象的 ID,并进行比较;而 `'=='` 操作符则会递归地遍历对象的所有值,并逐一比较。 +- 浅拷贝中的元素,是原对象中子对象的引用,因此,如果原对象中的元素是可变的,改变其也会影响拷贝后的对象,存在一定的副作用。 +- 深度拷贝则会递归地拷贝原对象中的每一个子对象,因此拷贝后的对象和原对象互不相关。另外,深度拷贝中会维护一个字典,记录已经拷贝的对象及其 ID,来提高效率并防止无限递归的发生。 + +## 4. 思考题 + +最后,我为你留下一道思考题。这节课我曾用深度拷贝,拷贝过一个无限嵌套的列表。那么。当我们用等于操作符 `'=='` 进行比较时,输出会是什么呢?是 True 或者 False 还是其他?为什么呢?建议你先自己动脑想一想,然后再实际跑一下代码,来检验你的猜想。 + +```python +import copy +x = [1] +x.append(x) + +y = copy.deepcopy(x) + +# 以下命令的输出是? +x == y +``` + +欢迎在留言区写下你的答案和学习感想,也欢迎你把这篇文章分享给你的同事、朋友。我们一起交流,一起进步。 + +--- + +关于思考题: SCAR 说的很对,程序会报错:'RecursionError: maximum recursion depth exceeded in comparison'。因为x是一个无限嵌套的列表,y深度拷贝x也是一个无限嵌套的列表,理论上 `x==y` 应该返回 True,但是 `x==y` 内部执行是会递归遍历列表x和y中每一个元素的值,由于 x 和 y 是无限嵌套的,因此会stack overflow,报错 + + + + + + + + + +欢迎关注我公众号:AI悦创,有更多更好玩的等你发现! + +::: details 公众号:AI悦创【二维码】 + +![](/gzh.jpg) + +::: + +::: info AI悦创·编程一对一 + +AI悦创·推出辅导班啦,包括「Python 语言辅导班、C++ 辅导班、java 辅导班、算法/数据结构辅导班、少儿编程、pygame 游戏开发」,全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践等。当然,还有线下线上摄影课程、Photoshop、Premiere 一对一教学、QQ、微信在线,随时响应!微信:Jiabcdefh + +C++ 信息奥赛题解,长期更新!长期招收一对一中小学信息奥赛集训,莆田、厦门地区有机会线下上门,其他地区线上。微信:Jiabcdefh + +方法一:[QQ](http://wpa.qq.com/msgrd?v=3&uin=1432803776&site=qq&menu=yes) + +方法二:微信:Jiabcdefh + +::: + +![](/zsxq.jpg) + diff --git a/src/Python/Python-core-technology-and-practice/16.assets/8620c97a3924414e1f63f2f40d5a369f.jpg b/src/Python/Python-core-technology-and-practice/16.assets/8620c97a3924414e1f63f2f40d5a369f.jpg new file mode 100644 index 00000000000..c73b676ae3b Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/16.assets/8620c97a3924414e1f63f2f40d5a369f.jpg differ diff --git a/src/Python/Python-core-technology-and-practice/16.assets/97c05df49cfe051d7b76addd833f33eb.png b/src/Python/Python-core-technology-and-practice/16.assets/97c05df49cfe051d7b76addd833f33eb.png new file mode 100644 index 00000000000..0ad16b6bef0 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/16.assets/97c05df49cfe051d7b76addd833f33eb.png differ diff --git a/src/Python/Python-core-technology-and-practice/16.assets/b16d29112c361f596952961d13da345f.png b/src/Python/Python-core-technology-and-practice/16.assets/b16d29112c361f596952961d13da345f.png new file mode 100644 index 00000000000..f619461b665 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/16.assets/b16d29112c361f596952961d13da345f.png differ diff --git a/src/Python/Python-core-technology-and-practice/16.assets/c00c9fc013cea4eb840921eb4b3e499f.png b/src/Python/Python-core-technology-and-practice/16.assets/c00c9fc013cea4eb840921eb4b3e499f.png new file mode 100644 index 00000000000..0abf1d14bac Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/16.assets/c00c9fc013cea4eb840921eb4b3e499f.png differ diff --git a/src/Python/Python-core-technology-and-practice/16.assets/c2f8e0d9a8570bd56a43a21b7bb25af9.png b/src/Python/Python-core-technology-and-practice/16.assets/c2f8e0d9a8570bd56a43a21b7bb25af9.png new file mode 100644 index 00000000000..05805239545 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/16.assets/c2f8e0d9a8570bd56a43a21b7bb25af9.png differ diff --git a/src/Python/Python-core-technology-and-practice/16.assets/fc10cd3e3512e984d530a4b82259e917.png b/src/Python/Python-core-technology-and-practice/16.assets/fc10cd3e3512e984d530a4b82259e917.png new file mode 100644 index 00000000000..b3acde14a0f Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/16.assets/fc10cd3e3512e984d530a4b82259e917.png differ diff --git a/src/Python/Python-core-technology-and-practice/16.md b/src/Python/Python-core-technology-and-practice/16.md new file mode 100755 index 00000000000..9acfa3ed9d5 --- /dev/null +++ b/src/Python/Python-core-technology-and-practice/16.md @@ -0,0 +1,315 @@ +--- +title: 16-值传递,引用传递 or 其他,Python 里参数是如何传递的? +icon: python +date: 2023-08-01 10:06:00 +author: AI悦创 +isOriginal: true +category: Python 进阶 +tag: + - Python 进阶 +sticky: false +star: false +article: true +timeline: true +image: false +navbar: true +sidebarIcon: true +headerDepth: 5 +comment: true +lastUpdated: true +editLink: false +backToTop: true +toc: true +--- + +![](./16.assets/8620c97a3924414e1f63f2f40d5a369f.jpg) + +你好,我是悦创。 + +在前面的第一大章节中,我们一起学习了 Python 的函数基础及其应用。我们大致明白了,所谓的传参,就是把一些参数从一个函数传递到另一个函数,从而使其执行相应的任务。但是你有没有想过,参数传递的底层是如何工作的,原理又是怎样的呢? + +实际工作中,很多人会遇到这样的场景:写完了代码,一测试,发现结果和自己期望的不一样,于是开始一层层地 debug。花了很多时间,可到最后才发现,是传参过程中数据结构的改变,导致了程序的“出错”。 + +比如,我将一个列表作为参数传入另一个函数,期望列表在函数运行结束后不变,但是往往“事与愿违”,由于某些操作,它的值改变了,那就很有可能带来后续程序一系列的错误。 + +因此,了解 Python 中参数的传递机制,具有十分重要的意义,这往往能让我们写代码时少犯错误,提高效率。今天我们就一起来学习一下,Python 中参数是如何传递的。 + +## 1. 什么是值传递和引用传递 + +如果你接触过其他的编程语言,比如 C/C++,很容易想到,常见的参数传递有 2 种:**值传递**和**引用传递**。所谓值传递,通常就是拷贝参数的值,然后传递给函数里的新变量。这样,原变量和新变量之间互相独立,互不影响。 + +比如,我们来看下面的一段 C++ 代码: + +```cpp +#include +using namespace std; + +// 交换2个变量的值 +void swap(int x, int y) { +int temp; +temp = x; // 交换x和y的值 + x = y; + y = temp; + return; +} +int main () { + int a = 1; + int b = 2; + cout << "Before swap, value of a :" << a << endl; + cout << "Before swap, value of b :" << b << endl; + swap(a, b); + cout << "After swap, value of a :" << a << endl; + cout << "After swap, value of b :" << b << endl; + return 0; +} +Before swap, value of a :1 +Before swap, value of b :2 +After swap, value of a :1 +After swap, value of b :2 +``` + +这里的 `swap()` 函数,把 a 和 b 的值拷贝给了 x 和 y,然后再交换 x 和 y 的值。这样一来,x 和 y 的值发生了改变,但是 a 和 b 不受其影响,所以值不变。这种方式,就是我们所说的值传递。 + +所谓引用传递,通常是指把参数的引用传给新的变量,这样,原变量和新变量就会指向同一块内存地址。如果改变了其中任何一个变量的值,那么另外一个变量也会相应地随之改变。 + +还是拿我们刚刚讲到的 C++ 代码为例,上述例子中的 `swap()` 函数,如果改成下面的形式,声明引用类型的参数变量: + +```cpp +void swap(int& x, int& y) { + int temp; + temp = x; // 交换x和y的值 + x = y; + y = temp; + return; +} +``` + +那么输出的便是另一个结果: + +```cpp +Before swap, value of a :1 +Before swap, value of b :2 +After swap, value of a :2 +After swap, value of b :1 +``` + +原变量 a 和 b 的值被交换了,因为引用传递使得 a 和 x,b 和 y 一模一样,对 x 和 y 的任何改变必然导致了 a 和 b 的相应改变。 + +不过,这是 C/C++ 语言中的特点。那么 Python 中,参数传递到底是如何进行的呢?它们到底属于值传递、引用传递,还是其他呢? + +在回答这个问题之前,让我们先来了解一下,Python 变量和赋值的基本原理。 + +## 2. Python 变量及其赋值 + +我们首先来看,下面的 Python 代码示例: + +```python +a = 1 +b = a +a = a + 1 +``` + +这里首先将 1 赋值于 a,即 a 指向了 1 这个对象,如下面的流程图所示: + +![](./16.assets/97c05df49cfe051d7b76addd833f33eb.png) + +接着 `b = a` 则表示,让变量 b 也同时指向 1 这个对象。这里要注意,Python 里的对象可以被多个变量所指向或引用。 + +![](./16.assets/c00c9fc013cea4eb840921eb4b3e499f.png) + +最后执行 `a = a + 1`。需要注意的是,Python 的数据类型,例如整型(int)、字符串(string)等等,是不可变的。所以,`a = a + 1`,并不是让 a 的值增加 1,而是表示重新创建了一个新的值为 2 的对象,并让 a 指向它。但是 b 仍然不变,仍然指向 1 这个对象。 + +因此,最后的结果是,a 的值变成了 2,而 b 的值不变仍然是 1。 + +![](./16.assets/fc10cd3e3512e984d530a4b82259e917.png) + +通过这个例子你可以看到,这里的 a 和 b,开始只是两个指向同一个对象的变量而已,或者你也可以把它们想象成同一个对象的两个名字。简单的赋值 `b = a`,并不表示重新创建了新对象,只是让同一个对象被多个变量指向或引用。 + +同时,指向同一个对象,也并不意味着两个变量就被绑定到了一起。如果你给其中一个变量重新赋值,并不会影响其他变量的值。 + +明白了这个基本的变量赋值例子,我们再来看一个列表的例子: + +```python +l1 = [1, 2, 3] +l2 = l1 +l1.append(4) +l1 +[1, 2, 3, 4] +l2 +[1, 2, 3, 4] +``` + +同样的,我们首先让列表 l1 和 l2 同时指向了 `[1, 2, 3]` 这个对象。 + +![](./16.assets/c2f8e0d9a8570bd56a43a21b7bb25af9.png) + +由于列表是可变的,所以 `l1.append(4)` 不会创建新的列表,只是在原列表的末尾插入了元素 4,变成 `[1, 2, 3, 4]` 。由于 l1 和 l2 同时指向这个列表,所以列表的变化会同时反映在 l1 和 l2 这两个变量上,那么,l1 和 l2 的值就同时变为了 `[1, 2, 3, 4]`。 + +![](./16.assets/b16d29112c361f596952961d13da345f.png) + +另外,需要注意的是,Python 里的变量可以被删除,但是对象无法被删除。比如下面的代码: + +```python +l = [1, 2, 3] +del l +``` + +`del l` 删除了 l 这个变量,从此以后你无法访问 l,但是对象 `[1, 2, 3]` 仍然存在。Python 程序运行时,其自带的垃圾回收系统会跟踪每个对象的引用。如果 `[1, 2, 3]` 除了 l 外,还在其他地方被引用,那就不会被回收,反之则会被回收。 + +由此可见,在 Python 中: + +- 变量的赋值,只是表示让变量指向了某个对象,并不表示拷贝对象给变量;而一个对象,可以被多个变量所指向。 +- 可变对象(列表,字典,集合等等)的改变,会影响所有指向该对象的变量。 +- 对于不可变对象(字符串、整型、元组等等),所有指向该对象的变量的值总是一样的,也不会改变。但是通过某些操作(`+=` 等等)更新不可变对象的值时,会返回一个新的对象。 +- 变量可以被删除,但是对象无法被删除。 + +## 3. Python 函数的参数传递 + +从上述 Python 变量的命名与赋值的原理讲解中,相信你能举一反三,大概猜出 Python 函数中参数是如何传递了吧? + +这里首先引用 Python 官方文档中的一段说明: + +> “Remember that arguments are passed by assignment in Python. Since assignment just creates references to objects, there’s no alias between an argument name in the caller and callee, and so no call-by-reference per Se.” + +准确地说,Python 的参数传递是**赋值传递** (pass by assignment),或者叫作对象的**引用传递**(pass by object reference)。Python 里所有的数据类型都是对象,所以参数传递时,只是让新变量与原变量指向相同的对象而已,并不存在值传递或是引用传递一说。 + +比如,我们来看下面这个例子: + +```python +def my_func1(b): + b = 2 + +a = 1 +my_func1(a) +a +1 +``` + +这里的参数传递,使变量 a 和 b 同时指向了 1 这个对象。但当我们执行到 `b = 2` 时,系统会重新创建一个值为 2 的新对象,并让 b 指向它;而 a 仍然指向 1 这个对象。所以,a 的值不变,仍然为 1。 + +那么对于上述例子的情况,是不是就没有办法改变 a 的值了呢? + +答案当然是否定的,我们只需稍作改变,让函数返回新变量,赋给 a。这样,a 就指向了一个新的值为 2 的对象,a 的值也因此变为 2。 + +```python +def my_func2(b): + b = 2 + return b + +a = 1 +a = my_func2(a) +a +2 +``` + +不过,当可变对象当作参数传入函数里的时候,改变可变对象的值,就会影响所有指向它的变量。比如下面的例子: + +```python +def my_func3(l2): + l2.append(4) + +l1 = [1, 2, 3] +my_func3(l1) +l1 +[1, 2, 3, 4] +``` + +这里 l1 和 l2 先是同时指向值为 `[1, 2, 3]` 的列表。不过,由于列表可变,执行 `append()` 函数,对其末尾加入新元素 4 时,变量 l1 和 l2 的值也都随之改变了。 + +但是,下面这个例子,看似都是给列表增加了一个新元素,却得到了明显不同的结果。 + +```python +def my_func4(l2): + l2 = l2 + [4] + +l1 = [1, 2, 3] +my_func4(l1) +l1 +[1, 2, 3] +``` + +为什么 l1 仍然是 `[1, 2, 3]`,而不是 `[1, 2, 3, 4]` 呢? + +要注意,这里 `l2 = l2 + [4]`,表示创建了一个“末尾加入元素 4“的新列表,并让 l2 指向这个新的对象。这个过程与 l1 无关,因此 l1 的值不变。当然,同样的,如果要改变 l1 的值,我们就得让上述函数返回一个新列表,再赋予 l1 即可: + +```python +def my_func5(l2): + l2 = l2 + [4] + return l2 + +l1 = [1, 2, 3] +l1 = my_func5(l1) +l1 +[1, 2, 3, 4] +``` + +这里你尤其要记住的是,改变变量和重新赋值的区别: + +- `my_func3()` 中单纯地改变了对象的值,因此函数返回后,所有指向该对象的变量都会被改变; +- 但 `my_func4()` 中则创建了新的对象,并赋值给一个本地变量,因此原变量仍然不变。 + +至于 `my_func3()` 和 `my_func5()` 的用法,两者虽然写法不同,但实现的功能一致。不过,在实际工作应用中,我们往往倾向于类似 `my_func5()` 的写法,添加返回语句。这样更简洁明了,不易出错。 + +## 5. 总结 + +今天,我们一起学习了 Python 的变量及其赋值的基本原理,并且解释了 Python 中参数是如何传递的。和其他语言不同的是,Python 中参数的传递既不是值传递,也不是引用传递,而是赋值传递,或者是叫对象的引用传递。 + +需要注意的是,这里的赋值或对象的引用传递,不是指向一个具体的内存地址,而是指向一个具体的对象。 + +- 如果对象是可变的,当其改变时,所有指向这个对象的变量都会改变。 +- 如果对象不可变,简单的赋值只能改变其中一个变量的值,其余变量则不受影响。 + +清楚了这一点,如果你想通过一个函数来改变某个变量的值,通常有两种方法。一种是直接将可变数据类型(比如列表,字典,集合)当作参数传入,直接在其上修改;第二种则是创建一个新变量,来保存修改后的值,然后将其返回给原变量。在实际工作中,我们更倾向于使用后者,因为其表达清晰明了,不易出错。 + +## 6. 思考题 + +最后,我为你留下了两道思考题。 + +第一个问题,下面的代码中, l1、l2 和 l3 都指向同一个对象吗? + +```python +l1 = [1, 2, 3] +l2 = [1, 2, 3] +l3 = l2 +``` + +第二个问题,下面的代码中,打印 d 最后的输出是什么呢? + +```python +def func(d): + d['a'] = 10 + d['b'] = 20 + +d = {'a': 1, 'b': 2} +func(d) +print(d) +``` + +欢迎留言和我分享,也欢迎你把这篇文章分享给你的同事、朋友,一起在交流中进步。 + +--- + +关于思考题: 第一题: l2和l3是指向同一个对象,因为两者之间用等号赋值了,l1并不是,l1所指向的 `[1, 2, 3]` 是另外一块内存空间,大家可以通过 `id()` 这个函数验证 第二题: 输出的是 `{'a': 10, 'b': 20}`,字典是可变的,传入函数后,函数里的d和外部的d实际上都指向同一个对象 `d[idx] = value` 语句改变了字典对应 key 所指向的值 + +欢迎关注我公众号:AI悦创,有更多更好玩的等你发现! + +::: details 公众号:AI悦创【二维码】 + +![](/gzh.jpg) + +::: + +::: info AI悦创·编程一对一 + +AI悦创·推出辅导班啦,包括「Python 语言辅导班、C++ 辅导班、java 辅导班、算法/数据结构辅导班、少儿编程、pygame 游戏开发」,全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践等。当然,还有线下线上摄影课程、Photoshop、Premiere 一对一教学、QQ、微信在线,随时响应!微信:Jiabcdefh + +C++ 信息奥赛题解,长期更新!长期招收一对一中小学信息奥赛集训,莆田、厦门地区有机会线下上门,其他地区线上。微信:Jiabcdefh + +方法一:[QQ](http://wpa.qq.com/msgrd?v=3&uin=1432803776&site=qq&menu=yes) + +方法二:微信:Jiabcdefh + +::: + +![](/zsxq.jpg) + diff --git a/src/Python/Python-core-technology-and-practice/17.assets/dc830dd58d35cfd9025bfea084aeec0f.jpg b/src/Python/Python-core-technology-and-practice/17.assets/dc830dd58d35cfd9025bfea084aeec0f.jpg new file mode 100644 index 00000000000..2d9bef7a047 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/17.assets/dc830dd58d35cfd9025bfea084aeec0f.jpg differ diff --git a/src/Python/Python-core-technology-and-practice/17.md b/src/Python/Python-core-technology-and-practice/17.md new file mode 100755 index 00000000000..c485713ce7d --- /dev/null +++ b/src/Python/Python-core-technology-and-practice/17.md @@ -0,0 +1,244 @@ +--- +title: 17-强大的装饰器 +icon: python +date: 2023-08-01 12:07:09 +author: AI悦创 +isOriginal: true +category: Python 进阶 +tag: + - Python 进阶 +sticky: false +star: false +article: true +timeline: true +image: false +navbar: true +sidebarIcon: true +headerDepth: 5 +comment: true +lastUpdated: true +editLink: false +backToTop: true +toc: true +--- + +![](./17.assets/dc830dd58d35cfd9025bfea084aeec0f.jpg) + +你好,我是悦创。 + +装饰器一直以来都是 Python 中很有用、很经典的一个 feature,在工程中的应用也十分广泛,比如日志、缓存等等的任务都会用到。然而,在平常工作生活中,我发现不少人,尤其是初学者,常常因为其相对复杂的表示,对装饰器望而生畏,认为它“too fancy to learn”,实际并不如此。 + +今天这节课,我会以前面所讲的函数、闭包为切入点,引出装饰器的概念、表达和基本用法,最后,再通过实际工程中的例子,让你再次加深理解。 + +接下来,让我们进入正文一起学习吧! + +## 1. 函数 -> 装饰器 + +### 1.1 函数核心回顾 + +引入装饰器之前,我们首先一起来复习一下,必须掌握的函数的几个核心概念。 + +第一点,我们要知道,在 Python 中,函数是一等公民(first-class citizen),函数也是对象。我们可以把函数赋予变量,比如下面这段代码: + +```python +def func(message): + print('Got a message: {}'.format(message)) + +send_message = func +send_message('hello world') + +# 输出 +Got a message: hello world +``` + +这个例子中,我们把函数 func 赋予了变量 `send_message`,这样之后你调用 `send_message`,就相当于是调用函数 `func()`。 + +第二点,我们可以把函数当作参数,传入另一个函数中,比如下面这段代码: + +```python +def get_message(message): + return 'Got a message: ' + message + + +def root_call(func, message): + print(func(message)) + +root_call(get_message, 'hello world') + +# 输出 +Got a message: hello world +``` + +这个例子中,我们就把函数 `get_message` 以参数的形式,传入了函数 `root_call()` 中然后调用它。 + +第三点,我们可以在函数里定义函数,也就是函数的嵌套。这里我同样举了一个例子: + +```python +def func(message): + def get_message(message): + print('Got a message: {}'.format(message)) + return get_message(message) + +func('hello world') + +# 输出 +Got a message: hello world +``` + +这段代码中,我们在函数 `func()` 里又定义了新的函数 `get_message()`,调用后作为 `func()` 的返回值返回。 + +第四点,要知道,函数的返回值也可以是函数对象(闭包),比如下面这个例子: + +```python +def func_closure(): + def get_message(message): + print('Got a message: {}'.format(message)) + return get_message + +send_message = func_closure() +send_message('hello world') + +# 输出 +Got a message: hello world +``` + +这里,函数 `func_closure()` 的返回值是函数对象 `get_message` 本身,之后,我们将其赋予变量 `send_message`,再调用 `send_message(‘hello world’)`,最后输出了`'Got a message: hello world'`。 + +### 1.2 简单的装饰器 + +简单的复习之后,我们接下来学习今天的新知识——装饰器。按照习惯,我们可以先来看一个装饰器的简单例子: + +```python +def my_decorator(func): + def wrapper(): + print('wrapper of decorator') + func() + return wrapper + +def greet(): + print('hello world') + +greet = my_decorator(greet) +greet() + +# 输出 +wrapper of decorator +hello world +``` + +这段代码中,变量 greet 指向了内部函数 `wrapper()`,而内部函数 `wrapper()` 中又会调用原函数 `greet()`,因此,最后调用 `greet()` 时,就会先打印`'wrapper of decorator'`,然后输出`'hello world'`。 + +这里的函数 `my_decorator()` 就是一个装饰器,它把真正需要执行的函数 `greet()` 包裹在其中,并且改变了它的行为,但是原函数 `greet()` 不变。 + +事实上,上述代码在 Python 中有更简单、更优雅的表示: + +```python +def my_decorator(func): + def wrapper(): + print('wrapper of decorator') + func() + return wrapper + +@my_decorator +def greet(): + print('hello world') + +greet() +``` + +这里的`@`,我们称之为语法糖,`@my_decorator` 就相当于前面的 `greet=my_decorator(greet)` 语句,只不过更加简洁。因此,如果你的程序中有其它函数需要做类似的装饰,你只需在它们的上方加上 `@decorator` 就可以了,这样就大大提高了函数的重复利用和程序的可读性。 + +### 1.3 带有参数的装饰器 + +你或许会想到,如果原函数 `greet()` 中,有参数需要传递给装饰器怎么办? + +一个简单的办法,是可以在对应的装饰器函数 `wrapper()` 上,加上相应的参数,比如: + +```python +def my_decorator(func): + def wrapper(message): + print('wrapper of decorator') + func(message) + return wrapper + + +@my_decorator +def greet(message): + print(message) + + +greet('hello world') + +# 输出 +wrapper of decorator +hello world +``` + +不过,新的问题来了。如果我另外还有一个函数,也需要使用 `my_decorator()` 装饰器,但是这个新的函数有两个参数,又该怎么办呢?比如: + +```python +@my_decorator +def celebrate(name, message): + ... +``` + +事实上,通常情况下,我们会把 `*args` 和 `**kwargs` ,作为装饰器内部函数 `wrapper()` 的参数。`*args` 和 `**kwargs`,表示接受任意数量和类型的参数,因此装饰器就可以写成下面的形式: + +```python +def my_decorator(func): + def wrapper(*args, **kwargs): + print('wrapper of decorator') + func(*args, **kwargs) + return wrapper +``` + +### 1.4 带有自定义参数的装饰器 + +其实,装饰器还有更大程度的灵活性。刚刚说了,装饰器可以接受原函数任意类型和数量的参数,除此之外,它还可以接受自己定义的参数。 + + + + + + + + + + + + + + + + + + + + + + + + + +欢迎关注我公众号:AI悦创,有更多更好玩的等你发现! + +::: details 公众号:AI悦创【二维码】 + +![](/gzh.jpg) + +::: + +::: info AI悦创·编程一对一 + +AI悦创·推出辅导班啦,包括「Python 语言辅导班、C++ 辅导班、java 辅导班、算法/数据结构辅导班、少儿编程、pygame 游戏开发」,全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践等。当然,还有线下线上摄影课程、Photoshop、Premiere 一对一教学、QQ、微信在线,随时响应!微信:Jiabcdefh + +C++ 信息奥赛题解,长期更新!长期招收一对一中小学信息奥赛集训,莆田、厦门地区有机会线下上门,其他地区线上。微信:Jiabcdefh + +方法一:[QQ](http://wpa.qq.com/msgrd?v=3&uin=1432803776&site=qq&menu=yes) + +方法二:微信:Jiabcdefh + +::: + +![](/zsxq.jpg) + diff --git a/src/Python/Python-core-technology-and-practice/18.assets/110770f3c6a193ebdac124ca171a73c3.jpg b/src/Python/Python-core-technology-and-practice/18.assets/110770f3c6a193ebdac124ca171a73c3.jpg new file mode 100644 index 00000000000..2893be75afa Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/18.assets/110770f3c6a193ebdac124ca171a73c3.jpg differ diff --git a/src/Python/Python-core-technology-and-practice/18.md b/src/Python/Python-core-technology-and-practice/18.md new file mode 100755 index 00000000000..1c1d5e3d435 --- /dev/null +++ b/src/Python/Python-core-technology-and-practice/18.md @@ -0,0 +1,54 @@ +--- +title: 18-metaclass,是潘多拉魔盒还是阿拉丁神灯? +icon: python +date: 2023-08-01 12:24:45 +author: AI悦创 +isOriginal: true +category: Python 进阶 +tag: + - Python 进阶 +sticky: false +star: false +article: true +timeline: true +image: false +navbar: true +sidebarIcon: true +headerDepth: 5 +comment: true +lastUpdated: true +editLink: false +backToTop: true +toc: true +--- + +![](./18.assets/110770f3c6a193ebdac124ca171a73c3.jpg) + +你好,我是蔡元楠。今天我想和你分享的主题是:metaclass,是潘多拉魔盒还是阿拉丁神灯? + +Python 中有很多黑魔法,比如今天我将分享的 metaclass。我认识许多人,对于这些语言特性有两种极端的观点。 + + + +欢迎关注我公众号:AI悦创,有更多更好玩的等你发现! + +::: details 公众号:AI悦创【二维码】 + +![](/gzh.jpg) + +::: + +::: info AI悦创·编程一对一 + +AI悦创·推出辅导班啦,包括「Python 语言辅导班、C++ 辅导班、java 辅导班、算法/数据结构辅导班、少儿编程、pygame 游戏开发」,全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践等。当然,还有线下线上摄影课程、Photoshop、Premiere 一对一教学、QQ、微信在线,随时响应!微信:Jiabcdefh + +C++ 信息奥赛题解,长期更新!长期招收一对一中小学信息奥赛集训,莆田、厦门地区有机会线下上门,其他地区线上。微信:Jiabcdefh + +方法一:[QQ](http://wpa.qq.com/msgrd?v=3&uin=1432803776&site=qq&menu=yes) + +方法二:微信:Jiabcdefh + +::: + +![](/zsxq.jpg) + diff --git a/src/Python/Python-core-technology-and-practice/19.assets/2b6338efb386c02b260fe5ce20962bcb.jpg b/src/Python/Python-core-technology-and-practice/19.assets/2b6338efb386c02b260fe5ce20962bcb.jpg new file mode 100755 index 00000000000..2db959b4e2a Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/19.assets/2b6338efb386c02b260fe5ce20962bcb.jpg differ diff --git a/src/Python/Python-core-technology-and-practice/19.md b/src/Python/Python-core-technology-and-practice/19.md new file mode 100755 index 00000000000..46855afe5e8 --- /dev/null +++ b/src/Python/Python-core-technology-and-practice/19.md @@ -0,0 +1,386 @@ +--- +title: 19-深入理解迭代器和生成器 +icon: python +date: 2023-01-24 09:18:29 +author: AI悦创 +isOriginal: true +category: Python 进阶 +tag: + - Python 进阶 +sticky: false +star: false +article: true +timeline: true +image: false +navbar: true +sidebarIcon: true +headerDepth: 5 +comment: true +lastUpdated: true +editLink: false +backToTop: true +toc: true +--- + +![img](./19.assets/2b6338efb386c02b260fe5ce20962bcb.jpg) + +你好,我是悦创。 + +在第一次接触 Python 的时候,你可能写过类似 `for i in [2, 3, 5, 7, 11, 13]: print(i)` 这样的语句。`for in` 语句理解起来很直观形象,比起 C++ 和 java 早期的 `for (int i = 0; i < n; i ++) printf("%d\n", a[i])` 这样的语句,不知道简洁清晰到哪里去了。 + +但是,你想过 Python 在处理 `for in` 语句的时候,具体发生了什么吗?什么样的对象可以被 `for in` 来枚举呢? + +这一节课,我们深入到 Python 的容器类型实现底层去走走,了解一种叫做迭代器和生成器的东西。 + +## 你肯定用过的容器、可迭代对象和迭代器 + +容器这个概念非常好理解。我们说过,在 Python 中一切皆对象,对象的抽象就是类,而对象的集合就是容器。 + +列表(`list: [0, 1, 2]`),元组(`tuple: (0, 1, 2)`),字典(`dict: {0:0, 1:1, 2:2}`),集合(`set: set([0, 1, 2])`)都是容器。 + +**对于容器,你可以很直观地想象成多个元素在一起的单元;而不同容器的区别,正是在于内部数据结构的实现方法。** + +然后,你就可以针对不同场景,选择不同时间和空间复杂度的容器。 + +所有的容器都是可迭代的(iterable)。这里的迭代,和枚举不完全一样。迭代可以想象成是你去买苹果,卖家并不告诉你他有多少库存。这样,每次你都需要告诉卖家,你要一个苹果,然后卖家采取行为:要么给你拿一个苹果;要么告诉你,苹果已经卖完了。你并不需要知道,卖家在仓库是怎么摆放苹果的。 + +严谨地说,迭代器(iterator)提供了一个 next 的方法。调用这个方法后,你要么得到这个容器的下一个对象,要么得到一个 StopIteration 的错误(苹果卖完了)。你不需要像列表一样指定元素的索引,因为字典和集合这样的容器并没有索引一说。比如,字典采用哈希表实现,那么你就只需要知道,next 函数可以不重复不遗漏地一个一个拿到所有元素即可。 + +而可迭代对象,通过 `iter()` 函数返回一个迭代器,再通过 `next()` 函数就可以实现遍历。`for in` 语句将这个过程隐式化,所以,你只需要知道它大概做了什么就行了。 + +我们来看下面这段代码,主要向你展示怎么判断一个对象是否可迭代。当然,这还有另一种做法,是 `isinstance(obj, Iterable)`。 + +```python +def is_iterable(param): + try: + iter(param) + return True + except TypeError: + return False + +params = [ + 1234, + '1234', + [1, 2, 3, 4], + set([1, 2, 3, 4]), + {1:1, 2:2, 3:3, 4:4}, + (1, 2, 3, 4) +] + +for param in params: + print('{} is iterable? {}'.format(param, is_iterable(param))) + +########## 输出 ########## + +1234 is iterable? False +1234 is iterable? True +[1, 2, 3, 4] is iterable? True +{1, 2, 3, 4} is iterable? True +{1: 1, 2: 2, 3: 3, 4: 4} is iterable? True +(1, 2, 3, 4) is iterable? True +``` + +通过这段代码,你就可以知道,给出的类型中,除了数字 1234 之外,其它的数据类型都是可迭代的。 + +## 生成器,又是什么? + +据我所知,很多人对生成器这个概念会比较陌生,因为生成器在很多常用语言中,并没有相对应的模型。 + +这里,你只需要记着一点:**生成器是懒人版本的迭代器。** + +我们知道,在迭代器中,如果我们想要枚举它的元素,这些元素需要事先生成。这里,我们先来看下面这个简单的样例。 + +```python +import os +import psutil # process and system utilities + +# 显示当前 python 程序占用的内存大小 +def show_memory_info(hint): + pid = os.getpid() + p = psutil.Process(pid) + + info = p.memory_full_info() + memory = info.uss / 1024. / 1024 + print('{} memory used: {} MB'.format(hint, memory)) +``` + +```python +def test_iterator(): + show_memory_info('initing iterator') + list_1 = [i for i in range(100000000)] + show_memory_info('after iterator initiated') + print(sum(list_1)) + show_memory_info('after sum called') + +def test_generator(): + show_memory_info('initing generator') + list_2 = (i for i in range(100000000)) + show_memory_info('after generator initiated') + print(sum(list_2)) + show_memory_info('after sum called') + +%time test_iterator() +%time test_generator() + +########## 输出 ########## + +initing iterator memory used: 48.9765625 MB +after iterator initiated memory used: 3920.30078125 MB +4999999950000000 +after sum called memory used: 3920.3046875 MB +Wall time: 17 s +initing generator memory used: 50.359375 MB +after generator initiated memory used: 50.359375 MB +4999999950000000 +after sum called memory used: 50.109375 MB +Wall time: 12.5 s +``` + +声明一个迭代器很简单,`[i for i in range(100000000)]` 就可以生成一个包含一亿元素的列表。每个元素在生成后都会保存到内存中,你通过代码可以看到,它们占用了巨量的内存,内存不够的话就会出现 OOM 错误。 + +不过,我们并不需要在内存中同时保存这么多东西,比如对元素求和,我们只需要知道每个元素在相加的那一刻是多少就行了,用完就可以扔掉了。 + +于是,生成器的概念应运而生,在你调用 next() 函数的时候,才会生成下一个变量。生成器在 Python 的写法是用小括号括起来,`(i for i in range(100000000))`,即初始化了一个生成器。 + +这样一来,你可以清晰地看到,生成器并不会像迭代器一样占用大量内存,只有在被使用的时候才会调用。而且生成器在初始化的时候,并不需要运行一次生成操作,相比于 `test_iterator()` ,`test_generator()` 函数节省了一次生成一亿个元素的过程,因此耗时明显比迭代器短。 + +到这里,你可能说,生成器不过如此嘛,我有的是钱,不就是多占一些内存和计算资源嘛,我多出点钱就是了呗。 + +哪怕你是土豪,请坐下先喝点茶,再听我继续讲完,这次,我们来实现一个自定义的生成器。 + +## 生成器,还能玩什么花样? + +数学中有一个恒等式,`(1 + 2 + 3 + ... + n)^2 = 1^3 + 2^3 + 3^3 + ... + n^3`,想必你高中就应该学过它。现在,我们来验证一下这个公式的正确性。老规矩,先放代码,你先自己阅读一下,看不懂的也不要紧,接下来我再来详细讲解。 + +```python +def generator(k): + i = 1 + while True: + yield i ** k + i += 1 + +gen_1 = generator(1) +gen_3 = generator(3) +print(gen_1) +print(gen_3) + +def get_sum(n): + sum_1, sum_3 = 0, 0 + for i in range(n): + next_1 = next(gen_1) + next_3 = next(gen_3) + print('next_1 = {}, next_3 = {}'.format(next_1, next_3)) + sum_1 += next_1 + sum_3 += next_3 + print(sum_1 * sum_1, sum_3) + +get_sum(8) + +########## 输出 ########## + + + +next_1 = 1, next_3 = 1 +next_1 = 2, next_3 = 8 +next_1 = 3, next_3 = 27 +next_1 = 4, next_3 = 64 +next_1 = 5, next_3 = 125 +next_1 = 6, next_3 = 216 +next_1 = 7, next_3 = 343 +next_1 = 8, next_3 = 512 +1296 1296 +``` + +这段代码中,你首先注意一下 `generator()` 这个函数,它返回了一个生成器。 + +接下来的 yield 是魔术的关键。对于初学者来说,你可以理解为,函数运行到这一行的时候,程序会从这里暂停,然后跳出,不过跳到哪里呢?答案是 `next()` 函数。那么 `i ** k` 是干什么的呢?它其实成了 `next() `函数的返回值。 + +这样,每次 `next(gen)` 函数被调用的时候,暂停的程序就又复活了,从 yield 这里向下继续执行;同时注意,局部变量 i 并没有被清除掉,而是会继续累加。我们可以看到 `next_1` 从 1 变到 8,`next_3` 从 1 变到 512。 + +聪明的你应该注意到了,这个生成器居然可以一直进行下去!没错,事实上,迭代器是一个有限集合,生成器则可以成为一个无限集。我只管调用 `next()`,生成器根据运算会自动生成新的元素,然后返回给你,非常便捷。 + +到这里,土豪同志应该也坐不住了吧,那么,还能再给力一点吗? + +别急,我们再来看一个问题:给定一个 list 和一个指定数字,求这个数字在 list 中的位置。 + +下面这段代码你应该不陌生,也就是常规做法,枚举每个元素和它的 index,判断后加入 result,最后返回。 + +```python +def index_normal(L, target): + result = [] + for i, num in enumerate(L): + if num == target: + result.append(i) + return result + +print(index_normal([1, 6, 2, 4, 5, 2, 8, 6, 3, 2], 2)) + +########## 输出 ########## + +[2, 5, 9] +``` + +那么使用迭代器可以怎么做呢?二话不说,先看代码。 + +```python +def index_generator(L, target): + for i, num in enumerate(L): + if num == target: + yield i + +print(list(index_generator([1, 6, 2, 4, 5, 2, 8, 6, 3, 2], 2))) + +########## 输出 ########## + +[2, 5, 9] +``` + +聪明的你应该看到了明显的区别,我就不做过多解释了。唯一需要强调的是, `index_generator` 会返回一个 Generator 对象,需要使用 list 转换为列表后,才能用 print 输出。 + +这里我再多说两句。在 Python 语言规范中,用更少、更清晰的代码实现相同功能,一直是被推崇的做法,因为这样能够很有效提高代码的可读性,减少出错概率,也方便别人快速准确理解你的意图。当然,要注意,这里“更少”的前提是清晰,而不是使用更多的魔术操作,虽说减少了代码却反而增加了阅读的难度。 + +回归正题。接下来我们再来看一个问题:给定两个序列,判定第一个是不是第二个的子序列。(LeetCode 链接如下:[https://leetcode.com/problems/is-subsequence/](https://leetcode.com/problems/is-subsequence/) ) + +[https://leetcode.cn/problems/is-subsequence/](https://leetcode.cn/problems/is-subsequence/) + +先来解读一下这个问题本身。序列就是列表,子序列则指的是,一个列表的元素在第二个列表中都按顺序出现,但是并不必挨在一起。举个例子,`[1, 3, 5]` 是 `[1, 2, 3, 4, 5]` 的子序列,`[1, 4, 3]` 则不是。 + +要解决这个问题,常规算法是贪心算法。我们维护两个指针指向两个列表的最开始,然后对第二个序列一路扫过去,如果某个数字和第一个指针指的一样,那么就把第一个指针前进一步。第一个指针移出第一个序列最后一个元素的时候,返回 True,否则返回 False。 + +不过,这个算法正常写的话,写下来怎么也得十行左右。 + +那么如果我们用迭代器和生成器呢? + +```python +def is_subsequence(a, b): + b = iter(b) + return all(i in b for i in a) + +print(is_subsequence([1, 3, 5], [1, 2, 3, 4, 5])) +print(is_subsequence([1, 4, 3], [1, 2, 3, 4, 5])) + +########## 输出 ########## + +True +False +``` + +这简短的几行代码,你是不是看得一头雾水,不知道发生了什么? + +来,我们先把这段代码复杂化,然后一步步看。 + +```python +def is_subsequence(a, b): + b = iter(b) + print(b) + + gen = (i for i in a) + print(gen) + + for i in gen: + print(i) + + gen = ((i in b) for i in a) + print(gen) + + for i in gen: + print(i) + + return all(((i in b) for i in a)) + +print(is_subsequence([1, 3, 5], [1, 2, 3, 4, 5])) +print(is_subsequence([1, 4, 3], [1, 2, 3, 4, 5])) + +########## 输出 ########## + + +. at 0x000001E70651C570> +1 +3 +5 +. at 0x000001E70651C5E8> +True +True +True +False + +. at 0x000001E70651C5E8> +1 +4 +3 +. at 0x000001E70651C570> +True +True +False +False +``` + +首先,第二行的 `b = iter(b)` ,把列表 b 转化成了一个迭代器,这里我先不解释为什么要这么做。 + +接下来的 `gen = (i for i in a)` 语句很好理解,产生一个生成器,这个生成器可以遍历对象 a,因此能够输出 `1, 3, 5`。而 `(i in b)` 需要好好揣摩,这里你是不是能联想到 `for in` 语句? + +没错,这里的 `(i in b)`,大致等价于下面这段代码: + +```python +while True: + val = next(b) + if val == i: + yield True +``` + +这里非常巧妙地利用生成器的特性,`next()` 函数运行的时候,保存了当前的指针。比如再看下面这个示例: + +```python +b = (i for i in range(5)) + +print(2 in b) +print(4 in b) +print(3 in b) + +########## 输出 ########## + +True +True +False +``` + +至于最后的 `all()` 函数,就很简单了。它用来判断一个迭代器的元素是否全部为 True,如果是则返回 True,否则就返回 False。 + +于是到此,我们就很优雅地解决了这道面试题。不过你一定注意,面试的时候尽量不要用这种技巧,因为你的面试官有可能并不知道生成器的用法,他们也没有看过我的专栏。不过,在这个技术知识点上,在实际工作的应用上,你已经比很多人更加熟练了。继续加油! + +## 总结 + +总结一下,今天我们讲了四种不同的对象,分别是容器、可迭代对象、迭代器和生成器。 + +- 容器是可迭代对象,可迭代对象调用 `iter()` 函数,可以得到一个迭代器。迭代器可以通过 `next()` 函数来得到下一个元素,从而支持遍历。 +- 生成器是一种特殊的迭代器(注意这个逻辑关系反之不成立)。使用生成器,你可以写出来更加清晰的代码;合理使用生成器,可以降低内存占用、优化程序结构、提高程序速度。 +- 生成器在 Python 2 的版本上,是协程的一种重要实现方式;而 Python 3.5 引入 async await 语法糖后,生成器实现协程的方式就已经落后了。我们会在下节课,继续深入讲解 Python 协程。 + +## 思考题 + +最后给你留一个思考题。对于一个有限元素的生成器,如果迭代完成后,继续调用 `next()` ,会发生什么呢?生成器可以遍历多次吗? + +欢迎留言和我讨论,也欢迎你把这篇文章分享给你的同事、朋友,一起在交流中进步。 + +欢迎关注我公众号:AI悦创,有更多更好玩的等你发现! + +::: details 公众号:AI悦创【二维码】 + +![](/gzh.jpg) + +::: + +::: info AI悦创·编程一对一 + +AI悦创·推出辅导班啦,包括「Python 语言辅导班、C++ 辅导班、java 辅导班、算法/数据结构辅导班、少儿编程、pygame 游戏开发、Linux、Web」,全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践等。当然,还有线下线上摄影课程、Photoshop、Premiere 一对一教学、QQ、微信在线,随时响应!微信:Jiabcdefh + +C++ 信息奥赛题解,长期更新!长期招收一对一中小学信息奥赛集训,莆田、厦门地区有机会线下上门,其他地区线上。微信:Jiabcdefh + +方法一:[QQ](http://wpa.qq.com/msgrd?v=3&uin=1432803776&site=qq&menu=yes) + +方法二:微信:Jiabcdefh + +::: + +![](/zsxq.jpg) + diff --git a/src/Python/Python-core-technology-and-practice/20.assets/20210108002250273.png b/src/Python/Python-core-technology-and-practice/20.assets/20210108002250273.png new file mode 100755 index 00000000000..1eeff82efbf Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/20.assets/20210108002250273.png differ diff --git a/src/Python/Python-core-technology-and-practice/20.assets/e04a4e460bd42f7732d1c56b1fc165cd.jpg b/src/Python/Python-core-technology-and-practice/20.assets/e04a4e460bd42f7732d1c56b1fc165cd.jpg new file mode 100755 index 00000000000..b8388dbf124 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/20.assets/e04a4e460bd42f7732d1c56b1fc165cd.jpg differ diff --git a/src/Python/Python-core-technology-and-practice/20.assets/image-20230130101101621.png b/src/Python/Python-core-technology-and-practice/20.assets/image-20230130101101621.png new file mode 100755 index 00000000000..8daed273dce Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/20.assets/image-20230130101101621.png differ diff --git a/src/Python/Python-core-technology-and-practice/20.assets/image-20230130101219537.png b/src/Python/Python-core-technology-and-practice/20.assets/image-20230130101219537.png new file mode 100755 index 00000000000..7e059f2647f Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/20.assets/image-20230130101219537.png differ diff --git a/src/Python/Python-core-technology-and-practice/20.assets/pixel-jeff-homecoming.gif b/src/Python/Python-core-technology-and-practice/20.assets/pixel-jeff-homecoming.gif new file mode 100755 index 00000000000..637df829d47 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/20.assets/pixel-jeff-homecoming.gif differ diff --git a/src/Python/Python-core-technology-and-practice/20.md b/src/Python/Python-core-technology-and-practice/20.md new file mode 100755 index 00000000000..1a5cf74c92f --- /dev/null +++ b/src/Python/Python-core-technology-and-practice/20.md @@ -0,0 +1,1294 @@ +--- +title: 20-揭秘 Python 协程 +icon: python +date: 2023-01-24 10:27:51 +author: AI悦创 +isOriginal: true +category: Python 进阶 +tag: + - Python 进阶 +sticky: false +star: false +article: true +timeline: true +image: false +navbar: true +sidebarIcon: true +headerDepth: 5 +comment: true +lastUpdated: true +editLink: false +backToTop: true +toc: true +--- + +![img](./20.assets/e04a4e460bd42f7732d1c56b1fc165cd.jpg) + +![](./20.assets/pixel-jeff-homecoming.gif) + +你好,我是悦创。 + +上一节课的最后,我们留下一个小小的悬念:生成器在 Python 2 中还扮演了一个重要角色,就是用来实现 Python 协程。 + +那么首先你要明白,什么是协程? + +协程是实现并发编程的一种方式。一说并发,你肯定想到了多线程 / 多进程模型,没错,多线程 / 多进程,正是解决并发问题的经典模型之一。最初的互联网世界,多线程 / 多进程在服务器并发中,起到举足轻重的作用。 + +随着互联网的快速发展,你逐渐遇到了 C10K 瓶颈,也就是同时连接到服务器的客户达到了一万个。于是很多代码跑崩了,进程上下文切换占用了大量的资源,线程也顶不住如此巨大的压力,这时, NGINX 带着事件循环出来拯救世界了。 + +如果将多进程 / 多线程类比为起源于唐朝的藩镇割据,那么事件循环,就是宋朝加强的中央集权制。事件循环启动一个统一的调度器,让调度器来决定一个时刻去运行哪个任务,于是省却了多线程中启动线程、管理线程、同步锁等各种开销。同一时期的 NGINX,在高并发下能保持低资源低消耗高性能,相比 Apache 也支持更多的并发连接。 + +再到后来,出现了一个很有名的名词,叫做回调地狱(callback hell),手撸过 JavaScript 的朋友肯定知道我在说什么。我们大家惊喜地发现,这种工具完美地继承了事件循环的优越性,同时还能提供 async / await 语法糖,解决了执行性和可读性共存的难题。于是,协程逐渐被更多人发现并看好,也有越来越多的人尝试用 Node.js 做起了后端开发。(讲个笑话,JavaScript 是一门编程语言。) + +回到我们的 Python。使用生成器,是 Python 2 开头的时代实现协程的老方法了,Python 3.7 提供了新的基于 asyncio 和 async / await 的方法。我们这节课,同样的,跟随时代,抛弃掉不容易理解、也不容易写的旧的基于生成器的方法,直接来讲新方法。 + +我们先从一个爬虫实例出发,用清晰的讲解思路,带你结合实战来搞懂这个不算特别容易理解的概念。之后,我们再由浅入深,直击协程的核心。 + +## 1. 从一个爬虫说起 + +爬虫,就是互联网的蜘蛛,在搜索引擎诞生之时,与其一同来到世上。爬虫每秒钟都会爬取大量的网页,提取关键信息后存储在数据库中,以便日后分析。爬虫有非常简单的 Python 十行代码实现,也有 Google 那样的全球分布式爬虫的上百万行代码,分布在内部上万台服务器上,对全世界的信息进行嗅探。 + +话不多说,我们先看一个简单的爬虫例子: + +```python +import time + +def crawl_page(url): + print('crawling {}'.format(url)) + sleep_time = int(url.split('_')[-1]) + time.sleep(sleep_time) + print('OK {}'.format(url)) + +def main(urls): + for url in urls: + crawl_page(url) + +%time main(['url_1', 'url_2', 'url_3', 'url_4']) + +########## 输出 ########## + +crawling url_1 +OK url_1 +crawling url_2 +OK url_2 +crawling url_3 +OK url_3 +crawling url_4 +OK url_4 +Wall time: 10 s +``` + +(注意:本节的主要目的是协程的基础概念,因此我们简化爬虫的 scrawl_page 函数为休眠数秒,休眠时间取决于 url 最后的那个数字。) + +这是一个很简单的爬虫,`main()` 函数执行时,调取 `crawl_page()` 函数进行网络通信,经过若干秒等待后收到结果,然后执行下一个。 + +看起来很简单,但你仔细一算,它也占用了不少时间,五个页面分别用了 1 秒到 4 秒的时间,加起来一共用了 10 秒。这显然效率低下,该怎么优化呢? + +于是,一个很简单的思路出现了——我们这种爬取操作,完全可以并发化。我们就来看看使用协程怎么写。 + +```python +import asyncio + +async def crawl_page(url): + print('crawling {}'.format(url)) + sleep_time = int(url.split('_')[-1]) + await asyncio.sleep(sleep_time) + print('OK {}'.format(url)) + +async def main(urls): + for url in urls: + await crawl_page(url) + +%time asyncio.run(main(['url_1', 'url_2', 'url_3', 'url_4'])) + +########## 输出 ########## + +crawling url_1 +OK url_1 +crawling url_2 +OK url_2 +crawling url_3 +OK url_3 +crawling url_4 +OK url_4 +Wall time: 10 s +``` + +看到这段代码,你应该发现了,在 Python 3.7 以上版本中,使用协程写异步程序非常简单。 + +首先来看 import asyncio,这个库包含了大部分我们实现协程所需的魔法工具。 + +async 修饰词声明异步函数,于是,这里的 crawl_page 和 main 都变成了异步函数。而调用异步函数,我们便可得到一个协程对象(coroutine object)。 + +举个例子,如果你 `print(crawl_page(''))`,便会输出,提示你这是一个 Python 的协程对象,而并不会真正执行这个函数。 + +再来说说协程的执行。执行协程有多种方法,这里我介绍一下常用的三种。 + +首先,我们可以通过 await 来调用。await 执行的效果,和 Python 正常执行是一样的,也就是说程序会阻塞在这里,进入被调用的协程函数,执行完毕返回后再继续,而这也是 await 的字面意思。代码中 `await asyncio.sleep(sleep_time)` 会在这里休息若干秒,`await crawl_page(url) `则会执行 `crawl_page()` 函数。 + +其次,我们可以通过 `asyncio.create_task()` 来创建任务,这个我们下节课会详细讲一下,你先简单知道即可。 + +最后,我们需要 `asyncio.run` 来触发运行。`asyncio.run` 这个函数是 Python 3.7 之后才有的特性,可以让 Python 的协程接口变得非常简单,你不用去理会事件循环怎么定义和怎么使用的问题(我们会在下面讲)。一个非常好的编程规范是,`asyncio.run(main())` 作为主程序的入口函数,在程序运行周期内,只调用一次 `asyncio.run`。 + +这样,你就大概看懂了协程是怎么用的吧。不妨试着跑一下代码,欸,怎么还是 10 秒? + +10 秒就对了,还记得上面所说的,await 是同步调用,因此, `crawl_page(url)` 在当前的调用结束之前,是不会触发下一次调用的。于是,这个代码效果就和上面完全一样了,相当于我们用异步接口写了个同步代码。 + +现在又该怎么办呢? + +其实很简单,也正是我接下来要讲的协程中的一个重要概念,任务(Task)。老规矩,先看代码。 + +::: code-tabs#python + +@tab old + +```python +import asyncio + +async def crawl_page(url): + print('crawling {}'.format(url)) + sleep_time = int(url.split('_')[-1]) + await asyncio.sleep(sleep_time) + print('OK {}'.format(url)) + +async def main(urls): + tasks = [asyncio.create_task(crawl_page(url)) for url in urls] + for task in tasks: + await task + +%time asyncio.run(main(['url_1', 'url_2', 'url_3', 'url_4'])) + +########## 输出 ########## + +crawling url_1 +crawling url_2 +crawling url_3 +crawling url_4 +OK url_1 +OK url_2 +OK url_3 +OK url_4 +Wall time: 3.99 s +``` + +@tab Run in Jupyter Notebook + +```python {20-22} +import time +import asyncio + + +async def crawl_page(url): + print("crawling {}".format(url)) + sleep_time = int(url.split("_")[-1]) + # time.sleep(sleep_time) + await asyncio.sleep(sleep_time) + print("OK {}".format(url)) + + +async def main(urls): + tasks = [asyncio.create_task(crawl_page(url)) for url in urls] + for task in tasks: + await task + # for url in urls: + # await crawl_page(url) +# %time asyncio.run(main(['url_1', 'url_2', 'url_3', 'url_4'])) +start_time = time.time() +await main(['url_1', 'url_2', 'url_3', 'url_4']) +print(time.time() - start_time) + +########## 输出 ########## +crawling url_1 +crawling url_2 +crawling url_3 +crawling url_4 +OK url_1 +OK url_2 +OK url_3 +OK url_4 +4.005578994750977 +``` + +@tab Python file + +```python {20-22} +import time +import asyncio + + +async def crawl_page(url): + print("crawling {}".format(url)) + sleep_time = int(url.split("_")[-1]) + # time.sleep(sleep_time) + await asyncio.sleep(sleep_time) + print("OK {}".format(url)) + + +async def main(urls): + tasks = [asyncio.create_task(crawl_page(url)) for url in urls] + for task in tasks: + await task + # for url in urls: + # await crawl_page(url) +# %time asyncio.run(main(['url_1', 'url_2', 'url_3', 'url_4'])) +start_time = time.time() +asyncio.run(main(['url_1', 'url_2', 'url_3', 'url_4'])) +print(time.time() - start_time) +``` + +::: + +你可以看到,我们有了协程对象后,便可以通过 `asyncio.create_task` 来创建任务。任务创建后很快就会被调度执行,这样,我们的代码也不会阻塞在任务这里。所以,我们要等所有任务都结束才行,用 `for task in tasks: await task` 即可。 + +这次,你就看到效果了吧,**结果显示,运行总时长等于运行时间最长的爬虫。** + +::: info 探究协程原因 + +**举个例子🌰:** + +目标:小 Cava 要做一道美味的鱼汤。 + +**正常的流程:** + +1. 🐟杀鱼「3min」 +2. 🫀清洗内脏「2min」 +3. 🍳锅热入凉油「2min」 +4. :flags:加入备好的:生姜、大葱「3min」 +5. :high_brightness:煎至:两面金黄「5min」 +6. :jack_o_lantern:加入凉水煮,等到煮开「15min」 +7. 开吃~ +8. PS:煮开水:15min +9. ⌚️Total Time:30 min + +> 忽略细节部分,主要理解协程真谛。 + +**更好的流程:** + +1. :jack_o_lantern:煮开水:15min + 1. 🐟杀鱼「3min」 + 2. 🫀清洗内脏「2min」 + 3. 🍳锅热入凉油「2min」 + 4. :flags:加入备好的:生姜、大葱「3min」 + 5. :high_brightness:煎至:两面金黄「5min」 +2. 🐟鱼加入煮开的开水,再煮 2min,直接出锅~「鱼不是程序,还是得煮出鱼汤的,而计算机就不用。」 +3. 开吃~ +4. ⌚️Total Time:17min「要是计算机的话,不需要添加煮鱼的 2min」 + +--- + +煮开水被第二种方法,类似:挂起。那挂起就没有在煮开水吗?——No,还是在继续煮开水,不会因为你挂起而停止煮开水。 + +那协程是什么意思?就是把耗时的、请求慢的、下载慢的,挂起继续后台下载,在下载的时候呢,去请求其他的链接🔗。 + +::: + +当然,你也可以想一想,这里用多线程应该怎么写?而如果需要爬取的页面有上万个又该怎么办呢?再对比下协程的写法,谁更清晰自是一目了然。 + +其实,对于执行 tasks,还有另一种做法: + +::: code-tabs#python + +@tab old + +```python +import asyncio + +async def crawl_page(url): + print('crawling {}'.format(url)) + sleep_time = int(url.split('_')[-1]) + await asyncio.sleep(sleep_time) + print('OK {}'.format(url)) + +async def main(urls): + tasks = [asyncio.create_task(crawl_page(url)) for url in urls] + await asyncio.gather(*tasks) + +%time asyncio.run(main(['url_1', 'url_2', 'url_3', 'url_4'])) + +########## 输出 ########## + +crawling url_1 +crawling url_2 +crawling url_3 +crawling url_4 +OK url_1 +OK url_2 +OK url_3 +OK url_4 +Wall time: 4.01 s +``` + +@tab Pycharm + +```python +import asyncio, time + + +async def crawl_page(url): + print('crawling {}'.format(url)) + sleep_time = int(url.split('_')[-1]) + await asyncio.sleep(sleep_time) + print('OK {}'.format(url)) + + +async def main(urls): + tasks = [asyncio.create_task(crawl_page(url)) for url in urls] + await asyncio.gather(*tasks) + +start_time = time.time() +asyncio.run(main(['url_1', 'url_2', 'url_3', 'url_4'])) +print(time.time() - start_time) +``` + +::: + +这里的代码也很好理解。唯一要注意的是,`*tasks` 解包列表,将列表变成了函数的参数;与之对应的是, `** dict` 将字典变成了函数的参数。 + +另外,`asyncio.create_task`,`asyncio.run` 这些函数都是 Python 3.7 以上的版本才提供的,自然,相比于旧接口它们也更容易理解和阅读。 + +## 2. 补充 + +### 2.1 asyncio.gather vs asyncio.wait + +在上面的内容,我们知道有:`asyncio.gather` 与 `asyncio.wait`,他们都可以让多个协程并发执行。那为什么提供 2 个方法呢?它们有什么区别,适用场景是怎么样的呢?其实我之前也是有点困惑,直到我读了 asyncio 的源码。我们先看 2 个协程的例子: + +```python +import asyncio + + +async def a(): + print('Suspending a') + await asyncio.sleep(3) + print('Resuming a') + return 'A' + + +async def b(): + print('Suspending b') + await asyncio.sleep(1) + print('Resuming b') + return 'B' +``` + +在 IPython 里面用 gather 执行一下: + +::: tabs + +@tab Ipython + +```python +In [2]: return_value_a, return_value_b = await asyncio.gather(a(), b()) +Suspending a +Suspending b +Resuming b +Resuming a + +In [3]: return_value_a, return_value_b +Out[3]: ('A', 'B') +``` + +Ok,`asyncio.gather` 方法的名字说明了它的用途,gather 的意思是「搜集」,也就是能够收集协程的结果,而且要注意,它会按输入协程的顺序保存的对应协程的执行结果。 + +接着我们说 `asyncio.await` ,先执行一下: + +```python +In [4]: done, pending = await asyncio.wait([a(), b()]) +:1: DeprecationWarning: The explicit passing of coroutine objects to asyncio.wait() is deprecated since Python 3.8, and scheduled for removal in Python 3.11. + done, pending = await asyncio.wait([a(), b()]) +Suspending a +Suspending b +Resuming b +Resuming a + +In [5]: done +Out[5]: +{:4> result='A'>, + :11> result='B'>} + +In [6]: pending +Out[6]: set() + +In [7]: task = list(done)[0] + +In [8]: task +Out[8]: :11> result='B'> + +In [9]: task.result +Out[9]: + +In [10]: task.result() +Out[10]: 'B' +``` + +`asyncio.wait` 的返回值有 2 项,第一项表示完成的任务列表 (done),第二项表示等待 (Future) 完成的任务列表 (pending),每个任务都是一个 Task 实例,由于这 2 个任务都已经完成,所以可以执行 `task.result()` 获得协程返回值。 + +Ok, 说到这里,我总结下它俩的区别的第一层区别: + +1. `asyncio.gather` 封装的 Task 全程黑盒,只告诉你协程结果。 +2. `asyncio.wait` 会返回封装的 Task (包含已完成和挂起的任务),如果你关注协程执行结果你需要从对应 Task 实例里面用 result 方法自己拿。 + +为什么说「第一层区别」,`asyncio.wait` 看名字可以理解为「等待」,所以返回值的第二项是 pending 集合,但是看上面的例子,pending 是空集合,那么在什么情况下,pending 里面不为空呢?这就是第二层区别:`asyncio.wait` 支持选择返回的时机。 + +`asyncio.wait` 支持一个接收参数 `return_when`,在默认情况下,`asyncio.wait` 会等待全部任务完成 `(return_when='ALL_COMPLETED')`,它还支持 `FIRST_COMPLETED`(第一个协程完成就返回)和 `FIRST_EXCEPTION`(出现第一个异常就返回): + +```python +In [11]: done, pending = await asyncio.wait([a(), b()], return_when=asyncio.tasks.FIRST_COMPLETED) +:1: DeprecationWarning: The explicit passing of coroutine objects to asyncio.wait() is deprecated since Python 3.8, and scheduled for removal in Python 3.11. + done, pending = await asyncio.wait([a(), b()], return_when=asyncio.tasks.FIRST_COMPLETED) +Suspending a +Suspending b +Resuming b + +In [12]: done +Out[12]: {:11> result='B'>} + +In [13]: pending +Out[13]: {:6> wait_for=>} + +In [14]: type(done), type(pending) +Out[14]: (set, set) +``` + +看到了吧,这次只有协程 b 完成了,协程 a 还是 pending 状态。 + +在大部分情况下,用 `asyncio.gather` 是足够的,如果你有特殊需求,可以选择 `asyncio.wait`,举 2 个例子: + +1. 需要拿到封装好的 Task,以便取消或者添加成功回调等 +2. 业务上需要 `FIRST_COMPLETED/FIRST_EXCEPTION` 即返回的 + +@tab Pycharm + +```python +import asyncio + + +async def a(): + print('Suspending a') + await asyncio.sleep(3) + print('Resuming a') + return 'A' + + +async def b(): + print('Suspending b') + await asyncio.sleep(1) + print('Resuming b') + return 'B' + + +renwu = [a(), b()] +loops = asyncio.get_event_loop() +return_value_a, return_value_b = loops.run_until_complete(asyncio.wait(renwu)) +# return_value_a, return_value_b = loops.run_until_complete(asyncio.gather(*renwu)) +# return_value_a, return_value_b = loops.run_until_complete(asyncio.gather(a(), b())) +print(return_value_a, return_value_b) +``` + +@tab coro1.py + +```python +import asyncio + +async def a(): + print('Suspending a') + await asyncio.sleep(3) + print('Resuming a') + return 'A' + + +async def b(): + print('Suspending b') + await asyncio.sleep(1) + print('Resuming b') + return 'B' + + +async def c1(): + print(await asyncio.gather(a(), b())) + + +async def c2(): + print(await asyncio.wait([a(), b()])) + + +async def c3(): + print(await asyncio.wait( + [a(), b()], + return_when=asyncio.tasks.FIRST_COMPLETED)) + + +if __name__ == '__main__': + for f in (c1, c2, c3): + asyncio.run(f()) +``` + +::: + +### 2.2 asyncio.create_task vs loop.create_task vs asyncio.ensure_future + +创建一个 Task 一共有 3 种方法,如这小节的标题。从 Python 3.7 开始可以统一的使用更高阶的 `asyncio.create_task` 。其实`asyncio.create_task` 就是用的 `loop.create_task` : + +```python +from asyncio import events + + +def create_task(coro): + loop = events.get_running_loop() + return loop.create_task(coro) +``` + +`loop.create_task` 接受的参数需要是一个协程,但是 `asyncio.ensure_future` 除了接受协程,还可以是 Future 对象或者 awaitable 对象: + +1. 如果参数是协程,其实底层还是用的 `loop.create_task`,返回 Task 对象 +2. 如果是 Future 对象会直接返回 +3. 如果是一个 awaitable 对象会 await 这个对象的 `__await__` 方法,再执行一次 `ensure_future`,最后返回 Task 或者 Future + +所以就像 `ensure_future` 名字说的,确保这个是一个 Future 对象:Task 是 Future 子类,前面说过一般情况下开发者不需要自己创建 Future + +其实前面说的 `asyncio.wait` 和 `asyncio.gather` 里面都用了 `asyncio.ensure_future` 。对于绝大多数场景要并发执行的是协程,所以直接用 `asyncio.create_task` 就足够了~ + +### 2.3 shield + +接着说 `asyncio.shield`,用它可以屏蔽取消操作。一直到这里,我们还没有见识过 Task 的取消。看一个例子: + +```python +In : loop = asyncio.get_event_loop() + +In : task1 = loop.create_task(a()) + +In : task2 = loop.create_task(b()) + +In : task1.cancel() +Out: True + +In : await asyncio.gather(task1, task2) +Suspending a +Suspending b +--------------------------------------------------------------------------- +CancelledError Traceback (most recent call last) +cell_name in async-def-wrapper() + +CancelledError: +``` + +在上面的例子中,task1 被取消了后再用 `asyncio.gather` 收集结果,直接抛 CancelledError 错误了。这里有个细节,gather 支持 `return_exceptions` 参数: + +```python +In : await asyncio.gather(task1, task2, return_exceptions=True) +Out: [concurrent.futures._base.CancelledError(), 'B'] +``` + +可以看到,task2 依然会执行完成,但是 task1 的返回值是一个 CancelledError 错误,也就是任务被取消了。如果一个创建后就不希望被任何情况取消,可以使用 `asyncio.shield` 保护任务能顺利完成: + +```python +In : task1 = asyncio.shield(a()) + +In : task2 = loop.create_task(b()) + +In : ts = asyncio.gather(task1, task2, return_exceptions=True) + +In : task1.cancel() +Out: True + +In : await ts +Suspending a +Suspending b +Resuming a +Resuming b +Out: [concurrent.futures._base.CancelledError(), 'B'] +``` + +可以看到虽然结果是一个 CancelledError 错误,但是看输出能确认协程实际上是执行了的。 + +::: info 注 + +此处之前有一个理解错误,已经在 [深入 asyncio.shield](#) 中重新解释和理解,推荐阅读。 + +::: + +### 2.4 asynccontextmanager + +如果你了解 Python,之前可能听过或者用过 contextmanager ,一个上下文管理器。通过一个计时的例子就理解它的作用: + +```python +from contextlib import contextmanager + + +async def a(): + await asyncio.sleep(3) + return 'A' + + +async def b(): + await asyncio.sleep(1) + return 'B' + + +async def s1(): + return await asyncio.gather(a(), b()) + + +@contextmanager +def timed(func): + start = time.perf_counter() + yield asyncio.run(func()) + print(f'Cost: {time.perf_counter() - start}') +``` + +timed 函数用了 contextmanager 装饰器,把协程的运行结果 yield 出来,执行结束后还计算了耗时: + +```python +In : from contextmanager import * + +In : with timed(s1) as rv: +...: print(f'Result: {rv}') +...: +Result: ['A', 'B'] +Cost: 3.0052654459999992 +``` + +大家先体会一下。在 Python 3.7 添加了 asynccontextmanager,也就是异步版本的 contextmanager,适合异步函数的执行,上例可以这么改: + +```python +@asynccontextmanager +async def async_timed(func): + start = time.perf_counter() + yield await func() + print(f'Cost: {time.perf_counter() - start}') + + +async def main(): + async with async_timed(s1) as rv: + print(f'Result: {rv}') + +In : asyncio.run(main()) +Result: ['A', 'B'] +Cost: 3.00414147500004 +``` + +async 版本的 with 要用 `async with`,另外要注意 `yield await func()` 这句,相当于 yield +`await func()` + +PS: contextmanager 和 asynccontextmanager 最好的理解方法是去看源码注释,可以看延伸阅读链接 2,另外延伸阅读链接 3 包含的 PR 中相关的测试代码部分也能帮助你理解。 + +### 2.5 延伸阅读 + +1. [https://github.com/python/cpython/blob/3.7/Lib/asyncio/tasks.py#L574](https://github.com/python/cpython/blob/3.7/Lib/asyncio/tasks.py#L574) +2. [https://github.com/python/cpython/blob/3.7/Lib/contextlib.py#L243](https://github.com/python/cpython/blob/3.7/Lib/contextlib.py#L243) +3. [https://github.com/python/cpython/pull/360/](https://github.com/python/cpython/pull/360/) + + + +## 3. 解密协程运行时 + +说了这么多,现在,我们不妨来深入代码底层看看。有了前面的知识做基础,你应该很容易理解这两段代码。 + +```python +import asyncio + +async def worker_1(): + print('worker_1 start') + await asyncio.sleep(1) + print('worker_1 done') + +async def worker_2(): + print('worker_2 start') + await asyncio.sleep(2) + print('worker_2 done') + +async def main(): + print('before await') + await worker_1() + print('awaited worker_1') + await worker_2() + print('awaited worker_2') + +%time asyncio.run(main()) + +########## 输出 ########## + +before await +worker_1 start +worker_1 done +awaited worker_1 +worker_2 start +worker_2 done +awaited worker_2 +Wall time: 3 s +``` + +```python +import asyncio + +async def worker_1(): + print('worker_1 start') + await asyncio.sleep(1) + print('worker_1 done') + +async def worker_2(): + print('worker_2 start') + await asyncio.sleep(2) + print('worker_2 done') + +async def main(): + task1 = asyncio.create_task(worker_1()) + task2 = asyncio.create_task(worker_2()) + print('before await') + await task1 + print('awaited worker_1') + await task2 + print('awaited worker_2') + +%time asyncio.run(main()) + +########## 输出 ########## + +before await +worker_1 start +worker_2 start +worker_1 done +awaited worker_1 +worker_2 done +awaited worker_2 +Wall time: 2.01 s +``` + +不过,第二个代码,到底发生了什么呢?为了让你更详细了解到协程和线程的具体区别,这里我详细地分析了整个过程。步骤有点多,别着急,我们慢慢来看。 + +1. `asyncio.run(main())`,程序进入 `main()` 函数,事件循环开启; +2. task1 和 task2 任务被创建,并进入事件循环等待运行;运行到 print,输出 `'before await'`; +3. await task1 执行,用户选择从当前的主任务中切出,事件调度器开始调度 `worker_1`; +4. `worker_1` 开始运行,运行 print 输出 `'worker_1 start'`,然后运行到 `await asyncio.sleep(1)`, 从当前任务切出,事件调度器开始调度 `worker_2`; +5. `worker_2` 开始运行,运行 print 输出 `'worker_2 start'`,然后运行 `await asyncio.sleep(2)` 从当前任务切出; +6. 以上所有事件的运行时间,都应该在 1ms 到 10ms 之间,甚至可能更短,事件调度器从这个时候开始暂停调度; +7. 一秒钟后,`worker_1` 的 sleep 完成,事件调度器将控制权重新传给 `task_1`,输出 `'worker_1 done'`,`task_1` 完成任务,从事件循环中退出; +8. `await task1` 完成,事件调度器将控制器传给主任务,输出 `'awaited worker_1'`,·然后在 `await task2` 处继续等待; +9. 两秒钟后,`worker_2` 的 sleep 完成,事件调度器将控制权重新传给 `task_2`,输出 `'worker_2 done'`,`task_2` 完成任务,从事件循环中退出; +10. 主任务输出 `'awaited worker_2'`,协程全任务结束,事件循环结束。 + +接下来,我们进阶一下。如果我们想给某些协程任务限定运行时间,一旦超时就取消,又该怎么做呢?再进一步,如果某些协程运行时出现错误,又该怎么处理呢?同样的,来看代码。 + +```python +import asyncio + +async def worker_1(): + await asyncio.sleep(1) + return 1 + +async def worker_2(): + await asyncio.sleep(2) + return 2 / 0 + +async def worker_3(): + await asyncio.sleep(3) + return 3 + +async def main(): + task_1 = asyncio.create_task(worker_1()) + task_2 = asyncio.create_task(worker_2()) + task_3 = asyncio.create_task(worker_3()) + + await asyncio.sleep(2) + task_3.cancel() + + res = await asyncio.gather(task_1, task_2, task_3, return_exceptions=True) + print(res) + +%time asyncio.run(main()) + +########## 输出 ########## + +[1, ZeroDivisionError('division by zero'), CancelledError()] +Wall time: 2 s +``` + +你可以看到,`worker_1` 正常运行,`worker_2` 运行中出现错误,`worker_3` 执行时间过长被我们 cancel 掉了,这些信息会全部体现在最终的返回结果 res 中。 + +不过要注意 `return_exceptions=True` 这行代码。如果不设置这个参数,错误就会完整地 throw 到我们这个执行层,从而需要 `try except` 来捕捉,这也就意味着其他还没被执行的任务会被全部取消掉。为了避免这个局面,我们将 `return_exceptions` 设置为 True 即可。 + +到这里,发现了没,线程能实现的,协程都能做到。那就让我们温习一下这些知识点,用协程来实现一个经典的生产者消费者模型吧。 + +```python +import asyncio +import random + +async def consumer(queue, id): + while True: + val = await queue.get() + print('{} get a val: {}'.format(id, val)) + await asyncio.sleep(1) + +async def producer(queue, id): + for i in range(5): + val = random.randint(1, 10) + await queue.put(val) + print('{} put a val: {}'.format(id, val)) + await asyncio.sleep(1) + +async def main(): + queue = asyncio.Queue() + + consumer_1 = asyncio.create_task(consumer(queue, 'consumer_1')) + consumer_2 = asyncio.create_task(consumer(queue, 'consumer_2')) + + producer_1 = asyncio.create_task(producer(queue, 'producer_1')) + producer_2 = asyncio.create_task(producer(queue, 'producer_2')) + + await asyncio.sleep(10) + consumer_1.cancel() + consumer_2.cancel() + + await asyncio.gather(consumer_1, consumer_2, producer_1, producer_2, return_exceptions=True) + +%time asyncio.run(main()) + +########## 输出 ########## + +producer_1 put a val: 5 +producer_2 put a val: 3 +consumer_1 get a val: 5 +consumer_2 get a val: 3 +producer_1 put a val: 1 +producer_2 put a val: 3 +consumer_2 get a val: 1 +consumer_1 get a val: 3 +producer_1 put a val: 6 +producer_2 put a val: 10 +consumer_1 get a val: 6 +consumer_2 get a val: 10 +producer_1 put a val: 4 +producer_2 put a val: 5 +consumer_2 get a val: 4 +consumer_1 get a val: 5 +producer_1 put a val: 2 +producer_2 put a val: 8 +consumer_1 get a val: 2 +consumer_2 get a val: 8 +Wall time: 10 s +``` + +## 4. 实战:豆瓣近日推荐电影爬虫 + +最后,进入今天的实战环节——实现一个完整的协程爬虫。 + +任务描述:[https://movie.douban.com/cinema/later/beijing/](https://movie.douban.com/cinema/later/beijing/) 这个页面描述了北京最近上映的电影,你能否通过 Python 得到这些电影的名称、上映时间和海报呢?这个页面的海报是缩小版的,我希望你能从具体的电影描述页面中抓取到海报。 + +听起来难度不是很大吧?我在下面给出了同步版本的代码和协程版本的代码,通过运行时间和代码写法的对比,希望你能对协程有更深的了解。(注意:为了突出重点、简化代码,这里我省略了异常处理。) + +不过,在参考我给出的代码之前,你是不是可以自己先动手写一下、跑一下呢? + +:::: tabs + +@tab 正常版本 + +```python +import requests +from bs4 import BeautifulSoup + + +def main(): + headers = { + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36", + } + url = "https://movie.douban.com/cinema/later/beijing/" + init_page = requests.get(url, headers=headers).content + # print(init_page) + init_soup = BeautifulSoup(init_page, 'lxml') + # print(init_soup) + all_movies = init_soup.find('div', id="showing-soon") + # print(all_movies) + for each_movie in all_movies.find_all('div', class_="item"): + # print(each_movie) + all_a_tag = each_movie.find_all('a') + # print(all_a_tag) + all_li_tag = each_movie.find_all('li') + # print(all_li_tag) + movie_name = all_a_tag[1].text + # print(movie_name) + url_to_fetch = all_a_tag[1]['href'] + # print(url_to_fetch) + movie_date = all_li_tag[0].text + # print(movie_date) + response_item = requests.get(url_to_fetch, headers=headers).content + # print(response_item) + soup_item = BeautifulSoup(response_item, 'lxml') + img_tag = soup_item.find('img') + # print(img_tag) + print('{} {} {}'.format(movie_name, movie_date, img_tag['src'])) + + +%time main() +``` + +::: details 输出 + +```python +风再起时 02月05日 https://img9.doubanio.com/view/photo/s_ratio_poster/public/p2887039584.jpg +黑豹2 02月07日 https://img9.doubanio.com/view/photo/s_ratio_poster/public/p2886589774.jpg +不能流泪的悲伤 02月14日 https://img1.doubanio.com/view/photo/s_ratio_poster/public/p2886531289.jpg +蚁人与黄蜂女:量子狂潮 02月17日 https://img1.doubanio.com/view/photo/s_ratio_poster/public/p2886589789.jpg +中国乒乓之绝地反击 02月17日 https://img9.doubanio.com/view/photo/s_ratio_poster/public/p2887100255.jpg +印式英语 02月24日 https://img1.doubanio.com/view/photo/s_ratio_poster/public/p2886633470.jpg +毒舌律师 02月24日 https://img9.doubanio.com/view/photo/s_ratio_poster/public/p2886619074.jpg +会考试的猛犸象 02月24日 https://img9.doubanio.com/view/photo/s_ratio_poster/public/p2879572685.jpg +拨浪鼓咚咚响 02月25日 https://img2.doubanio.com/view/photo/s_ratio_poster/public/p2886538033.jpg +宇宙探索编辑部 04月01日 https://img1.doubanio.com/view/photo/s_ratio_poster/public/p2886529968.jpg +龙马精神 04月07日 https://img2.doubanio.com/view/photo/s_ratio_poster/public/p2887204283.jpg +长空之王 04月28日 https://img9.doubanio.com/view/photo/s_ratio_poster/public/p2886235064.jpg +人生路不熟 04月28日 https://img1.doubanio.com/view/photo/s_ratio_poster/public/p2887091779.jpg +检察风云 04月29日 https://img1.doubanio.com/view/photo/s_ratio_poster/public/p2886533208.jpg +请别相信她 05月20日 https://img1.doubanio.com/view/photo/s_ratio_poster/public/p2886540928.jpg +伟大的胜利 09月30日 https://img1.doubanio.com/view/photo/s_ratio_poster/public/p2886621940.jpg +CPU times: user 564 ms, sys: 29.9 ms, total: 594 ms +Wall time: 19.7 s +``` + +::: + +@tab 协程版本 + +```python +import asyncio +import aiohttp + +from bs4 import BeautifulSoup + +header = { + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36", +} + +async def fetch_content(url): + async with aiohttp.ClientSession( + headers=header, connector=aiohttp.TCPConnector(ssl=False) + ) as session: + async with session.get(url) as response: + return await response.text() + +async def main(): + url = "https://movie.douban.com/cinema/later/beijing/" + init_page = await fetch_content(url) + init_soup = BeautifulSoup(init_page, 'lxml') + + movie_names, urls_to_fetch, movie_dates = [], [], [] + + all_movies = init_soup.find('div', id="showing-soon") + for each_movie in all_movies.find_all('div', class_="item"): + all_a_tag = each_movie.find_all('a') + all_li_tag = each_movie.find_all('li') + + movie_names.append(all_a_tag[1].text) + urls_to_fetch.append(all_a_tag[1]['href']) + movie_dates.append(all_li_tag[0].text) + + tasks = [fetch_content(url) for url in urls_to_fetch] + pages = await asyncio.gather(*tasks) + + for movie_name, movie_date, page in zip(movie_names, movie_dates, pages): + soup_item = BeautifulSoup(page, 'lxml') + img_tag = soup_item.find('img') + + print('{} {} {}'.format(movie_name, movie_date, img_tag['src'])) + +# %time asyncio.run(main()) + +if __name__ == '__main__': + start_time = time.time() + loops = asyncio.get_event_loop() + loops.run_until_complete(main()) + print(time.time() - start_time) + +########## 输出 ########## + +阿拉丁 05月24日 https://img3.doubanio.com/view/photo/s_ratio_poster/public/p2553992741.jpg +龙珠超:布罗利 05月24日 https://img3.doubanio.com/view/photo/s_ratio_poster/public/p2557371503.jpg +五月天人生无限公司 05月24日 https://img3.doubanio.com/view/photo/s_ratio_poster/public/p2554324453.jpg +... ... +直播攻略 06月04日 https://img3.doubanio.com/view/photo/s_ratio_poster/public/p2555957974.jpg +Wall time: 4.98 s +``` + +:::: + +## 5. 总结 + +到这里,今天的主要内容就讲完了。今天我用了较长的篇幅,从一个简单的爬虫开始,到一个真正的爬虫结束,在中间穿插讲解了 Python 协程最新的基本概念和用法。这里带你简单复习一下。 + +- 协程和多线程的区别,主要在于两点,一是协程为单线程;二是协程由用户决定,在哪些地方交出控制权,切换到下一个任务。 +- 协程的写法更加简洁清晰,把 async / await 语法和 `create_task` 结合来用,对于中小级别的并发需求已经毫无压力。 +- 写协程程序的时候,你的脑海中要有清晰的事件循环概念,知道程序在什么时候需要暂停、等待 I/O,什么时候需要一并执行到底。 + +最后的最后,请一定不要轻易炫技。多线程模型也一定有其优点,一个真正牛逼的程序员,应该懂得,在什么时候用什么模型能达到工程上的最优,而不是自觉某个技术非常牛逼,所有项目创造条件也要上。技术是工程,而工程则是时间、资源、人力等纷繁复杂的事情的折衷。 + +## 6. 思考题 + +最后给你留一个思考题。协程怎么实现回调函数呢?欢迎留言和我讨论,也欢迎你把这篇文章分享给你的同事朋友,我们一起交流,一起进步。 + +## 7. 评论 + +### 7.1 讲师 + +发现评论区好多朋友说无法运行,在这里统一解释下: + +1. `%time` 是 jupyter notebook 自带的语法糖,用来统计一行命令的运行时间;如果你的运行时是纯粹的命令行 python,或者 pycharm,那么请把 `%time` 删掉,自己用传统的时间戳方法来记录时间也可以;或者使用 jupyter notebook +2. 我的本地解释器是 Anaconda Python 3.7.3,亲测 windows / ubuntu 均可正常运行,如无法执行可以试试 `pip install nest-asyncio`,依然无法解决请尝试安装 Anaconda Python +3. 这次代码因为使用了较新的 API,所以需要较新的版本号,但是朋友们依然出现了一些运行时问题,这里先表示下歉意;同时也想说明的是,在提问之前自己经过充分搜索,尝试后解决问题,带来的快感,和能力的提升,相应也是很大的,一门工程最需要的是 hands on dirty work(动手做脏活),才能让自己的能力得到本质的提升,加油! + +--- + +### 7.2 讲师 + +**思考题答案:** + +在 Python 3.7 及以上的版本中,我们对 task 对象调用 `add_done_callback() `函数,即可绑定特定回调函数。回调函数接受一个 future 对象,可以通过 `future.result()` 来获取协程函数的返回值。 + +**示例如下:** + +::: code-tabs#python + +@tab old + +```python +import asyncio + +async def crawl_page(url): + print('crawling {}'.format(url)) + sleep_time = int(url.split('_')[-1]) + await asyncio.sleep(sleep_time) + return 'OK {}'.format(url) + +async def main(urls): + tasks = [asyncio.create_task(crawl_page(url)) for url in urls] + for task in tasks: + task.add_done_callback(lambda future: print('result: ', future.result())) + await asyncio.gather(*tasks) + +%time asyncio.run(main(['url_1', 'url_2', 'url_3', 'url_4'])) + +# 输出: + +crawling url_1 +crawling url_2 +crawling url_3 +crawling url_4 +result: OK url_1 +result: OK url_2 +result: OK url_3 +result: OK url_4 +Wall time: 4 s +``` + +@tab Run on Jupyter Notebook + +```python {15-17} +import asyncio, time + +async def crawl_page(url): + print('crawling {}'.format(url)) + sleep_time = int(url.split('_')[-1]) + await asyncio.sleep(sleep_time) + return 'OK {}'.format(url) + +async def main(urls): + tasks = [asyncio.create_task(crawl_page(url)) for url in urls] + for task in tasks: + task.add_done_callback(lambda future: print('result: ', future.result())) + await asyncio.gather(*tasks) + +start_time = time.time() +await main(['url_1', 'url_2', 'url_3', 'url_4']) +print(time.time() - start_time) +``` + +@tab Run on Pycharm + +```python {18-20} +import asyncio, time + + +async def crawl_page(url): + print('crawling {}'.format(url)) + sleep_time = int(url.split('_')[-1]) + await asyncio.sleep(sleep_time) + return 'OK {}'.format(url) + + +async def main(urls): + tasks = [asyncio.create_task(crawl_page(url)) for url in urls] + for task in tasks: + task.add_done_callback(lambda future: print('result: ', future.result())) + await asyncio.gather(*tasks) + + +start_time = time.time() +asyncio.run(main(['url_1', 'url_2', 'url_3', 'url_4'])) +print(time.time() - start_time) +``` + +::: + +--- + +### 7.3 helloworld + +说一下我对 await 的理解: 开发者要提前知道一个任务的哪个环节会造成 I/O 阻塞,然后把这个环节的代码异步化处理,并且通过 await来标识在任务的该环节中断该任务执行,从而去执行下一个事件循环任务。这样可以充分利用 CPU 资源,避免 CPU 等待 I/O 造成 CPU 资源白白浪费。当之前任务的那个环节的 I/O 完成后,线程可以从 await 获取返回值,然后继续执行没有完成的剩余代码。 由上面分析可知,如果一个任务不涉及到网络或磁盘 I/O 这种耗时的操作,而只有 CPU 计算和内存 I/O 的操作时,协程并发的性能还不如单线程 loop 循环的性能高。 + +--- + +### 7.4 大侠110 + +感觉还是有很多人看不懂,我试着用通俗的语句讲一下:协成里面重要的是一个关键字 await 的理解,async 表示其修饰的是协程任务即 task,await 表示的是当线程执行到这一句,此时该 task 在此处挂起,然后调度器去执行其他的 task,当这个挂起的部分处理完,会调用回掉函数告诉调度器我已经执行完了,那么调度器就返回来处理这个 task 的余下语句。 + +--- + +### 7.5 Airnm.毁 + +豆瓣那个发现 `requests.get(url).content/text` 返回都为空,然后打了下 status_code 发现是 418,网上找 418 的解释,一般是网站反爬虫基础机制,需要加请求头模仿浏览器就可跳过,改为下面的样子就可通过: + +```python +url = "https://movie.douban.com/cinema/later/beijing/" +head={ 'User-Agent':'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)Chrome/81.0.4044.113 Safari/537.36', + 'Referer':'https://time.geekbang.org/column/article/101855', + 'Connection':'keep-alive'} +res = requests.get(url, headers=head) +``` + +> 作者回复: 👍,可能是增加了反爬虫机制 + +--- + +### 7.7 长期规划 + +老师,在最后那个协程例子中为何没用 requests 库呢?是因为它不支持协程吗 + +> 作者回复: 协程使用的是 aiohttp 并发网络 io 库,因此就不需要 requests 了 + +## 8. 报错处理 + +### 8.1 RuntimeError: asyncio.run() cannot be called from a running event loop + +学习协程异步操作出现的问题: + +```python +import asyncio +import time +async def func_4(): + print("营养快线") + # time.sleep(3) + # print("娃哈哈") + +if __name__=='__main__': + g = func_4() # 此时的函数是异步协程函数,此时函数执行得到一个协程对象 + asyncio.run(g) +``` + +代码报错: + +`RuntimeError: asyncio.run() cannot be called from a running event loop` + +意思大致就是 jupyter 已经运行了 loop,无需自己激活,修改为: + +```python +import asyncio +import time +async def func_4(): + print("营养快线") + # time.sleep(3) + # print("娃哈哈") + +if __name__=='__main__': + g = func_4() # 此时的函数是异步协程函数,此时函数执行得到一个协程对象 + #asyncio.run(g) + await g +``` + +### 8.2 SyntaxError: ‘await‘ outside async function的原因与解决 + +我们看下面这个代码,表面上没什么问题: + +```python +import asyncio + + +async def do1(): + await asyncio.sleep(2) + print('两秒过去了') + + +async def do2(): + await asyncio.sleep(2) + print('两秒又过去了') + + +async def do3(): + await asyncio.sleep(4) + print('四秒过去了') + + +await do1() +await do2() +await do3() +``` + +但运行了以后会这样报错: + +![](./20.assets/image-20230130101101621.png) + +**原因:** await 是要和创建协程时 async 一起搭配使用的,直接使用 await 就会找不到他所在的函数的,于是报错在函数外。 + +解决方法: + +- 创建任务单 +- 创建事件 +- 实现并发运行 + +改进代码: + +```python +import asyncio + + +async def do1(): + await asyncio.sleep(2) + print('两秒过去了') + + +async def do2(): + await asyncio.sleep(3) + print('三秒过去了') + + +async def do3(): + await asyncio.sleep(4) + print('四秒过去了') + + +renwu = [do1(), do2(), do3()] +loops = asyncio.get_event_loop() +loops.run_until_complete(asyncio.wait(renwu)) +``` + +![image-20230130101219537](./20.assets/image-20230130101219537.png) + +完成的很顺利。 + +欢迎关注我公众号:AI悦创,有更多更好玩的等你发现! + +::: details 公众号:AI悦创【二维码】 + +![](/gzh.jpg) + +::: + +::: info AI悦创·编程一对一 + +AI悦创·推出辅导班啦,包括「Python 语言辅导班、C++ 辅导班、java 辅导班、算法/数据结构辅导班、少儿编程、pygame 游戏开发」,全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践等。当然,还有线下线上摄影课程、Photoshop、Premiere 一对一教学、QQ、微信在线,随时响应!微信:Jiabcdefh + +C++ 信息奥赛题解,长期更新!长期招收一对一中小学信息奥赛集训,莆田、厦门地区有机会线下上门,其他地区线上。微信:Jiabcdefh + +方法一:[QQ](http://wpa.qq.com/msgrd?v=3&uin=1432803776&site=qq&menu=yes) + +方法二:微信:Jiabcdefh + +::: + +![](/zsxq.jpg) + diff --git a/src/Python/Python-core-technology-and-practice/21.assets/f684a50ba70d605e5f7087f29b9aeaae.jpg b/src/Python/Python-core-technology-and-practice/21.assets/f684a50ba70d605e5f7087f29b9aeaae.jpg new file mode 100755 index 00000000000..3f8828984e7 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/21.assets/f684a50ba70d605e5f7087f29b9aeaae.jpg differ diff --git a/src/Python/Python-core-technology-and-practice/21.md b/src/Python/Python-core-technology-and-practice/21.md new file mode 100755 index 00000000000..9ff68d42f6e --- /dev/null +++ b/src/Python/Python-core-technology-and-practice/21.md @@ -0,0 +1,74 @@ +--- +title: 21-Python 并发编程之 Futures +icon: python +date: 2023-01-24 10:56:47 +author: AI悦创 +isOriginal: true +category: Python 进阶 +tag: + - Python 进阶 +sticky: false +star: false +article: true +timeline: true +image: false +navbar: true +sidebarIcon: true +headerDepth: 5 +comment: true +lastUpdated: true +editLink: false +backToTop: true +toc: true +--- + +![img](./21.assets/f684a50ba70d605e5f7087f29b9aeaae.jpg) + +你好,我是悦创。 + +无论对于哪门语言,并发编程都是一项很常用很重要的技巧。比如我们上节课所讲的很常见的爬虫,就被广泛应用在工业界的各个领域。我们每天在各个网站、各个 App 上获取的新闻信息,很大一部分便是通过并发编程版的爬虫获得。 + +正确合理地使用并发编程,无疑会给我们的程序带来极大的性能提升。今天这节课,我就带你一起来学习理解、运用 Python 中的并发编程——Futures。 + +## 区分并发和并行 + + + + + + + + + + + + + + + + + + + +欢迎关注我公众号:AI悦创,有更多更好玩的等你发现! + +::: details 公众号:AI悦创【二维码】 + +![](/gzh.jpg) + +::: + +::: info AI悦创·编程一对一 + +AI悦创·推出辅导班啦,包括「Python 语言辅导班、C++ 辅导班、java 辅导班、算法/数据结构辅导班、少儿编程、pygame 游戏开发」,全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践等。当然,还有线下线上摄影课程、Photoshop、Premiere 一对一教学、QQ、微信在线,随时响应!微信:Jiabcdefh + +C++ 信息奥赛题解,长期更新!长期招收一对一中小学信息奥赛集训,莆田、厦门地区有机会线下上门,其他地区线上。微信:Jiabcdefh + +方法一:[QQ](http://wpa.qq.com/msgrd?v=3&uin=1432803776&site=qq&menu=yes) + +方法二:微信:Jiabcdefh + +::: + +![](/zsxq.jpg) + diff --git a/src/Python/Python-core-technology-and-practice/Algorithm/01.assets/5dea246400017ceb08640148.png b/src/Python/Python-core-technology-and-practice/Algorithm/01.assets/5dea246400017ceb08640148.png new file mode 100644 index 00000000000..dab90bb5b90 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/Algorithm/01.assets/5dea246400017ceb08640148.png differ diff --git a/src/Python/Python-core-technology-and-practice/Algorithm/01.assets/5dea246c0001c9ef06180414.png b/src/Python/Python-core-technology-and-practice/Algorithm/01.assets/5dea246c0001c9ef06180414.png new file mode 100644 index 00000000000..995a82f8a94 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/Algorithm/01.assets/5dea246c0001c9ef06180414.png differ diff --git a/src/Python/Python-core-technology-and-practice/Algorithm/01.md b/src/Python/Python-core-technology-and-practice/Algorithm/01.md new file mode 100644 index 00000000000..f4d299b1e6e --- /dev/null +++ b/src/Python/Python-core-technology-and-practice/Algorithm/01.md @@ -0,0 +1,132 @@ +--- +title: 01-开篇词:你知道什么是算法吗? +icon: shujujiegou-01 +date: 2023-08-05 19:28:34 +author: AI悦创 +cover: /banner/suanfa01.jpg +isOriginal: true +category: Python 算法科普指南 +tag: + - Python 算法科普指南 +sticky: false +star: false +article: true +timeline: true +navbar: true +sidebarIcon: true +headerDepth: 5 +comment: true +lastUpdated: true +editLink: false +backToTop: true +toc: true +--- + +::: tip 手札 + +勤能补拙是良训,一分辛劳一分才。——华罗庚 + +::: + +## 1. 算法的定义 + +你好,我是悦创。 + +对于算法的解释,全世界的定义是不唯一的。 + +我给出的算法的定义是:**一系列用来解决单个或多个问题,或有执行计算功能的命令的集合。再结合上输入与输出,算法就是将输入转换为输出的一系列计算步骤的集合。** + +听起来很拗口,对不对? + +没关系,我们可以把一个算法比作是一个菜谱。如图1-1所示,原材料就是输入,做出来的成品即为输出,而算法,就是做菜过程中的复杂步骤。 + +![图1-1](./01.assets/5dea246400017ceb08640148.png) + +再换一种方式看算法,算法的本质其实是数学的理论与推导。在还没有发明求和公式之前,如何求出 $1+2+3+4+5+…+n ?$ 逐个数求和虽能算出答案,但过于繁杂。但反观求和公式,无论 n 取多大的值,计算的步骤和繁琐程度基本不会增加。这就是算法存在的意义。人类在解决复杂问题时所采用的一系列特定的方法,即为算法。 + +## 2. 算法与程序的区别 + +明白了什么是算法,再来看算法和程序的区别。通常来说,程序指一组计算机能识别和执行,并有一定功能的指令。 + +后者的定义似乎和算法很相似,但算法和程序之间最大的区别在于程序是以计算机能够理解的各式各样的编程语言编写而成的,而算法是可以通过编程语言、图绘、口述等人能够理解的方式来描述的,不一定局限于编程语言的诠释,如图1-2 所示。 + +![图1-2](./01.assets/5dea246c0001c9ef06180414.png) + + + + + +刚才曾提过,算法是一种以数学为本质的计算方法。然而作为方法,则必有正确(可行)、不正确(不可行)和高效、低效之分。 + +若一个算法对于每一个恰当的输入都以正确的输出终止程序,则可以称这算法是正确的,并正确地解决了给定的问题。若算法以不正确的输出而终止程序,或根本无法终止程序,则这个算法是不正确的。 + +但显而易见,不是所有的算法都可以通过输入和输出被正确和不正确而简单地分为两大类。譬如人们要预测未来特定事件发生概率,而这种问题无法用结果来检验解决方法是正确与否。因此,算法的正确性的检验也可以回溯到其本质,就是数学的检验,也就是说用数学来证明算法的正确性或可行性。 + +对于算法至关重要的不只有其正确性,还有它的效率。人类至今的发展,提高最迅速的可以说是计算的效率了。从原始人的结绳计数,到现在的超级计算机太湖之光,计算能力的提升不是区区几个数量级能说明清楚的。 + +但很不幸,当今计算机的运算速度不是无限快,存储器也不是免费的,如果是,那还研究算法干什么呢?所以如何高效地利用好有限的时间和空间就是算法存在的意义。有趣的是,求解相同问题而设计的不同算法在效率方面通常有着显著的差异,而这些算法效率上的差异要比在硬件或者软件效率上的差异大得多。 + +回到 $1+2+3+4+5+…+n$ 这个求和问题中。一定程度上说,逐个数相加也可以被看作一种解决求和问题的算法,一种简单,低效的算法。相反,求和公式则是一种较复杂的,高效的算法。但如何来评判一个算法是否高效?时间复杂度和空间复杂度就是很好的丈量工具。时间复杂度和空间复杂度将会在后面的章节中介绍。 + +## 3. 算法的应用 + +算法的应用无处不在,小到普通的排序问题,大到最近举世瞩目的神经网络和深度学习,都很容易寻觅到算法的影子。列举几个赫赫有名的例子: + +### 3.1 机器学习 + +神经网络和深度学习可谓当今最热门的算法,而如今这两个算法的应用范围有目共睹——从图像识别到 Alpha Go,再到语音识别和机器翻译,人工智能一次又一次地刷新人们对信息学的认识。 + +深度学习的主旨在于强调神经网络模型的深度。其在神经网络模型的基础上减少了参数的繁杂度,更加逼近人脑工作的机制。最著名的例子便是卷积神经网络 (CNN),其极大地加强了计算机的图像识别功能。 + +### 3.2 搜索引擎和网络爬虫 + +搜索引擎的核心机制其实就是网络爬虫。为了高效地为客户提供搜索结果,搜索引擎往往会先收集互联网中成千上万的网页,并根据网页中的关键字建立数据索引库。 + +搜索引擎和网络爬虫收集网页的过程都会以基础的 BFS(广度优先搜索),DFS(深度优先搜索)为核心思想,并针对要抓取的网页附加更有针对性的复杂算法,如网页过滤算法等。 + +随着搜索技术越来越成熟,其算法的复杂程度也逐渐提高,但溯其本源仍是简单的搜索算法。刚刚提到的广度优先搜索、深度优先搜索等算法都会在后面章节中详细讲解,希望读者们能够认真学习掌握。 + +## 4. 小结 + +本章详细介绍了算法的本质、意义和应用。算法是编程的核心,就像一台计算机的 CPU,算法的好坏决定了一个系统的效率高低。同时也是谷歌、阿里等大厂主要考察的内容。修炼好算法这门“内功”,再辅以新技术这些“招式”,才能独霸“武林”。 + + + + + + + + + + + + + + + + + + + +欢迎关注我公众号:AI悦创,有更多更好玩的等你发现! + +::: details 公众号:AI悦创【二维码】 + +![](/gzh.jpg) + +::: + +::: info AI悦创·编程一对一 + +AI悦创·推出辅导班啦,包括「Python 语言辅导班、C++ 辅导班、java 辅导班、算法/数据结构辅导班、少儿编程、pygame 游戏开发」,全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践等。当然,还有线下线上摄影课程、Photoshop、Premiere 一对一教学、QQ、微信在线,随时响应!微信:Jiabcdefh + +C++ 信息奥赛题解,长期更新!长期招收一对一中小学信息奥赛集训,莆田、厦门地区有机会线下上门,其他地区线上。微信:Jiabcdefh + +方法一:[QQ](http://wpa.qq.com/msgrd?v=3&uin=1432803776&site=qq&menu=yes) + +方法二:微信:Jiabcdefh + +::: + +![](/zsxq.jpg) + diff --git a/src/Python/Python-core-technology-and-practice/Algorithm/02.md b/src/Python/Python-core-technology-and-practice/Algorithm/02.md new file mode 100644 index 00000000000..402ed2ca462 --- /dev/null +++ b/src/Python/Python-core-technology-and-practice/Algorithm/02.md @@ -0,0 +1,173 @@ +--- +title: 02-为什么这个算法要执行这么长时间? +icon: shujujiegou-01 +date: 2023-08-05 20:18:34 +author: AI悦创 +cover: /banner/suanfa02.jpg +isOriginal: true +category: Python 算法科普指南 +tag: + - Python 算法科普指南 +sticky: false +star: false +article: true +timeline: true +navbar: true +sidebarIcon: true +headerDepth: 5 +comment: true +lastUpdated: true +editLink: false +backToTop: true +toc: true +--- + +::: tip 手札 + +今天应做的事没有做,明天再早也是耽误了。——裴斯泰洛齐 + +::: + +## 1. 什么是时间复杂度 + +你好,我是悦创。 + +一个高级语言编写的程序的运行时间主要取决于三个因素: + +- 算法的方法,策略; +- 问题的输入规模; +- 计算机执行指令的速度。 + +问题的输入规模是客观的、限定的,要加快算法的效率绝不能影响问题的输入规模。计算机执行指令的速度虽然可以有显著提升,但其发展时间较长,也不是确定的,总不能终日盼着计算机性能的提升。所以提高算法的效率,减少程序运行时间,改进算法的策略是至关重要的。 + +在讲解时间复杂度之前,需先引入一个概念,时间频度。时间频度代表一个算法中的语句执行次数,其又可以被称为语句频度。显然,时间频度越高的算法运行的时间越长。时间频度也可被记为 $T(n)$,其中 n 为问题的规模,即输入值的规模。 + +> 在讲解时间复杂度之前,我们需要理解一个基础概念:算法中语句的执行次数。执行次数是指在算法执行过程中,某些特定语句的执行频次。显然,一个算法中的语句执行次数越多,其运行的时间可能会越长。为了便于分析,我们可以把这个执行次数记作 $T(n)$,其中 $n$ 代表了问题的规模,即输入的大小。但需要注意的是,$T(n)$ 并非直接等于实际运行时间,而是反映了算法运行时间随问题规模变化的趋势,即时间复杂度 + +时间复杂度的具体定义为:若有某个辅助函数 $f(n)$,使得的 $\frac{T(n)}{f(n)}$ 极限值(当 n 趋近于无穷大时)为不等于零的常数,则称 $f(n)$ 是 $T(n)$ 的同数量级函数。记作:$T(n)=O(f(n))$ + +称 $O(f(n))$ 为算法的渐进时间复杂度,简称时间复杂度。在数学上,大 O 符号用来描述一个函数数量级的渐近上界。 + +以上是纯数学分析,看不太懂的读者可以理解为时间复杂度就是算法运行时间和输入规模的关系。 + +## 2. 时间复杂度是渐进的 + +如果我们将算法中的一次计算记为 1,那么如果有 n 个输入值,算法对每一个输入值做一次运算,那么整个算法的运算量即为 n。这个时候,我们就可以说,这是一个时间复杂度为 $O(n)$ 的算法。 + +同理,如果仍有 n 个输入值,但算法对每一个输入值做一次运算这个操作需要再重复 n 次,那么整个算法的运算量即为 $n∗n=n^2$,时间复杂度为 $O(n^2)$ 。这时如果对每一个输入值做一次运算这个操作需要重复 $n+1$ 次,算法运算量变为:$n*(n+1)=n^2+n$ + +这时的时间复杂度是否改变为 $O(n^2+n)$ ?上文曾提到时间复杂度考察的是当输入量趋于无穷时的情况,所以当 n 趋于无穷的时候,$n^2$ 对算法运行时间占主导地位,而 n 在 $n^2$ 面前就无足轻重,不计入时间复杂度中。 + +换句话说,因为 $n^2+n$ 渐近地(在取极限时)与 $n^2$ 相等。此外,就运行时间来说,n 前面的常数因子远没有输入规模 $n$ 的依赖性重要,所以是可以被忽略的,也就是说 $O(n^2)$ 和 是 $O\frac{n^2}{2}$ 相同时间复杂度的,都为 $O(n^2)$。 + +## 3. 时间复杂度分析 + +让我们先看一段代码: + +```python +def square(n): + Partial_Sum = 0 + for i in range(1, n + 1): + Partial_Sum += i * i + return Partial_Sum +``` + +代码的第二行只占一次运算,第三行的 for 循环中 i 从 1 加到了 n,其中过程包括 i 的初始化, i 的累加,和 i 的范围判断,分别消耗 1 次运算,n 次运算,和 n+1 次运算。 + +至此,代码的前三行共进行了 2n+3 次运算。第四行是相乘,相加,和赋值三个功能的组合的代码,相乘所需 n 次运算,相加所需 n 次运算,而赋值也需 n 次运算。所以第四行一共进行了 3xn 次运算。最后一行返回消耗一次运算。 + +总体来看,这段代码一共需进行 `2n+3+3n+1=5n+4` 次运算。根据上文的渐进原则,这段代码的时间复杂度为 $O(n)$。 + +通过上面的分析,可以看出细致的时间复杂度分析十分繁琐。但毕竟时间复杂度追求渐进原则,所以在这里为大家整理了一下快速算时间复杂度的技巧: + +### 3.1 循环结构 + +```python +for i in range(1, k*n+m+1): + i += 1 +``` + +上示代码中的 n 为输入规模,k,m 均为已知常数。因此根据渐进原则,只要 for 循环的循环范围是在 n 的有限倍数以内(range 的上界始终可以被表示为 `k*m+n` 的形式),则一个 for 循环的时间复杂度必然为 $O(n)$。 + +```python +for i in range(n): + for j in range(n): + Partial_Sum += 1 +``` + +我们将两个 for 循环迭代在一起。有 n 个不同的 i,每个 i 会对应 n 的不同的 j 的情况下,会有 $n*n=n^2$ 次第三行的操作。在这里我们可以说这段代码的时间复杂度为 $O(n^2)$。实际上,真实的运算次数会有 $k*n^2$ 次(k 为一个常数),其中 k 始终是有限的尽管 k 有时会非常大。 + +综上所述,我们可以总结出循环结构时间复杂度的一些规律: + +- 无论是 for 还是 while 循环,只要循环的范围可以表示为 $k*n+m$,则该循环的时间复杂度为 $O(n)$; +- 如果循环中嵌套循环,则时间复杂度将便成每一层循环的时间复杂度相乘的结果; +- 在决定时间复杂度时,往往只需要关注层数最多 for 循环结构的时间复杂度,因为其它循环的时间复杂度很大可能上会被忽略。 + +### 3.2 递归结构 + +```python +def feb(n): + if n <= 1: + return 1 + else: + return feb(n - 1) + feb(n - 2) +``` + +如上所示的是一个计算斐波那契数列函数。而我们都是道斐波那契数列的公式为: + +::: center + +$\large f(n)=f(n−1)+f(n−2)$ + +::: + +假设当输入规模为时该函数的运行次数为 $T(n)$,通过上示公式我们可以得到: + +::: center + +$T(n)=(T(n−1)+1)+(T(n−2)+1)$ + +::: + +由于常数不会影响到函数整体的时间复杂度,所以可以被简化为: + +::: center + +$\large T(n)=T(n−1)+T(n−2)$ + +::: + +到这一步,我们已经知道当输入规模为 n 时,斐波那契数列函数时间复杂度的递推公式,而我们只需求出通项公式即可分析出其时间复杂度为 $\large O((\frac{1 + \sqrt{5}}{2})^n)$,约为 $O(1.618^{n})$,简化默认为指数级复杂度 $O(2^{n})$ 。可以看出,该斐波那契数列函数的时间复杂度成指数增长,说明这是较为低效的一个递归函数。 + +## 4. 小结 + +在了解算法本质的同时,要掌握时间复杂度来判断一个算法的效率和实用性。相同问题时,算法的复杂度越低,证明算法的效率越高。本质上,输入规模往往是对空间复杂度与时间复杂度影响最大的因素。对于时间复杂度来说,输入量越多,所需处理的数据量越多,计算次数越多,算法运行的时间越多。 + + + + + + + +欢迎关注我公众号:AI悦创,有更多更好玩的等你发现! + +::: details 公众号:AI悦创【二维码】 + +![](/gzh.jpg) + +::: + +::: info AI悦创·编程一对一 + +AI悦创·推出辅导班啦,包括「Python 语言辅导班、C++ 辅导班、java 辅导班、算法/数据结构辅导班、少儿编程、pygame 游戏开发」,全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践等。当然,还有线下线上摄影课程、Photoshop、Premiere 一对一教学、QQ、微信在线,随时响应!微信:Jiabcdefh + +C++ 信息奥赛题解,长期更新!长期招收一对一中小学信息奥赛集训,莆田、厦门地区有机会线下上门,其他地区线上。微信:Jiabcdefh + +方法一:[QQ](http://wpa.qq.com/msgrd?v=3&uin=1432803776&site=qq&menu=yes) + +方法二:微信:Jiabcdefh + +::: + +![](/zsxq.jpg) + diff --git a/src/Python/Python-core-technology-and-practice/Algorithm/03.md b/src/Python/Python-core-technology-and-practice/Algorithm/03.md new file mode 100644 index 00000000000..e1add75b8d4 --- /dev/null +++ b/src/Python/Python-core-technology-and-practice/Algorithm/03.md @@ -0,0 +1,64 @@ +--- +title: 03-这个算法为啥占了这么大空间? +icon: shujujiegou-01 +date: 2023-08-05 22:57:06 +author: AI悦创 +cover: /banner/suanfa03.jpg +isOriginal: true +category: Python 算法科普指南 +tag: + - Python 算法科普指南 +sticky: false +star: false +article: true +timeline: true +navbar: true +sidebarIcon: true +headerDepth: 5 +comment: true +lastUpdated: true +editLink: false +backToTop: true +toc: true +--- + +::: tip 手札 + +从不浪费时间的人,没有工夫抱怨时间不够。——杰弗逊 + +::: + +## 1. 空间复杂度 + +你好,我是悦创。 + +一个算法在计算机存储器上所占用的存储空间,包括算法本身所占用的存储空间,算法的输入、输出数据所占用的存储空间和算法在运行过程中临时占用的存储空间这三个方面。 + + + + + + + +欢迎关注我公众号:AI悦创,有更多更好玩的等你发现! + +::: details 公众号:AI悦创【二维码】 + +![](/gzh.jpg) + +::: + +::: info AI悦创·编程一对一 + +AI悦创·推出辅导班啦,包括「Python 语言辅导班、C++ 辅导班、java 辅导班、算法/数据结构辅导班、少儿编程、pygame 游戏开发」,全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践等。当然,还有线下线上摄影课程、Photoshop、Premiere 一对一教学、QQ、微信在线,随时响应!微信:Jiabcdefh + +C++ 信息奥赛题解,长期更新!长期招收一对一中小学信息奥赛集训,莆田、厦门地区有机会线下上门,其他地区线上。微信:Jiabcdefh + +方法一:[QQ](http://wpa.qq.com/msgrd?v=3&uin=1432803776&site=qq&menu=yes) + +方法二:微信:Jiabcdefh + +::: + +![](/zsxq.jpg) + diff --git a/src/Python/Python-core-technology-and-practice/Algorithm/12.assets/5e0da4c600014d0806080199.png b/src/Python/Python-core-technology-and-practice/Algorithm/12.assets/5e0da4c600014d0806080199.png new file mode 100644 index 00000000000..22e8fe64623 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/Algorithm/12.assets/5e0da4c600014d0806080199.png differ diff --git a/src/Python/Python-core-technology-and-practice/Algorithm/12.assets/5e0da4ea0001510b06140097.png b/src/Python/Python-core-technology-and-practice/Algorithm/12.assets/5e0da4ea0001510b06140097.png new file mode 100644 index 00000000000..98b7b06a05a Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/Algorithm/12.assets/5e0da4ea0001510b06140097.png differ diff --git a/src/Python/Python-core-technology-and-practice/Algorithm/12.assets/5e0da516000199d506050092.png b/src/Python/Python-core-technology-and-practice/Algorithm/12.assets/5e0da516000199d506050092.png new file mode 100644 index 00000000000..ab2dd7e3c81 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/Algorithm/12.assets/5e0da516000199d506050092.png differ diff --git a/src/Python/Python-core-technology-and-practice/Algorithm/12.assets/5e0e9d340001101106400359.jpg b/src/Python/Python-core-technology-and-practice/Algorithm/12.assets/5e0e9d340001101106400359.jpg new file mode 100644 index 00000000000..29f73e40f67 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/Algorithm/12.assets/5e0e9d340001101106400359.jpg differ diff --git a/src/Python/Python-core-technology-and-practice/Algorithm/12.md b/src/Python/Python-core-technology-and-practice/Algorithm/12.md new file mode 100644 index 00000000000..65853ea0f15 --- /dev/null +++ b/src/Python/Python-core-technology-and-practice/Algorithm/12.md @@ -0,0 +1,328 @@ +--- +title: 12 排序八大金刚-插入排序 +icon: shujujiegou-01 +date: 2023-06-01 21:33:07 +author: AI悦创 +isOriginal: true +category: Python 算法科普指南 +tag: + - Python 算法科普指南 +sticky: false +star: false +article: true +timeline: true +image: false +navbar: true +sidebarIcon: true +headerDepth: 5 +comment: true +lastUpdated: true +editLink: false +backToTop: true +toc: true +--- + +![](./12.assets/5e0e9d340001101106400359.jpg) + +## 1. 排序简介 + +排序通常指把毫无规律的数据,按照一种特定的规律,整理成有序排列的状态。一般情况下,排序算法按照关键字的大小,以从小到大或从大到小的顺序将数据排列。 + +排序算法是最基础也最重要的算法之一,在处理大量数据时,使用一个优秀的排序算法可以节省大量时间和空间。因为不同的排序算法拥有不同的特点,所以我们根据情况选择合适的排序算法。 + +直观地讲,插入排序算法把给定数组中的元素依次插入到一个新的数组中,最终得到一个完整的有序数组。 + +## 2. 插入排序效率分析 + +在第一章中,我们已经讲过如何计算时间复杂度与空间复杂度,所以本章不再给出计算过程。插入排序的平均时间复杂度是 $O(n^2)$,最好情况下的时间复杂度是 $O(n)$, 最坏情况下的时间复杂度是 $O(n^2)$。它的空间复杂度是 $O(1)$。 + +插入排序还是一个稳定的排序算法。这里涉及到一个新的概念:排序算法的稳定性。 排序算法可以分为稳定的算法和不稳定的算法两类。在一个数组中,我们假设存在多个有相同关键字的元素。如果使用算法进行排序后,这些具有相同关键字的元素相对顺序一定保持不变,那么我们称这个排序算法为稳定的排序算法。冒泡排序、插入排序和归并排序等都是稳定的排序算法。而不能保证这些元素排序前后的相对位置相同的算法,就是不稳定的排序算法。选择排序,希尔排序和快速排序等都是不稳定的排序算法。 + + + +## 3. 插入排序原理 + +直接插入排序的实现过程较为直观。 + +排序开始时,我们对范例数组的每一个元素进行遍历。如图1所示,虚线的左侧表示已经有序的元素,右侧表示待排序的元素。 + +初始状态下,所有的元素都处于无序的状态,所以它们都在虚线的右侧。首先遍历的是第一个元素,这时候有序的数组为空(暂且把整个数组在虚线左侧的部分考虑成一个整体),所以第一个元素插入左侧的数组后必定是有序的。 + +第一个元素插入完成后,接下来遍历的是整个数组中的第二个元素。 + +![](./12.assets/5e0da4c600014d0806080199.png) + +此时,我们就要考虑:如何使得左侧有序的数组在新元素插入后保持有序?答案是再遍历一遍左侧有序的数组,找到正确的位置再插入新的元素。如下图所示,第二个元素3比有序数组中的5小,所以应该把它插入到5的左侧。 + +![](./12.assets/5e0da4ea0001510b06140097.png) + +如下图所示,随后的过程是相似的。我们依次遍历无序数组中的元素,并把它们插入到有序数组中正确的位置。 + +![](./12.assets/5e0da516000199d506050092.png) + +当对无序数组的遍历完成后,有序数组中就包含了所有原始数组中的元素。这时候对原始数组的排序就完成了。 + +## 4. 插入排序代码 + +插入排序的代码再现了这个移动元素的过程。以下代码将数组 nums 正序排序。 + +插入排序代码: + +::: code-tabs + +@tab simple code + +```python +nums = [5,3,6,4,1,2,8,7] +for i in range(1, len(nums)): #遍历未排序的元素 + for j in range(i): #遍历已有序的元素 + if nums[j]>nums[i]: #找到插入位置 + ins = nums[i] + nums.pop(i) + nums.insert(j, ins) + break #完成插入后跳出for循环 +print(nums) +``` + +@tab 详细注释 + +```python +# 原数组 +numbers = [5,3,6,4,1,2,8,7] + +# 外层循环,开始于数组的第二个元素,因为我们将第一个元素看作是已经排序的 +for current_index in range(1, len(numbers)): + + # 内层循环,遍历已排序部分的元素 + for sorted_index in range(current_index): + + # 如果在已排序部分找到一个比当前元素大的值,说明需要将当前元素插入到这个位置 + if numbers[sorted_index] > numbers[current_index]: + + # 取出当前需要排序的元素 + to_insert = numbers[current_index] + + # 从数组中移除当前元素 + numbers.pop(current_index) + + # 将当前元素插入到正确的位置 + numbers.insert(sorted_index, to_insert) + + # 完成插入后跳出内层for循环,进入下一个元素的排序 + break + +# 打印排序后的数组 +print(numbers) +``` + +::: + +运行程序,输出结果为: + +```python +[1, 2, 3, 4, 5, 6, 7, 8] +``` + +代码中,第一个 for 循环用于遍历未排序元素。在上面的演示中,我们知道下标为 0 的元素,也就是第一个元素,已经处于有序状态,所以可以直接从第二个元素开始插入排序,使用 `range(1, len(nums))` 。 + +第二个 for 循环用于遍历已排序的元素,也就是下标小于当前元素的所有元素,所以使用 `range(i)`。判断插入位置时,由于我们想把元素递增地排列,所以当前元素的插入位置应当是在第一个大于它的数据之前。 + +因为找到比当前元素大的数据后,程序会立刻进行插入排序并跳出循环,从而可以确定已经遍历过的元素必定小于当前元素。如果所有有序的元素都小于当前元素,那么当前元素应当留在原来的位置上,不必再进行插入排序。 + + + +## 5. 小结 + +本节讲解了插入排序算法,插入排序算法是一种较为基础且容易理解的排序算法。在本章中,初级排序算法包含插入排序、选择排序和冒泡排序三种算法。虽然它们的效率相对于高级排序算法偏低,但是了解初级排序算法之后,再去学习相对复杂的高级排序算法会容易许多。 + +## 6. 练习 + +1. **题目1**: 编写一个 Python 函数,使用选择排序算法对列表进行排序。然后在一组随机生成的数上测试你的函数。 + +2. **题目2**: 编写一个 Python 程序,使用选择排序算法对字符串列表进行字典排序。 + +3. **题目3**: 在 Python 中,尝试修改标准的选择排序算法以逆序排序数组。 + +4. **题目4**: 编写一个 Python 程序,将选择排序算法应用于字典,根据字典的值进行排序。 + +5. **题目5**: 编写一个 Python 函数,用选择排序算法对元组数组进行排序。例如,给定元组数组 `[(2, 5), (1, 3), (4, 1), (2, 3)]`,应返回 `[(1, 3), (2, 3), (2, 5), (4, 1)]` 。 + +**题目1**: 编写一个Python函数,使用选择排序算法对列表进行排序。然后在一组随机生成的数上测试你的函数。 + +```python +import random + +def selection_sort(arr): + # 遍历所有数组元素 + for i in range(len(arr)): + # 找到当前序列中最小元素的索引 + min_index = i + for j in range(i+1, len(arr)): + if arr[min_index] > arr[j]: + min_index = j + + # 交换当前序列最小元素与当前元素 + arr[i], arr[min_index] = arr[min_index], arr[i] + + return arr + +# 生成一个长度为10的随机整数列表,每个元素的值在1-100之间 +random_list = random.sample(range(1, 100), 10) +print('Before sorting:', random_list) +sorted_list = selection_sort(random_list) +print('After sorting:', sorted_list) +``` + +**题目2**: 编写一个Python程序,使用选择排序算法对字符串列表进行字典排序。 + +```python +def selection_sort(arr): + # 遍历所有数组元素 + for i in range(len(arr)): + # 找到当前序列中最小元素的索引 + min_index = i + for j in range(i+1, len(arr)): + if arr[min_index] > arr[j]: + min_index = j + + # 交换当前序列最小元素与当前元素 + arr[i], arr[min_index] = arr[min_index], arr[i] + + return arr + +str_list = ['apple', 'banana', 'cherry', 'date', 'elderberry'] +print('Before sorting:', str_list) +sorted_list = selection_sort(str_list) +print('After sorting:', sorted_list) +``` + +**题目3**: 在Python中,尝试修改标准的选择排序算法以逆序排序数组。 + +```python +def selection_sort_desc(arr): + # 遍历所有数组元素 + for i in range(len(arr)): + # 找到当前序列中最大元素的索引 + max_index = i + for j in range(i+1, len(arr)): + if arr[max_index] < arr[j]: + max_index = j + + # 交换当前序列最大元素与当前元素 + arr[i], arr[max_index] = arr[max_index], arr[i] + + return arr + +arr = [64, 25, 12, 22, 11] +print("Before sorting:", arr) +sorted_arr = selection_sort_desc(arr) +print("After sorting in descending order:", sorted_arr) +``` + +**题目4**: 编写一个Python程序,将选择排序算法应用于字典,根据字典的值进行排序。 + +```python +def selection_sort_dict(d): + # 将字典转换为元组列表 + items = list(d.items()) + + # 选择排序,但是此次比较的是元组的第二个元素 + for i in range(len(items)): + min_index = i + for j in range(i+1, len(items)): + if items[min_index][1] > items[j][1]: + min_index = j + + items[i], items[min_index + +] = items[min_index], items[i] + + # 将排序后的元组列表再转换回字典 + sorted_dict = dict(items) + return sorted_dict + +d = {'a': 2, 'b': 1, 'c': 5, 'd': 4, 'e': 3} +print("Before sorting:", d) +sorted_dict = selection_sort_dict(d) +print("After sorting by value:", sorted_dict) +``` + +**题目5**: 编写一个Python函数,用选择排序算法对元组数组进行排序。 + +```python +def selection_sort_tuples(arr): + # 遍历所有数组元素 + for i in range(len(arr)): + # 找到当前序列中最小元素的索引 + min_index = i + for j in range(i+1, len(arr)): + if arr[min_index] > arr[j]: + min_index = j + + # 交换当前序列最小元素与当前元素 + arr[i], arr[min_index] = arr[min_index], arr[i] + + return arr + +tuples = [(2, 5), (1, 3), (4, 1), (2, 3)] +print('Before sorting:', tuples) +sorted_tuples = selection_sort_tuples(tuples) +print('After sorting:', sorted_tuples) +``` + +## 7. 杂谈 + +### 7.1 选择排序的思路是什么? + +选择排序(Selection Sort)是一种简单直观的排序算法。其工作原理如下: + +1. 在未排序序列中找到最小(或最大)的元素,存放到排序序列的起始位置。 +2. 从剩余未排序元素中继续寻找最小(或最大)元素,然后放到已排序序列的末尾。 +3. 以此类推,直到所有元素均排序完毕。 + +选择排序的主要优点是实现简单,对于小规模数据的排序,它是有效的。但由于其时间复杂度是 O(n²),所以当数据规模较大时,效率并不高。 + + + + + + + + + + + + + + + + + + + + + + + +欢迎关注我公众号:AI悦创,有更多更好玩的等你发现! + +::: details 公众号:AI悦创【二维码】 + +![](/gzh.jpg) + +::: + +::: info AI悦创·编程一对一 + +AI悦创·推出辅导班啦,包括「Python 语言辅导班、C++ 辅导班、java 辅导班、算法/数据结构辅导班、少儿编程、pygame 游戏开发」,全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践等。当然,还有线下线上摄影课程、Photoshop、Premiere 一对一教学、QQ、微信在线,随时响应!微信:Jiabcdefh + +C++ 信息奥赛题解,长期更新!长期招收一对一中小学信息奥赛集训,莆田、厦门地区有机会线下上门,其他地区线上。微信:Jiabcdefh + +方法一:[QQ](http://wpa.qq.com/msgrd?v=3&uin=1432803776&site=qq&menu=yes) + +方法二:微信:Jiabcdefh + +::: + +![](/zsxq.jpg) + diff --git a/src/Python/Python-core-technology-and-practice/Algorithm/13.assets/5e0da5e40001128405930216.png b/src/Python/Python-core-technology-and-practice/Algorithm/13.assets/5e0da5e40001128405930216.png new file mode 100644 index 00000000000..6ed5579cad6 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/Algorithm/13.assets/5e0da5e40001128405930216.png differ diff --git a/src/Python/Python-core-technology-and-practice/Algorithm/13.assets/5e0da60200013df805190218.png b/src/Python/Python-core-technology-and-practice/Algorithm/13.assets/5e0da60200013df805190218.png new file mode 100644 index 00000000000..98c03a5ca7d Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/Algorithm/13.assets/5e0da60200013df805190218.png differ diff --git a/src/Python/Python-core-technology-and-practice/Algorithm/13.assets/5e0da6180001915d06010232.png b/src/Python/Python-core-technology-and-practice/Algorithm/13.assets/5e0da6180001915d06010232.png new file mode 100644 index 00000000000..01e1cad7e7d Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/Algorithm/13.assets/5e0da6180001915d06010232.png differ diff --git a/src/Python/Python-core-technology-and-practice/Algorithm/13.assets/5e0da6780001720405990083.png b/src/Python/Python-core-technology-and-practice/Algorithm/13.assets/5e0da6780001720405990083.png new file mode 100644 index 00000000000..a9c77ec1c2c Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/Algorithm/13.assets/5e0da6780001720405990083.png differ diff --git a/src/Python/Python-core-technology-and-practice/Algorithm/13.assets/5e0da6b80001864a06050091.png b/src/Python/Python-core-technology-and-practice/Algorithm/13.assets/5e0da6b80001864a06050091.png new file mode 100644 index 00000000000..b6c91e6a28a Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/Algorithm/13.assets/5e0da6b80001864a06050091.png differ diff --git a/src/Python/Python-core-technology-and-practice/Algorithm/13.assets/5e0da6c100019e9a05900082.png b/src/Python/Python-core-technology-and-practice/Algorithm/13.assets/5e0da6c100019e9a05900082.png new file mode 100644 index 00000000000..83845c0e682 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/Algorithm/13.assets/5e0da6c100019e9a05900082.png differ diff --git a/src/Python/Python-core-technology-and-practice/Algorithm/13.assets/5e0da6c800010af805910090.png b/src/Python/Python-core-technology-and-practice/Algorithm/13.assets/5e0da6c800010af805910090.png new file mode 100644 index 00000000000..d0f10b94764 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/Algorithm/13.assets/5e0da6c800010af805910090.png differ diff --git a/src/Python/Python-core-technology-and-practice/Algorithm/13.assets/5e0da6cf000182fc05890092.png b/src/Python/Python-core-technology-and-practice/Algorithm/13.assets/5e0da6cf000182fc05890092.png new file mode 100644 index 00000000000..70e77bf724d Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/Algorithm/13.assets/5e0da6cf000182fc05890092.png differ diff --git a/src/Python/Python-core-technology-and-practice/Algorithm/13.assets/5e0da6d500014d5b05960091.png b/src/Python/Python-core-technology-and-practice/Algorithm/13.assets/5e0da6d500014d5b05960091.png new file mode 100644 index 00000000000..2f1a2eeed10 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/Algorithm/13.assets/5e0da6d500014d5b05960091.png differ diff --git a/src/Python/Python-core-technology-and-practice/Algorithm/13.assets/5e0da6dc0001459d05950086.png b/src/Python/Python-core-technology-and-practice/Algorithm/13.assets/5e0da6dc0001459d05950086.png new file mode 100644 index 00000000000..71dab2045c7 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/Algorithm/13.assets/5e0da6dc0001459d05950086.png differ diff --git a/src/Python/Python-core-technology-and-practice/Algorithm/13.assets/5e12922c0001eb8d06400426.jpg b/src/Python/Python-core-technology-and-practice/Algorithm/13.assets/5e12922c0001eb8d06400426.jpg new file mode 100644 index 00000000000..a3c1fb09f92 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/Algorithm/13.assets/5e12922c0001eb8d06400426.jpg differ diff --git a/src/Python/Python-core-technology-and-practice/Algorithm/13.md b/src/Python/Python-core-technology-and-practice/Algorithm/13.md new file mode 100644 index 00000000000..7c2dd72a21f --- /dev/null +++ b/src/Python/Python-core-technology-and-practice/Algorithm/13.md @@ -0,0 +1,167 @@ +--- +title: 13 排序八大金刚-选择排序 +icon: shujujiegou-01 +date: 2023-06-07 22:08:20 +author: AI悦创 +isOriginal: true +category: Python 算法科普指南 +tag: + - Python 算法科普指南 +sticky: false +star: false +article: true +timeline: true +image: false +navbar: true +sidebarIcon: true +headerDepth: 5 +comment: true +lastUpdated: true +editLink: false +backToTop: true +toc: true +--- + +![](./13.assets/5e12922c0001eb8d06400426.jpg) + +选择排序表示从无序的数组中,每次选择最小或最大的数据,从无序数组中放到有序数组的末尾,以达到排序的效果。 + +选择排序的平均时间复杂度,最好情况下的时间复杂度$O(n^2)$,最坏情况下的时间复杂度都是$O(n^2)$。另外,它是一个不稳定的排序算法。 + +## 1. 选择排序 + +选择排序的过程很容易理解。如下图所示,我们仍以递增排序的算法为例,先遍历未排序的数组,找到最小的元素。然后,把最小的元素从未排序的数组中删除,添加到有序数组的末尾。 + +![](./13.assets/5e0da5e40001128405930216.png) + + + +因为最小的元素是1,所以1被添加到仍为空的有序数组末尾。 + +如下图所示,我们继续对剩余元素进行遍历。这次,最小的元素是2。我们把它添加到已排序的数组末尾。由于已在有序数组中的元素必定小于未排序数组中的所有元素,所以这步操作是正确无误的。 + +![](./13.assets/5e0da60200013df805190218.png) + +如下图所示,重复上述步骤,当未排序数组中只剩下一个元素时,把它添加到已排序的数组末尾,整个数组的排序就完成了。 + +![](./13.assets/5e0da6180001915d06010232.png) + +采用图中的思路,以下代码将数组 nums 进行正序排序。 + +选择排序代码(基础版): + +```python +nums = [5, 3, 6, 4, 1, 2, 8, 7] +res = [] # 用于存储已排序元素的数组 +while len(nums): # 当未排序数组内还有元素时,重复执行选择最小数的代码 + minInd = 0 # 初始化存储最小数下标的变量,默认为第一个数 + for i in range(1, len(nums)): + if (nums[i] < nums[minInd]): # 更新最小数的下标 + minInd = i + temp = nums[minInd] + nums.pop(minInd) # 把最小数从未排序数组中删除 + res.append(temp) # 把最小数插入到已排序数组的末尾 +print(res) +``` + +运行程序,输出结果为: + +```python +[1, 2, 3, 4, 5, 6, 7, 8] +``` + +代码中,最外层的 while 循环用于判断是否所有的元素都已经进入有序的数组,从而确定排序是否已经完成。如果无序数组中已经没有元素,说明排序已经完成。 + +在开始遍历无序数组之前,先初始化记录最小值下标的变量为 0,所以 for 循环可以从第二个元素,也就是下标为1的元素开始遍历。找到最小值后,用 temp 存储最小数的值。执行 pop 函数把最小数从原数组中删除,这样它不会影响下一步的选择。最后,用 append 把 temp 存储的元素插入到有序数组末尾。 + +## 2. 选择排序改进版 + +虽然这样实现排序较为直观,代码逻辑也比较简单,但可以注意到,这样实现插入排序需要两个同样大小数组的空间。如果要处理的数据量较大,这样的算法会浪费资源。所以,我们要对算法做一些改动,使选择排序能够在同一个数组内完成。同样地,我们用图片来展示这个过程。 + +首先,如下图所示,在未排序的数组中找到最小的数1。 + +![](./13.assets/5e0da6780001720405990083.png) + +此时,它是我们找到的第一个最小数。如下图所示,我们把它与数组的第一个元素交换。 + +![](./13.assets/5e0da6dc0001459d05950086.png) + + + +如下图所示,这时候,数组中的第一个位置就成为了有序数据的一部分。 + +![](./13.assets/5e0da6d500014d5b05960091.png) + +接下来,如下图所示,由于第一个元素已经有序了,所以我们只需要在它之后的数组中搜索最小值。这一趟搜索过后,最小值是 2,所以把 2 和第二个元素交换位置。 + +![](./13.assets/5e0da6cf000182fc05890092.png) + +如下图所示,2 和第二个元素交换位置后,第二个位置就成为了这个有序数组的一部分。 + +![](./13.assets/5e0da6c800010af805910090.png) + + + +接下来,如下图所示,继续重复以上步骤,直到所有元素都被加入到有序数组中。下面给出了确定第三小的数的过程。 + +![](./13.assets/5e0da6c100019e9a05900082.png) + +如下图,当所有元素都加入有序数组后,排序就完成了。 + +![](./13.assets/5e0da6b80001864a06050091.png) + +使用这样的思路,我们可以使用代码实现选择排序。 + +选择排序代码(原地版): + +```python +nums = [5, 3, 6, 4, 1, 2, 8, 7] +for i in range(len(nums) - 1): # 更新有序数组的末尾位置 + minInd = i + for j in range(i, len(nums)): # 找出未排序数组中最小值的下标 + if nums[j] < nums[minInd]: + minInd = j + nums[i], nums[minInd] = nums[minInd], nums[i] # 把最小值加到有序数组末尾 +print(nums) +``` + +运行程序,输出结果为: + +```python +[1, 2, 3, 4, 5, 6, 7, 8] +``` + +在程序中,第一个 for 循环中的i代表了有序数组之后的第一个位置,也就是未排序数组中的第一个位置。随后,再使用一个 for 循环,在未排序数组中找到最小值的下标。首先,把最小值下标 minInd 初始化为未排序数组中第一个元素的下标。随后,遍历整个数组,遇到比目前的最小值更小的元素时,更新下标即可。找出最小值后,把它和未排序数组中的第一个元素交换位置,这时它就成为了有序数组中的最后一个元素。 + +## 3. 小结 + +本节介绍了选择排序算法,在其他一些编程语言中,不能像 Python 一样使用 pop、insert 等函数对数组进行操作。插入一个数时,需要把插入位置及后面的所有元素都向后移动一位。这时候,本小节中的原地版算法优势更加明显。 + + + + + + + +欢迎关注我公众号:AI悦创,有更多更好玩的等你发现! + +::: details 公众号:AI悦创【二维码】 + +![](/gzh.jpg) + +::: + +::: info AI悦创·编程一对一 + +AI悦创·推出辅导班啦,包括「Python 语言辅导班、C++ 辅导班、java 辅导班、算法/数据结构辅导班、少儿编程、pygame 游戏开发」,全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践等。当然,还有线下线上摄影课程、Photoshop、Premiere 一对一教学、QQ、微信在线,随时响应!微信:Jiabcdefh + +C++ 信息奥赛题解,长期更新!长期招收一对一中小学信息奥赛集训,莆田、厦门地区有机会线下上门,其他地区线上。微信:Jiabcdefh + +方法一:[QQ](http://wpa.qq.com/msgrd?v=3&uin=1432803776&site=qq&menu=yes) + +方法二:微信:Jiabcdefh + +::: + +![](/zsxq.jpg) + diff --git a/src/Python/Python-core-technology-and-practice/Algorithm/14.assets/5e152f960001e57006400391.jpg b/src/Python/Python-core-technology-and-practice/Algorithm/14.assets/5e152f960001e57006400391.jpg new file mode 100644 index 00000000000..aaabe121f12 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/Algorithm/14.assets/5e152f960001e57006400391.jpg differ diff --git a/src/Python/Python-core-technology-and-practice/Algorithm/14.assets/5e1530280001288c05910082-20230612205226666.png b/src/Python/Python-core-technology-and-practice/Algorithm/14.assets/5e1530280001288c05910082-20230612205226666.png new file mode 100644 index 00000000000..51f763b2932 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/Algorithm/14.assets/5e1530280001288c05910082-20230612205226666.png differ diff --git a/src/Python/Python-core-technology-and-practice/Algorithm/14.assets/5e1530280001288c05910082.png b/src/Python/Python-core-technology-and-practice/Algorithm/14.assets/5e1530280001288c05910082.png new file mode 100644 index 00000000000..51f763b2932 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/Algorithm/14.assets/5e1530280001288c05910082.png differ diff --git a/src/Python/Python-core-technology-and-practice/Algorithm/14.assets/5e1530440001559405910085-20230612205249491.png b/src/Python/Python-core-technology-and-practice/Algorithm/14.assets/5e1530440001559405910085-20230612205249491.png new file mode 100644 index 00000000000..797688a3985 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/Algorithm/14.assets/5e1530440001559405910085-20230612205249491.png differ diff --git a/src/Python/Python-core-technology-and-practice/Algorithm/14.assets/5e1530440001559405910085.png b/src/Python/Python-core-technology-and-practice/Algorithm/14.assets/5e1530440001559405910085.png new file mode 100644 index 00000000000..797688a3985 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/Algorithm/14.assets/5e1530440001559405910085.png differ diff --git a/src/Python/Python-core-technology-and-practice/Algorithm/14.assets/5e1530520001f89405880083-20230612205314825.png b/src/Python/Python-core-technology-and-practice/Algorithm/14.assets/5e1530520001f89405880083-20230612205314825.png new file mode 100644 index 00000000000..c25a1dea18c Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/Algorithm/14.assets/5e1530520001f89405880083-20230612205314825.png differ diff --git a/src/Python/Python-core-technology-and-practice/Algorithm/14.assets/5e1530520001f89405880083.png b/src/Python/Python-core-technology-and-practice/Algorithm/14.assets/5e1530520001f89405880083.png new file mode 100644 index 00000000000..c25a1dea18c Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/Algorithm/14.assets/5e1530520001f89405880083.png differ diff --git a/src/Python/Python-core-technology-and-practice/Algorithm/14.assets/5e1530660001f2ef05890083.png b/src/Python/Python-core-technology-and-practice/Algorithm/14.assets/5e1530660001f2ef05890083.png new file mode 100644 index 00000000000..ec61a431994 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/Algorithm/14.assets/5e1530660001f2ef05890083.png differ diff --git a/src/Python/Python-core-technology-and-practice/Algorithm/14.assets/5e15308f0001d1d905990269.png b/src/Python/Python-core-technology-and-practice/Algorithm/14.assets/5e15308f0001d1d905990269.png new file mode 100644 index 00000000000..652ab3f031d Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/Algorithm/14.assets/5e15308f0001d1d905990269.png differ diff --git a/src/Python/Python-core-technology-and-practice/Algorithm/14.assets/5e15309d000125d105910077.png b/src/Python/Python-core-technology-and-practice/Algorithm/14.assets/5e15309d000125d105910077.png new file mode 100644 index 00000000000..e005ce1e8df Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/Algorithm/14.assets/5e15309d000125d105910077.png differ diff --git a/src/Python/Python-core-technology-and-practice/Algorithm/14.assets/5e1530b40001bb7e05910381.png b/src/Python/Python-core-technology-and-practice/Algorithm/14.assets/5e1530b40001bb7e05910381.png new file mode 100644 index 00000000000..26e538c286f Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/Algorithm/14.assets/5e1530b40001bb7e05910381.png differ diff --git a/src/Python/Python-core-technology-and-practice/Algorithm/14.md b/src/Python/Python-core-technology-and-practice/Algorithm/14.md new file mode 100644 index 00000000000..8bac9711c11 --- /dev/null +++ b/src/Python/Python-core-technology-and-practice/Algorithm/14.md @@ -0,0 +1,150 @@ +--- +title: 14 排序八大金刚-冒泡排序 +icon: shujujiegou-01 +date: 2023-06-12 20:45:05 +author: AI悦创 +isOriginal: true +category: Python 算法科普指南 +tag: + - Python 算法科普指南 +sticky: false +star: false +article: true +timeline: true +image: false +navbar: true +sidebarIcon: true +headerDepth: 5 +comment: true +lastUpdated: true +editLink: false +backToTop: true +toc: true +--- + +![](./14.assets/5e152f960001e57006400391.jpg) + +你好,我是悦创。 + +最后一种基础排序是冒泡排序。算法采用重复遍历数组并依次比较相邻元素的方法来排序。由于在冒泡算法进行排序的过程中,最大数或者最小数会慢慢“浮”到数组的末尾,所以算法由此命名。 + +冒泡排序的平均时间复杂度是 $O(n^2)$,最好情况下的时间复杂度是 $O(n)$, 最坏情况下的时间复杂度是 $O(n^2)$。空间复杂度是 $O(1)$。冒泡排序算法是一个稳定的排序算法。 + +冒泡排序的过程同样可以用图片说明。我们的目标还是把无序数组以从小到大的顺序排列。 + +## 1. 冒泡排序原理 + +首先,如下图所示,我们从第一个数开始遍历。将第一个数与它后面的元素进行对比,发现后面的元素比它小。 + +![](./14.assets/5e1530280001288c05910082-20230612205226666.png) + +这时候,如下图所示,我们需交换这两个元素的值。 + +![](./14.assets/5e1530440001559405910085-20230612205249491.png) + +接下来遍历到的是第二个元素。如下图所示,此时第二个元素的值已经变为 5。把它和它后方的元素 6 对比,发现 5 和 6 的排列顺序已经是正确的(前面的数小于后面的数),这时候不用进行元素交换,直接继续遍历。 + +![](./14.assets/5e1530520001f89405880083-20230612205314825.png) + +如下图所示,遍历到第三个元素时,发现它比后面的元素更大,这时候就继续交换这两个元素的值。 + +![](./14.assets/5e1530660001f2ef05890083.png) + +如图所示,在类似的一系列操作后,数组中的最大值被交换到了数组中的最后一个(第8个)位置上。 + +![](./14.assets/5e15308f0001d1d905990269.png) + +如图所示,这时候,我们可以确定末尾元素的值是正确的,所以接下来我们只需要对第1-7个位置上的元素再进行遍历了。 + +![](./14.assets/5e15309d000125d105910077.png) + +在对第 1-7 个位置上的的元素进行遍历之后,我们可以确定排在第 7 位的数。同理,在对第 1-6 个位置上的元素,第 1-5 个位置上的元素等进行遍历后,我们可以确定数组中排在第6位,第 5 位的数等。冒泡排序的剩下过程如图所示。 + +![](./14.assets/5e1530b40001bb7e05910381.png) + +但是,我们发现,在排好第五个数之后,整个数组的排序就已经完成了,在接下来的遍历中不会再产生元素的交换。这时候,我们可以直接结束遍历。 + +## 2. 冒泡排序代码 + +了解了冒泡排序的流程之后,我们再来看看冒泡排序的代码。 + +冒泡排序的代码: + +::: code-tabs + +@tab 1 + +```python +nums = [5,3,6,4,1,2,8,7] +for i in range(len(nums),0,-1): #更新本趟遍历确定的元素位置 + flag = 0 #flag用于标记是否有元素交换发生 + for j in range(i-1): #遍历未排序的数组 + if nums[j]>nums[j+1]: + nums[j],nums[j+1] = nums[j+1],nums[j] + flag = 1 #标记存在元素交换 + if not flag: + break #如果本趟遍历没有经历元素交换,直接跳出循环 +print(nums) +``` + +@tab 2 + +```python +nums = [5,3,6,4,1,2,8,7] +for i in range(len(nums),0,-1): # 从列表的末尾开始遍历,因为冒泡排序是每一趟将最大的元素"冒泡"到列表的末尾 + flag = 0 # 初始设定flag为0,表示没有元素交换发生 + for j in range(i-1): # 遍历未排序的列表 + if nums[j]>nums[j+1]: # 如果当前元素大于后一个元素,则交换它们的位置 + nums[j],nums[j+1] = nums[j+1],nums[j] + flag = 1 # 如果发生交换,则将flag设为1 + if not flag: # 如果一趟遍历结束后flag仍然为0,表示没有发生过交换,即列表已经排序好 + break # 直接跳出循环 +print(nums) # 打印排序好的列表 +``` + +::: + +运行程序,输出结果为: + +```python +[1,2,3,4,5,6,7,8] +``` + +这段冒泡排序的代码中使用了两个 for 循环。外层 for 循环中的i代表每一次遍历后确定位置的元素的下标。 + +变量 flag 用于记录是否有元素交换发生,初始为 0,在遍历开始后,一旦两位元素进行交换,它的值就会变为 1。 + +随后,再用一个 for 循环对未排序数组进行遍历。为什么遍历的范围是 `range(i-1)` ?因为未排序数组的最后一个元素下标为 i,而我们在遍历时要同时访问下标为 j 和 j+1 的元素。把遍历范围设为 `range(i-1)` ,访问数组时才不会越界。另一个需要注意的点是交换元素的条件:`num[j] > num[j+1]`。注意不要把大于号写成大于等于号。当这两个元素相等时,为保留它们的原有相对位置,不要进行交换。如果把运算符写成大于等于号,排序算法的稳定性就被破坏了。 + +遍历结束后,如果 flag 的值仍然是 0,那么说明在整一次遍历中没有元素交换发生,也就是说,所有元素都是有序排列的。这时候就可以直接跳出循环,节省时间。 + +## 3. 小结 + +初级排序算法至此结束了。掌握了初级排序算法之后,我们再进入高级排序算法的学习。 + + + + + +欢迎关注我公众号:AI悦创,有更多更好玩的等你发现! + +::: details 公众号:AI悦创【二维码】 + +![](/gzh.jpg) + +::: + +::: info AI悦创·编程一对一 + +AI悦创·推出辅导班啦,包括「Python 语言辅导班、C++ 辅导班、java 辅导班、算法/数据结构辅导班、少儿编程、pygame 游戏开发」,全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践等。当然,还有线下线上摄影课程、Photoshop、Premiere 一对一教学、QQ、微信在线,随时响应!微信:Jiabcdefh + +C++ 信息奥赛题解,长期更新!长期招收一对一中小学信息奥赛集训,莆田、厦门地区有机会线下上门,其他地区线上。微信:Jiabcdefh + +方法一:[QQ](http://wpa.qq.com/msgrd?v=3&uin=1432803776&site=qq&menu=yes) + +方法二:微信:Jiabcdefh + +::: + +![](/zsxq.jpg) + diff --git a/src/Python/Python-core-technology-and-practice/Algorithm/15.assets/5e15321d0001aad106400426.jpg b/src/Python/Python-core-technology-and-practice/Algorithm/15.assets/5e15321d0001aad106400426.jpg new file mode 100644 index 00000000000..4addc5ef7cd Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/Algorithm/15.assets/5e15321d0001aad106400426.jpg differ diff --git a/src/Python/Python-core-technology-and-practice/Algorithm/15.assets/5e15327d0001af1904150408.png b/src/Python/Python-core-technology-and-practice/Algorithm/15.assets/5e15327d0001af1904150408.png new file mode 100644 index 00000000000..382b74b0141 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/Algorithm/15.assets/5e15327d0001af1904150408.png differ diff --git a/src/Python/Python-core-technology-and-practice/Algorithm/15.md b/src/Python/Python-core-technology-and-practice/Algorithm/15.md new file mode 100644 index 00000000000..52371fcae1a --- /dev/null +++ b/src/Python/Python-core-technology-and-practice/Algorithm/15.md @@ -0,0 +1,257 @@ +--- +title: 15 排序八大金刚-归并排序 +icon: shujujiegou-01 +date: 2023-06-15 22:23:53 +author: AI悦创 +isOriginal: true +category: Python 算法科普指南 +tag: + - Python 算法科普指南 +sticky: false +star: false +article: true +timeline: true +image: false +navbar: true +sidebarIcon: true +headerDepth: 5 +comment: true +lastUpdated: true +editLink: false +backToTop: true +toc: true +--- + +![](./15.assets/5e15321d0001aad106400426.jpg) + +## 1. 归并排序原理 + +先来看第一种高级排序算法:归并排序。“归并”一词出自《后汉书》,意为“合并”。 + +顾名思义,归并排序算法就是一个先把数列拆分为子数列,对子数列进行排序后,再把有序的子数列合并为完整的有序数列的算法。它实际上采用了分治的思想,我们会在后面文章中深度讲解分治思想。 + +归并排序的平均时间复杂度是 $O(nlogn)$,最好情况下的时间复杂度是$O(nlogn)$, 最坏情况下的时间复杂度也是$O(nlogn)$。它的空间复杂度是 $O(1)$,另外还是一个稳定的排序算法。 + +以升序排序为例,归并算法的流程就如下图所示。 + +![](./15.assets/5e15327d0001af1904150408.png) + +原始数组是一个有八个数的无序数组。一次操作后,把八个数的数组分成两个四个数组成的无序数组。接下来的每次操作都是把无序数组不停分成两半,直到每个最小的数组里都只有一个元素为止。 + +**当数组里只有一个元素时,这个数组必定是有序的。** + +然后,程序开始把小的有序数组每两个合并成为大的有序数组。先是从两个 1 个数的数组合并成 2 个数的数组,再到 4 个数然后 8 个数。这时候,所有的有序数组全部合并完成,最后产生的最长的有序数组就排序完成了。 + +## 2. 归并排序代码 + +归并排序代码: + +```python +#归并排序 +nums = [5,3,6,4,1,2,8,7] +def MergeSort(num): + if(len(num)<=1): #递归边界条件 + return num #到达边界时返回当前的子数组 + mid = int(len(num)/2) #求出数组的中 + llist,rlist = MergeSort(num[:mid]),MergeSort(num[mid:])#调用函数分别为左右数组排序 + result = [] + i,j = 0,0 + while i < len(llist) and j < len(rlist): #while循环用于合并两个有序数组 + if rlist[j] +#include + +using namespace std; + +int searchInsert(vector &nums, int target) { + // 左索引的初始值为 0 + int left = 0; + // 右索引的初始值是数组中元素的数量 + int right = nums.size(); + // left + 1 >= right 将完成 while 循环 + while (left + 1 < right) { + int mid = (right + left) / 2; + + if (nums[mid] == target) { + // mid是目标索引 + return mid; + } else if (nums[mid] < target) { + // 在数组的左半部分搜索是没有意义的 + left = mid; + } else { + // 在数组的右半部分搜索没有意义 + right = mid; + } + } + // left 可以是目标的索引 + if (nums[left] == target) { + return left; + } + // 目标不存在于数组中 + return -1; +} + +int main() { + vector nums = {1, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59}; + + cout << "Index of 37 is ---> " << searchInsert(nums, 37) << endl; + cout << "Index of 1 is ---> " << searchInsert(nums, 1) << endl; + cout << "Index of 59 is ---> " << searchInsert(nums, 59) << endl; + cout << "Index of 25 is ---> " << searchInsert(nums, 25) << endl; + + return 0; +} +``` + +@tab Java + +```python +class Solution { + int binarySearch(int[] nums, int target) { + if (nums == null || nums.length == 0) { + return -1; + } + + // 左索引的初始值为 0 + int left = 0; + // 右索引的初始值是数组中元素的数量 + int right = nums.length; + // left + 1 >= right 将完成 while 循环 + while (left + 1 < right) { + int mid = (right + left) / 2; + + if (nums[mid] == target) { + // mid是目标索引 + return mid; + } else if (nums[mid] < target) { + // 在数组的左半部分搜索是没有意义的 + left = mid; + } else { + // 在数组的右半部分搜索没有意义 + right = mid; + } + } + // left 可以是目标的索引 + if (nums[left] == target) { + return left; + } + // 目标不存在于数组中 + return -1; + } + + public static void main(String[] args) { + Solution sol = new Solution(); + int[] nums = new int[]{1, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59}; + + System.out.println("Index of 37 is ---> " + sol.binarySearch(nums, 37)); + System.out.println("Index of 1 is ---> " + sol.binarySearch(nums, 1)); + System.out.println("Index of 59 is ---> " + sol.binarySearch(nums, 59)); + System.out.println("Index of 25 is ---> " + sol.binarySearch(nums, 25)); + } +} +``` + +@tab JavaScript + +```javascript +const binarySearch = function (nums, target) { + if (!nums || nums.length === 0) { + return -1; + } + + // 左索引的初始值为 0 + let left = 0; + // 右索引的初始值是数组中元素的数量 + let right = nums.length; + // left + 1 >= right 将完成 while 循环 + while (left + 1 < right) { + let mid = Math.floor((right + left) / 2); + + if (nums[mid] === target) { + // mid是目标索引 + return mid; + } else if (nums[mid] < target) { + // 在数组的左半部分搜索是没有意义的 + left = mid; + } else { + // 在数组的右半部分搜索没有意义 + right = mid; + } + } + // left 可以是目标的索引 + if (nums[left] === target) { + return left; + } + // 目标不存在于数组中 + return -1; +} + +const nums = [1, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59]; + +console.log(`Index of 37 is ---> ${binarySearch(nums, 37)}`); +console.log(`Index of 1 is ---> ${binarySearch(nums, 1)}`); +console.log(`Index of 59 is ---> ${binarySearch(nums, 59)}`); +console.log(`Index of 25 is ---> ${binarySearch(nums, 25)}`); +``` + +@tab Python + +```python +def binarySearch(nums, target): + if len(nums) == 0: + return -1 + + # 左索引的初始值为 0 + left = 0 + # 右索引的初始值是数组中元素的数量 + right = len(nums) + # left + 1 >= right 时将完成while循环 + while left + 1 < right: + mid = (right + left) // 2; + + if nums[mid] == target: + # mid是目标的索引 + return mid + elif nums[mid] < target: + # 在数组的左半部分搜索没有意义 + left = mid + else: + # 在数组的右半部分搜索没有意义 + right = mid + # left 可以是目标的索引 + if nums[left] == target: + return left + # 目标不存在于数组中 + return -1 + + +def main(): + nums = [1, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59]; + + print("Index of 37 is ---> " + str(binarySearch(nums, 37))) + print("Index of 1 is ---> " + str(binarySearch(nums, 1))) + print("Index of 59 is ---> " + str(binarySearch(nums, 59))) + print("Index of 25 is ---> " + str(binarySearch(nums, 25))) + + +main() +``` + +::: + +下面是算法的可视化过程,以 `37` 为目标数字: + + + + + + + + + +欢迎关注我公众号:AI悦创,有更多更好玩的等你发现! + +::: details 公众号:AI悦创【二维码】 + +![](/gzh.jpg) + +::: + +::: info AI悦创·编程一对一 + +AI悦创·推出辅导班啦,包括「Python 语言辅导班、C++ 辅导班、java 辅导班、算法/数据结构辅导班、少儿编程、pygame 游戏开发」,全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践等。当然,还有线下线上摄影课程、Photoshop、Premiere 一对一教学、QQ、微信在线,随时响应!微信:Jiabcdefh + +C++ 信息奥赛题解,长期更新!长期招收一对一中小学信息奥赛集训,莆田、厦门地区有机会线下上门,其他地区线上。微信:Jiabcdefh + +方法一:[QQ](http://wpa.qq.com/msgrd?v=3&uin=1432803776&site=qq&menu=yes) + +方法二:微信:Jiabcdefh + +::: + diff --git a/src/Python/Python-core-technology-and-practice/Binary-search/practice.md b/src/Python/Python-core-technology-and-practice/Binary-search/practice.md new file mode 100755 index 00000000000..3f192441503 --- /dev/null +++ b/src/Python/Python-core-technology-and-practice/Binary-search/practice.md @@ -0,0 +1,268 @@ +--- +title: 二分查找专项练习 +icon: python +date: 2023-08-21 00:27:38 +author: AI悦创 +isOriginal: true +category: Python 进阶 +tag: + - Python 进阶 +sticky: false +star: false +article: true +timeline: true +image: false +navbar: true +sidebarIcon: true +headerDepth: 5 +comment: true +lastUpdated: true +editLink: false +backToTop: true +toc: true +--- + +## 0. 二分查找代码 + +```python +# 定义二分查找函数,接受一个有序列表 arr 和一个目标值 x 作为参数 +def binary_search(arr, x): + # 初始化两个指针 low 和 high + # low 指向数组的开始,high 指向数组的结束 + low, high = 0, len(arr) - 1 + + # 当 low 指针不大于 high 指针时,循环继续 + while low <= high: + # 计算中间索引 mid + mid = (low + high) // 2 + + # 如果 mid 位置的元素小于目标值 x + # 说明目标值在 mid 右边,所以更新 low 指针到 mid + 1 + if arr[mid] < x: + low = mid + 1 + # 如果 mid 位置的元素大于目标值 x + # 说明目标值在 mid 左边,所以更新 high 指针到 mid - 1 + elif arr[mid] > x: + high = mid - 1 + # 如果 mid 位置的元素等于目标值 x,直接返回 mid 索引 + else: + return mid + + # 如果循环结束还没有返回,说明目标值 x 不在列表中,返回 -1 + return -1 + +# 示例测试 +arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] # 一个已排序的数组 +result = binary_search(arr, 5) # 在数组中查找数字5 + +# 输出查找结果 +if result != -1: + print(f"元素在数组中的索引为 {result}") +else: + print("元素不在数组中") +``` + +## 1. 基础题 + +> 请注意,这些题目可能需要对二分查找进行一些修改和调整,以满足特定的查找需求,例如查找最接近的值,而不仅仅是精确匹配的值。 + +### 1.1 电影播放时长查找 + +假设你有一个有序的电影时长列表(以分钟为单位),现在你想找到一个电影,其播放时长接近于你现在的空闲时间。写一个函数,使用二分查找来找到与你的空闲时间最接近的电影时长,并返回该电影的索引。 + +```python +def find_closest_movie_duration(durations, free_time): + # ... 你的二分查找代码 ... + +movie_durations = [80, 95, 105, 123, 138, 150, 165, 176, 188, 210] +free_time = 130 +print(find_closest_movie_duration(movie_durations, free_time)) +``` + +### 1.2 书籍页数查找 + +假设你在一个大型图书馆,这个图书馆的书按页数从少到多进行了有序排列。现在,你只记得你要找的书有大约 `n` 页,但你不确定具体是多少。写一个函数,使用二分查找来帮助你快速定位到那本书的大致位置。 + +```python +def find_book_by_pages(books, target_pages): + # ... 你的二分查找代码 ... + +book_pages = [100, 150, 200, 250, 300, 350, 400, 450, 500] +target = 340 +print(find_book_by_pages(book_pages, target)) +``` + +### 1.3 理想温度查找 + +你有一个按日期排序的气温记录列表,你想知道在过去的某一段时间内,何时的气温最接近你的理想温度。写一个函数,使用二分查找来帮助你找到与你的理想温度最接近的那一天的日期。 + +```python +def find_ideal_temperature(temperatures, ideal_temp): + # ... 你的二分查找代码 ... + +recorded_temperatures = [15, 17, 19, 21, 22, 23, 24, 25, 27, 29] +ideal = 24 +print(find_ideal_temperature(recorded_temperatures, ideal)) +``` + +## 答案 + +:::: details 1. 基础题目答案 + +::: tabs + +@tab 1.1 电影播放时长查找 + +```python +def find_closest_movie_duration(durations, free_time): + # 初始化两个指针 + low, high = 0, len(durations) - 1 + closest_index = -1 + + # 当 low 不大于 high 时,继续查找 + while low <= high: + mid = (low + high) // 2 + # 如果找到准确的时长,直接返回 + if durations[mid] == free_time: + return mid + # 如果当前时长小于目标时长,更新 low + if durations[mid] < free_time: + low = mid + 1 + # 否则更新 high + else: + high = mid - 1 + + # 更新最接近的时长索引 + if closest_index == -1 or abs(durations[mid] - free_time) < abs(durations[closest_index] - free_time): + closest_index = mid + + return closest_index + +movie_durations = [80, 95, 105, 123, 138, 150, 165, 176, 188, 210] +free_time = 130 +print(find_closest_movie_duration(movie_durations, free_time)) +``` + +@tab 郑同学代码 + +```python +def find_closest_movie_duration(durations, free_time): + low, high = 0, len(durations) - 1 + + while low <= high: + mid = (low + high) // 2 + + # If the mid duration is less than free_time, increase low + if durations[mid] < free_time: + low = mid + 1 + # If the mid duration is greater than free_time, decrease high + else: + high = mid - 1 + + # After the loop, we will have two candidates: low and high. + # Compare the gaps between free_time and the durations at these indices to determine which one is closer. + + # In case low is out of bounds, return high + if low == len(durations): + return high + # In case high is out of bounds (or -1), return low + if high == -1: + return low + + gap_low = abs(durations[low] - free_time) + gap_high = abs(durations[high] - free_time) + + # Return the index with the smallest gap + if gap_low < gap_high: + return low + else: + return high + +movie_durations = [80, 95, 105, 123, 138, 150, 165, 176, 188, 210] +free_time = 130 +print(find_closest_movie_duration(movie_durations, free_time)) +``` + +@tab 1.2 书籍页数查找 + +```python +def find_book_by_pages(books, target_pages): + low, high = 0, len(books) - 1 + while low <= high: + mid = (low + high) // 2 + # 如果找到相应的页数,直接返回 + if books[mid] == target_pages: + return mid + # 如果当前页数小于目标页数,更新 low + if books[mid] < target_pages: + low = mid + 1 + # 否则更新 high + else: + high = mid - 1 + + # 如果没有找到,返回 -1 + return -1 + +book_pages = [100, 150, 200, 250, 300, 350, 400, 450, 500] +target = 340 +print(find_book_by_pages(book_pages, target)) +``` + +@tab 1.3 理想温度查找 + +```python +def find_ideal_temperature(temperatures, ideal_temp): + low, high = 0, len(temperatures) - 1 + closest_index = -1 + + while low <= high: + mid = (low + high) // 2 + # 如果找到准确的温度,直接返回 + if temperatures[mid] == ideal_temp: + return mid + # 如果当前温度低于理想温度,更新 low + if temperatures[mid] < ideal_temp: + low = mid + 1 + # 否则更新 high + else: + high = mid - 1 + + # 更新最接近的温度索引 + if closest_index == -1 or abs(temperatures[mid] - ideal_temp) < abs(temperatures[closest_index] - ideal_temp): + closest_index = mid + + return closest_index + +recorded_temperatures = [15, 17, 19, 21, 22, 23, 24, 25, 27, 29] +ideal = 24 +print(find_ideal_temperature(recorded_temperatures, ideal)) +``` + +::: + +:::: + + + + + +欢迎关注我公众号:AI悦创,有更多更好玩的等你发现! + +::: details 公众号:AI悦创【二维码】 + +![](/gzh.jpg) + +::: + +::: info AI悦创·编程一对一 + +AI悦创·推出辅导班啦,包括「Python 语言辅导班、C++ 辅导班、java 辅导班、算法/数据结构辅导班、少儿编程、pygame 游戏开发」,全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践等。当然,还有线下线上摄影课程、Photoshop、Premiere 一对一教学、QQ、微信在线,随时响应!微信:Jiabcdefh + +C++ 信息奥赛题解,长期更新!长期招收一对一中小学信息奥赛集训,莆田、厦门地区有机会线下上门,其他地区线上。微信:Jiabcdefh + +方法一:[QQ](http://wpa.qq.com/msgrd?v=3&uin=1432803776&site=qq&menu=yes) + +方法二:微信:Jiabcdefh + +::: + diff --git a/src/Python/Python-core-technology-and-practice/Casual-essay/01-binary-tree.md b/src/Python/Python-core-technology-and-practice/Casual-essay/01-binary-tree.md new file mode 100644 index 00000000000..8b9b7da02e1 --- /dev/null +++ b/src/Python/Python-core-technology-and-practice/Casual-essay/01-binary-tree.md @@ -0,0 +1,315 @@ +--- +title: 二叉树基础 +icon: tree1 +date: 2023-08-27 23:05:47 +author: AI悦创 +isOriginal: true +category: Python 进阶 +tag: + - Python 进阶 +sticky: false +star: false +article: true +timeline: true +image: false +navbar: true +sidebarIcon: true +headerDepth: 5 +comment: true +lastUpdated: true +editLink: false +backToTop: true +toc: true +--- + +## 1. 初识二叉树 + +你好,我是悦创。 + +**二叉树**是一个有限元素的集合,这个集合要么是空集,要么是由一个称为根的元素以及两个不相交的、分别称为左子树和右子树的二叉树组成。 + +让我们使用文本来表示一个二叉树。 + +考虑这样一个简单的二叉树: + +```text + 1 + / \ + 2 3 + / \ / \ +4 5 6 7 +``` + +- 这棵树的根是 1。 +- 1 的左子树是以 2 为根的二叉树,右子树是以 3 为根的二叉树。 +- 2 的左子节点是 4,右子节点是 5;3 的左子节点是 6,右子节点是 7。 + +**前序遍历** (根-左-右):1 -> 2 -> 4 -> 5 -> 3 -> 6 -> 7 + +**中序遍历** (左-根-右):4 -> 2 -> 5 -> 1 -> 6 -> 3 -> 7 + +**后序遍历** (左-右-根):4 -> 5 -> 2 -> 6 -> 7 -> 3 -> 1 + +这种以文本方式表示的二叉树可以帮助你在没有图形界面的情况下更好地理解二叉树的结构和遍历方式。不过,当树结构更复杂时,这种方式可能不如图形直观。 + +## 2. 探究前中后序遍历 + +前序、中序和后序遍历是二叉树的三种基本的遍历方式,它们描述了访问树的节点的顺序。具体来说: + +1. **前序遍历 (Pre-order Traversal)** + + - 先访问根节点。 + - 再递归地遍历左子树。 + - 最后递归地遍历右子树。 + + 对于之前的示例: + + ``` + 1 + / \ + 2 3 + / \ / \ + 4 5 6 7 + ``` + + 前序遍历的结果是:1 -> 2 -> 4 -> 5 -> 3 -> 6 -> 7 + +2. **中序遍历 (In-order Traversal)** + + - 先递归地遍历左子树。 + - 然后访问根节点。 + - 最后递归地遍历右子树。 + + 对于上述示例,中序遍历的结果是:4 -> 2 -> 5 -> 1 -> 6 -> 3 -> 7 + + 注意:当二叉树是一个二叉搜索树时,中序遍历将输出升序的值。 + +3. **后序遍历 (Post-order Traversal)** + + - 先递归地遍历左子树。 + - 再递归地遍历右子树。 + - 最后访问根节点。 + + 对于上述示例,后序遍历的结果是:4 -> 5 -> 2 -> 6 -> 7 -> 3 -> 1 + +理解这三种遍历方式的关键是观察根节点的访问顺序相对于其左右子树的顺序。在实际应用中,这三种遍历方式根据需要有各自的应用场景,例如在某些算法和问题求解中。 + +## 3. 二叉树的基本实现 + +### 3.1 构建 Node 类 + +```python +# 定义二叉树的节点 +class Node: + def __init__(self, key): + # 左子节点 + self.left = None + # 右子节点 + self.right = None + # 当前节点的值 + self.val = key +``` + +### 3.2 创建 BinaryTree 类 + +```python +class BinaryTree: + def __init__(self): + # 初始化时树是空的,根节点为 None + self.root = None +``` + +### 3.3 编写插入函数 insert + +```python + # 插入一个新的值到二叉搜索树中 + def insert(self, key): + # 如果树是空的,则直接创建一个新的根节点 + if not self.root: + self.root = Node(key) + else: + # 如果树不是空的,递归地插入新值 + self._insert_recursive(self.root, key) +``` + +### 3.4 编写递归插入子树 _insert_recursive + +```python + # 递归地将新值插入到二叉搜索树的适当位置 + def _insert_recursive(self, node, key): + # 如果新值小于当前节点的值,插入到左子树 + if key < node.val: + # 如果左子节点是空的,直接在该位置创建新节点 + if node.left is Node: + node.left = Node(key) + else: + # 如果左子节点不是空的,递归地插入到左子树 + self._insert_recursive(node.left, key) + else: + # 如果新值大于或等于当前节点的值,插入到右子树 + if node.right is None: + node.right = Node(key) + else: + # 如果右子节点不是空的,递归地插入到右子树 + self._insert_recursive(node.right, key) +``` + +### 3.5 编写可遍历的 inorder + +```python + # 返回二叉搜索树的中序遍历结果 + def inorder(self): + return self._inorder_recursive(self.root, []) +``` + +### 3.6 实现中序遍历 _inorder_recursive + +```python + # 递归地进行中序遍历 + def _inorder_recursive(self, node, result): + # 如果当前节点不是空的 + if node: + # 首先遍历左子树 + self._inorder_recursive(node.left, result) + # 然后访问当前节点 + result.append(node.val) + # 最后遍历右子树 + self._inorder_recursive(node.right, result) + return result +``` + +### 3.7 主程序 + +```python +# 主程序 +if __name__ == "__main__": + # 创建一个空的二叉搜索树 + bt = BinaryTree() + # 向树中插入一些值 + for val in [20, 10, 30, 5, 15, 25, 35]: + bt.insert(val) + # 打印树的中序遍历结果 + print(bt.inorder()) # 预期输出: [5, 10, 15, 20, 25, 30, 35] +``` + +### 3.8 完整代码 + +```python +# 定义二叉树的节点 +class Node: + def __init__(self, key): + # 左子节点 + self.left = None + # 右子节点 + self.right = None + # 当前节点的值 + self.val = key + +# 定义二叉搜索树 +class BinaryTree: + def __init__(self): + # 初始化时树是空的,根节点为None + self.root = None + + # 插入一个新的值到二叉搜索树中 + def insert(self, key): + # 如果树是空的,则直接创建一个新的根节点 + if not self.root: + self.root = Node(key) + else: + # 如果树不是空的,递归地插入新值 + self._insert_recursive(self.root, key) + + # 递归地将新值插入到二叉搜索树的适当位置 + def _insert_recursive(self, node, key): + # 如果新值小于当前节点的值,插入到左子树 + if key < node.val: + # 如果左子节点是空的,直接在该位置创建新节点 + if node.left is None: + node.left = Node(key) + else: + # 如果左子节点不是空的,递归地插入到左子树 + self._insert_recursive(node.left, key) + else: + # 如果新值大于或等于当前节点的值,插入到右子树 + if node.right is None: + node.right = Node(key) + else: + # 如果右子节点不是空的,递归地插入到右子树 + self._insert_recursive(node.right, key) + + # 返回二叉搜索树的中序遍历结果 + def inorder(self): + return self._inorder_recursive(self.root, []) + + # 递归地进行中序遍历 + def _inorder_recursive(self, node, result): + # 如果当前节点不是空的 + if node: + # 首先遍历左子树 + self._inorder_recursive(node.left, result) + # 然后访问当前节点 + result.append(node.val) + # 最后遍历右子树 + self._inorder_recursive(node.right, result) + return result + +# 主程序 +if __name__ == "__main__": + # 创建一个空的二叉搜索树 + bt = BinaryTree() + # 向树中插入一些值 + for val in [20, 10, 30, 5, 15, 25, 35]: + bt.insert(val) + # 打印树的中序遍历结果 + print(bt.inorder()) # 预期输出: [5, 10, 15, 20, 25, 30, 35] +``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +欢迎关注我公众号:AI悦创,有更多更好玩的等你发现! + +::: details 公众号:AI悦创【二维码】 + +![](/gzh.jpg) + +::: + +::: info AI悦创·编程一对一 + +AI悦创·推出辅导班啦,包括「Python 语言辅导班、C++ 辅导班、java 辅导班、算法/数据结构辅导班、少儿编程、pygame 游戏开发」,全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践等。当然,还有线下线上摄影课程、Photoshop、Premiere 一对一教学、QQ、微信在线,随时响应!微信:Jiabcdefh + +C++ 信息奥赛题解,长期更新!长期招收一对一中小学信息奥赛集训,莆田、厦门地区有机会线下上门,其他地区线上。微信:Jiabcdefh + +方法一:[QQ](http://wpa.qq.com/msgrd?v=3&uin=1432803776&site=qq&menu=yes) + +方法二:微信:Jiabcdefh + +::: + diff --git a/src/Python/Python-core-technology-and-practice/README.md b/src/Python/Python-core-technology-and-practice/README.md new file mode 100755 index 00000000000..a7b66ad3acf --- /dev/null +++ b/src/Python/Python-core-technology-and-practice/README.md @@ -0,0 +1,66 @@ +--- +blog: false +home: true +icon: python +title: Python 核心技术实战 +heroImage: /ColumnImages/Pythonjinjie/Python-jinjie.jpg +heroText: Python 核心技术实战 +heroFullScreen: false +tagline: 听得懂、学得会、用得上 +actions: + - text: 开始学习 💡 + link: /column/Python-core-technology-and-practice/00 + type: primary + + - text: 与作者联系 👋 + link: http://wpa.qq.com/msgrd?v=3&uin=1432803776&site=qq&menu=yes + +footer: 立志降低教育成本,普及编程教育,提供优质资源教程——流沙团队宣 +--- + +## 你将获得 + +- 从工程角度掌握 Python 高阶用法; +- 独立开发 Python 项目的能力; +- 一线工程师的独家经验分享; +- 完整的 Python 学习路径。 + +## 课程介绍 + +人工智能时代下,Python 毫无疑问是最热的编程语言。有人夸它功能强大还上手轻松,有人说它学习曲线不那么陡峭,但是更多的人,在推开 Python 的大门后却发现,Python 入门容易但精通却不易。 + +- 你是否也曾傻傻分不清“列表”“元组”“字典”“集合”等的用法,甚至试图在集合中采用索引方式? +- 你是否也曾苦苦钻研面向对象的理念,却在被要求设计一个稍复杂点的系统时束手无策? +- 你是否也曾羡慕别人能巧用装饰器、生成器等高级操作,可自己在写代码时,却连异常抛出、内存不足等边界条件都战战兢兢搞不定呢? + +由此可见,想要精通这门语言,必须真正理解知识概念,比如适当从源码层面深化认知,然后熟悉实际的工程应用,独立完成项目开发。这样,你才能成为真正的语言高手。 + +在这个专栏里,悦创会从工程的角度,带你学习 Python。专栏基于 Python 3.7 版本,以语言知识结合工程应用为主线,其中包含了大量的独家解读和实际工作案例。内容难易兼顾,既可以带你巩固核心基础,更会教你各种高级进阶操作,让你循序渐进、系统掌握 Python 这门语言。 + +专栏按照**进阶难度**分为 4 个模块。 + +前两部分主要是**Python 的基础篇和进阶篇。除去必要的概念、操作讲解,基础篇和进阶篇都着重强调了学习中的重难点和易错点**,并从性能分析、实际应用举例等不同维度出发,让你轻松理解和掌握它们。 + +第三部分是**规范篇**,通过讲解合理分解代码、运用 assert、写单元测试等具体编程技巧,教你写出高质量的 Python 程序。 + +第四部分则是**实战篇,这部分会通过量化交易系统项目的开发,带你串联起前面所学的 Python 知识**,并加入大量的实战经验和技巧,让你在独立项目开发中获得质的提高。 + +::: details 公众号:AI悦创【二维码】 + +![](/gzh.jpg) + +::: + +::: info AI悦创·编程一对一 + +AI悦创·推出辅导班啦,包括「Python 语言辅导班、C++ 辅导班、java 辅导班、算法/数据结构辅导班、少儿编程、pygame 游戏开发」,全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践等。当然,还有线下线上摄影课程、Photoshop、Premiere 一对一教学、QQ、微信在线,随时响应!微信:Jiabcdefh + +C++ 信息奥赛题解,长期更新!长期招收一对一中小学信息奥赛集训,莆田、厦门地区有机会线下上门,其他地区线上。微信:Jiabcdefh + +方法一:[QQ](http://wpa.qq.com/msgrd?v=3&uin=1432803776&site=qq&menu=yes) + +方法二:微信:Jiabcdefh + +::: + +![](/zsxq.jpg) \ No newline at end of file diff --git a/src/Python/Python-core-technology-and-practice/supplement/01-why-args-kwargs.md b/src/Python/Python-core-technology-and-practice/supplement/01-why-args-kwargs.md new file mode 100755 index 00000000000..33de2ca5cee --- /dev/null +++ b/src/Python/Python-core-technology-and-practice/supplement/01-why-args-kwargs.md @@ -0,0 +1,270 @@ +--- +title: 理解*args和**kwargs +icon: python +date: 2023-02-06 21:50:21 +author: AI悦创 +isOriginal: true +category: + - Python 进阶 + - 小白补充 +tag: + - Python 进阶 + - 小白补充 +sticky: false +star: false +article: true +timeline: true +image: false +navbar: true +sidebarIcon: true +headerDepth: 5 +comment: true +lastUpdated: true +editLink: false +backToTop: true +toc: true +--- + +你好,我是悦创。 + +星号 (`*`) 可用于 Python 中的不同情况: + +- 乘法和幂运算 +- 创建具有重复元素的列表、元组或字符串 +- `*args` , `**kwargs` 和关键字参数 +- 为函数参数解包列表/元组/字典 +- 拆包容器 +- 将容器合并到列表/合并字典 + +## 1. 乘法和幂运算 + +```python +print(7 * 5) +print(2**4) + +# ---output--- +35 +16 +``` + +## 2. 创建具有重复元素的列表、元组或字符串 + +```python +# 列表 +zeros = [0] * 10 +onetwos = [1, 2] * 5 +print(zeros) +print(onetwos) +# 元组 +zeros = (0,) * 10 +onetwos = (1, 2) * 5 +print(zeros) +print(onetwos) +# 字符串 +A_string = "A" * 10 +AB_string = "AB" * 5 +print(A_string) +print(AB_string) + +# ---output--- +[0, 0, 0, 0, 0, 0, 0, 0, 0, 0] +[1, 2, 1, 2, 1, 2, 1, 2, 1, 2] +(0, 0, 0, 0, 0, 0, 0, 0, 0, 0) +(1, 2, 1, 2, 1, 2, 1, 2, 1, 2) +AAAAAAAAAA +ABABABABAB +``` + +## 3. `*args` , `**kwargs` 和关键字参数 + +在 Python 中,`*args` 和 `**kwargs` 都代表 1个 或 多个 参数的意思。`*args` 传入 tuple 类型的无名参数,而 `**kwargs` 传入的参数是 dict 类型。下文举例说明。 + +- 将 `*args` 用于可变长度参数 +- 将 `**kwargs` 用于可变长度的关键字参数 +- 使用 `*` 后跟更多函数参数来强制仅使用关键字参数 + +```python +#可变参数 +def my_function(*args, **kwargs): + for arg in args: + print(arg) + for key in kwargs: + print(key, kwargs[key]) +my_function("Hey", 3, [0, 1, 2], name="Alex", age=8) + +#强制关键词参数 +def my_function2(name, *, age): + print(name) + print(age) +my_function2("Michael", age=5) + +# ---output--- +Hey +3 +[0, 1, 2] +name Alex +age 8 +Michael +5 +``` + +## 4. 参数解包 + +- 如果长度与参数匹配,则列表/元组/集合/字符串可以使用一个 `*` 解压缩为函数参数。 +- 如果长度和键与参数匹配,字典可以用两个 `**` 解包。 + +```python +def foo(a, b, c): + print(a, b, c) +my_list = [1, 2, 3] +foo(*my_list) # *list传参 +my_string = "ABC" +foo(*my_string) +my_dict = {'a': 4, 'b': 5, 'c': 6} +foo(**my_dict) # **dict传参 + +# ---output--- +1 2 3 +A B C +4 5 6 +``` + +## 5. 拆分容器 + +将列表、元组或集合的元素解包为单个和多个剩余元素。 + +```python +numbers = (1, 2, 3, 4, 5, 6, 7, 8) +print("*在开始:") +*beginning, last = numbers +print(beginning, last) +print("*在末尾:") +first, *end = numbers +print(first, end) +print("*在中间位置:") +first, *middle, last = numbers +print(first, middle, last) + +# ---output--- +*在开始: +[1, 2, 3, 4, 5, 6, 7] 8 +*在末尾: +1 [2, 3, 4, 5, 6, 7, 8] +*在中间位置: +1 [2, 3, 4, 5, 6, 7] 8 +``` + +## 6. 将迭代元素合并到列表/字典 + +```python +# 合并列表 +my_tuple = (1, 2, 3) +my_set = {4, 5, 6} +my_list = [*my_tuple, *my_set] +print(my_list) +# 合并字典 +dict_a = {"one": 1, "two": 2} +dict_b = {"three": 3, "four": 4} +dict_c = {**dict_a, **dict_b} +print(dict_c) + + +# ---output--- +[1, 2, 3, 4, 5, 6] +{'one': 1, 'two': 2, 'three': 3, 'four': 4} +``` + +## 7. 更多测试代码 + +```python +def p(url, *args): + print(url) + print(args[0]) +p(1, 2, 3, 4) +``` + +```python +def test(*args): + print("test-args", args) + for i in args: + print("test-i", i) + +test(1,2,3) + + +def p(a, *args, **kwargs): + print("p-a", a) + print("p-*args", *args) + print("p-**kwargs", **kwargs) + +p(1, 2, 3, 4) +``` + +运行结果: + +```python +test-args (1, 2, 3) +test-i 1 +test-i 2 +test-i 3 +p-a 1 +p-*args 2 3 4 +p-**kwargs +``` + +```python +def test(**kwargs): + print(kwargs) + keys = kwargs.keys() + value = kwargs.values() + print(keys) + print(value) + +test(a=1,b=2,c=3,d=4) + +# 输出值分别为 +# {'a': 1, 'b': 2, 'c': 3, 'd': 4} +# dict_keys(['a', 'b', 'c', 'd']) +# dict_values([1, 2, 3, 4]) +``` + +欢迎关注我公众号:AI悦创,有更多更好玩的等你发现! + +::: details 公众号:AI悦创【二维码】 + +![](/gzh.jpg) + +::: + +::: info AI悦创·编程一对一 + +AI悦创·推出辅导班啦,包括「Python 语言辅导班、C++ 辅导班、java 辅导班、算法/数据结构辅导班、少儿编程、pygame 游戏开发」,全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践等。当然,还有线下线上摄影课程、Photoshop、Premiere 一对一教学、QQ、微信在线,随时响应!微信:Jiabcdefh + +C++ 信息奥赛题解,长期更新!长期招收一对一中小学信息奥赛集训,莆田、厦门地区有机会线下上门,其他地区线上。微信:Jiabcdefh + +方法一:[QQ](http://wpa.qq.com/msgrd?v=3&uin=1432803776&site=qq&menu=yes) + +方法二:微信:Jiabcdefh + +::: + +![](/zsxq.jpg) + + + + + + + + + + + + + + + + + + + diff --git a/src/Python/Python-core-technology-and-practice/supplement/02-if-not.md b/src/Python/Python-core-technology-and-practice/supplement/02-if-not.md new file mode 100755 index 00000000000..b0c265c3fc2 --- /dev/null +++ b/src/Python/Python-core-technology-and-practice/supplement/02-if-not.md @@ -0,0 +1,103 @@ +--- +title: if not 的理解 +icon: python +date: 2023-02-06 22:00:52 +author: AI悦创 +isOriginal: true +category: + - Python 进阶 + - 小白补充 +tag: + - Python 进阶 + - 小白补充 +sticky: false +star: false +article: true +timeline: true +image: false +navbar: true +sidebarIcon: true +headerDepth: 5 +comment: true +lastUpdated: true +editLink: false +backToTop: true +toc: true +--- + +你好,我是悦创。 + +## if not 的理解 + +```python +l = None # 空 False +# not False >>> True +if not l: + print('ok') +else: + print('No') +print(not False) +``` + +运行结果: + +```python +ok +True +``` + +把 l 修改成不为空: + +```python +l = 1 # 空 False +# not False >>> True +if not l: + print('ok') +else: + print('No') +print(not False) +print(not True) +``` + +运行结果: + +```python +No +True +False +``` + +欢迎关注我公众号:AI悦创,有更多更好玩的等你发现! + +::: details 公众号:AI悦创【二维码】 + +![](/gzh.jpg) + +::: + +::: info AI悦创·编程一对一 + +AI悦创·推出辅导班啦,包括「Python 语言辅导班、C++ 辅导班、java 辅导班、算法/数据结构辅导班、少儿编程、pygame 游戏开发」,全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践等。当然,还有线下线上摄影课程、Photoshop、Premiere 一对一教学、QQ、微信在线,随时响应!微信:Jiabcdefh + +C++ 信息奥赛题解,长期更新!长期招收一对一中小学信息奥赛集训,莆田、厦门地区有机会线下上门,其他地区线上。微信:Jiabcdefh + +方法一:[QQ](http://wpa.qq.com/msgrd?v=3&uin=1432803776&site=qq&menu=yes) + +方法二:微信:Jiabcdefh + +::: + +![](/zsxq.jpg) + + + + + + + + + + + + + diff --git a/src/Python/Python-core-technology-and-practice/supplement/03-python-dictionary-comprehension-guide-and-examples.md b/src/Python/Python-core-technology-and-practice/supplement/03-python-dictionary-comprehension-guide-and-examples.md new file mode 100755 index 00000000000..8802f827aee --- /dev/null +++ b/src/Python/Python-core-technology-and-practice/supplement/03-python-dictionary-comprehension-guide-and-examples.md @@ -0,0 +1,123 @@ +--- +title: Python字典推导式详解与应用实例 +icon: python +date: 2023-10-07 21:57:51 +author: AI悦创 +isOriginal: true +category: + - Python 进阶 + - 小白补充 +tag: + - Python 进阶 + - 小白补充 +sticky: false +star: false +article: true +timeline: true +image: false +navbar: true +sidebarIcon: true +headerDepth: 5 +comment: true +lastUpdated: true +editLink: false +backToTop: true +toc: true +--- + +你好,我是悦创。 + +Python字典表达式(也叫字典推导式)是一种简洁的方法来创建字典。它类似于列表推导式,但是用于生成字典对象。 + +以下是一个简单的例子。假设我们想要基于一个列表生成一个字典,其中列表的元素作为字典的键,并且值是该元素的平方: + +```python +numbers = [1, 2, 3, 4, 5] +squared_dict = {x: x**2 for x in numbers} +print(squared_dict) +``` + +输出: +```python +{1: 1, 2: 4, 3: 9, 4: 16, 5: 25} +``` + +字典表达式还可以结合条件来生成字典。例如,只生成奇数的平方: + +```python +odd_squared_dict = {x: x**2 for x in numbers if x % 2 == 1} +print(odd_squared_dict) +``` + +输出: +```python +{1: 1, 3: 9, 5: 25} +``` + +还可以使用双重循环。例如,假设我们有两个列表,并希望将其中一个列表的每个元素与另一个列表的每个元素结合生成一个字典: + +```python +keys = ['a', 'b', 'c'] +values = [1, 2, 3] +combined_dict = {k: v for k in keys for v in values} +print(combined_dict) +``` + +这样做实际上是不太有用的,因为它会重复覆盖字典的值,但这只是为了展示如何结合多个循环。 + +更实际的用途可能是将两个列表结合为一个字典,其中一个列表的元素作为键,另一个列表的元素作为值: + +```python +keys = ['a', 'b', 'c'] +values = [1, 2, 3] +combined_dict = {keys[i]: values[i] for i in range(len(keys))} +print(combined_dict) +``` + +输出: +```python +{'a': 1, 'b': 2, 'c': 3} +``` + +这只是一些基本的例子,你可以根据需要进一步扩展和定制字典表达式。 + + + + + + + +欢迎关注我公众号:AI悦创,有更多更好玩的等你发现! + +::: details 公众号:AI悦创【二维码】 + +![](/gzh.jpg) + +::: + +::: info AI悦创·编程一对一 + +AI悦创·推出辅导班啦,包括「Python 语言辅导班、C++ 辅导班、java 辅导班、算法/数据结构辅导班、少儿编程、pygame 游戏开发」,全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践等。当然,还有线下线上摄影课程、Photoshop、Premiere 一对一教学、QQ、微信在线,随时响应!微信:Jiabcdefh + +C++ 信息奥赛题解,长期更新!长期招收一对一中小学信息奥赛集训,莆田、厦门地区有机会线下上门,其他地区线上。微信:Jiabcdefh + +方法一:[QQ](http://wpa.qq.com/msgrd?v=3&uin=1432803776&site=qq&menu=yes) + +方法二:微信:Jiabcdefh + +::: + +![](/zsxq.jpg) + + + + + + + + + + + + + diff --git a/src/Python/Python-core-technology-and-practice/supplement/04-Python-Logic-Pitfalls-Changing-Lists.assets/image-20231207113248613.png b/src/Python/Python-core-technology-and-practice/supplement/04-Python-Logic-Pitfalls-Changing-Lists.assets/image-20231207113248613.png new file mode 100644 index 00000000000..1d3a1185d05 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/supplement/04-Python-Logic-Pitfalls-Changing-Lists.assets/image-20231207113248613.png differ diff --git a/src/Python/Python-core-technology-and-practice/supplement/04-Python-Logic-Pitfalls-Changing-Lists.assets/image-20231207113303444.png b/src/Python/Python-core-technology-and-practice/supplement/04-Python-Logic-Pitfalls-Changing-Lists.assets/image-20231207113303444.png new file mode 100644 index 00000000000..479c5a9a8c7 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/supplement/04-Python-Logic-Pitfalls-Changing-Lists.assets/image-20231207113303444.png differ diff --git a/src/Python/Python-core-technology-and-practice/supplement/04-Python-Logic-Pitfalls-Changing-Lists.assets/image-20231207113448125.png b/src/Python/Python-core-technology-and-practice/supplement/04-Python-Logic-Pitfalls-Changing-Lists.assets/image-20231207113448125.png new file mode 100644 index 00000000000..d64b07d37a5 Binary files /dev/null and b/src/Python/Python-core-technology-and-practice/supplement/04-Python-Logic-Pitfalls-Changing-Lists.assets/image-20231207113448125.png differ diff --git a/src/Python/Python-core-technology-and-practice/supplement/04-Python-Logic-Pitfalls-Changing-Lists.md b/src/Python/Python-core-technology-and-practice/supplement/04-Python-Logic-Pitfalls-Changing-Lists.md new file mode 100755 index 00000000000..ba27cb2e12a --- /dev/null +++ b/src/Python/Python-core-technology-and-practice/supplement/04-Python-Logic-Pitfalls-Changing-Lists.md @@ -0,0 +1,425 @@ +--- +title: Python 逻辑陷阱「变化中的列表」 +icon: python +date: 2023-12-07 10:11:38 +author: AI悦创 +isOriginal: true +category: + - Python 进阶 + - 小白补充 +tag: + - Python 进阶 + - 小白补充 +sticky: false +star: false +article: true +timeline: true +image: false +navbar: true +sidebarIcon: true +headerDepth: 5 +comment: true +lastUpdated: true +editLink: false +backToTop: true +toc: true +--- + +## 1. 问题 + +你好,我是悦创。 + +我们先来阅读下面的代码: + +```python +def remove_all(L, x): + assert type(L) is list and x is not None + for i in L: + if i == x: + print(f"i: {i}-L: {L}") + L.remove(i) + else: + print(f"i: {i}-L: {L}") + # pass + return L + + +print(remove_all([9, 9, 1, 9, 8, 1], 9)) +``` + +上面的代码无法得到正确结果: + +```python +i: 9-L: [9, 9, 1, 9, 8, 1] +i: 1-L: [9, 1, 9, 8, 1] +i: 9-L: [9, 1, 9, 8, 1] +i: 1-L: [1, 9, 8, 1] +[1, 9, 8, 1] +``` + +::: tip 问题 + +为什么无法正确得到把数字 9 剔除之后的结果? + +::: + +## 2. 分析 + +**按正常逻辑:** + +遍历一边目标列表,并判断是否是 x,并使用 remove 提出,这个整体思路是对的。但是在具体实施的时候,需要注意的是: + +- 我们没有把列表进行 copy,所以是直接修改列表本身。 +- 因为我们是直接修改列表本身,所以列表的长度是一直在变化的。 +- 所以我们逐步分析其中过程,在分析我们需要查看三个参数变化:`L`、`remove()`、`i` + +所以,修改代码如下: + +```python +def remove_all(L, x): + print(f"L 原始的数据为: {L}") + for i in L: + print(f"i 每次的值: {i}") + if i == x: + L.remove(i) + print(f"L 删除 x 之后的列表数据: {L}") + else: + print(f"L 本次没有删除: {L}") + print(f"L 最终的数据为: {L}") + + +remove_all([9, 99, 1, 9, 8, 1], 9) +``` + +## 3. 分析结论「详细分析」 + +在执行 `remove_all([9, 99, 1, 9, 8, 1], 9)` 时: + +1. 初始列表为 `[9, 99, 1, 9, 8, 1]`。 +2. 第一次循环时,`i` 的值为列表的第一个元素 `9`。因为 `i == 9`,函数将删除这个元素。此时列表变为 `[99, 1, 9, 8, 1]`。 +3. 第二次循环时,由于第一个元素 `9` 被删除,所有后续元素向左移动一位。但是循环的索引也向前移动到了第二个元素,这时的第二个元素是原始列表的第三个元素 `1`。所以,`i` 的值变成了 `1` 而不是 `99`。「这也是跳过 99 的原因」 +4. 第三次循环时,`i` 的值为 `9`(这是原始列表的第四个元素,但现在是新列表的第三个元素)。因为 `i == 9`,函数将删除这个元素。此时列表变为 `[99, 1, 8, 1]`。 +5. 接下来的循环将不再遇到值为 `9` 的元素,因为所有的 `9` 已被删除。「后续跳过 8 也是这个原因」 + + + +## 4. 解决方法 + +1. 因为产生的原因,是列表改变引起的,本身整体逻辑是没有问题的,所以只要保证被循环的列表不会被改变即可。 +2. 使用 copy 函数,copy 一个列表就可以解决了。 + + + +## 5. 列表深浅拷贝 + +### 5.1 现存问题 + +::: tip 问题 + +现在所存在的问题:单纯的修改 y 会影响 x 的值 + +::: + +```python +x = [1, 2, 3, 4, 5, 6] +y = x +print("x 原本的值: {}".format(x)) +print("y 原本的值: {}".format(y)) +# 修改 y +y[0] = "cava" +print("x 现在的值: {}".format(x)) +print("y 现在的值: {}".format(y)) + +# ---output--- +x 原本的值: [1, 2, 3, 4, 5, 6] +y 原本的值: [1, 2, 3, 4, 5, 6] +x 现在的值: ['cava', 2, 3, 4, 5, 6] +y 现在的值: ['cava', 2, 3, 4, 5, 6] +``` + +![](./04-Python-Logic-Pitfalls-Changing-Lists.assets/image-20231207113248613.png) + +![](./04-Python-Logic-Pitfalls-Changing-Lists.assets/image-20231207113303444.png) + + + +### 5.2 浅拷贝 copy() + +```python +x = [1, 2, 3, 4, 5, 6] +y = x.copy() +print("x 原本的值: {}".format(x)) +print("y 原本的值: {}".format(y)) +# 修改 y +y[0] = "cava" +print("x 现在的值: {}".format(x)) +print("y 现在的值: {}".format(y)) + +# ---output--- +x 原本的值: [1, 2, 3, 4, 5, 6] +y 原本的值: [1, 2, 3, 4, 5, 6] +x 现在的值: [1, 2, 3, 4, 5, 6] +y 现在的值: ['cava', 2, 3, 4, 5, 6] +``` + + + +![](./04-Python-Logic-Pitfalls-Changing-Lists.assets/image-20231207113448125.png) + +为什么说它是浅拷贝呢?——因为,当列表出现嵌套的时候,就无能为力了。 + +```python +x = [1, 2, 3, 4, 5, 6, ["rxx", "nb", "good"]] +y = x.copy() +print("x 原本的值: {}".format(x)) +print("y 原本的值: {}".format(y)) +# 修改 y +y[0] = "cava" +y[6][1] = "Python" +print("x 现在的值: {}".format(x)) +print("y 现在的值: {}".format(y)) + +# ---output--- +x 原本的值: [1, 2, 3, 4, 5, 6, ['rxx', 'nb', 'good']] +y 原本的值: [1, 2, 3, 4, 5, 6, ['rxx', 'nb', 'good']] +x 现在的值: [1, 2, 3, 4, 5, 6, ['rxx', 'Python', 'good']] +y 现在的值: ['cava', 2, 3, 4, 5, 6, ['rxx', 'Python', 'good']] +``` + +因为,上面的 copy 只 copy 了第一层列表,嵌套的列表或者其他就没有完全拷贝了。 + +如何解决呢? + +### 5.3 深拷贝 deepcopy() + +```python +from copy import deepcopy +x = [1, 2, 3, 4, 5, 6, ["rxx", "nb", "good"]] +y = deepcopy(x) +print("x 原本的值: {}".format(x)) +print("y 原本的值: {}".format(y)) +# 修改 y +y[0] = "cava" +y[6][1] = "Python" +print("x 现在的值: {}".format(x)) +print("y 现在的值: {}".format(y)) + +# ---output--- +x 原本的值: [1, 2, 3, 4, 5, 6, ['rxx', 'nb', 'good']] +y 原本的值: [1, 2, 3, 4, 5, 6, ['rxx', 'nb', 'good']] +x 现在的值: [1, 2, 3, 4, 5, 6, ['rxx', 'nb', 'good']] +y 现在的值: ['cava', 2, 3, 4, 5, 6, ['rxx', 'Python', 'good']] +``` + + + +### 5.4 code + +```python +x = [1, 2, 3, 4, 5, 6] +y = x +print("x 原本的值: {}".format(x)) +print("y 原本的值: {}".format(y)) +# 修改 y +y[0] = "cava" +print("x 现在的值: {}".format(x)) +print("y 现在的值: {}".format(y)) +# 现在所存在的问题:单纯的修改 y 会影响 x 的值 +# 如何解决问题?——copy() +print("-*-" * 8) +x = [1, 2, 3, 4, 5, 6] +y = x.copy() +print("x 原本的值: {}".format(x)) +print("y 原本的值: {}".format(y)) +# 修改 y +y[0] = "cava" +print("x 现在的值: {}".format(x)) +print("y 现在的值: {}".format(y)) +print("-*-" * 8) +# 当列表里面出现嵌套的时候,copy 只能 copy 第一层 +x = [1, 2, 3, 4, 5, 6, ["rxx", "nb", "good"]] +y = x.copy() +print("x 原本的值: {}".format(x)) +print("y 原本的值: {}".format(y)) +# 修改 y +y[0] = "cava" +y[6][1] = "Python" +print("x 现在的值: {}".format(x)) +print("y 现在的值: {}".format(y)) +print("-*-" * 8) +from copy import deepcopy +x = [1, 2, 3, 4, 5, 6, ["rxx", "nb", "good"]] +y = deepcopy(x) +print("x 原本的值: {}".format(x)) +print("y 原本的值: {}".format(y)) +# 修改 y +y[0] = "cava" +y[6][1] = "Python" +print("x 现在的值: {}".format(x)) +print("y 现在的值: {}".format(y)) + + +# ---output--- +x 原本的值: [1, 2, 3, 4, 5, 6] +y 原本的值: [1, 2, 3, 4, 5, 6] +x 现在的值: ['cava', 2, 3, 4, 5, 6] +y 现在的值: ['cava', 2, 3, 4, 5, 6] +-*--*--*--*--*--*--*--*- +x 原本的值: [1, 2, 3, 4, 5, 6] +y 原本的值: [1, 2, 3, 4, 5, 6] +x 现在的值: [1, 2, 3, 4, 5, 6] +y 现在的值: ['cava', 2, 3, 4, 5, 6] +-*--*--*--*--*--*--*--*- +x 原本的值: [1, 2, 3, 4, 5, 6, ['rxx', 'nb', 'good']] +y 原本的值: [1, 2, 3, 4, 5, 6, ['rxx', 'nb', 'good']] +x 现在的值: [1, 2, 3, 4, 5, 6, ['rxx', 'Python', 'good']] +y 现在的值: ['cava', 2, 3, 4, 5, 6, ['rxx', 'Python', 'good']] +-*--*--*--*--*--*--*--*- +x 原本的值: [1, 2, 3, 4, 5, 6, ['rxx', 'nb', 'good']] +y 原本的值: [1, 2, 3, 4, 5, 6, ['rxx', 'nb', 'good']] +x 现在的值: [1, 2, 3, 4, 5, 6, ['rxx', 'nb', 'good']] +y 现在的值: ['cava', 2, 3, 4, 5, 6, ['rxx', 'Python', 'good']] +``` + +## 6. 解决上面的问题代码 + +- 下面的两种方法都是在修改原本的列表 L + +::: code-tabs + +@tab code1 + +```python +def remove_all(L, x): + new_l = L.copy() + for i in new_l: + if i == x: + L.remove(i) + return L + +print(remove_all([9, 9, 1, 9, 8, 1], 9)) +``` + +@tab code2 + +```python +from copy import deepcopy +def remove_all(L, x): + new_l = deepcopy(L) + for i in new_l: + if i == x: + L.remove(i) + return L + +print(remove_all([9, 9, 1, 9, 8, 1], 9)) +``` + +::: + +- 如何不修改原本的列表实现呢? + +::: code-tabs + +@tab code1 + +```python +def remove_all(L, x): + new_l = L.copy() + for i in L: + if i == x: + new_l.remove(i) + return new_l + +print(remove_all([9, 9, 1, 9, 8, 1], 9)) +``` + +@tab code2 + +```python +from copy import deepcopy +def remove_all(L, x): + new_l = deepcopy(L) + for i in L: + if i == x: + new_l.remove(i) + return new_l + +print(remove_all([9, 9, 1, 9, 8, 1], 9)) +``` + +::: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +欢迎关注我公众号:AI悦创,有更多更好玩的等你发现! + +::: details 公众号:AI悦创【二维码】 + +![](/gzh.jpg) + +::: + +::: info AI悦创·编程一对一 + +AI悦创·推出辅导班啦,包括「Python 语言辅导班、C++ 辅导班、java 辅导班、算法/数据结构辅导班、少儿编程、pygame 游戏开发」,全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践等。当然,还有线下线上摄影课程、Photoshop、Premiere 一对一教学、QQ、微信在线,随时响应!微信:Jiabcdefh + +C++ 信息奥赛题解,长期更新!长期招收一对一中小学信息奥赛集训,莆田、厦门地区有机会线下上门,其他地区线上。微信:Jiabcdefh + +方法一:[QQ](http://wpa.qq.com/msgrd?v=3&uin=1432803776&site=qq&menu=yes) + +方法二:微信:Jiabcdefh + +::: + +![](/zsxq.jpg) + + + + + + + + + + + + +