能力说明:
了解Python语言的基本特性、编程环境的搭建、语法基础、算法基础等,了解Python的基本数据结构,对Python的网络编程与Web开发技术具备初步的知识,了解常用开发框架的基本特性,以及Python爬虫的基础知识。
暂时未有相关云产品技术能力~
文章&教程1、Python修饰器的函数式编程介绍了装饰器的实现原理、带参装饰器、多装饰器、类装饰器和几个典型的示例。文章发布于 2014 年,代码用的还是 Python 2。之所以分享这篇文章,因为它是左耳朵耗子唯一以 Python 为话题的文章,而且写得详细到位。2、asyncio 的一些高级用法出自我们的老朋友@古明地觉 的新系列《asyncio 系列》,半个月内已连载 14 篇。真想问问他是如何做到如此高产又高质量的?!文章回答了:如何设计既能接收协程又能接收普通 Python 函数的 API,如何强制事件循环的迭代,如何在不传递参数的情况下在任务之间传递状态……3、Nginx+uWSGI 部署 Django 以及负载均衡操作介绍了 uWSGI 和 Nginx 的配置,实现对 Django 服务的反向代理及负载均衡。该文出自仍在连载的《Django 系列》,目前该系列包含 44 篇文章,能作为系统学习 Django 的参考材料。4、Rye:一个实验性质的Python包管理系统Python 目前的包管理工具多得让人眼花缭乱,而 Conda 和操作系统的包管理器也存在诸多问题(本周刊第一期就有两则相关内容)。Flask 作者 Armin Ronacher 用 Rust 开发的 rye,借鉴了 Rust 包管理的经验,试图提供一个标准化的解决方案。这篇文章介绍了 rye 的安装及使用。5、PyInstaller:将你的Python代码打包成独立应用程序PyInstaller 可将 Python 程序打包为一个可执行文件,支持多个平台如 Windows、Mac 和 Linux。这是一篇简单清晰的使用教程,除了基础介绍外,难得的是它还介绍了两种打包方式的优缺点,以及打包后常见的 5 个问题。6、如何在 Python 中实现真正的多线程(英文)Python 3.12 即将推出“Per-Interpreter GIL(PEP-684)”特性,它允许 Python 实现真正的并行处理。代码虽然已在 alpha 版本中,但目前只能通过 C-API 使用。文章使用 CPython 的test 模块演示了子解释器的示例。7、GIL vs. nogil: 改动一行代码,提升十倍 I/O 性能(英文)nogil 项目是另一个试图实现真正多线程的方案,这篇文章测试发现 CPython 3.9-nogil 在单文件和多文件的情况下,比未修改的 CPython 3.9 分别快 2.5 倍和 10 倍。nogil 项目最新的进展是形成了正式的 PEP-703,相关介绍在此。8、如何在 PyCharm 中创建一个密码生成器?(英文)PyCharm 官方推出的文章教程,指导在 PyCharm 中创建项目、导入包、使用 Typer 库创建 CLI 应用、运行和调试代码、创建和编辑运行配置,适合于新人学习练手。另外,PyCharm 2023.1.2 版本刚刚发布,可以去尝鲜!9、Python 元类教程(带示例)(英文)在 Python 中,一切都是对象,包括类。元类是 Python 的一项强大功能,允许你在运行时动态地创建类(实际是创建一个type 类型的对象)。文章探讨元类的基础知识,以及更高级的功能和示例。10、当在终端输入“ls”后会发生什么?(英文)有一道很常见的面试题:“当在浏览器输入 google.com 后会发生什么?”由于见得多了,每个人都能回答个一二,但是,经常跟终端打交道的我们,能否回答这个问题呢:当在终端输入命令后会发生什么?文章主要介绍了终端的历史、启动过程、命令的解析和执行过程。项目&资源1、WingetUI:更好用的包管理器 UI(英文)该项目的目标是为 Win 10-11 中最常见的 CLI 包管理器(如 Winget、Scoop 和 Chocolatey)创建一个直观的 GUI。已支持软件包的安装、更新和卸载、排队安装、消息通知、黑暗模式、导入/导出等功能。2、pandas-ai:支持 AI 功能的 Pandas(英文)Pandas 无疑是目前最流行的数据分析和处理工具,当它结合了生成式 AI 的能力后,会不会更好用呢?答案似乎是的!pandasai 项目支持用文字的方式操作 Pandas 的数据对象,可简化很多 Pandas 库的操作。3、promptulate:一个强大的 LLM Prompt Layer 框架一个专为 Prompt Engineer 设计的 LLM Prompt Layer 框架,支持连续对话、角色预设、对话存储、工具扩展等功能,可以无需代理直接访问,开箱即用。 通过 promptulate,你可以轻松构建起属于自己的 GPT 应用程序。4、MicroPython:面向微控制器和嵌入式系统的 Python(英文)MicroPython 新发布了 1.20 版本,引入了一个新的轻量级包管理器,减小了代码大小,并增加了对许多新板的支持。另外,LWN 的这篇文章对此版本做了介绍,文章还提到 Anaconda 有可能在 Q2 将 PyScript 的运行时从 Pyodide 替换为 MicroPython。5、DB-GPT:以数据库为基础的 GPT 实验项目使用本地化的 GPT 大模型与你的数据和环境交互,无数据泄露风险,100% 私密,100% 安全。基于 FastChat 构建大模型运行环境,并提供 vicuna 作为基础的大语言模型,通过 LangChain 提供私域知识库问答能力,支持插件模式,在设计上原生支持 Auto-GPT 插件。播客&视频1、Ep 40. Rust 和 PyO3:让 Python 再次伟大断更许久的《捕蛇者说》播客回归了!本期的嘉宾是 PyO3 项目的维护者,他的另一个身份是 wechatpy 的作者。Rust 和 PyO3 项目能放大 Python 的优势,并能改造 Python 的应用生态。我们曾推荐过性能最快的代码分析工具 Ruff,另外 Flask 作者新开发的包管理工具 rye,它们都是 Rust 与 Python 结合的产物。(题外话:看到了捕蛇者说的三位主播发推/发博缅怀左耳朵耗子,想不到他对 Python 圈子有这么多渊源。R.I.P)2、Talk Python to Me #415: Future of Pydantic and FastAPI(英文)Pydantic 2.0 使用 Rust 重写了核心及顶层的代码,将对构建在其之上的库产生积极的影响,比如 FastAPI。播客邀请了 Pydantic 的 Samuel Colvin 以及 FastAPI 的 Sebastián Ramírez 一起采访,话题度很新!3、再访《流畅的 Python》作者 Luciano Ramalho(英文)我在上个月推荐过新上市的《流畅的 Python》中文第二版(链接),这里补充两则相关材料。这期播客来自 thoughtworks,是在《Fluent Python》英文第二版上市前的访谈,介绍了关于 Python 发展、不同语言的对比、新书的变化等。另外,他们还在 2020 年新书写作期间录了一期“The future of Python”,两期播客都有完整的文字稿。4、最常用的七种分布式系统模式(英文)一则简短的科普视频,介绍了七种分布式系统模式:Ambassador、Circuit Breaker、CQRS、Event Sourcing、Leader Election、Publisher/Subscriber、Sharding。视频中的动画和图例都非常直观和舒适,让人赏心悦目。问题&讨论1、作为程序员,有什么提升生活/工作体验的 App、硬件、服务?V2ex 上的一个帖子,大家对这样的话题似乎很有发言欲。我在此最想推荐的 APP 是 Feedly 和 Substack,用于阅读 RSS 和 Newsletter。Feedly 对本周刊的素材采集帮助极大!(心愿:依靠读者的打赏,让我用上 Feedly Pro+ 的 AI 功能!)2、rye 应该存在么?(英文)前文已提到过 rye,那么,mitsuhiko 是出于什么考虑而开发了它呢?它想解决什么样的问题,想打造出一款什么样的工具呢?Python 官方对包管理会有什么发展支持呢?Github 上的这个问题引起了广泛的讨论。3、你是怎样开始程序员职业生涯的?V2ex 上的帖子,楼主分享了自己从读书到就业前几年的故事,评论区有不少人分享了自己的经历。你是如何开始自己的程序员之路的呢?
在编程时,我们经常要作条件判断,并根据条件的结果选择执行不同的语句块。在许多编程语言中,最常见的写法是三元运算符,但是,Python 并不支持三元运算符,无独有偶,两个最热门的新兴语言 Go 和 Rust 也不支持!为什么 Python 不支持三元运算符呢?本文将主要分析 Python 在设计条件选择语法时的过程,科普为什么它会采用现今的与众不同的实现方案,同时,我们也将考察为什么其它语言也要抛弃传统的三元运算符。在开篇之前,我再声明一下:就像“Python为什么”系列的大部分文章一样,本文关注的仅是一个很小的语法点,但它并不是“茴香豆有几种写法”那种毫无意义的话题。因为,细微之处见真功夫,深入研究语言设计背后的原因、历史和哲学,可以让我们在编程时有更加清晰和自由的思维。什么是三元运算符?三元运算符通常指的是“?:”,其语法形式为:condition ? expression1 : expression2,如果 condition 为真,则取 expression1,若不为真,则取 expression2。语法简化形式“a ? b : c”,可以读成“如果 a 条件成立,则为 b,否则为 c”。三元运算符是对普通一重 if-else 结构的简化,常用于在一条语句中同时实现条件判断和取值操作。// 常规 if-else if (a > b) { result = x; } else { result = y; } // 简化后的写法 result = a > b ? x : y;采用了这种语法设计的编程语言有很多,比如 C、C#、C++、Java、JavaScript、PHP、Perl、Ruby、Swift 等等。毫无争议,它就是编程语言界的主流设计方案(至今仍是)。这种语法非常简洁高效,代码的可读性也很强(如果你不是第一次接触的话),深得很多人的喜欢。但是,它并非毫无缺点。Python 是这种语法设计的最著名的挑战者,接下来,我们将看看为什么 Python 要另辟蹊径。Python 社区的投票Python 发布于 1991 年,但在接下来的 15 年里,除了 if-else 语法外,它并不支持三元运算符和其它条件表达式。而且,在 2006 年引入条件表达式前,社区对此进行了漫长而曲折的争论,可以说这是一个设计得很艰难的语法了。最初,由于时常有人请求添加 if-then-else(三元)表达式,因此在 2003 年 2 月,PEP 308 – Conditional Expressions 被提了出来,目的是让社区选出一个让多数人支持的方案。很快,除了少部分人希望啥也不做外,社区里出现了好几种方案:(1)使用标点符号构建的三元运算符即常规的三元运算符,跟前文介绍的语法一样:<condition> ? <expression1> : <expression2>这个方案的呼声挺高,有开发者甚至已提交了实现代码。但是,Guido 给出了两个反对的理由:冒号在 Python 中已经有许多用途(即使它实际上不会产生歧义,因为问号需要匹配冒号);对于不习惯 C 衍生语言的人来说,理解起来很困难。(2)使用现有和新的关键字构建引入新的“then”关键字,结合现有的“else”关键字:then else它的优点是简单明了、不需要括号、不改变现有关键字的语义,不大可能与语句混淆,而且不需要重载冒号。缺点是引入新关键字的实现成本较高。(3)其它思路跟上一种方案的思路相似,但没有上述两类方案的支持度高。(if : else: ) and else if else cond(, , )值得一提的是(if : else: ) ,它是常规 if-else 语法的扁平化,容易理解,但缺点是需要使用圆括号,容易跟生成器表达式混淆,而且需要解释器对冒号做特殊化处理。另外值得一提的是 if else ,它是 PEP-308 最早版本的推荐方案,但是这种不将条件放在首位的风格让一些人感觉不舒服,而且,当“expression1”很长的时候,很容易就忽略掉它的条件。当时参与投票的全部设计方案:总体上,开发者们希望引入某种形式的 if-then-else 表达式,但投票后却没有哪种方案能取得绝对的优势。概括起来,分歧的问题主要有:是否用标点符号、是否复用关键字、是否复用圆括号、是否引入新关键字、是否引入新语法……由于得票太分散,因此,这个 PEP 在当时被拒绝了。PEP 中写道:“Python 的一个设计原则是在不确定采取哪条路线时,则保持现状。”and-or 用于条件选择的问题以上的投票事件发生在 2004 年 3 月,但是,在 PEP 被拒绝后,相关话题的讨论并未平息,因为大家总想找一种简洁的方式来替换“if-else“。时间到了 2005 年 9 月,邮件组中有人提议在 Py3.0 中变更"and"与"or"操作符的逻辑,提议将"and" 和 "or" 运算符简化成始终返回布尔值,而不是返回最后一个被求值的参数。之所以发起这个提议,原因是他使用了 and or 的方式来实现条件判断与选择。但是这种写法在 Python 中的行为跟有些语言并不一样,使用不严谨的话,可能会酿成 Bug!看看下面的两个例子,你觉得它们会得到什么结果呢?a = True and True or "Python猫" b = True and False or "Python猫"对于 and or ,若 condition 为假,则会直接对 expression2 求值并返回结果;若 condition 为真,则先对 expression1 求值,若也为真,则不会继续对 expression2 求值,若 expression1 不为真,则对 expression2 求值。因此,上述例子得到的 a 是“True”,而 b 会得到“Python猫”。本系列的《Python 为什么能支持任意的真值判断? 》介绍过 Python 在真值判断的特殊之处,运用到以上结构中,将出现更不易察觉的问题。比如,该邮件的作者就是遇到了“expression1”为复数“0+4i”,这个数的真值判断为 False,因此导致最后返回的不是预期的“expression1”,而是“expression2”!在没有更好的方案前,“and-or”是比较常见的条件选择写法,PEP-308 也提及了它,也指出了当“expression1”为假的情况,还认为这种方案是丑陋和令人费解的。这封邮件再次引发了社区对条件选择语法的讨论,大佬们纷纷登场。以我现在的视角分析,其实就是开发者们不满足于“if-else”的现状,但是当时流行的“and-or”写法并不够好,因此,大家期望 Python 设计出新的规范性语法,来解决这个痛点。与众不同的条件表达式在经过 10 天的邮件讨论后,Guido van Rossum 最终决定添加一个条件表达式,语法形式为X if C else Y 。因此,PEP-308 被重开和更新,并很快就在次年的 2.5 版本中实现了。前文已提到过这个让一些人感觉不舒服的方案了,因为它没有将条件判断逻辑放在最前面。那么,为什么最后的胜者会是它呢?这是不是最优的设计呢?不可否认,起到决定性作用的原因是 Guido。由于社区在一年半前投票时没有形成多数意见,因此他行使 BDFL (终身仁慈独裁者)的决策权力,裁定出一个他认为是最佳的方案。X if C else Y 非常易于理解,可读性高。它延续了“明确优于隐式”的风格,使用了直观口语化的“if-else”,而不是引入可能引起混淆的标点符号,就像 Python 选择“and”和“or”两个单词,而不是“&&”和“||”两个符号,它们有着异曲同工之妙。虽然调整后的语法顺序让人不太习惯,但其实这样的实现却大有好处。首先,它只需复用“if-else”两个关键字,而不需要引入“then”、“when”和其它语法要素,也不像(if : else: ) 那样的繁琐。其次,为了验证X if C else Y 的有效性,Guido 排查了标准库中所有“and-or”组合的写法,发现那些C and X or Y 写法都可以被X if C else Y 替换掉。标准库的情况,证明了这新的语法是可行的。回顾这段历史,我们可以梳理出一条线索:Python 没有设计三元运算符“?:”,主要是因为它不符合 Python 明确直观的设计风格。最后采用X if C else Y 这种设计,主要的意图其实是消除“and-or”写法的隐患,这种设计简明易读,非常好用。总体而言,Python 设计者非常看重可读性与可维护性,不采用三元运算符而创造条件表达式语法,这是一个经过了开放讨论、谨慎评估与权衡取舍的结果。Go、Rust 为什么不支持三元运算符?考察完 Python 的设计原因后,我们再来考察“反派阵营”中两门最热门的语言。首先是 Go 语言,官网的 FAQ 专门列出了一个问题:“Why does Go not have the ?: operator?”。Go 语言不支持“?:”运算符,而是推荐使用原生的“if-else”写法。文档的解释很简短,只有一段话:Go 语言没有 ?: 运算符,因为语言的设计者们经常看到它被用来创建难以理解的复杂表达式。虽然 if-else 形式比较长,但是它无疑更清晰易懂。一个语言只需要一个条件控制流结构。接着是 Rust 语言,它的官方文档中似乎没有任何关于不支持三元运算符的解释。但在查阅资料后,我发现它也有一段特殊的故事,非常有意思:在 2011 年 6 月时,Rust 曾经引入过三元运算符(#565),然而半年后,设计者意识到这个特性是多余的,因此又把它移除了(#1698、#4632)!为什么三元运算符在 Rust 是多余的呢?因为它的 if 语法并不像其它语言是“语句(statement)”,而是一个“表达式(expression)”,这意味着你可以直接将 if 表达式赋值给变量:// 若条件为真,得到 5,否则 6 let number = if condition { 5 } else { 6 };这种语法形式足够简单明了,不就是将大家都熟悉的“if-else”直接用于赋值么,太方便了,替换成三元运算符的话,确实有点画蛇添足之感。另外,Rust 使用花括号划分代码块,因此上例的花括号内可以包含多条表达式,也支持换行,例如这个例子:let x = 42; let result = if x > 50 { println!("x is greater than 50"); x * 2 // 这是一个表达式,将返回的值赋给 result } else { println!("x is less than or equal to 50"); x / 2 // 也是一个表达式,将返回的值赋给 result };这种用法,Python 是不可能做到的。最关键的区别在于,Rust 的 if 是表达式而不是语句。这两个概念的区别是:表达式(expression)通常指的是由变量、常量、运算符等组成的一个可求值的代码片段,它的求值结果可以用到其它表达式或语句中。语句(statement)通常指的是完成某个任务的单个指令或一组指令,例如赋值语句、条件语句、循环语句等,它没有返回值(或者为空),不能用于赋值操作。除了 Rust 外,还有一些编程语言中的 if 是表达式而不是语句,例如 Kotlin、Scala、F#、Swift,它们在理论上也不需要使用三元运算符。(题外话:Swift 是个例外,它也有三元运算符。Kotlin 有“?:”运算符,注意两个符号是连在一起的,val result = a ?: b 表示:如果 a 不为 null,则赋值给 result ;否则将 b 赋给 result)由于有这种语言设计层面的区别,因此在面对“是否要支持三元运算符”这个问题时,Rust 和 Python/Go 的思考角度有着天然不同的起点。知道了这种区别后,我们对编程语言会有更明晰地认知。回到本文的问题:为什么有些编程语言不采用主流的三元运算符语法呢?不可否认,“?:”确实是一种简洁好用的设计,然而,标点符号的负面影响是过于抽象,可读性并不及“if-else”那样强。另外,不同语言的设计风格与使用习惯,也会导致不同的选择。Python 在经过一番波折后,最后设计出了与众不同的条件表达式。Go 语言明确表示不支持三元运算符。Rust 先设计后舍去,主要的原因在于 if 表达式的语言基础。考察完这三个热门语言后,我相信你已收获了一个满意的答案。如果是这样,请点赞支持一下本文吧!
Python 中有一把著名的锁——全局解释器锁(Global Interpreter Lock,简写 GIL),它的作用是防止多个本地线程同时执行 Python 字节码,这会导致 Python 无法实现真正的多线程执行。(注:本文中 Python 解释器特指 CPython)这把锁在 Python 的早期发展中具有积极的作用(单核 CPU 时代),然而,它阻碍了 Python 在多核 CPU 上的并行编程,引起了开发者们与日俱增的诟病。GIL 影响的主要是 CPU 密集型任务,比如科学计算与数值计算任务。在最近发布的 PEP-703 中,它概括了 GIL 对科学计算(主要是 AI/ML)造成的四类问题:GIL 导致许多并行化操作难以表达(影响强化学习、DeepMind、医学治疗及生物研究等领域)GIL 影响了 Python 库的可用性(例如 PyTorch、scikit-learn、NumPy)GIL 导致无法充分利用 GPU 资源(例如计算机视觉任务)GIL 导致难以部署 Python AI 模型(例如基于神经网络的 AI 模型)社区中想要移除 GIL 的呼声以及尝试,此起彼伏,绵绵不绝,但这个话题一直悬而未决。抱怨、质疑、不满、不甘、期盼等这些诸多的情绪,不是那么容易平息的。然而,从一个积重已久的庞大的项目中移除一个根基性的设计,又谈何容易?2023 新年刚过,这个话题又一次热了起来,又一轮对 GIL 的挑战开始了。这一次,事情似乎有了新的转机,这次也许能成功了呢?PEP-703 在今年 1 月 9 日新鲜出炉,虽然它目前仍是“草案”状态未被采纳,但是这份 PEP 的意义十分重大!(注:每个 Python 学习者都应该基本了解 PEP,建议阅读《学习Python,怎能不懂点PEP呢? 》)这个 PEP 的作者是 Sam Gross,他是 nogil 项目的作者。Python猫的老读者应该有印象,我们在 2021 年曾翻译过他与 Python 核心开发者们的一次研讨会的纪要,这份纪要里概括了 nogil 的主要设计思路,同时回答了核心开发者们最为关注的约 20 个问题。经过一年多时间的沉淀,nogil 项目现在终于形成了正式的 PEP,这意味着它被采纳进 Python 主分支的可能性变大了一些啦!PEP 的标题是《使 CPython 的 GIL 成为可选项》(Making the Global Interpreter Lock Optional in CPython),内容详实,正文超过 1 万字,这个体量的 PEP 绝对够得上排在所有 PEP 的前十了。简单而言,这份提案提议给 CPython 增加一个构建时配置项--disable-gil ,作用是构建出一个线程安全的无 GIL 的解释器。为了实现无 GIL 的解释器,Python 底层的部分设计必须作出变更,内容可以概括成四类:引用计数内存管理容器线程安全锁和原子 API如果这份 PEP 被采纳实现的话,它会带来一个不容忽视的问题:Python 将发布两个不同版本的解释器,而第三方库也要相应地开发/维护/发布两个版本的软件包。PEP-703 的作者也考虑到了这个问题,他提出的解决方案是与 Anaconda 一起发布无 GIL 的 Python,同时在 conda 里集中发布管理那些兼容了新 Python 的库。考虑到 Anaconda 在科学计算与数值计算领域的强大影响力,此举既能较好地发挥 nogil Python 的用处,又能减少用户及三方库开发者面对两种发行版时的割裂感。值得注意的是,nogil 的 Python 还有一个更大的问题,那就是会影响单线程程序的性能。基于 Python 3.11 版本,实现了有偏见的引用计数及永生对象后,Python 单线程性能会变慢 10%。尽管这个数值在最新的 nogil 原型版本上可以降低到 5%,但是,另外至少还有两项难以规避的性能下降点:2% - 全局的自由列表(主要是元组和浮点数自由列表)1.5% - 集合中每个对象的互斥锁(字典、列表、队列)单线程的代码才是最广泛的使用场景,可以说这会影响到每一个 Python 用户。任何试图移除 GIL 的项目都不可避免要面临这项挑战。尽管存在着以上的两大问题,但 PEP-703 还是很有可取之处的。比如,相比于 2015 年提出的著名的 Gilectomy 项目(由 GIL ectomy 两个单词组合而成,ectomy 是一个医学上的术语“切除术”),nogil 在单线程的性能上要快得多,同时可扩展性也更好。比如,相比于 2021 年火热的“香农计划”的作者 Eric Snow 提出的 PEP-684 方案(给每个子解释器创建 GIL),后者一方面需要实现作为前提的多个 PEP(如 PEP-554、PEP-683),另一方面需要用户处理多子解释器间共享变量的麻烦。在香农计划的《Python 3.12 目标》中,PEP-554 与 PEP-684 已经囊括在内了,版本目标是充分利用 Python 的子解释器,让子解释器使用各自的 GIL,从而实现多线程的并行。好消息是,3.12 的计划跟本文的主角 PEP-703 并不冲突。事实上,它们的很多设计细节是一致的,也就是说,这两套对于 GIL 的改造方案是可以共存的,它们相互促进,事半功倍!香农计划有 Python 之父 Guido van Rossum 站台,还有财大气粗的微软支持着一支豪华的团队投入开发(含 Guido 和 Eric Snow),因此,多解释器多 GIL 的方案很可能会更快落地。而 PEP-703 有 PSF 首位全职开发者 Łukasz Langa 的倾力支持,社区的反响也不错,我觉得它今后落地的希望也挺大!无论如何,这次香农计划和 PEP-703 掀起的对 GIL 的挑战,比以往所有的尝试都更猛烈,更有成功的可能,让人不由得心生欢欣之喜~~但愿它们实现的一天不会太远吧。
大家好,我是猫哥,好久不见!2022 年末的时候,我不可避免地阳了,借着身体不舒服就停更了,接踵而至的是元旦和春节假期,又给自己放了假,连年终总结也鸽了,一懈怠就到了 2 月中旬……现在是我家娃出生的第三个月,全家人大部分的时间和精力都在他身上,结果是幸福与疲累共存。新生儿是那么的可爱,又是那么的“吵闹”,影响着我们的情绪和生活节奏。这三个月的基调跟过去的日子完全不同,它是新一年的开始,是未来日子的底色,引导着我们的生活重心偏移。在过去的两年时间里,我工作上的任务与 Python 基本无关了,转向了 Java 的阵营。然而,在业余时间里,我对 Python 的热情一直不灭(尽管有退减),直到近期,懒怠的念头变多了。身心状态与家庭节奏是这段时间停更的主要原因吧。今年的这第一篇文章,就当作给大家问声好,给自己打个气吧。唯愿 2023 年,家庭、工作与兴趣都能顺顺利利,不留遗憾,相信前方有美好的未来!最近的 Pycoder‘s Weekly 中有一篇《Three Python trends in 2023》,它介绍了当下较为热门的三个话题。我简略翻译/摘录出来,分享给大家。趋势一:Python🤝RustRust 对 Python 技术生态的影响越来越大了。关键的赋能者是 PyO3,它为 Python 提供了 Rust 绑定。有了 PyO3 后,Python 可以轻松调用 Rust 代码,同时 Rust 也能执行 Python 代码。另外,下面的工具在进一步加深这两门语言的友谊:pydantic-core:pydantic v2 的校验核心。pydantic 的作者 Samuel Colvin 将在 Pycon 2023 上发表相关演讲。ruff:速度极快的 linter。它拥有几乎与 Flake8 相同的功能,包括一些流行的插件。此外,它具有与 autoflake、isort、pydocstyle 和 pyupgrade 等工具相同的功能。因此,它基本上是检测 Python 代码的瑞士军刀。polars:更快的 DataFrames,是超级广泛使用的 pandas 的性能竞争对手。Robyn:带 Rust 运行时的异步 Python web 框架。这有一篇博客关于《Robyn 的 2023 年路线图》。Rust 目前的热度极高,未来它将融入到更多 Python 相关的项目和工具中。Python + Rust 的组合在未来的就业市场上,也可能有很高的需求。趋势二:Web 应用从历史上看,用户界面并不是 Python 的强项。然而,最近机器学习和数据应用的兴起,催生了一批”使用纯 Python 的 Web UI”框架,例如 Streamlit、NiceGUI 和 Pynecone。这样的框架为 Pythonistas 提供了构建 Web 应用的快捷方式,不再需要学习 JavaScript+HTML+CSS 技术栈。另一条线路是浏览器中的 Python。通过 Pyodide、PyScript和相关工具,这已经实现了。它的基础推动者是与所有主流浏览器兼容的 WASM (WebAssembly)。在写本文时, CPython 源码库中已经有了对 CPython 的 WASM 构建的实验性支持。如果你想深入了解,可以查看 Python 3.11 in the Web Browser,这是 Cristian Heimes 在 PyConDE 2022 上的演讲。WASM 的故事还处于早期阶段,但它有着巨大的潜力,将使 Python 更容易访问并支持新的使用场景。我希望在不久的将来这个领域会有大量的创新。趋势三:类型安全CPython 对类型的支持在不断发展。例如,Python 3.10 发布了 4 个与类型相关的 PEP, 3.11 发布了 5 个。此外,PyCon 还有专门的 Typing Summit。与此同时,与类型相关的工具已经成熟化和多样化。例如,现在有一大把静态类型检查器可供选择(例如 mypy、Pyright、pytype 和 Pyre)。此外,一些包(例如 pydantic)可以在运行时巧妙地利用类型信息。(延伸阅读:介绍几款 Python 类型检查工具)*args, **kwargs 的时代即将结束,它们将被带有类型注释的签名所取代。类型极大地提高了代码可读性。当可读性与便利的 IDE 相结合,阅读庞大的 Python 代码库将变得相对容易。另一方面,在习惯了类型信息带来的超能力之后,无类型的代码库会更让人感到难受。无论现今和未来的趋势如何,Python 比以往任何时候都更受欢迎。在写本文时(2023 年 2 月),PyPI 中有 431k 个项目和 665k 个用户。在“how often language tutorials are searched in Google”中,Python 以 27.93% 的份额领先(来源)。Reddit 上的 r/Python 话题有 1.1 万订阅,r/learnpython 有 68 万订阅。
在写上一篇《Python 为什么要有 pass 语句?》时,我想到一种特别的写法,很多人会把它当成 pass 语句的替代。在文章发布后,果然有三条留言提及了它。所谓特别的写法就是下面这个:# 用 ... 替代 pass def foo(): ... 复制代码它是中文标点符号中的半个省略号,也即由英文的 3 个点组成。如果你是第一次看到,很可能会觉得奇怪:这玩意是怎么回事?(PS:如果你知道它,仔细看过本文后,你同样可能会觉得奇怪!)1、认识一下“...”内置常量事实上,它是 Python 3 中的一个内置对象,有个正式的名字叫作——Ellipsis,翻译成中文就是“省略号”。更准确地说,它是一个内置常量(Built-in Constant),是 6 大内置常量之一(另外几个是 None、False、True、NotImplemented、__debug__)。“...“并不神秘,它只是一个可能不多见的符号型对象而已。用它替换 pass,在语法上并不会报错,因为 Python 允许一个对象不被赋值引用。严格来说, 这是旁门左道,在语义上站不住脚——把“...”或其它常量或已被赋值的变量放在一个空的缩进代码块中,它们是与动作无关的,只能表达出“这有个没用的对象,不用管它”。Python 允许这些不被实际使用的对象存在,然而聪明的 IDE 应该会有所提示(我用的是 Pycharm),比如告诉你:Statement seems to have no effect 。但是“...”这个常量似乎受到了特殊对待,我的 IDE 上没有作提示。很多人已经习惯上把它当成 pass 那样的空操作来用了(在最早引入它的邮件组讨论中,就是举了这种用法的例子)。但我本人还是倾向于使用 pass,不知道你是怎么想的呢?2、奇怪的 Ellipsis 和 ...... 在 PEP-3100 中被引入,最早合入在 Python 3.0 版本,而 Ellipsis 则在更早的版本中就已包含。如你所见,赋值给 ... 时会报错SyntaxError: cannot assign to Ellipsis ,然而 Ellipsis 却可以被赋值,它们的行为根本就不同嘛!被赋值之后,Ellipsis 的内存地址以及类型属性都改变了,它成了一个“变量”,不再是常量。作为对比,给 True 或 None 之类的常量赋值时,会报错SyntaxError: cannot assign to XXX,但是给 NotImplemented 常量赋值时不会报错。众所周知,在 Python 2 中也可以给布尔对象(True/False)赋值,然而 Python 3 已经把它们改造成不可修改的。所以有一种可能的解释:Ellipsis 和 NotImplemented 是 Python 2 时代的遗留产物,为了兼容性或者只是因为核心开发者遗漏了,所以它们在当前版本(3.8)中还可以被赋值修改。... 出生在 Python 3 的时代,或许在将来会完全取代 Ellipsis。目前两者共存,它们不一致的行为值得我们注意。我的建议:只使用"..."吧,就当 Ellipsis 已经被淘汰了。3、为什么要使用“...”对象?接下来,让我们回到标题的问题:Python 为什么要使用“...”对象?这里就只聚焦于 Python 3 的“...”了,不去追溯 Ellipsis 的历史和现状。之所以会问这个问题,我的意图是想知道:它有什么用处,能够解决什么问题?从而窥探到 Python 语言设计中的更多细节。大概有如下的几种答案:(1)扩展切片语法官方文档中给出了这样的说明:Special value used mostly in conjunction with extended slicing syntax for user-defined container data types.这是个特殊的值,通常跟扩展的切片语法相结合,用在自定义的数据类型容器上。文档中没有给出具体实现的例子,但用它结合__getitem__() 和 slice() 内置函数,可以实现类似于 [1, ..., 7] 取出 7 个数字的切片片段的效果。由于它主要用在数据操作上,可能大部分人很少接触。听说 Numpy 把它用在了一些语法糖用法上,如果你在用 Numpy 的话,可以探索一下都有哪些玩法?(2)表达“未完成的代码”语义... 可以被用作占位符,也就是我在《Python 为什么要有 pass 语句?》中提到 pass 的作用。前文中对此已有部分分析。(3)Type Hint 用法Python 3.5 引入的 Type Hint 是“...”的主要使用场合。它可以表示不定长的参数,比如Tuple[int, ...] 表示一个元组,其元素是 int 类型,但数量不限。它还可以表示不确定的变量类型,比如文档中给出的这个例子:from typing import TypeVar, Generic T = TypeVar('T') def fun_1(x: T) -> T: ... # T here def fun_2(x: T) -> T: ... # and here could be different fun_1(1) # This is OK, T is inferred to be int fun_2('a') # This is also OK, now T is str 复制代码T 在函数定义时无法确定,当函数被调用时,T 的实际类型才被确定。在 .pyi 格式的文件中,... 随处可见。这是一种存根文件(stub file),主要用于存放 Python 模块的类型提示信息,给 mypy、pytype 之类的类型检查工具 以及 IDE 来作静态代码检查。(4)表示无限循环最后,我认为有一个非常终极的原因,除了引入“...”来表示,没有更好的方法。对于列表和字典这样的容器,如果其内部元素是可变对象的话,则存储的是对可变对象的引用。那么,当其内部元素又引用容器自身时,就会递归地出现无限循环引用。无限循环是无法穷尽地表示出来的,Python 中用 ... 来表示,比较形象易懂,除了它,恐怕没有更好的选择。最后,我们来总结一下本文的内容:... 是 Python 3 中的一个内置常量,它是一个单例对象,虽然是 Python 2 中就有的 Ellipsis 的别称,但它的性质已经跟旧对象分道扬镳... 可以替代 pass 语句作为占位符使用,但是它作为一个常量对象,在占位符语义上并不严谨。很多人已经在习惯上接受它了,不妨一用... 在 Python 中不少的使用场景,除了占位符用法,还可以支持扩展切片语法、丰富 Type Hint 类型检查,以及表示容器对象的无限循环... 对大多数人来说,可能并不多见(有人还可能因为它是一种符号特例而排斥它),但它的存在,有些时候能够带来便利。希望本文能让更多人认识它,那么文章的目的也就达成了~
在 C/C++/Java 等等语言中,整型变量的自增或自减操作是标配,它们又可分为前缀操作(++i 和 --i)与后缀操作(i++ 和 i--),彼此存在着一些细微差别,各有不同的用途。Python 中虽然可能出现 ++i 这种前缀形式的写法,但是它并没有“++”自增操作符,此处只是两个“+”(正数符号)的叠加而已,至于后缀形式的“++”,则完全不支持(SyntaxError: invalid syntax)。首先,Python 当然可以实现自增效果,即写成i += 1 或者 i = i + 1 ,这在其它语言中也是通用的。虽然 Python 在底层用了不同的魔术方法(__add__() 和 __iadd__() )来完成计算,但表面上的效果完全相同。所以,我们的问题可以转化成:为什么上面的两种写法会胜过 i++,成为 Python 的最终选择呢?1、Python 的整数是不可变类型当我们定义i = 1000 时,不同语言会作出不同的处理:C 之类的语言(写法 int i = 1000)会申请一块内存空间,并给它“绑定”一个固定的名称 i,同时写入一个可变的值 1000。在这里,i 的地址以及类型是固定的,而值是可变的(在一定的表示范围内)Python(写法i = 1000)也会申请一块内存空间,但是它会“绑定”给数字 1000,即这个 1000 的地址以及类型是固定的(immutable),至于 i,只是一个名称标签贴在 1000 上,自身没有固定的地址和类型所以当我们令 i “自增”时(i = i + 1),它们的处理是不同的:C 之类的语言先找到 i 的地址上存的数值,然后令它加 1,操作后新的数值就取代了旧的数值Python 的操作过程是把 i 指向的数字加 1,然后把结果绑定到新申请的一块内存空间,再把名称标签 i “贴”到新的数字上。新旧数字可以同时存在,不是取代关系打一个不太恰当的比方:C 中的 i 就像一个宿主,数字 1000 寄生在它上面;而 Python 中的 1000 像个宿主,名称 i 寄生在它上面。C 中的 i 与 Python 中的 1000,它们则寄生在底层的内存空间上……还可以这样理解:C 中的变量 i 是一等公民,数字 1000 是它的一个可变的属性;Python 中的数字 1000 是一等公民,名称 i 是它的一个可变的属性。有了以上的铺垫,我们再来看看 i++,不难发现:C 之类的语言,i++ 可以表示 i 的数字属性的增加,它不会开辟新的内存空间,也不会产生新的一等公民Python 之类的语言,i++ 如果是对其名称属性的操作,那样就没有意义了(总不能按字母表顺序,把 i 变成 j 吧);如果理解成对数字本体的操作,那么情况就会变得复杂:它会产生新的一等公民 1001,因此需要给它分配一个内存地址,此时若占用 1000 的地址,则涉及旧对象的回收,那原有对于 1000 的引用关系都会受到影响,所以只能开辟新的内存空间给 1001Python 若支持 i++,其操作过程要比 C 的 i++ 复杂,而且其含义也不再是“令数字增加1”(自增),而是“创建一个新的数字”(新增), 这样的话,“自增操作符”(increment operator)就名不副实了。Python 在理论上可以实现 i++ 操作,但它就必须重新定义“自增操作符”,还会令有其它语言经验的人产生误解,不如就让大家直接写成i += 1 或者 i = i + 1 好了。2、Python 有可迭代对象C/C++ 等语言设计出 i++,最主要的目的是为了方便使用三段式的 for 结构:for(int i = 0; i < 100; i++){ // 执行 xxx } 复制代码这种程序关心的是数字本身的自增过程,数字做加法与程序体的执行相关联。Python 中没有这种 for 结构的写法,它提供了更为优雅的方式:for i in range(100): # 执行 xxx my_list = ["你好", "我是Python猫", "欢迎关注"] for info in my_list: print(info) 复制代码这里体现了不同的思维方式,它关心的是在一个数值范围内的迭代遍历,并不关心也不需要人为对数字做加法。Python 中的可迭代对象/迭代器/生成器提供了非常良好的迭代/遍历用法,能够做到对 i++ 的完全替代。例如,上例中实现了对列表内值的遍历,Python 还可以用 enumerate() 实现对下标与具体值的同时遍历:my_list = ["你好", "我是Python猫", "欢迎关注"] for i, info in enumerate(my_list): print(i, info) # 打印结果: 0 你好 1 我是Python猫 2 欢迎关注 复制代码再例如对于字典的遍历,Python 提供了 keys()、values()、items() 等遍历方法,非常好用:my_dict = {'a': '1', 'b': '2', 'c': '3'} for key in my_dict.keys(): print(key) for key, value in my_dict.items(): print(key, value) 复制代码有了这样的利器,哪里还有 i++ 的用武之地呢?不仅如此,Python 中基本上很少使用i += 1 或者 i = i + 1 ,由于存在着随处可见的可迭代对象,开发者们很容易实现对一个数值区间的操作,也就很少有对于某个数值作累加的诉求了。所以,回到我们开头的问题,其实这两种“自增”写法并没有胜出 i++ 多少,只因为它们是通用型操作,又不需要引入新的操作符,所以 Python 才延续了一种基础性的支持。真正的赢家其实是各种各样的可迭代对象!稍微小结下:Python 不支持自增操作符,一方面是因为它的整数是不可变类型的一等公民,自增操作(++)若要支持,则会带来歧义;另一方面主要因为它有更合适的实现,即可迭代对象,对遍历操作有很好的支持。如果你觉得本文分析得不错,那你应该会喜欢这些文章:1、Python为什么使用缩进来划分代码块?2、Python 的缩进是不是反人类的设计?3、Python 为什么不用分号作语句终止符?4、Python 为什么没有 main 函数?为什么我不推荐写 main 函数?5、Python 为什么推荐蛇形命名法?
众所周知,升级某个库(假设为 xxx),可以用pip install --upgrade xxx 命令,或者简写成pip install -U xxx 。如果有多个库,可以依次写在 xxx 后面,以空格间隔。那么,如何简单优雅地批量更新系统中全部已安装的库呢?接下来我们直奔主题,带大家学习几种方法/骚操作吧!方法一:pip list 结合 Linux 命令pip list 命令可以查询已安装的库,结合 Linux 的一些命令(cut、sed、awk、grep……),可以直接在命令行中实现批量升级。先查询一下,看看是什么格式的:可以看到,前两行是一些提示信息,我们需要从第 3 行开始过滤,那就可以使用awk 命令:python3 -m pip list | awk 'NR>=3{print}' | awk '{print $1}' | xargs python3 -m pip install -U 复制代码解释一下这句命令的操作过程:先 list 查询,接着第一个 awk 取出行号大于等于 3 的内容,第二个 awk 取出第一列的内容,然后作为参数传给最后的升级命令。(PS:测试服务器上有不同版本的 Python,所以作了指定。关于“-m”的用法,推荐阅读:Python 中 -m 的典型用法、原理解析与发展演变)pip 还支持查询已过期的库,即使用pip list --outdated 命令。默认情况下,查询出的格式跟pip list 相似,有效内容从第三行开始,大家可以试试。另外,我们还可以指定--format=freeze 格式,效果是这样的:这样的格式,可以用 cut 命令切割“=”号,然后取第一列:pip list --outdated --format=freeze | cut -d = -f 1 | xargs pip install -U 复制代码以上命令在 Windows 系统中用不了。有没有更为通用的方法呢?方法二:使用 pip freeze如果是全量升级已安装的库,可以先用pip freeze 命令生成依赖文件,获取到已安装的库及其当前版本号:pip freeze > requirements.txt 复制代码然后修改文件中的“==”为“>=”,接着执行:pip install -r requirements.txt --upgrade 复制代码此方法比较适合于带有依赖文件的具体项目,可以针对该项目来升级所需的库。方法三:代码中调用 pip 的方法早期的 pip 库(<10.0.1)提供了 get_installed_distributions() 方法查询已安装的库,可以在代码中使用:# 只在早期 pip 版本中用 import pip from subprocess import call packages = [dist.project_name for dist in pip.get_installed_distributions()] call("pip install --upgrade " + ' '.join(packages), shell=True) 复制代码在较新版本中,此方法已被废弃,同样的功能要这样写:# 较新的 pip 版本。但不建议使用 from subprocess import call from pip._internal.utils.misc import get_installed_distributions for dist in get_installed_distributions(): call("pip install --upgrade " + dist.project_name, shell=True) 复制代码但是,“_internal”带前缀下划线,表明它并不希望被导出使用。方法四:使用 pkg_resources 库跟方法二和方法三相似的还有一种方法。pkg_resources 是 setuptools 库的一部分,用于查找和管理 Python 库、版本依赖关系、相关联的资源文件等。可以这样写:# 需要安装 setuptools import pkg_resources from subprocess import call packages = [dist.project_name for dist in pkg_resources.working_set] call("pip install --upgrade " + ' '.join(packages), shell=True) 复制代码方法五:使用 pip-review 库pip-review 库是一个专门用来方便升级 Python 库的工具,可以查看已过期的库、自动升级或者交互式选择性地升级:还有一个类似的pip-upgrader 库,也是为了解决批量升级的问题,感兴趣的同学请自行搜索。方法六:pip 计划的全量升级命令pip 官方有计划要提供一个全量升级的(upgrade-all)命令,如果开发出来了,那应该会是最佳选择。然后,坏消息是这个计划被阻塞了近三年,目前 issue 仍处于 Open 状态,不知道何时能有进展。这里暂且一提吧,未来留意。前面介绍了六种方法,各有其适用的场景,小伙伴们都学会了么?除此之外,当然还有其它的方法,比如 stackoverflow 网站上有个“How to upgrade all Python packages with pip?”问题,其下就有比较多的回答。感谢阅读,如果你喜欢本文,请搜索关注“Python猫”,阅读更多精彩内容!mp.weixin.qq.com/s/yOMC1cxcm…
在编程语言中有两个很基础的概念,即方法(method)和函数(function)。如果达到了编程初级/入门级水平,那么你肯定在心中已有了初步的答案。除去入参、返回值、匿名函数之类的正确的形式内容之外,你也许会说“函数就是定义在类外面的,而方法就是定义在类里面的,跟类绑定的”。这种说法有没有问题呢?当然有!不然我就不会专门写这篇文章了,本文主要会来厘清这个问题。在标准库inspect 中,它提供了两个自省的函数,即 ismethod() 和 isfunction(),可以用来判断什么是方法,什么是函数。因此,本文想要先来研究一下这两个函数,看看 Python 在处理方法/函数的概念时,是怎么做的?关于它们的用法,先看一个最简单的例子:运行的结果分别是“True”和“False”,表明我们所定义的 test() 是一个函数,而不是一个方法。这两个函数也可以用来检测自身,不难验证出它们都是一种函数:那么,接下来的问题是:inspect 库的两个函数是什么工作原理呢?先来看看 inspect 中的实现代码:在源码中,我们看到了 isinstance() 函数,它主要用于判断一个对象(object)是否是某个类(class)的实例(instance)。我们还看到了 types.FunctionType 及types.MethodType ,它们指的就是目标类。继续点进去看源码:# 摘自 types.py def _f(): pass FunctionType = type(_f) class _C: def _m(self): pass MethodType = type(_C()._m) 复制代码这里只是定义了两个空的 _f() 和 _m(),然后就使用了内置的 type() 函数。所以,我们完全可以把它们摘出来,看看庐山真面目:梳理它们的关系,可以得到:经过简化处理后,我们发现最关键的是两个问题:type() 函数如何判断出一个对象是 function 或 method 类?instance() 函数如何判断出一个对象是某个类的实例?这两个内置函数都是用 C 语言实现的,这里我就不打算继续深究了……但是,让我们再回头看看 inspect 中的注释,就会注意到一些端倪:isfunction() 判断出的是用户定义的函数(user-defined function), 它拥有__doc__、__name__ 等等属性ismethod() 判断出的是实例方法(instance method), 它拥有函数的一些属性,最特别的是还有一个 __self__ 属性还是注释更管用啊,由此我们能得到如下的推论:1、非用户定义的函数,即内置函数,在 isfunction() 眼里并不是“函数”(FunctionType)!下面验证一下 len()、dir() 和 range():事实上,它们有专属的类别(BuiltinFunctionType、BuiltinMethodType):特别需要注意的是,内置函数都是builtin_function_or_method 类型,但是 range()、type()、list() 等看起来像是函数的,其实不然:(PS:关于这点,我这篇文章 曾提到过,就不再展开了。)2、一个类的静态方法,在 ismethod() 眼里并不是方法(MethodType)!创建了类的实例后,再看看:可以看出,除了 classmethod 之外,只有类实例的实例方法,才会被 ismethod() 判定为真!而静态方法,不管绑定在类还是实例上,都不算是“方法”!有没有觉得很不可思议(或者有点理不清了)?好了,回到本文开头的问题,我们最后来小结一下吧。若以 inspect 库的两个函数为判断依据,则 Python 中的“方法与函数”具有一定的狭义性。在判断什么是函数时,它们并不把内置函数计算在内。同时,在判断什么是方法时,并非定义在类内部的都算,而是只有类方法及绑定了实例的实例方法才算是“方法”。也许你会说,inspect 的两个判断函数并不足信,内置函数也应该算是“函数”,类里面的所有方法都应该算是“方法”。我承认这种说法在广义上是可接受的,毕竟我们一直叫的就是“XX函数”、“XX方法”嘛。但是,理论和广义概念只是方便人们的沟通理解,而代码实现才是本质的区别。也就是说,Python 在实际区别“方法与函数”时,并不是文中开头的简单说法,还有更多的细节值得关注。
有一个这样的问题:现要用 setuptools 把一个项目打包成 whl 文件,然后 pip install 在 Windows/Linux 两种操作系统上,但是该项目中有一些依赖库只有 Windows 上才有(例如 pywinauto、pywingui、pywinrm),那么问题是,如何实现打包文件的可兼容性安装?从打包的角度,这个问题的关键还是看 setup.py 和 requirements.txt 文件。关于 Python 的包构建分发和 setup.py 的使用,这里有篇文章 写得很好,推荐阅读。另外关于 Python 依赖库的管理(requirements.txt),这篇文章 详细比较了 pip、pipreqs、pigar、pip-tools 和 pipdeptree 等工具,也推荐一读。有一个比较笨的实现方法:维护两份 requirements.txt 文件,分别用来打包,然后分发给不同操作系统去使用。但是这样会有麻烦:维护两份依赖文件和两种包文件,本身就挺费劲的,而在生成过程中,每次还得对它们改名以作区分(注意包名有一定的规范约束,乱改的话,pip 可能识别不出),维护成本就很高。其实,维护软件包在不同操作系统的版本,并不少见。如果你曾留意过不同版本 Python 库文件的话,你会注意到很多库都会按不同操作系统而分发不同的版本。例如,下面是同一版本号的 Numpy 在不同操作系统上的分发版(pypi.org/simple/nump…):可以看出它根据 macos、linux 和 win 三类操作系统及其位数,分成了 5 个版本。维护这么多版本,肯定是一件麻烦事,但是出现了这样的结果,就意味着 Numpy 官方认为分发不同系统版本是利大于弊的,而且是有办法实现的。回到我们的问题,是否有必要像 Numpy 那样设法打包成多个操作系统定制的包呢?答案是否定的。主要的原因:Numpy 这么做是因为它是做科学计算的,为了提升效率,它把编译好的 C 拓展文件打包,从而不需要依赖环境上的 libxxx-devel 之类的库。如果你编译安装过 Python,应该有印象需要安装 zlib-devel、openssl-devel 和 libffi-devel 之类的系统依赖。但我们前面的问题比较简单,并不是有不同的编译依赖(系统级),而只是三方库依赖不同(项目级)。另一个主要的原因,Numpy 打包出的不同系统版本,并非简简单单地用 setuptools 之类的 Python 库就能打包,而是要借助标准的镜像进行构建。例如,manylinux 版本的打包,参见 Github(github.com/pypa/manyli…),就需要使用官方提供的 Docker 镜像。对于我们的问题,显然不想做到这么麻烦。简而言之,根据前面的分析,如果要实现操作系统兼容的打包,维护多份依赖文件、使用不同构建包的方法、维护多系统专用的包,方法可行,但并不是很适用。如果没有新的办法,这不失为一种考虑,但是有没有别的办法了呢?我曾被这个问题困扰过,但是没有深入去研究解决,直到无意中在loguru 这个用来记录日志的库的 setup.py 中看到:再翻看大名鼎鼎的requests 库文件,发现还可以这样写:两个示例都是写在 setup.py 文件中,其实如果我们用 requirements.txt 文件,也可以按这种格式写,然后再读取进来。这种神奇的写法是怎么回事呢?它的依据是 2015 年 11 月创建的 PEP-508(以及相关的但已被撤销或拒绝了的 PEP-390、PEP-426、PEP-459、PEP-496),该 PEP 的主要意图是增强 pip 等工具查找软件包的能力。比较重要的部分就是跟我们的问题相关的,即对操作系统作区分的标识,相关的有:有了这样的扩展支持,在打包依赖项时,就可以解决兼容性问题了。例如 colorama 库,如果我们只在 win32 系统才需要依赖,那么在打包时就可以指定:“colorama>=0.3.4 ; sys_platform=='win32' ”;如果不需要限定 win32 系统,而是在 windows 环境都安装,那么可以写成“colorama>=0.3.4 ; platform_system=='Windows' ”。
简单来说,它是一个用 Python 开发的轻量级的远程系统管理工具,在远程登录服务器、执行 Shell 命令、批量管理服务器、远程部署等场景中,十分好用。Fabric 2 是其最新的大版本,跟早前的 Fabric 1 有挺大的不同,更加好用了,但是没填上的坑也挺多的……本文继续来聊聊 Fabric,不过我不想再面面俱到了,而是专注于这一个话题:它是如何实现对批量服务器的串行/并发管理的?(友情提示:为了有更好的阅读体验,如果你还不了解 Fabric 的基础用法,建议先阅读前面的教程。)Fabric 通过 Group 来组合多台服务器。区别在于由 fabric.group.Group 基类(父类)派生出的两个子类:SerialGroup(*hosts, **kwargs):按串行方式执行操作ThreadingGroup(*hosts, **kwargs):按并发方式执行操作下面先看看这个基类:我把一些没用的信息折叠了,比较值得注意的内容有:Group 继承了 list,所以能够 extend() ,对传入的服务器分别建立 connection核心的 run() 方法没有写实现,用意是留给子类再实现最后的 __enter__() 和 __exit__() 实现了上下文管理器有了这个基类,接下来就要看 SerialGroup 和 ThreadingGroup 的具体实现了。SerialGroup 类很简单,只实现了一个 run() 方法。因为类在初始化时为所有 host 建立了连接而且存了起来,所以这里只需用 for 循环依次取出,再执行 Connection 的 run() 方法。这里可以看到一种非常实用的开发技巧: 创建类时,让它继承内置的数据结构(如 list、dict), 这样可以直接使用 self.append()、self.extend()、self.update() 等方法把关键的信息存到“自身”,再到取出时则“for xxx in self”,这样就免了创建临时的 list 或 dict,也免得要在参数中传来传去。GroupResult 和 GroupException 是对执行结果和异常的处理,不是我们关注的重点,这里略过。接下来看看 ThreadingGroup,它也只有一个 run() 方法:ExceptionHandlingThread 是一个继承了 threading.Thread 的类,这是一种创建多线程的方式。每个线程执行的方法主要做两件事:执行 connection 的 run() 方法,以及将执行成功的结果存入队列中。接下来再分别把执行成功的结果与出异常的结果都存入到 results 中。所以,Fabric 是使用了 threading 多线程的方式来实现并发。网络请求是 IO 密集型的,使用多线程是不错的方式。至此,对于我们在开头提的问题,就有了一个初步的答案:Fabric 封装了两种 Group 来批量管理服务器,其中串行方式就是用了简单的 for 循环,而并发方式使用了 threading 多线程方式。但是,通过分析这两种 Group 的实现代码(以及使用的实践),我们也可以发现 Fabric 的缺陷:Group 只实现了 run() 方法,但是 Connection 的 put()、get()、sudo() 等方法都没有,这意味着用这种方式管理服务器集群时,只能在上面执行 shell 命令……每次调用 run() 方法时,它要等所有主机都执行完,才会返回结果,这意味着先执行完的主机会被阻塞。更为致命的是,如果其中一台主机执行时出了异常,整个 run() 方法就抛异常,这意味着每次使用 run() 方法时,都需要作异常捕获run() 方法支持执行单条 shell 命令,但是命令的状态不会传递。假设先在一个 run() 方法中运行 cd 命令切到 A 目录(非根目录),再在下一个 run() 方法创建一个文件,最终结果是该文件并不在 A 目录,而是在默认目录。解决办法是用“&&”连接起多条命令,略显麻烦这几个问题在 Fabric 的 Github issue 中,被不同的人反复提出,但是还没有得到很好的回应……言归正传,本文主要分析了 Fabric 在批量管理服务器时的实现方案,阅读其源码,可以了解到串行/并发典型场景的用法,以及类定义、类继承、多线程、异常处理等内容,最后,我们还揭示出了它的几个特性缺陷。
关于 Python 自动化的话题,在上一篇文章中,我介绍了 Invoke 库,它是 Fabric 的最重要组件之一。Fabric 也是一个被广泛应用的自动化工具库,是不得不提的自动化运维利器,所以,本文将来介绍一下它。Fabric 主要用在应用部署与系统管理等任务的自动化,简单轻量级,提供有丰富的 SSH 扩展接口。在 Fabric 1.x 版本中,它混杂了本地及远程两类功能;但自 Fabric 2.x 版本起,它分离出了独立的 Invoke 库,来处理本地的自动化任务,而 Fabric 则聚焦于远程与网络层面的任务。为了做到这点,Fabric 主要依赖另一大核心组件 Paramiko,它是基于 SSH 协议的远程控制模块,Fabric 在其基础上封装出了更加友好的接口,可以远程执行 Shell 命令、传输文件、批量操作服务器、身份认证、多种配置与设置代理,等等。一、Fabric 的版本区分Python 2 版本已经被官宣在今年元旦“退休”了,未来只会是 Python 3 的舞台。为了适应 Python 版本的非兼容性迁移,很多项目也必须推出自己的新版本(兼容或只支持 Python 3),其中就包括本文的主角 Fabric。Fabric 自身存在着 2 个大版本:Fabric 1 和 Fabric 2,而在这个库的基础上,还有两个很容易混淆的相关库:Fabric2 和 Fabric3(注意这里的数字是库名的一部分)。它们的区分如下:Fabric 1.x:支持 Python 2.5-2.7,但不支持 Python 3Fabric 2.x:支持 Python 2.7 与 3.4+,但不兼容 Fabric 1.x 的 fabfileFabric2:等同于 Fabric 2.x,为了使不同版本共存(装一个 1.x 旧版本,再装它作为新版本)Fabric3:一个基于 Fabric 1.x 的 fork(非官方),兼容 Python 2&3,兼容 Fabric1.x 的 fabfile综上可见,我们推荐使用官方的 Fabric 2.x 系列版本,但同时要注意,某些过时的教程可能是基于早期版本的(或非官方的 Fabric3,也是基于 Fabric 1.x),需要注意识别。例如,在 Fabric 1.x 系列中这么写导入:from fabric.api import run;在新版本中将报错:“ImportError: No module named api”(PS:可根据是否有 fabric.api 来判断 Fabric 的版本,就像在 Python 中根据 print 语句或 print 函数来判断版本一样)。同时,由于新版本不支持老版本的 fabfile,在使用时就可能报错:“No idea what 'xxx' is!”Fabric 2 是非兼容性版本,相比于前个版本,它主要改进的点有:支持 Python 2.7 与 3.4+线程安全,取消了多进程的并发实现API 围绕 fabric.connection.Connection 进行了重组全面修改了命令行解析器,允许在每个任务的基础上使用规则的 GNU/POSIX 风格的标志和选项(不再需要 fab mytask:weird = custom,arg = format)可以声明前置任务与后置任务……(官方列了10几条 [1],本文不一一罗列)之前介绍过的 invoke,就是在开发 Fabric 2 时被分离出来的,具体的原因可参见这个回答 [2]。总而言之,在使用 Fabric 时,应该注意版本差异的问题。二、Fabric 的基本用法1、安装首先是安装:pip intall fabric ,安装后,可在命令行窗口查看版本信息:>>> fab -V Fabric 2.5.0 Paramiko 2.7.1 Invoke 1.4.0复制代码执行“fab -V”,以上结果可看出我安装的是 Fabric 2.5.0 版本,同时可看到它的两个核心依赖库 Paramiko 及 Invoke 的版本信息。2、一个简单的例子Fabric 主要用于远程任务,即要对远程服务器进行操作,下面是一个简单的例子:# 可使用任意的文件名 from fabric import Connection host_ip = '47.xx.xx.xx' # 服务器地址 user_name = 'root' # 服务器用户名 password = '****' # 服务器密码 cmd = 'date' # shell 命令,查询服务器上的时间 con = Connection(host_ip, user_name, connect_kwargs={'password': password}) result = con.run(cmd, hide=True) print(result)复制代码以上代码,通过账号+密码登录到远程服务器,然后执行date命令,查看服务器的时间,执行结果:Command exited with status 0. === stdout === Fri Feb 14 15:33:05 CST 2020 (no stderr)复制代码现在打印的结果中,除了服务器时间,还有一些无关的信息。这是因为它打印的“result”是一个"fabric.runners.Result"类,我们可以把其中的信息解析出来:print(result.stdout) # Fri Feb 14 15:33:05 CST 2020 print(result.exited) # 0 print(result.ok) # True print(result.failed) # False print(result.command) # date print(result.connection.host) # 47.xx.xx.xx复制代码上述代码使用了 Connection 类及其 run() 方法,可在连接的服务器上运行 shell 命令。如果需要用管理员权限,则需替换成 sudo() 方法。如果要在本地执行 shell 命令,则需替换成 local() 方法。除此之外,还有 get()、put() 等方法,详见下文介绍。3、命令行用法上例代码可写在任意的 .py 脚本中,然后运行该脚本,或者稍微封装下再导入到其它脚本中使用。另外,Fabric 还是个命令行工具,可以通过fab命令来执行任务。我们稍微改造一下上例的代码:# 文件名:fabfile.py from fabric import Connection from fabric import task host_ip = '47.xx.xx.xx' # 服务器地址 user_name = 'root' # 服务器用户名 password = '****' # 服务器密码 cmd = 'date' # shell 命令,查询服务器上的时间 @task def test(c): """ Get date from remote host. """ con = Connection(host_ip, user_name, connect_kwargs={'password': password}) result = con.run(cmd, hide=True) print(result.stdout) # 只打印时间复制代码解释一下,主要的改动点有:fabfile.py 文件名:入口代码的脚本名必须用这个名字@task 装饰器:需要从 fabric 中引入这个装饰器,它是对 invoke 的 @task 装饰器的封装,实际用法跟 invoke 一样(注意:它也需要有上下文参数“c”,但实际上它并没有在代码块中使用,而是用了 Connection 类的实例)然后,在该脚本同级目录的命令行窗口中,可以查看和执行相应的任务:>>> fab -l Available tasks: test Get date from remote host. >>> fab test Fri Feb 14 16:10:24 CST 2020复制代码fab 是 Invoke 的扩展实现,继承了很多原有功能,所以执行“fab --help”,与之前介绍的“inv --help”相比,你会发现它们的很多参数与解释都是一模一样的。fab 针对远程服务的场景,添加了几个命令行选项(已标蓝),其中:--prompt-for-login-password:令程序在命令行中输入 SSH 登录密码(上例在代码中指定了 connect_kwargs.password 参数,若用此选项,可要求在执行时再手工输入密码)--prompt-for-passphrase:令程序在命令行中输入 SSH 私钥加密文件的路径-H 或 --hosts:指定要连接的 host 名-i 或 --identity:指定 SSH 连接所用的私钥文件-S 或 --ssh-config:指定运行时要加载的 SSH 配置文件4、交互式操作远程服务器上若有交互式提示,要求输入密码或“yes”之类的信息,这就要求 Fabric 能够监听并作出回应。以下是一个简单示例。引入 invoke 的 Responder,初始化内容是一个正则字符串和回应信息,最后赋值给 watchers 参数:from invoke import Responder from fabric import Connection c = Connection('host') sudopass = Responder( pattern=r'\[sudo\] password:', response='mypassword\n') c.run('sudo whoami', pty=True, watchers=[sudopass])复制代码5、传输文件本地与服务器间的文件传输是常见用法。Fabric 在这方面做了很好的封装,Connection 类中有以下两个方法可用:get(args, *kwargs):拉取远端文件到本地文件系统或类文件(file-like)对象put(args, *kwargs):推送本地文件或类文件对象到远端文件系统在已建立连接的情况下,示例:# (略) con.get('/opt/123.txt', '123.txt') con.put('test.txt', '/opt/test.txt')复制代码第一个参数指的是要传输的源文件,第二个参数是要传输的目的地,可以指定成文件名或者文件夹(为空或 None 时,使用默认路径):# (略) con.get('/opt/123.txt', '') # 为空时,使用默认路径 con.put('test.txt', '/opt/') # 指定路径 /opt/复制代码get() 方法的默认存储路径是os.getcwd ,而 put() 方法的默认存储路径是 home 目录。6、服务器批量操作对于服务器集群的批量操作,最简单的实现方法是用 for 循环,然后逐一建立 connection 和执行操作,类似这样:for host in ('web1', 'web2', 'mac1'): result = Connection(host).run('uname -s')复制代码但有时候,这样的方案会存在问题:如果存在多组不同的服务器集群,需要执行不同操作,那么需要写很多 for 循环如果想把每组操作的结果聚合起来(例如字典形式,key-主机,value-结果),还得在 for 循环之外添加额外的操作for 循环是顺序同步执行的,效率太低,而且缺乏异常处理机制(若中间出现异常,会导致跳出后续操作)对于这些问题,Fabric 提出了 Group 的概念,可将一组主机定义成一个 Group,它的 API 方法跟 Connection 一样,即一个 Group 可简化地视为一个 Connection。然后,开发者只需要简单地操作这个 Group,最后得到一个结果集即可,减少了自己在异常处理及执行顺序上的工作。Fabric 提供了一个 fabric.group.Group 基类,并由其派生出两个子类,区别是:SerialGroup(hosts, *kwargs):按串行方式执行操作ThreadingGroup(hosts, *kwargs):按并发方式执行操作Group 的类型决定了主机集群的操作方式,我们只需要做出选择即可。然后,它们的执行结果是一个fabric.group.GroupResult类,它是 dict 的子类,存储了每个主机 connection 及其执行结果的对应关系。>>> from fabric import SerialGroup >>> results = SerialGroup('web1', 'web2', 'mac1').run('uname -s') >>> print(results) <GroupResult: { <Connection 'web1'>: <CommandResult 'uname -s'>, <Connection 'web2'>: <CommandResult 'uname -s'>, <Connection 'mac1'>: <CommandResult 'uname -s'>, }>复制代码另外,GroupResult 还提供了 failed 与 succeeded 两个属性,可以取出失败/成功的子集。由此,也可以方便地批量进行二次操作。 三、Fabric 的进阶用法1、身份认证Fabric 使用 SSH 协议来建立远程会话,它是一种相对安全的基于应用层的加密传输协议。基本来说,它有两种级别的安全认证方式:基于口令的身份认证:使用账号与密码来登录远程主机,安全性较低,容易受到“中间人”攻击基于密钥的身份认证:使用密钥对方式(公钥放服务端,私钥放客户端),不会受到“中间人”攻击,但登录耗时较长前文在举例时,我们用了第一种方式,即通过指定 connect_kwargs.password 参数,使用口令来登录。Fabric 当然也支持采用第二种方式,有三种方法来指定私钥文件的路径,优先级如下:优先查找 connectkwargs.keyfilename 参数,找到则用作私钥其次查找命令行用法的 --identify 选项最后默认使用操作系统的 ssh_config 文件中的`IdentityFile` 的值如果私钥文件本身还被加密过,则需要使用 connect_kwargs.passphrase 参数。2、配置文件Fabric 支持把一些参数项与业务代码分离,即通过配置文件来管理它们,例如前面提到的密码和私钥文件,可写在配置文件中,避免与代码耦合。Fabric 基本沿用了 Invoke 的配置文件体系(官方文档中列出了 9 层),同时增加了一些跟 SSH 相关的配置项。支持的文件格式有 .yaml、.yml、.json 与 .py(按此次序排优先级),推荐使用 yaml 格式(后缀可简写成 yml)。其中,比较常用的配置文件有:系统级的配置文件:/etc/fabric.yml用户级的配置文件:~/.fabric.yml(Windows 在 C:Usersxxx 下)项目级的配置文件:/myproject/fabric.yml以上文件的优先级递减,由于我本机是 Windows,为了方便,我在用户目录建一个".fabric.yml"文件,内容如下:# filename:.fabric.yml user: root connect_kwargs: password: xxxx # 若用密钥,则如下 # key_filename: # - your_key_file复制代码我们把用户名和密码抽离出来了,所以 fabfile 中就可以删掉这些内容:# 文件名:fabfile.py from fabric import Connection from fabric import task host_ip = '47.xx.xx.xx' # 服务器地址 cmd = 'date' # shell 命令,查询服务器上的时间 @task def test(c): """ Get date from remote host. """ con = Connection(host_ip) result = con.run(cmd, hide=True) print(result.stdout) 复制代码然后,在命令行中执行:>>> fab test Tue Feb 18 10:33:38 CST 2020复制代码配置文件中还可以设置很多参数。3、网络网关如果远程服务是网络隔离的,无法直接被访问到(处在不同局域网),这时候需要有网关/代理/隧道,这个中间层的机器通常被称为跳板机或堡垒机。Fabric 中有两种网关解决方案,对应到 OpenSSH 客户端的两种选项:ProxyJump:简单,开销少,可嵌套ProxyCommand:开销大,不可嵌套,更灵活在创建 Fabric 的 Connection 对象时,可通过指定 gateway 参数来应用这两种方案:ProxyJump 方式就是在一个 Connection 中嵌套一个 Connection 作为前者的网关,后者使用 SSH 协议的direct-tcpip 为前者打开与实际远程主机的连接,而且后者还可以继续嵌套使用自己的网关。from fabric import Connection c = Connection('internalhost', gateway=Connection('gatewayhost'))复制代码ProxyCommand 方式是客户端在本地用 ssh 命令(类似“ssh -W %h:%p gatewayhost”),创建一个子进程,该子进程与服务端进行通信,同时它能读取标准输入和输出。这部分的实现细节分别在paramiko.channel.Channel 和 paramiko.proxy.ProxyCommand,除了在参数中指定,也可以在 Fabric 支持的配置文件中定义。四、小结Fabric 的非兼容版本造成了一定程度的社区分裂,这无疑跟 Python 3 的推行脱不开关系,但是我们有理由相信,新版本优胜于老版本。网上关于 Fabric 的文章,很多已过时了。本文针对最新的官方文档,梳理出了较为全面的知识点,可以带大家很好地入门 Fabric。
1、invoke 可以做什么?invoke 是从著名的远程部署工具 Fabric 中分离出来的,它与 paramiko 一起是 Fabric 的两大最核心的基础组件。除了作为命令行工具,它专注于“任务执行”(task execution),可以标注和组织任务,并通过 CLI(command-line interface,即命令行界面) 和 shell 命令来执行任务。同样是任务自动化工具,invoke 与我们之前介绍过的 tox/nox 在侧重点上有所不同:tox/nox 主要是在打包、测试、持续集成等方面的自动化(当然它们能做的还不止于此)invoke 则更具普遍性,可以用在任何需要“执行任务”的场景,可以是无相关性的任务组,也可以是有顺序依赖的分步骤的工作流invoke 在 Github 上有 2.7K star,十分受欢迎,接下来我们看看它如何使用?2、怎么使用 invoke?首先,安装很简单:pip install invoke。其次,简单使用时有以下要素:任务文件。创建一个 tasks.py 文件。@task 装饰器。在一个函数上添加 @task 装饰器,即可将该函数标记为一个任务,接受 invoke 的调度管理。上下文参数。给被装饰的函数添加一个上下文参数(context argument),注意它必须作为第一个参数,而命名按约定可以是c 或ctx 或context 。命令行执行。在命令行中执行invoke --list 来查看所有任务,运行invoke xxx 来执行名为 xxx 的任务。命令行中的“invoke”可以简写成“inv”。以下是一个简单的示例:# 文件名:tasks.py from invoke import task @task def hello(c): print("Hello world!") @task def greet(c, name): c.run(f"echo {name}加油!")复制代码在上述代码中,我们定义了两个任务:”hello“任务调用了 Python 内置的 print 函数,会打印一个字符串“Hello world!”“greet”任务调用了上下文参数的 run() 方法,可以执行 shell 命令,同时本例中还可以接收一个参数。在 shell 命令中,echo 可理解成打印,所以这也是一个打印任务,会打印出“xxx加油!”(xxx 是我们传的参数)以上代码写在 tasks.py 文件中,首先导入装饰器 from invoke import task,@task 装饰器可以不带参数,也可以带参数(参见下一节),被它装饰了的函数就是一个任务。上下文参数(即上例的“c”)必须要显式地指明,如果缺少这个参数,执行时会抛出异常:“TypeError: Tasks must have an initial Context argument!”然后在 tasks.py 文件的同级目录中,打开命令行窗口,执行命令。如果执行的位置找不到这个任务文件,则会报错:“Can't find any collection named 'tasks'!”正常情况下,通过执行inv --list 或者inv -l ,可以看到所有任务的列表(按字母表顺序排序):>>> inv -l Available tasks: greet hello复制代码我们依次执行这两个任务,其中传参时可以默认按位置参数传参,也可以指定关键字传参。结果是:>>> inv hello Hello world! >>> inv greet 武汉 武汉加油! >>> inv greet --name="武汉" 武汉加油!复制代码缺少传参时,报错:'greet' did not receive required positional arguments: 'name';多余传参时,报错:No idea what '???' is!3、 如何用好 invoke?介绍完 invoke 的简单用法,我们知道了它所需的几项要素,也大致知道了它的使用步骤,接下来是它的其它用法。3.1 添加帮助信息在上例中,“inv -l”只能看到任务名称,缺少必要的辅助信息,为了加强可读性,我们可以这样写:@task(help={'name': 'A param for test'}) def greet(c, name): """ A test for shell command. Second line. """ c.run(f"echo {name}加油!")复制代码其中,文档字符串的第一行内容会作为摘录,在“inv -l”的查询结果中展示,而且完整的内容与 @task 的 help 内容,会对应在“inv --help”中展示:>>> inv -l Available tasks: greet A test for shell command. >>> inv --help greet Usage: inv[oke] [--core-opts] greet [--options] [other tasks here ...] Docstring: A test for shell command. Second line. Options: -n STRING, --name=STRING A param for test复制代码3.2 任务的分解与组合通常一个大任务可以被分解成一组小任务,反过来,一系列的小任务也可能被串连成一个大任务。在对任务作分解、抽象与组合时,这里有两种思路:对内分解,对外统一:只定义一个 @task 的任务,作为总体的任务入口,实际的处理逻辑可以抽象成多个方法,但是外部不感知到它们多点呈现,单点汇总:定义多个 @task 的任务,外部可以感知并分别调用它们,同时将有关联的任务组合起来,调用某个任务时,也执行其它相关联的任务第一种思路很容易理解,实现与使用都很简单,但是其缺点是缺少灵活性,难于单独执行其中的某个/些子任务。适用于相对独立的单个任务,通常也不需要 invoke 就能做到(使用 invoke 的好处是,拥有命令行的支持)。第二种思路更加灵活,既方便单一任务的执行,也方便多任务的组合执行。实际上,这种场景才是 invoke 发挥最大价值的场景。那么,invoke 如何实现分步任务的组合呢?可以在 @task 装饰器的“pre”与“post”参数中指定,分别表示前置任务与后置任务:@task def clean(c): c.run("echo clean") @task def message(c): c.run("echo message") @task(pre=[clean], post=[message]) def build(c): c.run("echo build")复制代码clean 与 message 任务作为子任务,可以单独调用,也可以作为 build 任务的前置与后置任务而组合使用:>>> inv clean clean >>> inv message message >>> inv build clean build message复制代码这两个参数是列表类型,即可设置多个任务。另外,在默认情况下,@task 装饰器的位置参数会被视为前置任务,接着上述代码,我们写一个:@task(clean, message) def test(c): c.run("echo test")复制代码然后执行,会发现两个参数都被视为了前置任务:>>> inv test clean message test复制代码3.3 模块的拆分与整合如果要管理很多相对独立的大型任务,或者需要多个团队分别维护各自的任务,那么,就有必要对 tasks.py 作拆分与整合。例如,现在有多份 tasks.py,彼此是相对完整而独立的任务模块,不方便把所有内容都放在一个文件中,那么,如何有效地把它们整合起来管理呢?invoke 提供了这方面的支持。首先,只能保留一份名为“tasks.py”的文件,其次,在该文件中导入其它改名后的任务文件,最后,使用 invoke 的 Collection 类把它们关联起来。我们把本文中第一个示例文件改名为 task1.py,并新建一个 tasks.py 文件,内容如下:# 文件名:tasks.py from invoke import Collection, task import task1 @task def deploy(c): c.run("echo deploy") namespace = Collection(task1, deploy)复制代码每个 py 文件拥有独立的命名空间,而在此处,我们用 Collection 可以创建出一个新的命名空间,从而实现对所有任务的统一管理。效果如下:>>> inv -l Available tasks: deploy task1.greet task1.hello >>> inv deploy deploy >>> inv task1.hello Hello world! >>> inv task1.greet 武汉 武汉加油!复制代码关于不同任务模块的导入、嵌套、混合、起别名等内容,还有不少细节。3.4 交互式操作某些任务可能需要交互式的输入,例如要求输入“y”,按回车键后才会继续执行。如果在任务执行期间需要人工参与,那自动化任务的能力将大打折扣。invoke 提供了在程序运行期的监控能力,可以监听stdout 和stderr ,并支持在stdin 中输入必要的信息。例如,假设某个任务(excitable-program)在执行时会提示“Are you ready? [y/n]”,只有输入了“y”并按下回车键,才会执行后续的操作。那么,在代码中指定 responses 参数的内容,只要监听到匹配信息,程序会自动执行相应的操作:responses = {r"Are you ready? \[y/n\] ": "y\n"} ctx.run("excitable-program", responses=responses)复制代码responses 是字典类型,键值对分别为监听内容及其回应内容。需注意,键值会被视为正则表达式,所以像本例中的方括号就要先转义。3.5 作为命令行工具库Python 中有不少好用的命令行工具库,比如标准库中的argparse、Flask 作者开源的click 与谷歌开源的fire 等等,而 invoke 也可以作为命令行工具库使用。(PS:有位 Prodesire 同学写了“Python 命令行之旅”的系列文章,详细介绍了其它几个命令行工具库的用法,我在公众号“Python猫”里转载过大部分,感兴趣的同学可查看历史文章。)事实上,Fabric 项目最初把 invoke 分离成独立的库,就是想让它承担解析命令行与执行子命令的任务。所以,除了作为自动化任务管理工具,invoke 也可以被用于开发命令行工具。官方文档中给出了一个示例,我们可以了解到它的基本用法。假设我们要开发一个 tester 工具,让用户pip install tester 安装,而此工具提供两个执行命令:tester unit 和tester intergration 。这两个子命令需要在 tasks.py 文件中定义:# tasks.py from invoke import task @task def unit(c): print("Running unit tests!") @task def integration(c): print("Running integration tests!")复制代码然后在程序入口文件中引入它:# main.py from invoke import Collection, Program from tester import tasks program = Program(namespace=Collection.from_module(tasks), version='0.1.0')复制代码最后在打包文件中声明入口函数:# setup.py setup( name='tester', version='0.1.0', packages=['tester'], install_requires=['invoke'], entry_points={ 'console_scripts': ['tester = tester.main:program.run'] } )复制代码如此打包发行的库,就是一个功能齐全的命令行工具了:$ tester --version Tester 0.1.0 $ tester --help Usage: tester [--core-opts] <subcommand> [--subcommand-opts] ... Core options: ... core options here, minus task-related ones ... Subcommands: unit integration $ tester --list No idea what '--list' is! $ tester unit Running unit tests!复制代码上手容易,开箱即用,invoke 不失为一款可以考虑的命令行工具库。4、小结invoke 作为从 Fabric 项目中分离出来的独立项目,它自身具备一些完整而强大的功能,除了可用于开发命令行工具,它还是著名的任务自动化工具。本文介绍了它的基础用法与 5 个方面的中级内容,相信读者们会对它产生一定的了解。
Command line driven CI frontend and development task automation tool命令行驱动的 CI 前端和开发任务自动化工具其核心作用是支持创建隔离的 Python 环境,在里面可以安装不同版本的 Python 解释器与各种依赖库,以此方便开发者做自动化测试、打包、持续集成等事情。简单来说,tox 是一个管理测试虚拟环境的命令行工具。 它已存在多年且广被开发者们使用,例如,著名的云计算平台 OpenStack 也采用了它,作为最基础的测试工具之一。1、tox 能做什么?细分的用途包括:创建开发环境运行静态代码分析与测试工具自动化构建包针对 tox 构建的软件包运行测试检查软件包是否能在不同的 Python 版本/解释器中顺利安装统一持续集成(CI)和基于命令行的测试创建和部署项目文档将软件包发布到 PyPI 或任何其它平台tox 官方文档中列出了 40 余种使用场景的示例,详细的列表可查看:tox.readthedocs.io/en/latest/e…2、tox 怎么配置?关于它的用法:使用pip install tox 安装,使用tox 运行全部测试环境,和tox -e envname 运行指定的环境。还有不少的命令行参数,通过tox -h 查看。tox 的行为由其配置文件控制,当前它支持 3 种配置文件:pyproject.tomltox.inisetup.cfg以 tox 项目自己的 tox.ini 配置内容为例,可以看到它是这样配置的(github.com/tox-dev/tox…):每个[xxx]及其下方内容组成一个章节(section),每个章节间使用空行作间隔。[tox]下面是全局性的配置项,envlist 字段定义了 tox 去操作的环境。[xxx]下面是 xxx 虚拟环境的配置项,[xxx:yyy]继承 xxx 的配置,同时其自身配置项的优先级更高。对于每个虚拟环境,可用的配置项很多,例如常用的有:description(描述信息)、basepython(Python解释器版本)、deps(环境依赖项)、commands(命令语句)等等。tox 还支持作变量替换,它提供了一些内置的基础变量(全局的或对于虚拟环境的):{toxinidir}、{homedir}、{envname}、{envdir}等等。除了基础性的变量替换,它还支持这些高级用法:取操作系统的环境变量:{env:KEY},效果等同于os.environ['KEY'] 。可以变化成:{env:KEY:DEFAULTVALUE},在取不到环境变量时则使用默认值;{env:KEY:{env:DEFAULTOFKEY}},达到 if-else 的取值效果传递命令行参数:{posargs:DEFAULTS},当没有命令行参数时,使用 DEFAULTS 值。使用方式:tox arg1 arg2 传两个参,或者tox -- --opt1 arg1 将“-- opt1 arg1”作为整体传入。章节间传值:{[sectionname]valuename},不同章节的内容可以传递使用。交互式控制台注入:{tty:ONVALUE:OFFVALUE},当交互式 shell 控制台开启时,使用第一个值,否则使用第二个。pytest 在使用“--pdb”时,是这样的例子。花括号“{}”除了可以做变量替换使用,它还可以作为“或关系”判断的取值。直接看下面的例子:[tox] envlist = {py27,py36}-django{15,16}复制代码{py27,py36}-django{15,16} 的 2 组花括号内各有 2 个值,它们实际可以组合成 4 个环境:py27-django15、py27-django16、py36-django15、py36-django16。关于 tox 有哪些配置项、使用条件、什么含义、高级用法等等内容,可在官方文档中查看:tox.readthedocs.io/en/latest/c…3、tox 的插件化除了自身强大的可配置性,tox 还具有很强的可扩展性,它是可插拔的(pluggable),围绕它产生了一个极为丰富的插件生态。使用pip search tox ,可以看到数量众多的“tox-”开头的库,它们都是 tox 的插件包。其中不乏 setuptools、pipenv、conda、travis、pytest、docker 等被大家熟知的名字。tox 开放了挺多的 API 接口,方便其他人定制开发插件。4、tox 的工作流程接下来看看 tox 是怎么运作的:其工作流程中主要的环节有:配置(从figuration):加载配置文件(如 tox.ini),解析命令行参数,读取系统环境变量等打包(packaging):可选的,对于带有 setup.py 文件的项目,可以在这步去生成它的源发行版创建虚拟环境:默认使用 virtualenv 来创建虚拟环境,并根据配置项中的“deps”安装所需的依赖项,然后执行配置好的命令(commands)报告(report):汇总所有虚拟环境的运行结果并罗列出来5、小结tox 本身定位是一个测试工具,它试图令 Pytho 测试工作变得自动化、标准化与流程化。但跟 unittest 和 pytest 这些测试框架不同,它作用的是代码层面之外的事情,是一种项目级的工具。因此,它需要跟这些测试框架相结合,或者同时处理多种自动化任务(如跑 pep8、测代码覆盖率、生成文档等等),这样才能更好地发挥它的价值。它的一大特色在于创建/管理虚拟环境,但这只是为了方便测试而使用的手段,因此相比其它可管理虚拟环境的工具,如 Virtualenvwrapper、conda、pipenv、poetry,它在某些方面就存在着不足。tox 还有强大的可配置性与丰富的插件支持,这使得它在运用上具有很大的可能性与自由度。因此,不少忠实开发者仍在持续地在使用它,比如,我刚翻译好的系列文章的作者就是它的维护者之一。最后还需补充一点,tox 使用配置文件作驱动,但配置文件还是挺繁琐的,因此有人开发了一个跟 tox 相似的nox,使用 Python 文件来做配置。这个项目也很受欢迎,吸引了很多项目投入其门下,例如 pipx、urllib3、Salt 等等。
之前,我曾转过一个单元测试框架系列的文章,里面介绍了 unittest、nose/nose2 与 pytest 这三个最受人欢迎的 Python 测试框架。本文想针对测试中一种很常见的测试场景,即参数化测试,继续聊聊关于测试的话题,并尝试将这几个测试框架串联起来,做一个横向的比对,加深理解。1、什么是参数化测试?对于普通测试来说,一个测试方法只需要运行一遍,而参数化测试对于一个测试方法,可能需要传入一系列参数,然后进行多次测试。比如,我们要测试某个系统的登录功能,就可能要分别传入不同的用户名与密码,进行测试:使用包含非法字符的用户名、使用未注册的用户名、使用超长的用户名、使用错误的密码、使用合理的数据等等。参数化测试是一种“数据驱动测试”(Data-Driven Test),在同一个方法上测试不同的参数,以覆盖所有可能的预期分支的结果。它的测试数据可以与测试行为分离,被放入文件、数据库或者外部介质中,再由测试程序读取。2、参数化测试的实现思路?通常而言,一个测试方法就是一个最小的测试单元,其功能应该尽量地原子化和单一化。先来看看两种实现参数化测试的思路:一种是写一个测试方法,在其内部对所有测试参数进行遍历;另一种是在测试方法之外写遍历参数的逻辑,然后依次调用该测试方法。这两种思路都能达到测试目的,在简单业务中,没有毛病。然而,实际上它们都只有一个测试单元,在统计测试用例数情况,或者生成测试报告的时候,并不乐观。可扩展性也是个问题。那么,现有的测试框架是如何解决这个问题的呢?它们都借助了装饰器,主要的思路是:利用原测试方法(例如 test()),来生成多个新的测试方法(例如 test1()、test2()……),并将参数依次赋值给它们。由于测试框架们通常把一个测试单元统计为一个“test”,所以这种“由一生多”的思路相比前面的两种思路,在统计测试结果时,就具有很大的优势。3、参数化测试的使用方法?Python 标准库中的unittest 自身不支持参数化测试,为了解决这个问题,有人专门开发了两个库:一个是ddt ,一个是parameterized 。ddt 正好是“Data-Driven Tests”(数据驱动测试)的缩写。典型用法:import unittest from ddt import ddt,data,unpack @ddt class MyTest(unittest.TestCase): @data((3, 1), (-1, 0), (1.2, 1.0)) @unpack def test_values(self, first, second): self.assertTrue(first > second) unittest.main(verbosity=2) 复制代码运行的结果如下:test_values_1__3__1_ (__main__.MyTest) ... ok test_values_2___1__0_ (__main__.MyTest) ... FAIL test_values_3__1_2__1_0_ (__main__.MyTest) ... ok ================================================== FAIL: test_values_2___1__0_ (__main__.MyTest) -------------------------------------------------- Traceback (most recent call last): File "C:\Python36\lib\site-packages\ddt.py", line 145, in wrapper return func(self, *args, **kwargs) File "C:/Users/pythoncat/PycharmProjects/study/testparam.py", line 9, in test_values self.assertTrue(first > second) AssertionError: False is not true ---------------------------------------------- Ran 3 tests in 0.001s FAILED (failures=1) 复制代码结果显示有 3 个 tests,并详细展示了运行状态以及断言失败的信息。需要注意的是,这 3 个 test 分别有一个名字,名字中还携带了其参数的信息,而原来的 test_values 方法则不见了,已经被一拆为三。在上述例子中,ddt 库使用了三个装饰器(@ddt、@data、@unpack),实在是很丑陋。下面看看相对更好用的 parameterized 库:import unittest from parameterized import parameterized class MyTest(unittest.TestCase): @parameterized.expand([(3,1), (-1,0), (1.5,1.0)]) def test_values(self, first, second): self.assertTrue(first > second) unittest.main(verbosity=2) 复制代码测试结果如下:test_values_0 (__main__.MyTest) ... ok test_values_1 (__main__.MyTest) ... FAIL test_values_2 (__main__.MyTest) ... ok ========================================= FAIL: test_values_1 (__main__.MyTest) ----------------------------------------- Traceback (most recent call last): File "C:\Python36\lib\site-packages\parameterized\parameterized.py", line 518, in standalone_func return func(*(a + p.args), **p.kwargs) File "C:/Users/pythoncat/PycharmProjects/study/testparam.py", line 7, in test_values self.assertTrue(first > second) AssertionError: False is not true ---------------------------------------- Ran 3 tests in 0.000s FAILED (failures=1) 复制代码这个库只用了一个装饰器 @parameterized.expand,写法上可就清爽多了。同样提醒下,原来的测试方法已经消失了,取而代之的是三个新的测试方法,只是新方法的命名规则与 ddt 的例子不同罢了。介绍完 unittest,接着看已经死翘翘了的nose 以及新生的nose2 。nose 系框架是带了插件(plugins)的 unittest,以上的用法是相通的。另外,nose2 中还提供了自带的参数化实现:import unittest from nose2.tools import params @params(1, 2, 3) def test_nums(num): assert num < 4 class Test(unittest.TestCase): @params((1, 2), (2, 3), (4, 5)) def test_less_than(self, a, b): assert a < b 复制代码最后,再来看下 pytest 框架,它这样实现参数化测试:import pytest @pytest.mark.parametrize("first,second", [(3,1), (-1,0), (1.5,1.0)]) def test_values(first, second): assert(first > second) 复制代码测试结果如下:==================== test session starts ==================== platform win32 -- Python 3.6.1, pytest-5.3.1, py-1.8.0, pluggy-0.13.1 rootdir: C:\Users\pythoncat\PycharmProjects\study collected 3 items testparam.py .F testparam.py:3 (test_values[-1-0]) first = -1, second = 0 @pytest.mark.parametrize("first,second", [(3,1), (-1,0), (1.5,1.0)]) def test_values(first, second): > assert(first > second) E assert -1 > 0 testparam.py:6: AssertionError . [100%] ========================= FAILURES ========================== _________________________ test_values[-1-0] _________________________ first = -1, second = 0 @pytest.mark.parametrize("first,second", [(3,1), (-1,0), (1.5,1.0)]) def test_values(first, second): > assert(first > second) E assert -1 > 0 testparam.py:6: AssertionError ===================== 1 failed, 2 passed in 0.08s ===================== Process finished with exit code 0 复制代码依然要提醒大伙注意,pytest 也做到了由一变三,然而我们却看不到有新命名的方法的信息。这是否意味着它并没有产生新的测试方法呢?或者仅仅是把新方法的信息隐藏起来了?4、最后小结上文中介绍了参数化测试的概念、实现思路,以及在三个主流的 Python 测试框架中的使用方法。我只用了最简单的例子,为的是快速科普(言多必失)。但是,这个话题其实还没有结束。对于我们提到的几个能实现参数化的库,抛去写法上大同小异的区别,它们在具体代码层面上,又会有什么样的差异呢?具体来说,它们是如何做到把一个方法变成多个方法,并且将每个方法与相应的参数绑定起来的呢?在实现中,需要解决哪些棘手的问题?
在命令行中使用 Python 时,它可以接收大约 20 个选项(option),语法格式如下:python [-bBdEhiIOqsSuvVWx?] [-c command | -m module-name | script | - ] [args]复制代码本文想要聊聊比较特殊的“-m”选项:关于它的典型用法、原理解析与发展演变的过程。首先,让我们用“--help”来看看它的解释:-m mod run library module as a script (terminates option list)"mod"是“module”的缩写,即“-m”选项后面的内容是 module(模块),其作用是把模块当成脚本来运行。“terminates option list”意味着“-m”之后的其它选项不起作用,在这点上它跟“-c”是一样的,都是“终极选项”。官方把它们定义为“接口选项”(Interface options),需要区别于其它的普通选项或通用选项。-m 选项的五个典型用法Python 中有很多使用 -m 选项的场景,相信大家可能会用到或者看见过,我在这里想分享 5 个。在 Python3 中,只需一行命令就能实现一个简单的 HTTP 服务:python -m http.server 8000 # 注:在 Python2 中是这样 python -m SimpleHTTPServer 8000复制代码执行后,在本机打开“http://localhost:8000”,或者在局域网内的其它机器上打开“http://本机ip:8000”,就能访问到执行目录下的内容,例如下图就是我本机的内容:与此类似,我们只需要一行命令“python -m pydoc -p xxx”,就能生成 HTML 格式的官方帮助文档,可以在浏览器中访问。上面的命令执行了 pydoc 模块,会在 9000 端口启动一个 http 服务,在浏览器中打开,我的结果如下:它的第三个常见用法是执行 pdb 的调试命令“python -m pdb xxx.py”,以调试模式来执行“xxx.py”脚本:第四个同样挺有用的场景是用 timeit 在命令行中测试一小段代码的运行时间。以下的 3 段代码,用不同的方式拼接 “0-1-2-……-99” 数字串。可以直观地看出它们的效率差异:最后,还有一种常常被人忽略的场景:“python -m pip install xxx”。我们可能会习惯性地使用“pip install xxx”,或者做了版本区分时用“pip3 install xxx”,总之不在前面用“python -m”做指定。但这种写法可能会出问题。很巧合的是,在本月初(2019.11.01),Python 的核心开发者、第一届指导委员会 五人成员之一的 Brett Cannon 专门写了一篇博客《Why you should use "python -m pip" 》,提出应该使用“python -m pip”的方式,并做了详细的解释。他的主要观点是:在存在多个 Python 版本的环境中,这种写法可以精确地控制三方库的安装位置。例如用“python3.8 -m pip”,可以明确指定给 3.8 版本安装,而不会混淆成其它的版本。(延伸阅读:关于 Brett 的文章,这有一篇简短的归纳《原来我一直安装 Python 库的姿势都不对呀!》)-m 选项的两种原理解析看了前面的几种典型用法,你是否开始好奇:“-m”是怎么运作的?它是怎么实现的?对于“python -m name”,一句话解释:Python 会检索sys.path ,查找名字为“name”的模块或者包(含命名空间包),并将其内容当成“__main__”模块来执行。1、对于普通模块以“.py”为后缀的文件就是一个模块,在“-m”之后使用时,只需要使用模块名,不需要写出后缀,但前提是该模块名是有效的,且不能是用 C 语言写成的模块。在“-m”之后,如果是一个无效的模块名,则会报错“No module named xxx”。如果是一个带后缀的模块,则首先会导入该模块,然后可能报错:Error while finding module specification for 'xxx.py' (AttributeError: module 'xxx' has no attribute '__path__'。对于一个普通模块,有时候这两种写法表面看起来是等效的:两种写法都会把定位到的模块脚本当成主程序入口来执行,即在执行时,该脚本的__name__ 都是”__main__“,跟 import 导入方式是不同的。但它的前提是:在执行目录中存在着“test.py”,且只有唯一的“test”模块。对于本例,如果换一个目录执行的话,“python test.py”当然会报找不到文件的错误,然而,“python -m test”却不会报错,因为解释器在遍历sys.path 时可以找到同名的“test”模块,并且执行:由此差异,我们其实可以总结出“-m”的用法:已知一个模块的名字,但不知道它的文件路径,那么使用“-m”就意味着交给解释器自行查找,若找到,则当成脚本执行。以前文的“python -m http.server 8000”为例,我们也可以找到“server”模块的绝对路径,然后执行,尽管这样会变得很麻烦。那么,“-m”方式与直接运行脚本相比,在实现上有什么不同呢?直接运行脚本时,相当于给出了脚本的完整路径(不管是绝对路径还是相对路径),解释器根据文件系统的查找机制, 定位到该脚本,然后执行使用“-m”方式时,解释器需要在不 import 的情况下,在所有模块命名空间 中查找,定位到脚本的路径,然后执行。为了实现这个过程,解释器会借助两个模块:pkgutil 和 runpy ,前者用来获取所有的模块列表,后者根据模块名来定位并执行脚本2、对于包内模块如果“-m”之后要执行的是一个包,那么解释器经过前面提到的查找过程,先定位到该包,然后会去执行它的“__main__”子模块,也就是说,在包目录下需要实现一个“__main__.py”文件。换句话说,假设有个包的名称是“pname”,那么,“python -m pname”,其实就等效于“python -m pname.__main__”。仍以前文创建 HTTP 服务为例,“http”是 Python 内置的一个包,它没有“__main__.py”文件,所以使用“-m”方式执行时,就会报错:No module named http.__main__; 'http' is a package and cannot be directly executed。作为对比,我们可以看看前文提到的 pip,它也是一个包,为什么“python -m pip”的方式可以使用呢?当然是因为它有“__main__.py”文件:“python -m pip”实际上执行的就是这个“__main__.py”文件,它主要作为一个调用入口,调用了核心的"pip._internal.main"。http 包因为没有一个统一的入口模块,所以采用了“python -m 包.模块”的方式,而 pip 包因为有统一的入口模块,所以加了一个“__main__.py”文件,最后只需要写“python -m 包”,简明直观。-m 选项的十年演变过程最早引入 -m 选项的是 Python 2.4 版本(2004年),当时功能还挺受限,只能作用于普通的内置模块(如 pdb 和 profile)。随后,知名开发者 Nick Coghlan 提出的《PEP 338 -- Executing modules as scripts》把它的功能提升了一个台阶。这个 PEP 在 2004 年提出,最终实现在 2006 年的 2.5 版本。(插个题外话:Nick Coghlan 是核心开发者中的核心之一,也是第一届指导委员会的五人成员之一。记得当初看材料,他是在 2005 年被选为核心开发者的,这时间与 PEP-338 的时间紧密贴合)这个 PEP 的几个核心点是:结合了 PEP-302 的新探针机制(new import hooks),提升了解释器查找包内模块的能力结合了其它的导入机制(例如zipimport 和冻结模块(frozen modules)),拓展了解释器查找模块的范围与精度开发了新的runpy.run_module(modulename) 来实现本功能,而不用修改 CPython 解释器,如此可方便移植到其它解释器至此,-m 选项使得 Python 可以在所有的命名空间内定位到命令行中给定的模块。2009 年,在 Python 3.1 版本中,只需给定包的名称,就能定位和运行它的“__main__”子模块。2014 年,-m 扩展到支持命名空间包。至此,经过十年的发展演变,-m 选项变得功能齐全,羽翼丰满。最后,我们来个 ending 吧:-m 选项可能看似不起眼,但它绝对是最特别的选项之一,它使得在命令行中,使用内置模块、标准包与三方库时变得更轻松便利。有机会就多用一下吧,体会它带来的愉悦体验。参考材料docs.python.org/3.7/using/c…snarky.ca/why-you-sho…www.python.org/dev/peps/pe…blog.csdn.net/jian3x/arti…
在 Python 的项目中,如何管理所用的全部依赖库呢?最主流的做法是维护一份“requirements.txt”,记录下依赖库的名字及其版本号。那么,如何来生成这份文件呢?在上篇文章《由浅入深:Python 中如何实现自动导入缺失的库?》中,我提到了一种常规的方法:pip freeze > requirements.txt 复制代码这种方法用起来方便,但有几点不足:它搜索依赖库的范围是全局环境,因此会把项目之外的库加入进来,造成冗余(一般是在虚拟环境中使用,但还是可能包含无关的依赖库)它只会记录以“pip install”方式安装的库它对依赖库之间的依赖关系不做区分它无法判断版本差异及循环依赖等情况可用于项目依赖管理的工具有很多,本文主要围绕与 requirements.txt 文件相关的、比较相似却又各具特色的 4 个三方库,简要介绍它们的使用方法,罗列一些显著的功能点。至于哪个是最好的管理方案呢?卖个关子,请往下看……pipreqs这是个很受欢迎的用于管理项目中依赖库的工具,可以用“pip install pipreqs”命令来安装。它的主要特点有:搜索依赖库的范围是基于目录的方式,很有针对性搜索的依据是脚本中所 import 的内容可以在未安装依赖库的环境上生成依赖文件查找软件包信息时,可以指定查询方式(只在本地查询、在 PyPi 查询、或者在自定义的 PyPi 服务)基本的命令选项如下:Usage: pipreqs [options] <path> Options: --use-local Use ONLY local package info instead of querying PyPI --pypi-server <url> Use custom PyPi server --proxy <url> Use Proxy, parameter will be passed to requests library. You can also just set the environments parameter in your terminal: $ export HTTP_PROXY="http://10.10.1.10:3128" $ export HTTPS_PROXY="https://10.10.1.10:1080" --debug Print debug information --ignore <dirs>... Ignore extra directories --encoding <charset> Use encoding parameter for file open --savepath <file> Save the list of requirements in the given file --print Output the list of requirements in the standard output --force Overwrite existing requirements.txt --diff <file> Compare modules in requirements.txt to project imports. --clean <file> Clean up requirements.txt by removing modules that are not imported in project. 复制代码其中需注意,很可能遇到编码错误:UnicodeDecodeError: 'gbk' codec can't decode byte 0xae in 。需要指定编码格式“--encoding=utf8”。在已生成依赖文件“requirements.txt”的情况下,它可以强行覆盖、比对差异以及清除不再使用的依赖项。pigarpigar 同样可以根据项目路径来生成依赖文件,而且会列出依赖库在文件中哪些位置使用到了。这个功能充分利用了 requirements.txt 文件中的注释,可以提供很丰富的信息。pigar 对于查询真实的导入源很有帮助,例如bs4 模块来自beautifulsoup4 库,MySQLdb 则来自于MySQL_Python 库。可以通过“-s”参数,查找真实的依赖库。$ pigar -s bs4 MySQLdb 复制代码它使用解析 AST 的方式,而非正则表达式的方式,可以很方便地从 exec/eval 的参数、文档字符串的文档测试中提取出依赖库。另外,它对于不同 Python 版本的差异可以很好地支持。例如,concurrent.futures 是 Python 3.2+ 的标准库,而在之前早期版本中,需要安装三方库futures ,才能使用它。pigar 做到了有效地识别区分。(PS:pipreqs 也支持这个识别,详见这个合入:github.com/bndr/pipreq…)pip-toolspip-tools 包含一组管理项目依赖的工具:pip-compile 与 pip-sync,可以使用命令“pip install pip-tools”统一安装。它最大的优势是可以精准地控制项目的依赖库。两个工具的用途及关系图如下:pip-compile 命令主要用于生成依赖文件和升级依赖库,另外它可以支持 pip 的“Hash-Checking Mode ”,并支持在一个依赖文件中嵌套其它的依赖文件(例如,在 requirements.in 文件内,可以用“-c requirements.txt”方式,引入一个依赖文件)。它可以根据 setup.py 文件来生成 requirements.txt,假如一个 Flask 项目的 setup.py 文件中写了“install_requires=['Flask']”,那么可以用命令来生成它的所有依赖:$ pip-compile # # This file is autogenerated by pip-compile # To update, run: # # pip-compile --output-file requirements.txt setup.py # click==6.7 # via flask flask==0.12.2 itsdangerous==0.24 # via flask jinja2==2.9.6 # via flask markupsafe==1.0 # via jinja2 werkzeug==0.12.2 # via flask 复制代码在不使用 setup.py 文件的情况下,可以创建“requirements.in”,在里面写入“Flask”,再执行“pip-compile requirements.in”,可以达到跟前面一样的效果。pip-sync 命令可以根据 requirements.txt 文件,来对虚拟环境中进行安装、升级或卸载依赖库(注意:除了 setuptools、pip 和 pip-tools 之外)。这样可以有针对性且按需精简地管理虚拟环境中的依赖库。另外,该命令可以将多个“*.txt”依赖文件归并成一个:$ pip-sync dev-requirements.txt requirements.txt 复制代码pipdeptree它的主要用途是展示 Python 项目的依赖树,通过有层次的缩进格式,显示它们的依赖关系,不像前面那些工具只会生成扁平的并列关系。除此之外,它还可以:生成普遍适用的 requirements.txt 文件逆向查找某个依赖库是怎么引入进来的提示出相互冲突的依赖库可以发现循环依赖,进行告警生成多种格式的依赖树文件(json、graph、pdf、png等等)它也有缺点,比如无法穿透虚拟环境。如果要在虚拟环境中工作,必须在该虚拟环境中安装 pipdeptree。因为跨虚拟环境会出现重复或冲突等情况,因此需要限定虚拟环境。但是每个虚拟环境都安装一个 pipdeptree,还是挺让人难受的。好啦,4 种库介绍完毕,它们的核心功能都是分析依赖库,生成 requirements.txt 文件,同时,它们又具有一些差异,补齐了传统的 pip 的某些不足。本文不对它们作全面的测评,只是选取了一些主要特性进行介绍,好在它们安装方便(pip install xxx),使用也简单,感兴趣的同学不妨一试。更多丰富的细节,请查阅官方文档:github.com/bndr/pipreq…github.com/damnever/pi…github.com/jazzband/pi…github.com/naiquevin/p…
在写 Python 项目的时候,我们可能经常会遇到导入模块失败的错误:ImportError: No module named 'xxx' 或者 ModuleNotFoundError: No module named 'xxx' 。导入失败问题,通常分为两种:一种是导入自己写的模块(即以 .py 为后缀的文件),另一种是导入三方库。本文主要讨论第二种情况,今后有机会,我们再详细讨论其它的相关话题。解决导入 Python 库失败的问题,其实关键是在运行环境中装上缺失的库(注意是否是虚拟环境),或者使用恰当的替代方案。这个问题又分为三种情况:一、单个模块中缺失的库在编写代码的时候,如果我们需要使用某个三方库(如 requests),但不确定实际运行的环境是否装了它,那么可以这样写:try: import requests except ImportError: import os os.system('pip install requests') import requests复制代码这样写的效果是,如果找不到 requests 库,就先安装,再导入。在某些开源项目中,我们可能还会看到如下的写法(以 json 为例):try: import simplejson as json except ImportError: import json复制代码这样写的效果是,优先导入三方库 simplejson,如果找不到,那就使用内置的标准库 json。这种写法的好处是不需要导入额外的库,但它有个缺点,即需要保证那两个库在使用上是兼容的,如果在标准库中找不到替代的库,那就不可行了。如果真找不到兼容的标准库,也可以自己写一个模块(如 my_json.py),实现想要的东西,然后在 except 语句中再导入它。try: import simplejson as json except ImportError: import my_json as json复制代码二、整个项目中缺失的库以上的思路是针对开发中的项目,但是它有几个不足:1、在代码中对每个可能缺失的三方库都 pip install,并不可取;2、某个三方库无法被标准库或自己手写的库替代,该怎么办?3、已成型的项目,不允许做这些修改怎么办?所以这里的问题是:有一个项目,想要部署到新的机器上,它涉及很多三方库,但是机器上都没有预装,该怎么办?对于一个合规的项目,按照约定,通常它会包含一个“requirements.txt ”文件,记录了该项目的所有依赖库及其所需的版本号。这是在项目发布前,使用命令pip freeze > requirements.txt 生成的。使用命令pip install -r requirements.txt (在该文件所在目录执行,或在命令中写全文件的路径),就能自动把所有的依赖库给装上。但是,如果项目不合规,或者由于其它倒霉的原因,我们没有这样的文件,又该如何是好?一个笨方法就是,把项目跑起来,等它出错,遇到一个导库失败,就手动装一个,然后再跑一遍项目,遇到导库失败就装一下,如此循环……三、自动导入任意缺失的库有没有一种更好的可以自动导入缺失的库的方法呢?在不修改原有的代码的情况下,在不需要“requirements.txt”文件的情况下,有没有办法自动导入所需要的库呢?当然有!先看看效果:我们以 tornado 为例,第一步操作可看出,我们没有装过 tornado,经过第二步操作后,再次导入 tornado 时,程序会帮我们自动下载并安装好 tornado,所以不再报错。autoinstall 是我们手写的模块,代码如下:# 以下代码在 python 3.6.1 版本验证通过 import sys import os from importlib import import_module class AutoInstall(): _loaded = set() @classmethod def find_spec(cls, name, path, target=None): if path is None and name not in cls._loaded: cls._loaded.add(name) print("Installing", name) try: result = os.system('pip install {}'.format(name)) if result == 0: return import_module(name) except Exception as e: print("Failed", e) return None sys.meta_path.append(AutoInstall)复制代码这段代码中使用了sys.meta_path ,我们先打印一下,看看它是个什么东西?Python 3 的 import 机制在查找过程中,大致顺序如下:在 sys.modules 中查找,它缓存了所有已导入的模块在 sys.meta_path 中查找,它支持自定义的加载器在 sys.path 中查找,它记录了一些库所在的目录名若未找到,则抛出 ImportError 异常其中要注意,sys.metapath 在不同的 Python 版本中有所差异,比如它在 Python 2 与 Python 3 中差异很大;在较新的 Python 3 版本(3.4+)中,自定义的加载器需要实现`findspec 方法,而早期的版本用的则是find_module` 。以上代码是一个自定义的类库加载器 AutoInstall,可以实现自动导入三方库的目的。需要说明一下,这种方法会“劫持”所有新导入的库,破坏原有的导入方式,因此也可能出现一些奇奇怪怪的问题,敬请留意。sys.meta_path 属于 Python 探针的一种运用。探针,即`import hook`,是 Python 几乎不受人关注的机制,但它可以做很多事,例如加载网络上的库、在导入模块时对模块进行修改、自动安装缺失库、上传审计信息、延迟加载等等。限于篇幅,我们不再详细展开了。最后小结一下:可以用 try...except 方式,实现简单的三方库导入或者替换已知全部缺失的依赖库时(如 requirements.txt),可以手动安装利用 sys.meta_path,可以自动导入任意的缺失库参考资料:github.com/liuchang081…blog.konghy.cn/2016/10/25/…docs.python.org/3/library/s…
明线:早期的 print 语句带有 C 和 Shell 的影子,是个应用程序级的 statement,在最初十几年里,经历过 PEP-214 和 PEP-259 的改进;再到 2009 年的大版本 3.0,由语句改成了 print() 函数,还在 3.3 版本,做过一次功能增强,最终上升成为一等的内置函数。暗线:介绍了 print 的竞争对手们,像传统的日志模块 logging、调试模块 pdb、主流 IDE 的调试功能,以及后起之秀 PySnooper,它们瞄准着 print 的位置,摩拳擦掌,虎视眈眈。pprint 是“pretty printer”的简写,“pretty”的含义是“漂亮的、美观的”,还有表示“相当地”的程度语气,因此它的含义便是:(相当)美观的打印。这是个相当简单却有用的模块,主要用于打印复杂的数据结构对象,例如多层嵌套的列表、元组和字典等。先看看 print() 打印的一个例子:mylist = ["Beautiful is better than ugly.", "Explicit is better than implicit.", "Simple is better than complex.", "Complex is better than complicated."] print(mylist) # 结果如下: ['Beautiful is better than ugly.', 'Explicit is better than implicit.', 'Simple is better than complex.', 'Complex is better than complicated.'] 复制代码这是一个简单的例子,全部打印在一行里。想象一下,如果对象中的元素是多层嵌套的内容(例如复杂的 Json 数据),或者有超多的元素(例如在列表中存了很多 URL 链接),再打印出来会是怎样?那肯定是一团糟的,不好阅读。使用 pprint 模块的 pprint() 替代 print(),可以解决如下痛点:设置合适的行宽度,作适当的换行设置打印的缩进、层级,进行格式化打印判断对象中是否出现无限循环,并优化打印内容1、简单使用语法:pprint(object, stream=None, indent=1, width=80, depth=None, *,compact=False)默认的行宽度参数为 80,当打印的字符(character)小于 80 时,pprint() 基本上等同于内置函数 print(),当字符超出时,它会作美化,进行格式化输出:import pprint # 打印上例的 mylist pprint.pprint(mylist) # 打印的元素是换行的(因为超出80字符): ['Beautiful is better than ugly.', 'Explicit is better than implicit.', 'Simple is better than complex.', 'Complex is better than complicated.'] 复制代码2、设置缩进为 4 个空格(默认为1)pprint.pprint(mylist, indent=4) [ 'Beautiful is better than ugly.', 'Explicit is better than implicit.', 'Simple is better than complex.', 'Complex is better than complicated.'] 复制代码3、设置打印的行宽mydict = {'students': [{'name':'Tom', 'age': 18},{'name':'Jerry', 'age': 19}]} pprint.pprint(mydict) # 未超长: {'students': [{'age': 18, 'name': 'Tom'}, {'age': 19, 'name': 'Jerry'}]} pprint.pprint(mydict, width=20) # 超长1: {'students': [{'age': 18, 'name': 'Tom'}, {'age': 19, 'name': 'Jerry'}]} pprint.pprint(mydict, width=70) # 超长2: {'students': [{'age': 18, 'name': 'Tom'}, {'age': 19, 'name': 'Jerry'}]} 复制代码4、设置打印的层级(默认全打印)newlist = [1, [2, [3, [4, [5]]]]] pprint.pprint(newlist, depth=3) # 超出的层级会用...表示 [1, [2, [3, [...]]]] 复制代码5、优化循环结构的打印当列表或其它数据结构中出现循环引用时,要完整打印出所有内容是不可能的。所以 print 作了简化处理,就像上例一样,只打印外层的壳,而不打印内层循环的东西。这种处理方式是简化了,但没有指出是谁导致了循环,还容易看漏。pprint() 方法作了改进,遇到无限循环结构时,会表示成<Recursion on typename with id=number> 的格式。还有个 saferepr() 方法,也是这样优化,而且返回的是个字符串:newlist = [1, 2] newlist.insert(0, newlist) # 列表元素指向列表自身,造成循环引用 # 直接 print 的结果是:[[...], 1, 2] pprint.pprint(newlist) # [<Recursion on list with id=1741283656456>, 1, 2] pprint.saferepr(newlist) # '[<Recursion on list with id=1741283656456>, 1, 2]' 复制代码6、判断是否出现循环结构有两个方法可以判断一个对象中是否出现无限循环:pprint.isrecursive(newlist) # True pprint.isreadable(newlist) # False 复制代码isreadable() 除了能像 isrecursive() 一样判断循环,还能判断该格式化内容是否可被 eval() 重构。以上就是 pprint 模块的快捷入门介绍,除此之外,还有 pformat() 方法、PrettyPrinter 类,以及某些参数的使用等内容,我觉得没有大用,就不多说了。如若感兴趣,你可查阅:官方介绍:docs.python.org/zh-cn/3/lib…源码地址:github.com/python/cpyt…最后,还有两个小小的点:1、用 pprint() 替换 print() 的技巧在不考虑 print() 函数本身的参数的情况下,可以在引入 pprint 模块后,写上 “print = pprint.pprint”,令 print() 起到改头换面的效果:import pprint print = pprint.pprint mylist = ["Beautiful is better than ugly.", "Explicit is better than implicit.", "Simple is better than complex.", "Complex is better than complicated."] print(mylist) # 可对比本文开头的例子 ['Beautiful is better than ugly.', 'Explicit is better than implicit.', 'Simple is better than complex.', 'Complex is better than complicated.'] 复制代码2、国人开发的 beeprint国内某位 pan 同学在 Github 开源了个beeprint,明显是对标 pprint 的。项目地址:github.com/panyanyany/…它优化了字典对象的打印,对于从其它语言转过来的同学而言(例如 Java),这是个福音:它还优化了长文本的打印,支持自定义对象的打印,看起来不错。但是,其它功能不够齐全,而且作者停止维护两年了,荒废已久……总体而言,pprint 算是 print() 的轻量级替代,简单实用,极其方便(毕竟是标准库),文档丰富而有保障。
这几天,我在用 Github page + hexo 搭建个人网站,为了延续风格,就想把配图与文章一起迁移过去。这时候就出现了一个难题:我所用的图片都是高清大图,放到网站上就严重拖慢了加载速度。因此,需要先把图片压缩,再上传。我把需求概括如下:需要批量压缩图片,现有大约 200 张,后会再增是压缩,不是切割截取,不改变图片尺寸原图片大部分是 10M - 30M,目标是压缩成 1M 以内,越小越好按着这几条线索,我搜索“批量压缩图片”、“图片压缩工具“、”批量处理图片“......一开始的想法是找轻量级的图片压缩工具,简单处理一下就好。然而不知是搜索的姿势不对,还是筛选过滤信息的姿势不对,结果都差强人气。查找到的工具有本地与在线两类,可试验后都不太理想:有的软件下载后才发现是付费的,有的在使用时直接导致程序卡死,有的压缩率不够需要多次压缩,有的要求原始图片大小不能超过 5 M,有的要求批量处理数量不超过 20 张,有的不支持批量压缩......群内小伙伴还帮忙推荐了“PS+批处理”、acdsee、甚至手机应用 snapseed,都不合我意。花了不少时间后,偶然看到有文章写用 Python 来压缩图片。一文惊醒梦中人,我怎么没想到呢?Tinypng 网站提供在线图片压缩服务,是所有图片压缩工具中最好用的之一,但它有所限制:批量最多处理 20 张,且每张大小不允许超过 5 M。这个网站非常良心,开放了免费的 API ,API 取消了每张大小的限制,只限定每个月处理 500 张图片。这对我来说,已经足足有余了。下面介绍怎么使用它。第一步是在它网站上注册,获得专属的 API_KEY。使用的是邮箱注册,很简单。然后是安装 package:pip install --upgrade tinify 复制代码接着是处理图片:import tinify import os tinify.key = '此处填入你的key' path = "C:\\Users\\yunpoyue\\Pictures\\cat" # 图片存放的路径 for dirpath, dirs, files in os.walk(path): for file in files: imgpath = os.path.join(dirpath, file) print("compressing ..."+ imgpath) tinify.from_file(imgpath).to_file(imgpath) 复制代码不到 10 行代码,轻轻松松就批量压缩图片,简直不要太爽!20 M 的图片能压缩到 2 M,压缩率达到惊人的 90%,成绩喜人。它的 API 还提供图片裁剪、加水印、保存压缩图片至云服务商等功能,非常强大。除了压缩过程有点慢,其它无可挑剔。
Python 的内置函数 sum() 可以接收两个参数,当第一个参数是二维列表,第二个参数是一维列表的时候,它可以实现列表降维的效果。在上一篇《如何给列表降维?sum()函数的妙用》中,我们介绍了这个用法,还对 sum() 函数做了扩展的学习。那篇文章发布后,猫哥收到了一些很有价值的反馈,不仅在知识面上获得了扩充,在思维能力上也得到了一些启发,因此,我决定再写一篇文章,继续跟大家聊聊 sum() 函数以及列表降维。若你读后有所启发,欢迎留言与我交流。有些同学表示,没想到 sum() 函数竟然可以这么用,涨见识了!猫哥最初在交流群里看到这种用法时,也有同样的想法。整理成文章后,能得到别人的认可,我非常开心。学到新东西,进行分享,最后令读者也有所获,这鼓舞了我——应该每日精进,并把所学分享出去。也有的同学早已知道 sum() 的这个用法,还指出它的性能并不好,不建议使用。这是我不曾考虑到的问题,但又不得不认真对待。是的,sum() 函数做列表降维有奇效,但它性能堪忧,并不是最好的选择。因此,本文想继续探讨的话题是:(1)sum() 函数的性能到底差多少,为什么会差?(2)既然 sum() 不是最好的列表降维方法,那是否有什么替代方案呢?在 stackoverflow 网站上,有人问了个“How to make a flat list out of list of lists”问题,正是我们在上篇文章中提出的问题。在回答中,有人分析了 7 种方法的时间性能。先看看测试代码:import functools import itertools import numpy import operator import perfplot def forfor(a): return [item for sublist in a for item in sublist] def sum_brackets(a): return sum(a, []) def functools_reduce(a): return functools.reduce(operator.concat, a) def functools_reduce_iconcat(a): return functools.reduce(operator.iconcat, a, []) def itertools_chain(a): return list(itertools.chain.from_iterable(a)) def numpy_flat(a): return list(numpy.array(a).flat) def numpy_concatenate(a): return list(numpy.concatenate(a)) perfplot.show( setup=lambda n: [list(range(10))] * n, kernels=[ forfor, sum_brackets, functools_reduce, functools_reduce_iconcat, itertools_chain, numpy_flat, numpy_concatenate ], n_range=[2**k for k in range(16)], logx=True, logy=True, xlabel='num lists' ) 复制代码代码囊括了最具代表性的 7 种解法,使用了 perfplot (注:这是该测试者本人开发的库)作可视化,结果很直观地展示出,随着数据量的增加,这几种方法的效率变化。从测试图中可看出,当数据量小于 10 的时候,sum() 函数的效率很高,但是,随着数据量增长,它所花的时间就出现剧增,远远超过了其它方法的损耗。值得注意的是,functools_reduce 方法的性能曲线几乎与 sum_brackets 重合。在另一个回答中,有人也做了 7 种方法的性能测试(巧合的是,所用的可视化库也是测试者自己开发的),在这几种方法中,functools.reduce 结合 lambda 函数,虽然写法不同,它的时间效率与 sum() 函数也基本重合:from itertools import chain from functools import reduce from collections import Iterable # or from collections.abc import Iterable import operator from iteration_utilities import deepflatten def nested_list_comprehension(lsts): return [item for sublist in lsts for item in sublist] def itertools_chain_from_iterable(lsts): return list(chain.from_iterable(lsts)) def pythons_sum(lsts): return sum(lsts, []) def reduce_add(lsts): return reduce(lambda x, y: x + y, lsts) def pylangs_flatten(lsts): return list(flatten(lsts)) def flatten(items): """Yield items from any nested iterable; see REF.""" for x in items: if isinstance(x, Iterable) and not isinstance(x, (str, bytes)): yield from flatten(x) else: yield x def reduce_concat(lsts): return reduce(operator.concat, lsts) def iteration_utilities_deepflatten(lsts): return list(deepflatten(lsts, depth=1)) from simple_benchmark import benchmark b = benchmark( [nested_list_comprehension, itertools_chain_from_iterable, pythons_sum, reduce_add, pylangs_flatten, reduce_concat, iteration_utilities_deepflatten], arguments={2**i: [[0]*5]*(2**i) for i in range(1, 13)}, argument_name='number of inner lists' ) b.plot() 复制代码这就证实了两点:sum() 函数确实性能堪忧;它的执行效果实际是每个子列表逐一相加(concat)。那么,问题来了,拖慢 sum() 函数性能的原因是啥呢?在它的实现源码中,我找到了一段注释:/* It's tempting to use PyNumber_InPlaceAdd instead of PyNumber_Add here, to avoid quadratic running time when doing 'sum(list_of_lists, [])'. However, this would produce a change in behaviour: a snippet like empty = [] sum([[x] for x in range(10)], empty) would change the value of empty. */ 复制代码为了不改变 sum() 函数的第二个参数值,CPython 没有采用就地相加的方法(PyNumber_InPlaceAdd),而是采用了较耗性能的普通相加的方法(PyNumber_Add)。这种方法所耗费的时间是二次方程式的(quadratic running time)。为什么在这里要牺牲性能呢?我猜想(只是浅薄猜测),可能有两种考虑,一是为了第二个参数(start)的一致性,因为它通常是一个数值,是不可变对象,所以当它是可变对象类型时,最好也不对它做修改;其次,为了确保 sum() 函数是个 纯函数 ,为了多次执行时能返回同样的结果。那么,我要继续问:哪种方法是最优的呢?综合来看,当子列表个数小于 10 时,sum() 函数几乎是最优的,与某几种方法相差不大,但是,当子列表数目增加时,最优的选择是 functools.reduce(operator.iconcat, a, []),其次是 list(itertools.chain.from_iterable(a)) 。事实上,最优方案中的 iconcat(a, b) 等同于 a += b,它是一种就地修改的方法。operator.iconcat(a, b) operator.__iconcat__(a, b) a = iconcat(a, b) is equivalent to a += b for a and b sequences. 复制代码这正是 sum() 函数出于一致性考虑,而舍弃掉的实现方案。至此,前文提出的问题都找到了答案。我最后总结一下吧:sum() 函数采用的是非就地修改的相加方式,用作列表降维时,随着数据量增大,其性能将是二次方程式的剧增,所以说是性能堪忧;而 reduce 结合 iconcat 的方法,才是大数据量时的最佳方案。这个结果是否与你所想的一致呢?希望本文的分享,能给你带来新的收获。相关链接:如何给列表降维?sum()函数的妙用 :mp.weixin.qq.com/s/cr_noDx6s…stackoverflow 问题:stackoverflow.com/questions/9…。
上个月,学习群里的 S 同学问了个题目,大意可理解为列表降维 ,例子如下:oldlist = [[1, 2, 3], [4, 5]] # 想得到结果: newlist = [1, 2, 3, 4, 5] 复制代码原始数据是一个二维列表,目的是获取该列表中所有元素的具体值。从抽象一点的角度来理解,也可看作是列表解压或者列表降维。这个问题并不难,但是,怎么写才比较优雅呢?# 方法一,粗暴拼接法: newlist = oldlist[0] + oldlist[1] 复制代码这种方法简单粗暴,需要拼接什么内容,就取出来直接拼接。然而,如果原列表有很多子列表,则这个方法就会变得繁琐了。我们把原问题升级一下:一个二维列表包含 n 个一维列表元素,如何优雅地把这些子列表拼成一个新的一维列表?方法一的做法需要写 n 个对象,以及 n - 1 次拼接操作。当然不可行。下面看看方法二:# 方法二,列表推导式: newlist = [i for j in range(len(oldlist)) for i in oldlist[j]] 复制代码这个表达式中出现了两个 for 语句,在第一个 for 语句中,我们先取出原列表的长度,然后构造 range 对象,此时 j 的取值范围是 [0, n-1] 的闭区间。在第二个 for 语句中,oldlist[j] 指的正是原列表的第 j 个子列表,for i in oldlist[j] 则会遍历取出 j 子列表的元素,由于 j 取值的区间正对应于原列表的全部索引值,所以,最终达到解题目的。这种方法足够优雅了,而且理解也并不难。然而,我们是否就能满足于此了呢?有没有其它奇技淫巧,哦不,是其它高级方法呢?F 同学贡献了一个思路:# 方法三,巧用sum: newlist = sum(oldlist,[]) 复制代码说实话,这个方法令我大感意外!sum() 函数不是用于求和的么?怎么竟然有此用法?这个写法利用了什么原理呢?由于我开始时不知道 sum() 函数可以接收两个参数,不清楚它们是怎么用于计算的,所以一度很困惑。但是,当我知道 sum() 的完整用法时,我恍然大悟。接下来也不卖关子了,直接揭晓吧。语法: sum(iterable[, start]) ,sum() 函数的第一个参数是可迭代对象,如列表、元组或集合等,第二个参数是起始值,默认为 0 。其用途是以 start 值为基础,再与可迭代对象的所有元素相“加”。在上例中,执行效果是 oldlist 中的子列表逐一与第二个参数相加,而列表的加法相当于 extend 操作,所以最终结果是由 [] 扩充成的列表。这里有两个关键点:sum() 函数允许带两个参数,且第二个参数才是起点。 可能 sum() 函数用于数值求和比较多,然而用于作列表的求和,就有奇效。它比列表推导式更加优雅简洁!至此,前面的升级版问题就得到了很好的回答。简单回顾一下,s 同学最初的问题可以用三种方法实现,第一种方法中规中矩,第二种方法正道进阶,而第三种方法旁门左道(没有贬义,只是说它出人意料,却效果奇佳)。这道并不算难的问题,在众人的讨论与分享后,竟还引出了很有价值的学习内容。前不久,同样是群内的一个问题,也产生了同样的学习效果,详见《Python进阶:如何将字符串常量转为变量?》。我从中得到了一个启示:应该多角度地思考问题,设法寻求更优解,同时,基础知识应掌握牢固,并灵活贯通起来。学无止境,这里我还想再开拓一下思路,看看能发现些什么。1、如果原列表的元素除了列表,还有其它类型的元素,怎么把同类的元素归并在一起呢?2、如果是一个三维或更高维的列表,怎么更好地把它们压缩成一维列表呢?3、sum() 函数还有什么知识要点呢?前两个问题增加了复杂度,解决起来似乎没有“灵丹妙药”了,只能用笨方法分别拆解,逐一解压。第三个思考题是关于 sum() 函数本身的用法,我们看看官方文档是怎么说的:The iterable’s items are normally numbers, and the start value is not allowed to be a string.For some use cases, there are good alternatives to sum(). The preferred, fast way to concatenate a sequence of strings is by calling ''.join(sequence). To add floating point values with extended precision, see math.fsum(). To concatenate a series of iterables, consider using itertools.chain().sum() 的第二个参数不允许是字符串。如果用了,会报错:TypeError: sum() can't sum strings [use ''.join(seq) instead]为什么不建议使用 sum() 来拼接字符串呢?哈哈,文档中建议使用 join() 方法,因为它更快。为了不给我们使用慢的方法,它竟特别限定不允许 sum() 的第二个参数是字符串。文档还建议,在某些使用场景时,不要用 sum() ,例如当以扩展精度对浮点数求和时,推荐使用 math.fsum() ;当要拼接一系列的可迭代对象时,应考虑使用 itertools.chain() 。浮点数的计算是个难题,我曾转载过一篇《如何在 Python 里面精确四舍五入?》,对此有精彩分析。而itertools.chain() 可以将不同类型的可迭代对象串联成一个更大的迭代器,这在旧文《Python进阶:设计模式之迭代器模式》中也有论及。不经意间,sum() 函数的注意事项,竟把 Python 其它的进阶内容都联系起来了。小小的函数,竟成为学习之路上的一个枢纽。前段时间,我还写过 range() 、locals() 和 eval() 等内置函数,也是通过一个问题点,而关联出多个知识点, 获益良多。这些内置函数/类的魔力可真不小啊。
1、如何动态生成变量名?M 同学的问题如下:打扰一下大家,请教一个问题,已知 list = ['A', 'B', 'C', 'D'] , 如何才能得到以 list 中元素命名的新列表 A = [], B = [], C = [], D = [] 呢?简单理解,这个问题的意思是,将字符串内容作为其它对象的变量名。list 中的元素是字符串,此处的 ‘A’-‘D’ 是常量 ,而在要求的结果中,A-D 是变量 。如果强行直接将常量当做变量使用,它会报错:>>> 'A' = [] ...SyntaxError: can't assign to literal 复制代码报错中的literal 指的是字面量 ,这是计算机科学中常见的一个概念,用于表达源代码中的固定值。 例如,整数、浮点数、字符串等基本类型,就是字面量。字面量指的就是一个量本身,可以理解为一种原子性的实体,当然不能再被赋值了。所以,取出的字符串内容,并不能直接用作变量名,需要另想办法。有初学者可能会想,list[0] = [] 行不行?当然不行,因为没有出现 A 。那 A = list[0] ,接着 A = [] 呢?那也不行,因为这里的 A 是你凭空定义出来的,而不是从已有条件中生成的。当时,群里只有两三个同学参与了讨论,我们没想到解决办法。但是,我觉得这个题目很有意思,值得玩味。因为,如果能解决这个问题,那就意味着可以不作预先定义,而是动态地生成变量名,这不仅能减少给变量取名的麻烦,还实现了自动编码!可以设想一下未来,人工智能在编写代码的时候,如果能根据已知条件,动态生成变量名,那编写代码的过程不就顺利多了么?(据说,现在已经有人工智能可以编写代码了,不知它在取变量名时,是用的什么方法?)2、办法总是有的最近,学习群里蒙混进来了几个打广告的,为此,我决定提高审核门槛,例如,用群里的问题来作个考核。万万没想到的是,第一个被考核到的 Q 同学,几乎不假思索地就说出了一个解决上述问题的思路。而偏偏就是那么巧 ,几乎在同时,群内的 J 同学给出了另外一个解决方法(他没看到群内的讨论,而是看到了知识星球的记录,才知道这个问题的)。也就是说,前一晚还以为无解的问题,在第二天竟得到了两种不同的解决方法!那么,他们的答案是什么呢?# J 同学的解答 >>> list1 = ['A', 'B', 'C', 'D'] >>> for i in list1: >>> globals()[i] = [] >>> A [] 复制代码这个方法通过修改全局命名空间,巧妙地“定义”出了新的变量。globals() 方法取出来的是一个字典,字符串 ‘A’ 是其中一个键值(key),而这个键值恰恰是全局命名空间中的一个变量,这就实现了从常量到变量的转化。在数据结构层面上,空列表 [] 作为一个值(value)跟它的字符串键值绑定在一起,而在运用层面上,它作为变量内容而跟变量名绑定在一起。看到这个回答的时候,我就突然想起来了,上个月转载过一篇《Python 动态赋值的陷阱》,讲的正是动态地进行变量赋值 的问题啊!J 同学说,他正是看了那篇文章,才学得了这个方法。这就有意思了,我分享了一个自己囫囵吞枣的知识,然后它被 J 同学吸收掌握,最后反馈回来解决了我的难题。我真切地感受到了知识分享的魅力:知识在流动中获得生命,在碰撞中锃亮色泽。同时,我也真切地明白了一个互助的学习团体的好处:利人者也利己,互助者共同进步。3、动态执行代码的方法新进群的 Q 同学,提供了一个不同的答案:# Q 同学的解答 >>> list1 = ['A', 'B', 'C', 'D'] >>> for i in list1: >>> exec(f"{i} = []") >>> A [] 复制代码他的写法用到了 Python 3.6 才引入的 f-strings 特性,事实上,在较低版本中,也是可以实现的,只需要保证 exec() 方法接收的参数是包含了变量 i 的字符串即可,例如这样写:# 以下代码可替换上例的第 4 行 exec(i + " = []") # 或者: exec("{} = []".format(i)) # 或者: exec(' '.join([i, '= []'])) 复制代码这几种写法的区别只是字符串拼接法的区别,关于如何拼接字符串,以及不同方法之间的区别,可参看《详解Python拼接字符串的七种方式》。Q 同学这个答案的核心在于 exec() 方法,它是内置的,用途是执行储存在字符串或文件中的代码段。它的基础用法如下:>>> exec('x = 1 + 2') >>> x 3 # 执行代码段 >>> s = """ >>> x = 10 >>> y = 20 >>> sum = x + y >>> print(sum) >>> """ >>> exec(s) 30 复制代码看完了 exec() 的用法,我们再回来看 Q 同学的答案。for-循环中取出来的 i 是字符串,而拼接后的字符串经过 exec() 的处理,就获得了动态编写代码的效果。也就是说,因为字符串常量的内容被当做有效代码而执行了,其中的 'A'-'D' 元素,就取得了新的身份,变成了最终的 A-D 变量名。这个方法看起来很简单啊,可是由于 exec() 方法太生僻了,直到 Q 同学提出,我们才醒悟过来。注意:在 Python3 中,exec() 是个内置方法;而在 Python2 中,exec 是个语句(statement),另外有个 execfile() 方法,两者相合并,就成了 Python3 中的 exec() 方法。本文使用的是 Python3。4、总结抽象一下最初的问题,它实际问的是“如何将字符串内容作为其它对象的变量名”,更进一步地讲是——“如何将常量转化为变量 ”。使用直接进行赋值的静态方法,行不通。两位同学提出的方法都是间接的动态方法:一个是动态地进行变量赋值,通过修改命名空间而植入变量;一个是动态地执行代码,可以说是通过“走后门”的方式,安插了变量。两种方法殊途同归,不管是白猫还是黑猫,它们都抓到了老鼠。这两种方法已经给我们带来了很有价值的启发,同时,因为它们,群内小伙伴们更是发散地讨论一些相关联的话题,例如:S 同学提出了另一种修改命名空间中变量的写法、L 同学提到了 eval() 的意义、eval() 与 exec() 的区别、我查到了为什么要慎用 eval() 、C 与 H 同学提到了 eval() 的安全用法......虽然,某些话题无法在群聊中充分展开,但是,这些话题知识的延展联系,大大地丰富了本文开头的问题,这一个微小的问题,牵连出来了两个大的知识体系。相关链接:《Python 动态赋值的陷阱》《详解Python拼接字符串的七种方式》eval()、exec()及其相关函数:www.tuicool.com/wx/vEbeumE
导读:切片系列文章连续写了三篇,本文是对它们做的汇总。为什么要把序列文章合并呢?在此说明一下,本文绝不是简单地将它们做了合并,主要是修正了一些严重的错误(如自定义序列切片的部分),还对行文结构与章节衔接做了大量改动,如此一来,本文结构的完整性与内容的质量都得到了很好的保证。众所周知,我们可以通过索引值(或称下标)来查找序列类型(如字符串、列表、元组...)中的单个元素,那么,如果要获取一个索引区间的元素该怎么办呢?切片(slice)就是一种截取索引片段的技术,借助切片技术,我们可以十分灵活地处理序列类型的对象。通常来说,切片的作用就是截取序列对象,然而,对于非序列对象,我们是否有办法做到切片操作呢?在使用切片的过程中,有什么要点值得重视,又有什么底层原理值得关注呢?本文将主要跟大家一起来探讨这些内容,希望我能与你共同学习进步。1、切片的基础用法列表是 Python 中极为基础且重要的一种数据结构,也是最能发挥切片的用处的一种数据结构,所以在前两节,我将以列表为例介绍切片的一些常见用法。首先是切片的书写形式:[i : i+n : m] ;其中,i 是切片的起始索引值,为列表首位时可省略;i+n 是切片的结束位置,为列表末位时可省略;m 可以不提供,默认值是1,不允许为0 ,当m为负数时,列表翻转。注意:这些值都可以大于列表长度,不会报越界。切片的基本含义是:从序列的第i位索引起,向右取到后n位元素为止,按m间隔过滤 。li = [1, 4, 5, 6, 7, 9, 11, 14, 16] # 以下写法都可以表示整个列表,其中 X >= len(li) li[0:X] == li[0:] == li[:X] == li[:] == li[::] == li[-X:X] == li[-X:] li[1:5] == [4,5,6,7] # 从1起,取5-1位元素 li[1:5:2] == [4,6] # 从1起,取5-1位元素,按2间隔过滤 li[-1:] == [16] # 取倒数第一个元素 li[-4:-2] == [9, 11] # 从倒数第四起,取-2-(-4)=2位元素 li[:-2] == li[-len(li):-2] == [1,4,5,6,7,9,11] # 从头开始,取-2-(-len(li))=7位元素 # 步长为负数时,列表先翻转,再截取 li[::-1] == [16,14,11,9,7,6,5,4,1] # 翻转整个列表 li[::-2] == [16,11,7,5,1] # 翻转整个列表,再按2间隔过滤 li[:-5:-1] == [16,14,11,9] # 翻转整个列表,取-5-(-len(li))=4位元素 li[:-5:-3] == [16,9] # 翻转整个列表,取-5-(-len(li))=4位元素,再按3间隔过滤 # 切片的步长不可以为0 li[::0] # 报错(ValueError: slice step cannot be zero) 复制代码上述的某些例子对于初学者(甚至很多老手)来说,可能还不好理解,但是它们都离不开切片的基本语法,所以为方便起见,我将它们也归入基础用法中。对于这些样例,我个人总结出两条经验:(1)牢牢记住公式[i : i+n : m] ,当出现缺省值时,通过想象把公式补全;(2)索引为负且步长为正时,按倒数计算索引位置;索引为负且步长为负时,先翻转列表,再按倒数计算索引位置。2、切片的高级用法一般而言,切片操作的返回结果是一个新的独立的序列(PS:也有例外,参见《Python是否支持复制字符串呢?》)。以列表为例,列表切片后得到的还是一个列表,占用新的内存地址。当取出切片的结果时,它是一个独立对象,因此,可以将其用于赋值操作,也可以用于其它传递值的场景。但是,切片只是浅拷贝 ,它拷贝的是原列表中元素的引用,所以,当存在变长对象的元素时,新列表将受制于原列表。li = [1, 2, 3, 4] ls = li[::] li == ls # True id(li) == id(ls) # False li.append(li[2:4]) # [1, 2, 3, 4, [3, 4]] ls.extend(ls[2:4]) # [1, 2, 3, 4, 3, 4] # 下例等价于判断li长度是否大于8 if(li[8:]): print("not empty") else: print("empty") # 切片列表受制于原列表 lo = [1,[1,1],2,3] lp = lo[:2] # [1, [1, 1]] lo[1].append(1) # [1, [1, 1, 1], 2, 3] lp # [1, [1, 1, 1]] 复制代码由于可见,将切片结果取出,它可以作为独立对象使用,但是也要注意,是否取出了变长对象的元素。切片既可以作为独立对象被“取出”原序列,也可以留在原序列,作为一种占位符使用。不久前,我介绍了几种拼接字符串的方法(链接见文末),其中三种格式化类的拼接方法(即 %、format()、template)就是使用了占位符的思想。对于列表来说,使用切片作为占位符,同样能够实现拼接列表的效果。特别需要注意的是,给切片赋值的必须是可迭代对象。li = [1, 2, 3, 4] # 在头部拼接 li[:0] = [0] # [0, 1, 2, 3, 4] # 在末尾拼接 li[len(li):] = [5,7] # [0, 1, 2, 3, 4, 5, 7] # 在中部拼接 li[6:6] = [6] # [0, 1, 2, 3, 4, 5, 6, 7] # 给切片赋值的必须是可迭代对象 li[-1:-1] = 6 # (报错,TypeError: can only assign an iterable) li[:0] = (9,) # [9, 0, 1, 2, 3, 4, 5, 6, 7] li[:0] = range(3) # [0, 1, 2, 9, 0, 1, 2, 3, 4, 5, 6, 7] 复制代码上述例子中,若将切片作为独立对象取出,那你会发现它们都是空列表,即 li[:0]==li[len(li):]==li[6:6]==[] ,我将这种占位符称为“纯占位符”,对纯占位符赋值,并不会破坏原有的元素,只会在特定的索引位置中拼接进新的元素。删除纯占位符时,也不会影响列表中的元素。与“纯占位符”相对应,“非纯占位符”的切片是非空列表,对它进行操作(赋值与删除),将会影响原始列表。如果说纯占位符可以实现列表的拼接,那么,非纯占位符可以实现列表的替换。li = [1, 2, 3, 4] # 不同位置的替换 li[:3] = [7,8,9] # [7, 8, 9, 4] li[3:] = [5,6,7] # [7, 8, 9, 5, 6, 7] li[2:4] = ['a','b'] # [7, 8, 'a', 'b', 6, 7] # 非等长替换 li[2:4] = [1,2,3,4] # [7, 8, 1, 2, 3, 4, 6, 7] li[2:6] = ['a'] # [7, 8, 'a', 6, 7] # 删除元素 del li[2:3] # [7, 8, 6, 7] 复制代码切片占位符可以带步长,从而实现连续跨越性的替换或删除效果。需要注意的是,这种用法只支持等长替换。li = [1, 2, 3, 4, 5, 6] li[::2] = ['a','b','c'] # ['a', 2, 'b', 4, 'c', 6] li[::2] = [0]*3 # [0, 2, 0, 4, 0, 6] li[::2] = ['w'] # 报错,attempt to assign sequence of size 1 to extended slice of size 3 del li[::2] # [2, 4, 6] 复制代码3、自定义对象实现切片功能切片是 Python 中最迷人最强大最 Amazing 的语言特性(几乎没有之一),以上两小节虽然介绍了切片的基础用法与高级用法,但这些还不足以充分地展露切片的魅力,所以,在接下来的两章节中,我们将聚焦于它的更高级用法。前两节内容都是基于原生的序列类型(如字符串、列表、元组......),那么,我们是否可以定义自己的序列类型并让它支持切片语法呢?更进一步,我们是否可以自定义其它对象(如字典)并让它支持切片呢?3.1、魔术方法:__getitem__()想要使自定义对象支持切片语法并不难,只需要在定义类的时候给它实现魔术方法 __getitem__() 即可。所以,这里就先介绍一下这个方法。语法: object.__getitem__(self, key)官方文档释义:Called to implement evaluation of self[key]. For sequence types, the accepted keys should be integers and slice objects. Note that the special interpretation of negative indexes (if the class wishes to emulate a sequence type) is up to the __getitem__() method. If key is of an inappropriate type, TypeError may be raised; if of a value outside the set of indexes for the sequence (after any special interpretation of negative values), IndexError should be raised. For mapping types, if key is missing (not in the container), KeyError should be raised.概括翻译一下:__getitem__() 方法用于返回参数 key 所对应的值,这个 key 可以是整型数值和切片对象,并且支持负数索引;如果 key 不是以上两种类型,就会抛 TypeError;如果索引越界,会抛 IndexError ;如果定义的是映射类型,当 key 参数不是其对象的键值时,则会抛 KeyError 。3.2、自定义序列实现切片功能接下来,我们定义一个简单的 MyList ,并给它加上切片功能。(PS:仅作演示,不保证其它功能的完备性)。import numbers class MyList(): def __init__(self, anylist): self.data = anylist def __len__(self): return len(self.data) def __getitem__(self, index): print("key is : " + str(index)) cls = type(self) if isinstance(index, slice): print("data is : " + str(self.data[index])) return cls(self.data[index]) elif isinstance(index, numbers.Integral): return self.data[index] else: msg = "{cls.__name__} indices must be integers" raise TypeError(msg.format(cls=cls)) l = MyList(["My", "name", "is", "Python猫"]) ### 输出结果: key is : 3 Python猫 key is : slice(None, 2, None) data is : ['My', 'name'] <__main__.MyList object at 0x0000019CD83A7A90> key is : hi Traceback (most recent call last): ... TypeError: MyList indices must be integers or slices 复制代码从输出结果来看,自定义的 MyList 既支持按索引查找,也支持切片操作,这正是我们的目的。3.3、自定义字典实现切片功能切片是序列类型的特性,所以在上例中,我们不需要写切片的具体实现逻辑。但是,对于其它非序列类型的自定义对象,就得自己实现切片逻辑。以自定义字典为例(PS:仅作演示,不保证其它功能的完备性):class MyDict(): def __init__(self): self.data = {} def __len__(self): return len(self.data) def append(self, item): self.data[len(self)] = item def __getitem__(self, key): if isinstance(key, int): return self.data[key] if isinstance(key, slice): slicedkeys = list(self.data.keys())[key] return {k: self.data[k] for k in slicedkeys} else: raise TypeError d = MyDict() d.append("My") d.append("name") d.append("is") d.append("Python猫") print(d[2]) print(d[:2]) print(d[-4:-2]) print(d['hi']) ### 输出结果: is {0: 'My', 1: 'name'} {0: 'My', 1: 'name'} Traceback (most recent call last): ... TypeError 复制代码上例的关键点在于将字典的键值取出,并对键值的列表做切片处理,其妙处在于,不用担心索引越界和负数索引,将字典切片转换成了字典键值的切片,最终实现目的。4、迭代器实现切片功能好了,介绍完一般的自定义对象如何实现切片功能,这里将迎来另一类非同一般的对象。迭代器是 Python 中独特的一种高级对象,它本身不具备切片功能,然而若能将它用于切片,这便仿佛是锦上添花,能达到如虎添翼的效果。所以,本节将隆重地介绍迭代器如何实现切片功能。4.1、迭代与迭代器首先,有几个基本概念要澄清:迭代、可迭代对象、迭代器。迭代 是一种遍历容器类型对象(例如字符串、列表、字典等等)的方式,例如,我们说迭代一个字符串“abc”,指的就是从左往右依次地、逐个地取出它的全部字符的过程。(PS:汉语中迭代一词有循环反复、层层递进的意思,但 Python 中此词要理解成单向水平线性 的,如果你不熟悉它,我建议直接将其理解为遍历。)那么,怎么写出迭代操作的指令呢?最通用的书写语法就是 for 循环。# for循环实现迭代过程 for char in "abc": print(char, end=" ") # 输出结果:a b c 复制代码for 循环可以实现迭代的过程,但是,并非所有对象都可以用于 for 循环,例如,上例中若将字符串“abc”换成任意整型数字,则会报错: 'int' object is not iterable .这句报错中的单词“iterable”指的是“可迭代的”,即 int 类型不是可迭代的。而字符串(string)类型是可迭代的,同样地,列表、元组、字典等类型,都是可迭代的。那怎么判断一个对象是否可迭代呢?为什么它们是可迭代的呢?怎么让一个对象可迭代呢?要使一个对象可迭代,就要实现可迭代协议,即需要实现__iter__() 魔术方法,换言之,只要实现了这个魔术方法的对象都是可迭代对象。那怎么判断一个对象是否实现了这个方法呢?除了上述的 for 循环外,我还知道四种方法:# 方法1:dir()查看__iter__ dir(2) # 没有,略 dir("abc") # 有,略 # 方法2:isinstance()判断 import collections isinstance(2, collections.Iterable) # False isinstance("abc", collections.Iterable) # True # 方法3:hasattr()判断 hasattr(2,"__iter__") # False hasattr("abc","__iter__") # True # 方法4:用iter()查看是否报错 iter(2) # 报错:'int' object is not iterable iter("abc") # <str_iterator at 0x1e2396d8f28> ### PS:判断是否可迭代,还可以查看是否实现__getitem__,为方便描述,本文从略。 复制代码这几种方法中最值得一提的是 iter() 方法,它是 Python 的内置方法,其作用是将可迭代对象变成迭代器 。这句话可以解析出两层意思:(1)可迭代对象跟迭代器是两种东西;(2)可迭代对象能变成迭代器。实际上,迭代器必然是可迭代对象,但可迭代对象不一定是迭代器。两者有多大的区别呢?如上图蓝圈所示,普通可迭代对象与迭代器的最关键区别可概括为:一同两不同 ,所谓“一同”,即两者都是可迭代的(__iter__),所谓“两不同”,即可迭代对象在转化为迭代器后,它会丢失一些属性(__getitem__),同时也增加一些属性(__next__)。首先看看增加的属性 __next__ , 它是迭代器之所以是迭代器的关键,事实上,我们正是把同时实现了 __iter__ 方法 和 __next__ 方法的对象定义为迭代器的。有了多出来的这个属性,可迭代对象不需要借助外部的 for 循环语法,就能实现自我的迭代/遍历过程。我发明了两个概念来描述这两种遍历过程(PS:为了易理解,这里称遍历,实际也可称为迭代):它遍历 指的是通过外部语法而实现的遍历,自遍历 指的是通过自身方法实现的遍历。借助这两个概念,我们说,可迭代对象就是能被“它遍历”的对象,而迭代器是在此基础上,还能做到“自遍历”的对象。ob1 = "abc" ob2 = iter("abc") ob3 = iter("abc") # ob1它遍历 for i in ob1: print(i, end = " ") # a b c for i in ob1: print(i, end = " ") # a b c # ob1自遍历 ob1.__next__() # 报错: 'str' object has no attribute '__next__' # ob2它遍历 for i in ob2: print(i, end = " ") # a b c for i in ob2: print(i, end = " ") # 无输出 # ob2自遍历 ob2.__next__() # 报错:StopIteration # ob3自遍历 ob3.__next__() # a ob3.__next__() # b ob3.__next__() # c ob3.__next__() # 报错:StopIteration 复制代码通过上述例子可看出,迭代器的优势在于支持自遍历,同时,它的特点是单向非循环的,一旦完成遍历,再次调用就会报错。对此,我想到一个比方:普通可迭代对象就像是子弹匣,它遍历就是取出子弹,在完成操作后又装回去,所以可以反复遍历(即多次调用for循环,返回相同结果);而迭代器就像是装载了子弹匣且不可拆卸的枪,进行它遍历或者自遍历都是发射子弹,这是消耗性的遍历,是无法复用的(即遍历会有尽头)。写了这么多,稍微小结一下:迭代是一种遍历元素的方式,按照实现方式划分,有外部迭代与内部迭代两种,支持外部迭代(它遍历)的对象就是可迭代对象,而同时还支持内部迭代(自遍历)的对象就是迭代器;按照消费方式划分,可分为复用型迭代与一次性迭代,普通可迭代对象是复用型的,而迭代器是一次性的。4.2、迭代器切片前面提到了“一同两不同”,最后的不同是,普通可迭代对象在转化成迭代器的过程中会丢失一些属性,其中关键的属性是 __getitem__ 。在前一节中,我已经介绍了这个魔术方法,并用它实现了自定义对象的切片特性。那么问题来了:为什么迭代器不继承这个属性呢?首先,迭代器使用的是消耗型的遍历,这意味着它充满不确定性,即其长度与索引键值对是动态衰减的,所以很难 get 到它的 item ,也就不再需要 __getitem__ 属性了。其次,若强行给迭代器加上这个属性,这并不合理,正所谓强扭的瓜不甜......由此,新的问题来了:既然会丢失这么重要的属性(还包括其它未标识的属性),为什么还要使用迭代器呢?这个问题的答案在于,迭代器拥有不可替代的强大的有用的功能,使得 Python 要如此设计它。限于篇幅,此处不再展开,后续我会专门填坑此话题。还没完,死缠烂打的问题来了:能否令迭代器拥有这个属性呢,即令迭代器继续支持切片呢?hi = "欢迎关注公众号:Python猫" it = iter(hi) # 普通切片 hi[-7:] # Python猫 # 反例:迭代器切片 it[-7:] # 报错:'str_iterator' object is not subscriptable 复制代码迭代器因为缺少__getitem__ ,因此不能使用普通的切片语法。想要实现切片,无非两种思路:一是自己造轮子,写实现的逻辑;二是找到封装好的轮子。Python 的 itertools 模块就是我们要找的轮子,用它提供的方法可轻松实现迭代器切片。import itertools # 例1:简易迭代器 s = iter("123456789") for x in itertools.islice(s, 2, 6): print(x, end = " ") # 输出:3 4 5 6 for x in itertools.islice(s, 2, 6): print(x, end = " ") # 输出:9 # 例2:斐波那契数列迭代器 class Fib(): def __init__(self): self.a, self.b = 1, 1 def __iter__(self): while True: yield self.a self.a, self.b = self.b, self.a + self.b f = iter(Fib()) for x in itertools.islice(f, 2, 6): print(x, end = " ") # 输出:2 3 5 8 for x in itertools.islice(f, 2, 6): print(x, end = " ") # 输出:34 55 89 144 复制代码itertools 模块的 islice() 方法将迭代器与切片完美结合,终于回答了前面的问题。然而,迭代器切片跟普通切片相比,前者有很多局限性。首先,这个方法不是“纯函数”(纯函数需遵守“相同输入得到相同输出”的原则);其次,它只支持正向切片,且不支持负数索引,这都是由迭代器的损耗性所决定的。那么,我不禁要问:itertools 模块的切片方法用了什么实现逻辑呢?下方是官网提供的源码:def islice(iterable, *args): # islice('ABCDEFG', 2) --> A B # islice('ABCDEFG', 2, 4) --> C D # islice('ABCDEFG', 2, None) --> C D E F G # islice('ABCDEFG', 0, None, 2) --> A C E G s = slice(*args) # 索引区间是[0,sys.maxsize],默认步长是1 start, stop, step = s.start or 0, s.stop or sys.maxsize, s.step or 1 it = iter(range(start, stop, step)) try: nexti = next(it) except StopIteration: # Consume *iterable* up to the *start* position. for i, element in zip(range(start), iterable): pass return try: for i, element in enumerate(iterable): if i == nexti: yield element nexti = next(it) except StopIteration: # Consume to *stop*. for i, element in zip(range(i + 1, stop), iterable): pass 复制代码islice() 方法的索引方向是受限的,但它也提供了一种可能性:即允许你对一个无穷的(在系统支持范围内)迭代器进行切片的能力。这是迭代器切片最具想象力的用途场景。除此之外,迭代器切片还有一个很实在的应用场景:读取文件对象中给定行数范围的数据。我们知道,从文件中读取内容主要有两种方法(参见之前关于文件读写的文章):read() 适合读取内容较少的情况,或者是需要一次性处理全部内容的情况;而 readlines() 适用性更广,因为它是迭代地读取内容,既减少内存压力,又方便逐行对数据处理。虽然 readlines() 有迭代读取的优势,但它是从头到尾逐行读取,若文件有几千行,而我们只想要读取少数特定行(例如第1000-1009行),那它还是效率太低了。考虑到文件对象天然就是迭代器 ,我们可以使用迭代器切片先行截取,然后再处理,如此效率将大大地提升。# test.txt 文件内容 ''' 猫 Python猫 python is a cat. this is the end. ''' from itertools import islice with open('test.txt','r',encoding='utf-8') as f: print(hasattr(f, "__next__")) # 判断是否迭代器 content = islice(f, 2, 4) for line in content: print(line.strip()) ### 输出结果: True python is a cat. this is the end. 复制代码本节内容较多,简单回顾一下:迭代器是一种特殊的可迭代对象,可用于它遍历与自遍历,但遍历过程是损耗型的,不具备循环复用性,因此,迭代器本身不支持切片操作;通过借助 itertools 模块,我们能实现迭代器切片,将两者的优势相结合,其主要用途在于截取大型迭代器(如无限数列、超大文件等等)的片段,实现精准的处理,从而大大地提升性能与效率。5、小结最后总结一下,切片是 Python 的一种高级特性,常用于截取序列类型的元素,但并不局限于此,本文主要介绍了它的基础用法、高级用法(如占位符用法)、自定义对象切片、以及迭代器切片等使用内容。除此之外,切片还有更广阔多样的使用场景,例如 Numpy 的多维切片、内存视图切片、异步迭代器切片等等,都值得我们去探索一番切片系列(原单篇):Python进阶:切片的误区与高级用法Python进阶:自定义对象实现切片功能Python进阶:迭代器与迭代器切片相关链接:官方文档getitem用法:t.cn/EbzoZyp切片赋值的源码分析:t.cn/EbzSaoZ官网itertools模块介绍:t.cn/EbNc0otPython是否支持复制字符串呢?来自Kenneth Reitz大神的建议:避免不必要的面向对象编程给Python学习者的文件读写指南(含基础与进阶,建议收藏)详解Python拼接字符串的七种方式
今天,我继续跟大家聊聊 Python 中跟身份密切相关的一个话题吧,那就是对象的边界问题 。如你所知,我本来是一只猫,现在略具一些人性了,但在此转型期间却十分敏感,总能在细微之处浮想联翩,最后竟然也薄有所获,真是万幸了。希望我的分享,也能启发你收获哪怕一点点的感悟,那我就有万分的开心啦 :)1、固定边界:自由与孤独Python 中有一些公民向来我行我素,它们特立独行,与他人之边界划定得清清楚楚。客气的人称它们是定长对象,或者叫不可变对象,然而,懂得一些历史典故的人又叫它们是铁公鸡 。这个典故出自何处呢?亏得猫猫我曾恶补过一段历史知识,知道这指的正是激进的道家弟子杨朱。损一毫利天下,不与也;悉天下奉一身,不取也;人人不损一毫,人人不利天下,天下治矣! ——春秋·杨朱对于定长对象,你不能为它增加元素,不能为它减少元素,不能为它修改元素,甚至不能轻易地复制和删除它!(参见本公众号Python猫中关于字符串的系列文章,链接见文末)这些对象自立于世,也自绝于世,你看它们长得是普普通通的,平平凡凡的,然而其灵魂却是自由自在的,其生命是富有尊严而不可侵犯的。若想与这些公民打交道,你就得依着它们的脾气,不可越雷池半步。>>> t1 = ('Python', '猫') >>> t2 = ('Python', '猫') >>> t1 is t2 # 对象独立 False >>> t1[1] = '蛇' # 不可修改元素 TypeError Traceback (most recent call last) TypeError: 'tuple' object does not support item assignment 复制代码在上一篇文章里,我们见识了 Python 世界中的“特权种族”,而特权种族无一例外地都出身于定长对象。它们是一脉相承的,其存在的合理性也是相似的,那就是便于共用内存资源,提高内存使用效率。上表就是定长对象的一份名单。可知,它们占据了多数。定长对象的特性让我不由地想到一种人类,它们严守自己的边界,刻板而严谨,一心只在乎份内之事,默默承担下自己的责任,追求的是内在的自由。虽然也会时常与别人打交道,但是,它们不贪图扩大自己的利益,也不妄想要侵犯别人的领土。独立的个体养成了个人的品牌,它们的不变性成就了外人能有所依赖的确定性。>>> key1 = 'Python 猫' >>> key2 = ['someone else'] >>> dict1 = {key1 : '好人'} {'Python 猫': '好人'} >>> dict2 = {key2 : '好人'} TypeError Traceback (most recent call last) TypeError: unhashable type: 'list' 复制代码Python 为了维护定长对象的独立性/确定性,在编译机制上做了不少优化,例如 Intern 机制与常量合并机制。其中的好处,我已经多次提及了。坏处也有,那就是孤独。它们的孤独不在于没有同类,而在于不能(不容易)复制自身。以字符串对象为例,你可以尝试多种多样的手段,然而到头来,却发现唯一通用的方法竟然要先把字符串“碎尸万段”,接着重新组装才行!s0 = "Python猫" # 以下7种方法,无法复制s0字符串,id(x)==id(s0) s1 = s0 s2 = str(s0) s3 = s0[:] s4 = s0 + '' s5 = '%s' % s0 s6 = s0 * 1 import copy s7 = copy.copy(s0) # 以下方法可以复制字符串,“打碎”再重组 s8 = "".join(s0) 复制代码哲学上有一个著名的脑洞题:假如把一个人粉碎成原子再组合,这个人还是原来的人么?这道题能令古往今来的哲学家打起架来,若是放到现今正火爆的电视节目《奇葩说》上,也能令辩手们“一本正经地胡说八道”个不休。在 Python 的世界里,不存在这种烦恼,因为判定两个对象是否相同的标准是确定的,也即是看它们的 id 是否相等。因此,借助 Python 来回答这道题,答案会是:如果用 join() 方法把字符串粉碎成字符再组合,新的字符串不再是原来的字符串了。过程很“残忍”,但总归能稍稍释缓自由个体的孤独感了吧。2、弹性边界:开放与节制与定长对象不同,变长对象/可变对象信奉的是另一套哲学。它们思想开放,采取的是兼容并包的处事观,会因地制宜式伸缩边界。 以列表对象为例,它乐意接纳所有其它的对象,肯花费精力去动态规划,也不惧于拔掉身上所有的“毛”。>>> l = ['Python', '猫'] >>> l.append('其它猫') # ['Python','猫','其它猫'] >>> l.pop(1) # ['Python','其它猫'] >>> l.clear() # [] 复制代码这些大胆的行为,在定长对象那里,都是不可想象的。在变长对象身上,你似乎能感受到一种海纳百川的风范,相比之下,定长对象的铁公鸡形象则立马显得格局忒小了。变长对象并非没有边界,相反,它们更在乎自身的边界,不惜花费大量的资源来维持动态的稳定。一旦边界确定下来,它们绝不会允许越界行为。跟某些编程语言动不动就数组越界不同,Python 不存在切片越界,因为切片操作始终被控制为边界范围之内,索引超出的部分会自动被舍弃。>>> q=[1, 2, 3, 4, 5] # 不允许索引越界 >>> q[10] IndexError Traceback (most recent call last) IndexError: list index out of range # 允许切片越界 >>> q[2:10] # [3, 4, 5] >>> q[-10:2] # [1, 2] 复制代码变长对象在本质上是一种可伸缩的容器,其主要好处就是支持不断添加或者取出元素。对应到计算机硬件层面,就是不断申请或者释放内存空间。这类操作是代价昂贵的操作,为了减少开销,Python 聪明地设计了一套分配超额空间的机制。以列表为例,在内存足够的前提下,最初创建列表时不分配超额空间,第一次 append() 扩充列表时,Python 会根据下列公式分配超额空间,即分配大于列表实际元素个数的内存空间,此后,每次扩充操作先看是否有超额空间,有则直接使用,没有则重新计算,再次分配一个超额空间。公式如下:new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6)其中,new_allocated 指的是超额分配的内存大小,newsize 是扩充元素后的实际长度。举例来说,一个长度为 4 的列表,append() 增加一个元素,此时实际长度为 5(即 newsize 为5),但是,Python 不会只给它分配 5 个内存空间,而是计算后给它超额分配 new_allocated == 3 个内存大小,所以最终加起来,该列表的元素实际占用的内存空间就是 8 。如此一来,当列表再次扩充时,只要最终长度不大于 8 ,就不需要再申请新的内存空间。当扩充后长度等于 9 时,new_allocated 等于 7 ,即额外获得 7 个内存大小,以此类推。以列表长度为横轴,以超额分配的内存大小为纵轴,我们就得到了如下美妙的图表:超额分配的空间就是定长对象的软边界 ,这意味着它们在扩张时是有法度的,意味着它们在发展时是有大胆计划与适度节制的。如此看来,与定长对象的“固步自封”相比,变长对象就显得既开明又理智了。3、结语回头看前面提到的定长对象,我佩服它们独善其身的个性,虽然铁公鸡形象略显小气,但对人却无害,反而你能感受到其浓浓的 “富贵不能淫,贫贱不能移,威武不能屈” 的大丈夫气度。再看变长对象,它们“本来无一物”,却能包容万物,对他人信任,对外部开放,更难得的是,它们张弛有度,孕生出的是无限的可能性。Python猫往期作品 :有了Python,我能叫出所有猫的名字Python对象的身份迷思:从全体公民到万物皆数字符串系列文章 :你真的知道Python的字符串是什么吗?你真的知道Python的字符串怎么用吗?Python是否支持复制字符串呢?join()方法的神奇用处与Intern机制的软肋
上篇文章《Python是否支持复制字符串呢?》刚发出一会,@发条橙 同学就在后台留言,指出了一处错误。我一惊,马上去验证,竟然真的错了,而且在完全没意料到的地方!我开始以为只是疏漏,一细想,发现不简单,遇到了百思不得其解的问题了。所以,这篇文章还得再聊聊字符串。照例先总结下本文内容:(1)join() 方法除了在拼接字符串时速度较快,它还是目前看来最通用有效的复制字符串的方法 (2)Intern 机制(字符串滞留)并非万能的,本文探索一下它的软肋有哪些1. join()方法不止是拼接我先把那个问题化简一下吧:ss0 = 'hi' ss1 = 'h' + 'i' ss2 = ''.join(ss0) print(ss0 == ss1 == ss2) >>> True print(id(ss0) == id(ss1)) >>> True print(id(ss0) == id(ss2)) >>> False 复制代码上面代码中,奇怪的地方就在于 ss2 竟然是一个独立的对象!按照最初想当然的认知,我认定它会被 Intern 机制处理掉,所以是不会占用独立内存的。上篇文章快写完的时候,我突然想到 join 方法,所以没做验证就临时加进去,导致了意外的发生。按照之前在“特权种族”那篇文章的总结,我对字符串 Intern 机制有这样的认识:Python中,字符串使用Intern机制实现内存地址共用,长度不超过20,且仅包括下划线、数字、字母的字符串才会被intern;涉及字符串拼接时,编译期优化结果会与运行期计算结果不同。为什么 join 方法拼接字符串时,可以不受 Intern 机制作用呢?回看那篇文章,发现可能存在编译期与运行期的差别!# 编译对字符串拼接的影响 s1 = "hell" s2 = "hello" "hell" + "o" is s2 >>>True s1 + "o" is s2 >>>False # "hell" + "o"在编译时变成了"hello", # 而s1+"o"因为s1是一个变量,在运行时才拼接,所以没有被intern 复制代码实验一下,看看:# 代码加上 ss3 = ''.join('hi') print(id(ss0) == id(ss3)) >>> False 复制代码ss3 仍然是独立对象,难道这种写法还是在运行期时拼接?那怎么判断某种写法在编译期还是在运行期起作用呢?继续实验:s0 = "Python猫" import copy s1 = copy.copy(s0) s2 = copy.copy("Python猫") print(id(s0) == id(s1)) >>> True print(id(s0) == id(s2)) >>> False 复制代码看来,不能通过是否显性传值来判断。那就只能从 join 方法的实现原理入手查看了。经某交流群的小伙伴提醒,可以去 Python Tutor 网站,看看可视化执行过程。但是,很遗憾,也没看出什么底层机制。我找了分析 CPython 源码的资料(含上期荐书栏目的《Python源码剖析》)来学习,但是,这些资料只比较 join() 方法与 + 号拼接法在原理与使用内存上的差异,并没提及为何 Intern 机制对前者会失效,而对后者却是生效的。现象已经产生,我只能暂时解释说,join 方法会不受 Intern 机制控制,它有独享内存的“特权”。那就是说,其实有复制字符串的方法!上篇《Python是否支持复制字符串呢?》由于没有发现这点,最后得出了错误的结论!由于这个特例,我要修改上篇文章的结论了:Python 本身并不限制字符串的复制操作,CPython 解释器出于优化性能的考虑,加入了一些小把戏,试图使字符串对象在内存中只有一份,尽管如此,仍存在有效复制字符串的方法,那就是 join() 方法。2. Intern 机制失效的情况join() 方法的神奇用处使我不得不改变对 Intern 机制的认识,本小节就带大家重新学习一下 Intern 机制吧。所谓 Intern 机制,即字符串滞留(string interning),它通过维护一个字符串常量池(string intern pool),从而试图只保存唯一的字符串对象,达到既高效又节省内存地处理字符串的目的。在创建一个新的字符串对象后,Python 先比较常量池中是否有相同的对象(interned),有的话则将指针指向已有对象,并减少新对象的指针,新对象由于没有引用计数,就会被垃圾回收机制回收掉,释放出内存。Intern 机制不会减少新对象的创建与销毁,但最终会节省出内存。这种机制还有另一个好处,即被 Interned 的相同字符串作比较时,几乎不花时间。实验数据如下(资料来源:t.cn/ELu9n7R):Intern 机制的大致原理很好理解,然而影响结果的还有 CPython 解释器的其它编译及运行机制,字符串对象受到这些机制的共同影响。实际上,只有那些“看起来像” Python 标识符的字符串才会被处理。源代码StringObject.h的注释中写道:/* … … This is generally restricted to strings that “looklike” Python identifiers, although the intern() builtin can be used to force interning of any string … … */这些机制的相互作用,不经意间带来了不少混乱的现象:# 长度超过20,不被intern VS 被intern 'a' * 21 is 'aaaaaaaaaaaaaaaaaaaaa' >>> False 'aaaaaaaaaaaaaaaaaaaaa' is 'aaaaaaaaaaaaaaaaaaaaa' >>> True # 长度不超过20,不被intern VS 被intern s = 'a' s * 5 is 'aaaaa' >>> False 'a' * 5 is 'aaaaa' >>> True # join方法,不被intern VS 被intern ''.join('hi') is 'hi' >>> False ''.join('h') is 'h' >>> True # 特殊符号,不被intern VS 被"intern" 'python!' is 'python!' >>> False a, b = 'python!', 'python!' a is b >>> True 复制代码这些现象当然都能被合理解释,然而由于不同机制的混合作用,就很容易造成误会。比如第一个例子,很多介绍 Intern 机制的文章在比较出 'a' * 21 的id有变化后,就认为 Intern 机制只对长度不超过20的字符串生效,可是,当看到长度超过20的字符串的id还相等时,这个结论就变错误了。当加入常量合并(Constant folding) 的机制后,长度不超过20的字符串会被合并的现象才得到解释。可是,在 CPython 的源码中,只有长度不超过1字节的字符串才会被 intern ,为何长度超标的情况也出现了呢? 再加入 CPython 的编译优化机制,才能解释。所以,看似被 intern 的两个字符串,实际可能不是 Intern 机制的结果,而是其它机制的结果。同样地,看似不能被 intern 的两个字符串,实际可能被其它机制以类似方式处理了。如此种种,便提高了理解 Intern 机制的难度。就我在上篇文章中所关心的“复制字符串”话题而言,只有当 Intern 机制与其它这些机制统统失效时,才能做到复制字符串。目前看来,join 方法最具通用性。3. 学习的方法论总而言之,因为重新学习 join 方法的神奇用处与 Intern 机制的例外情况,我得以修正上篇文章的错误。在此过程中,我得到了新的知识,以及思考学习的乐趣。《超人》电影中有一句著名的台词,在今年上映的《头号玩家》中也出现了:有的人从《战争与和平》里看到的只是一个普通的冒险故事,有的人则能通过阅读口香糖包装纸上的成分表来解开宇宙的奥秘。我读到的是一种敏锐思辨的思想、孜孜求索的态度和以小窥大的方法。作为一个低天赋的人,受此鼓舞,我会继续追问那些看似没意义的问题(“如何删除字符串”、“如何复制字符串”...),一点一点地学习 Python ,以我的方式理解它。同时,希望能给我的读者们带来一些收获。字符串系列文章:详解Python拼接字符串的七种方式你真的知道Python的字符串是什么吗?你真的知道Python的字符串怎么用吗?Python是否支持复制字符串呢?Python猫系列:有了Python,我能叫出所有猫的名字Python对象的身份迷思:从全体公民到万物皆数
连续几篇文章都在写 Python 字符串,这出乎我的意料了。但是,有的问题,不写不行,特别是那种灵机一动想到的问题,最后你发现,很多人根本不懂却又误以为自己懂了。那就继续刨根问底,探究个明白吧。在上一篇文章《你真的知道Python的字符串怎么用吗?》里,我突发奇想,将字符串跟列表做了比较,然后发现字符串竟然没有复制的方法。当时没有细想,只说要搁置疑问。过后,有好学的小伙伴在后台留言,与我交流这个问题,给了我一些启发。为了彻底弄懂它,我继续查了不少资料,今天,就跟大家分享一下我发现的东西吧。本文标题的问题分为两部分:(1)Python 中是否支持复制字符串?(2)如果不支持,为什么不支持?请读者花几分钟想一下,想清楚后,把你的答案记住,然后再往下看。让我们做一个约定(自愿遵守):如果看到最后,你推翻了现在的答案,建立了新的认知,这说明我写的内容有用,那请你任意赞赏,或者将本文分享给其他使用 Python 的小伙伴。1. 什么是复制字符串?首先,必须要大家对“复制”这个概念达成共识。复制,也叫拷贝,英文单词是 copy,具体意思是“将某事物通过某种方式制作成相同的一份或多份的行为”(释义来自维基百科)。复制的结果是,出现了多份极其相似但却相互独立的事物(副本),举例来说,你有一份文档 X,然后复制一份并重新命名为 Y,这两者是相互独立的,若你删除其中一个,另一个不会一起被删除。这个词用在 Python 里,我们想表达的是同样的意思,即复制行为会产生新的独立对象,它与原始对象极其相似,但两者的生命周期没有直接的关联关系。下面先用列表来举例:list1 = [1,2] id(list1) >>> 1981119454856 list2 = list1.copy() print(list1 == list2) >>> True id(list2) >>> 1981116983752 复制代码上例中,列表 list2 是 list1 的副本,两者字面量相等,但是内存地址(即 id )不相等,是两个相互独立的对象。如果字符串能够做到同样的效果,那我们就说,字符串可以被复制,否则,我们说字符串不可以被复制。2. 怎样能复制字符串?有了上面的概念和示例,请先思考,你会用什么方式复制字符串呢?(暂停,思考3分钟)好了,先看看下面的几种方法:s0 = "Python猫" s1 = s0 s2 = str(s0) s3 = s0[:] s4 = s0 + '' s5 = '%s' % s0 s6 = s0 * 1 s7 = "".join(s0) import copy s8 = copy.copy(s0) 复制代码你想到的复制方式是否在以上8种方式里呢?那么,如果把 s0 至 s8 的 id 打印出来,有哪些会跟 s0 不同呢?答案是,它们的内存地址 id 完全相同,也就是说,一顿操作猛如虎,结果却始终只有一份字符串,根本没有复制出新的字符串!Python猫 的老读者看到这,会心一笑,这不就是因为字符串的 Intern 机制嘛,短字符串在内存中只会存在一份,在《Python中的“特权种族”是什么?》这篇文章里提到过的。但请别开心得太早,你可以把 s0 改成一个超长的字符串,例如:s0 = "Python猫是来自喵星的客人,它喜欢地球和人类,正在学习Python,而且想借助Python变成人,它的微信公众号也叫Python猫,欢迎你关注哦,喵喵喵喵~~~~~~~"然后,再重复上面的操作。最终,你会发现,s0 到 s8 的 id 还是完全相同。是不是吃惊了呢?新的 s0 明明已经超过 Intern 机制的长度了,为什么不会产生新的字符串呢?首先,请你相信,超出 Intern 机制的字符串可以存在多份,即你可以创建出值完全相同的多个字符串对象,因为字符串对象在内存中并不一定是唯一的:s9 = "Python猫是来自喵星的客人,它喜欢地球和人类,正在学习Python,而且想借助Python变成人,它的微信公众号也叫Python猫,欢迎你关注哦,喵喵喵喵~~~" print(id(s0) == id(s9)) >>> False 复制代码上例表明,你可以创建出多个相同的字符串对象,但是这种方法与前面列举的8种不同,因为它是独立于 s0 的操作,并不是一种复制操作。从理论上讲,Python 完全可以提供一个方法,达到复制出新的副本的结果。现在的问题恰恰就是:为什么允许存在多个相等的字符串对象,但是却无法通过复制的方式来创建呢?3. 为什么不允许复制字符串?我发现,不仅字符串不允许复制,元祖也如此,事实上,还有 int 、float 也不支持复制。它们都是不可变对象,为什么不可变对象就不支持复制操作呢?在查资料的时候,我发现网上很多文章对于“不可变对象”的认识存在误区,这些人不知道 Intern 机制的存在,误以为字符串对象在内存只能有唯一一个,进而误以为不可变对象就是在内存中只有一份的对象。所以,这些文章很容易推断出错误的结论:因为字符串是不可变对象,所以字符串不支持复制。事实上,不可变对象跟复制操作之间,并没有必然的强相关的关系。肯定是出于别的原因,设计者才给不可变对象加上这种限制,这个原因是什么呢?在知乎上,有敏锐的同学提出了我的疑问“Python中如何复制一个值或字符串?”,可惜只有4个回答,而且都没答到点上。Stackoverflow上恰好也有一个问题“How can I copy a Python string?”,同样没多少人注意到,只有5个回答,好在最高票答案提到了一个点,即这样可以加快字典的查找速度。然而,他说的这个点并不靠谱。字典要求键值是可哈希对象,可是计算字符串的哈希值是根据字面值计算,所以对多个相等的字符串对象,其哈希值其实是一样的,对计算和查找根本无影响。w1 = "Python猫是来自喵星的客人,它喜欢地球和人类,正在学习Python,而且想借助Python变成人,它的微信公众号也叫Python猫,欢迎你关注哦,喵喵喵喵~~~" w2 = "Python猫是来自喵星的客人,它喜欢地球和人类,正在学习Python,而且想借助Python变成人,它的微信公众号也叫Python猫,欢迎你关注哦,喵喵喵喵~~~" print(w1 == w2) >>> True print(id(w1) == id(w2)) >>> False print(hash(w1) == hash(w2)) >>> True 复制代码继续查资料,终于在《流畅的Python》找到了明确的解释:这些细节是 CPython 核心开发者走的捷径和做的优化措施,对这门语言的用户而言无需了解,而且那些细节对其他 Python 实现可能没用,CPython 未来的版本可能也不会用。这本《流畅的Python》是进阶首选书目之一,我曾读过部分章节,没想到在一个不起眼的小节里,作者 “惊讶地发现” 元祖的不可复制性,在此之前,他还自以为“对元祖无所不知”,哈哈哈。虽然,我早猜测到原因是节省内存和提高速度,但看到这个明确的解释,知道这只是CPython 解释器的“善意的谎言”,而且在未来版本可能不会用,我感到特别意外。它证实了我的猜测,同时,也提供了超预期的信息:其它 Python 解释器可能支持复制不可变对象,目前 CPython 算是一种妥协,在未来可能会恢复不可变对象的复制操作呢!回到文章开头的两个问题,我们得到的答案是:Python 本身并不限制字符串的复制操作,只是当前版本的 CPython 做了优化,才导致出现这种“善意的谎言”,它这么做的原因为了对 Intern 机制做补充,设法使全部字符串对象在内存都只有一份,以达到节省内存的效果。CPython 是用 C 语言实现的 Python 解释器,是官方的、使用最广泛的解释器。除了它,还有用 Java 实现的 Jython 解释器、用 .NET 实现的 IronPython 解释器、用 Python 实现的 PyPy 解释器,等等。其它解释器都是怎么应对字符串的复制操作的呢?唉,学无止境,本人才疏学浅没有涉猎,还是先搁置疑问吧。这里,我就想提一个题外话,Python 最最最广为人诟病的就是 GIL(全局解释器锁),这导致它不支持真正意义的多线程,成为很多人指责 Python 慢的元凶。但是,这个问题是 CPython 解释器带来的,而像 Jython 解释器就不存在这个问题。参考学习:《流畅的Python》www.zhihu.com/question/41…dwz.cn/4o0WXy8G
几乎任何一种编程语言,都把字符串列为最基础和不可或缺的数据类型。而拼接字符串是必备的一种技能。今天,我跟大家一起来学习Python拼接字符串的七种方式。1、来自C语言的%方式print('%s %s' % ('Hello', 'world'))复制代码>>> Hello world%号格式化字符串的方式继承自古老的C语言,这在很多编程语言都有类似的实现。上例的%s是一个占位符,它仅代表一段字符串,并不是拼接的实际内容。实际的拼接内容在一个单独的%号后面,放在一个元组里。类似的占位符还有:%d(代表一个整数)、%f(代表一个浮点数)、%x(代表一个16进制数),等等。%占位符既是这种拼接方式的特点,同时也是其限制,因为每种占位符都有特定意义,实际使用起来太麻烦了。2、format()拼接方式# 简洁版复制代码s1 = 'Hello {}! My name is {}.'.format('World', 'Python猫')print(s1)>>>Hello World! My name is Python猫.# 对号入座版s2 = 'Hello {0}! My name is {1}.'.format('World', 'Python猫')s3 = 'Hello {name1}! My name is {name2}.'.format(name1='World', name2='Python猫')print(s2)>>>Hello World! My name is Python猫.print(s3)>>>Hello World! My name is Python猫.这种方式使用花括号{}做占位符,在format方法中再转入实际的拼接值。容易看出,它实际上是对%号拼接方式的改进。这种方式在Python2.6中开始引入。上例中,简洁版的花括号中无内容,缺点是容易弄错次序。对号入座版主要有两种,一种传入序列号,一种则使用key-value的方式。实战中,我们更推荐后一种,既不会数错次序,又更直观可读。3、() 类似元组方式s_tuple = ('Hello', ' ', 'world')复制代码s_like_tuple = ('Hello' ' ' 'world')print(s_tuple) >>>('Hello', ' ', 'world')print(s_like_tuple) >>>Hello worldtype(s_like_tuple) >>>str注意,上例中s_like_tuple并不是一个元组,因为元素间没有逗号分隔符,这些元素间可以用空格间隔,也可以不要空格。使用type()查看,发现它就是一个str类型。我没查到这是啥原因,猜测或许()括号中的内容是被Python优化处理了。这种方式看起来很快捷,但是,括号()内要求元素是真实字符串,不能混用变量,所以不够灵活。# 多元素时,不支持有变量复制代码str_1 = 'Hello'str_2 = (str_1 'world')>>> SyntaxError: invalid syntaxstr_3 = (str_1 str_1)>>> SyntaxError: invalid syntax# 但是下面写法不会报错str_4 = (str_1)4、面向对象模板拼接from string import 复制代码Templates = Template('${s1} ${s2}!') print(s.safe_substitute(s1='Hello',s2='world')) >>> Hello world!说实话,我不喜欢这种实现方式。浓浓的一股被面向对象思想毒害的臭味。就不多说了。5、常用的+号方式str_1 = 'Hello world! '复制代码 str_2 = 'My name is Python猫.'print(str_1 + str_2)>>>Hello world! My name is Python猫.print(str_1)>>>Hello world! 这种方式最常用、直观、易懂,是入门级的实现方式。但是,它也存在两处让人容易犯错的地方。首先,新入门编程的同学容易犯错,他们不知道字符串是不可变类型,新的字符串会独占一块新的内存,而原来的字符串保持不变。上例中,拼接前有两段字符串,拼接后实际有三段字符串。其次,一些有经验的老程序员也容易犯错,他们以为当拼接次数不超过3时,使用+号连接符就会比其它方式快(ps:不少Python教程都是如此建议),但这没有任何合理根据。事实上,在拼接短的字面值时,由于CPython中的 常数折叠 (constant folding)功能,这些字面值会被转换成更短的形式,例如'a'+'b'+'c' 被转换成'abc','hello'+'world'也会被转换成'hello world'。这种转换是在编译期完成的,而到了运行期时就不会再发生任何拼接操作,因此会加快整体计算的速度。常数折叠优化有一个限度,它要求拼接结果的长度不超过20。所以,当拼接的最终字符串长度不超过20时,+号操作符的方式,会比后面提到的join等方式快得多,这与+号的使用次数无关。题外话:你是否觉得20这个数字很熟悉呢?没错,我们之前在《Python中的“特权种族”是什么?》中提到过,字符串类的特权种族也是以20为限。当时也有一个例子,展示了编译期和运行期的区别,建议你去回看。6、join()拼接方式str_list = ['Hello', 'world']str_join1 = ' '.join(str_list)str_join2 = '-'.join(str_list)复制代码print(str_join1) >>>Hello worldprint(str_join2) >>>Hello-worldstr对象自带的join()方法,接受一个序列参数,可以实现拼接。拼接时,元素若不是字符串,需要先转换一下。可以看出,这种方法比较适用于连接序列对象中(例如列表)的元素,并设置统一的间隔符。当拼接长度超过20时,这种方式基本上是首选。不过,它的缺点就是,不适合进行零散片段的、不处于序列集合的元素拼接。7、f-string方式name = 'world'复制代码myname = 'python_cat'words = f'Hello {name}. My name is {myname}.'print(words)>>> Hello world. My name is python_cat.f-string方式出自PEP 498(Literal String Interpolation,字面字符串插值),从Python3.6版本引入。其特点是在字符串前加 f 标识,字符串中间则用花括号{}包裹其它字符串变量。这种方式在可读性上秒杀format()方式,处理长字符串的拼接时,速度与join()方法相当。尽管如此,这种方式与其它某些编程语言相比,还是欠优雅,因为它引入了一个 f 标识。而其它某些程序语言可以更简练,比如shell:name="world"复制代码myname="python_cat"words="Hello ${name}. My name is ${myname}."echo $words>>>Hello world. My name is python_cat.总结一下,我们前面说的“字符串拼接”,其实是从结果上理解。若从实现原理上划分的话,我们可以将这些方法划分出三种类型:格式化类:%、format()、template拼接类:+、()、join()插值类:f-string当要处理字符串列表等序列结构时,采用join()方式;拼接长度不超过20时,选用+号操作符方式;长度超过20的情况,高版本选用f-string,低版本时看情况使用format()或join()方式。
不学习使我心慌,今天优雅的本喵带大家充充电,学学Python中操纵JSON的知识。学完本文,你可以学到如下内容:1、JSON是什么?2、JSON与XML的优劣差异?3、将Python对象编码成JSON字符串4、将JSON字符串解码为Python对象5、解决JSON中文乱码问题JSON是什么?JSON的全称是 JavaScript Object Notation,是一种轻量级的数据交换格式。最初,JSON 只是 JavaScript 的子集,但由于其简单易用而迅速走红。现今大部分编程语言都支持对JSON的解析与生成,而近些年异军突起的NoSQL数据库也多参照JSON来设计数据存储格式,例如Mongodb的BSON(Binary JSON)。JSON有以下六种数据类型:number、boolean、string、null、array、object。前三种很好理解,第四个null对应Python的None,最后两种,对应Python的列表和字典。1{ 2 "name": "小明", 3 "age": 14, 4 "gender": true, 5 "grade": null, 6 "skills": [ 7 "JavaScript", 8 "Java", 9 "Python"10 ]11}复制代码JSON与XML的优劣差异?在JSON出现之前,人们用XML在网络上交换数据,在JSON出现后,它基本上就取代了XML的位置。两者的共同之处显而易见,它们都是结构化的语言,都可以用于网络数据的交换。两者最大的差异在于它们的“出身”不同,也就是它们被创造的目的不同。XML是W3C(万维网联盟)发布的可扩展标记语言(Extensible Markup Language),最初设计来弥补HTML的不足,以强大的扩展性满足网络信息发布的需要,与它“同级”的有:XHTML\CSS\ECMAScript等。它包含DTD、XSD、XPath、XSL等一大堆复杂的规范,在数据存储、扩展及高级检索等方面都有作用。后来被用于网络数据交换,颇有点大材小用的意思,虽然可胜任,却也有点复杂和冗余。而JSON是ECMAScript标准的子集,设计之初就是为了克服XML在数据交换上的劣势,所以一方面,它像XML一样具有简洁而清晰的层次结构,另一方面,它比XML小巧精致,更加适用于网络数据的传输。JSON也不是没有缺点,当结构层级很多的时候,它会让人陷入繁琐复杂的数据节点查找中,在可读性上要比XML差。将Python对象编码成JSON字符串将python的对象转化为字符串,这个过程也称为序列化,与之相对,将JSON字符串转化为python对象,这个过程被称为反序列化。序列化格式如下,json.dumps()把python对象序列化,json.dump() 先序列化,然后将内容存入文件:json.dumps(obj,* , skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, cls=None, indent=None, separators=None, default=None, sort_keys=False, **kw)json.dump(obj, fp, *, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, cls=None, indent=None, separators=None, default=None, sort_keys=False,**kw)1In [1]: import json2In [2]: d = dict(name='Tom', age='8', score=88)3In [3]: json.dumps(d)4Out[3]: '{"name": "Tom", "age": "8", "score": 88}'5In [4]: with open('test.json', 'w') as f:6 ...: json.dump(d, f)复制代码用的比较多的参数有:ensure_ascii=True 设置是否编码为ASCII,默认是,若False,则使用原编码码格式indent=None 设置打印时缩进,默认不缩进separators=None 设置分隔符,取值是(item_separator, dict_separator)元组,默认为(‘,’,’:’),这表示keys之间用“,”隔开,而key和value之间用“:”隔开sort_keys=False 设置按key值排序,默认不排序1In [15]: d = dict(name='Python猫', age='8', score=88) 2 3In [16]: json.dumps(d) 4Out[16]: '{"name": "Python\\u732b", "age": "8", "score": 88}' 5 6In [17]: json.dumps(d, ensure_ascii=False, indent=4, sort_keys=True) 7Out[17]: '{\n "age": "8",\n "name": "Python猫",\n "score": 88\n}' 8 9In [18]: print(json.dumps(d, ensure_ascii=False, indent=4, sort_keys=True))10{11 "age": "8",12 "name": "Python猫",13 "score": 8814}复制代码将JSON字符串解码为Python对象反序列化格式如下,json.loads()从内存中读取内容解析,json.load() 从文件中读取内容解析:json.loads(s, *, encoding=None, cls=None, object_hook=None, parse_float=None, parse_int=None, parse_constant=None, object_pairs_hook=None, **kw)json.load(fp, *, cls=None, object_hook=None, parse_float=None, parse_int=None, parse_constant=None, object_pairs_hook=None, **kw)1In [1]: import json2In [2]: d = dict(name='Tom', age='8', score=88)3In [3]: tom_json = json.dumps(d)4In [4]: json.loads(tom_json)5Out[4]: {'age': '8', 'name': 'Tom', 'score': 88}6In [5]: with open('test.json', 'r') as f:7 ...: print(json.load(f))8{'name': 'Tom', 'age': '8', 'score': 88}复制代码json.loads()比json.load() 多了一个encoding参数,可以将传入的字符串重新编码。解决中文乱码问题序列化的ensure_ascii参数与反序列化的encoding相对应,都是处理字符编码,一旦处理不好,就会导致中文乱码问题。Python2的字符编码乱七八糟,也广被人诟病,如果不幸遇到Python2项目,可参照如下例子解决。字符串在Python2内部的表示是unicode编码。因此,在做编码转换时,需要以unicode作为中间编码,即先将其他编码的字符串解码(decode)成unicode,再从unicode编码(encode)成另一种编码。1# -*- coding: utf-8 -*- 2m = {'a' : '你好'} 3 4print m 5=>{'a': '\xe4\xbd\xa0\xe5\xa5\xbd'} 6 7print json.dumps(m) 8=>{"a": "\u4f60\u597d"} 910print json.dumps(m,ensure_ascii=False)11=>{"a": "浣犲ソ"}1213print json.dumps(m,ensure_ascii=False).decode('utf8').encode('gb2312')14=>{"a": "你好"}复制代码Python3的默认编码格式是utf-8,以上例子,只需要ensure_ascii=False,就能解决。
对于初学者来说,一份详尽又清晰明白的指南很重要。今天,猫猫跟大家一起,好好学习Python文件读写的内容,这部分内容特别常用,掌握后对工作和实战都大有益处。学习是循序渐进的过程,欲速则不达。文章较长,建议大家收藏,以备复习查阅哦。1、如何将列表数据写入文件?2、如何从文件中读取内容?3、多样需求的读写任务4、从with语句到上下文管理器如何将列表数据写入文件?首先,我们来看看下面这段代码,并思考:这段代码有没有问题,如果有问题的话,要怎么改?li = ['python',' is',' a',' cat']with open('test.txt','w') as f: f.write(li)复制代码现在公布答案,这段代码会报错:TypeError Traceback (most recent call last)<ipython-input-6-57e0c2f5a453> in <module>() 1 with open('test.txt','w') as f:----> 2 f.write(li)TypeError: write() argument must be str, not list复制代码以上代码的想法是将list列表内容写入txt文件中,但是报错 TypeError: write() argument must be str。就是说,write()方法必须接受字符串(str)类型的参数。Python中内置了str()方法,可以返回字符串版本的对象(Return a string version of object)。所以,上面的例子中,我们试试把 f.write(li) 改为 f.write(str(li)) ,先做一下字符串类型的转化看看。代码略。这次没有报错了,但是打开文件就傻眼了吧,写入的内容是“['python',' is',' a',' cat']”。怎么才能写成“python is a cat”呢?文件写操作还有一个writelines()方法,它接收的参数是由字符串组成的序列(sequence),实际写入的效果是将全部字符串拼接在一起。字符串本身也是一种序列,所以当参数是字符串的时候,writelines()方法等价于write()。# 以下3种写法等价,都是写入字符串“python is a cat”In [20]: with open('test.txt','w') as f: ...: f.writelines(['python',' is',' a',' cat']) ...: f.writelines('python is a cat') ...: f.write('python is a cat')# 以下2种写法等价,都是写入列表的字符串版本“['python',' is',' a',' cat']”In [21]: with open('test.txt','w') as f: ...: f.write(str(['python',' is',' a',' cat'])) ...: f.writelines(str(['python',' is',' a',' cat']))# 作为反例,以下写法都是错误的:In [22]: with open('test.txt','w') as f: ...: f.writelines([2018,'is','a','cat']) # 含非字符串 ...: f.write(['python','is','a','cat']) # 非字符串复制代码由上可知,当多段分散的字符串存在于列表中的时候,要用writelines()方法,如果字符串是一整段,那直接使用write()方法。如果要以整个列表的形式写入文件,就使用str()方法做下转化。这个问题还没结束,如果列表中就是有元素不是字符串,而且要把全部元素取出来,怎么办呢?那就不能直接使用write()和writelines()了,需要先用for循环,把每个元素取出来,逐一str()处理。In [37]: content=[1,' is',' everything']In [38]: with open('test.txt','w') as f: ...: for i in content: ...: f.write(str(i))复制代码需要注意的是,writelines()不会自动换行。如果要实现列表元素间的换行,一个办法是在每个元素后面加上换行符“\n”,如果不想改变元素,最好是用for循环,在写入的时候加在末尾:for i in content: f.writelines(str(i)+“\n”).引申一下,经过实验,数字及元祖类型也可以作为write()的参数,不需转化。但是dict字典类型不可以,需要先用str()处理一下。字典类型比较特殊,最好是用json.dump()方法写到文件,具体操作方法以及注意事项,请看喵喵之前发的《假期玩得开心也不忘充电,学习Python操作JSON,网络数据交换不用愁》.总结一下,write()接收字符串参数,适用于一次性将全部内容写入文件;writelines()接收参数是由字符串组成的序列,适用于将列表内容逐行写入文件。str()返回Python对象的字符串版本,使用需注意。如何从文件中读取内容?从文件中读取内容有如下方法:file.read([size])从文件读取指定的字节数,如果未给定或为负则读取所有。file.readline([size])读取整行,包括 "\n" 字符。file.readlines([sizeint])读取所有行并返回列表,若给定sizeint>0,则是设置一次读多少字节,这是为了减轻读取压力。简而言之,在不传参数的情况下,read()对应write(),读取全部内容;readlines()对应writelines(),读取全部内容(含换行符)并以列表形式返回,每个换行的内容作为列表的一个元素。In [47]: with open('test.txt','r') as f: ...: print(f.read())1 is everything.python is a cat.this is the end.In [48]: with open('test.txt','r') as f: ...: print(f.readlines())['1 is everything.\n', 'python is a cat.\n', 'this is the end.']复制代码但是,以上两个方法有个缺点,当文件过大的时候,一次性读取太多内容,会对内存造成极大压力。读操作还有一个readline()方法,可以逐行读取。In [49]: with open('test.txt','r') as f: ...: print(f.readline())1 is everything.复制代码readline()读取第一行就返回,再次调用f.readline(),会读取下一行。喵喵,是否感觉跟《超强汇总:学习Python列表,只需这篇文章就够了》学习过的生成器很像,需要不停调用next()获取下一行。这么看来,readline()太笨拙了。那么,有什么办法可以优雅地读取文件内容呢?回过头来看readlines()方法,它返回的是一个列表。这不奇怪么,好端端的内容为啥要返回成列表呢?再想想writelines()方法,把字符串列表写入文件正是这家伙干的事,readlines()方法恰恰是它的逆操作!而writelines()方法要配合for循环,所以我们把readlines()与for循环结合,看看会怎样。In [61]: with open('test.txt','r') as f: ...: for line in f.readlines(): ...: print(line)1 is everything.python is a cat.this is the end.# 读取内容包含换行符,所以要strip()去掉换行符In [62]: with open('test.txt','r') as f: ...: for line in f.readlines(): ...: print(line.strip())1 is everything.python is a cat.this is the end.复制代码总结一下,readline()比较鸡肋,不咋用;read()适合读取内容较少的情况,或者是需要一次性处理全部内容的情况;而readlines()用的较多,比较灵活,因为for循环是一种迭代器,每次加载部分内容,既减少内存压力,又方便逐行对数据处理。多样需求的读写任务前两部分讲了文件读写的几大核心方法,它们能够起作用的前提就是,需要先打开一个文件对象,因为只有在文件操作符的基础上才可以进行读或者写的操作。打开文件用的是open()方法,所以我们再继续讲讲这个方法。open() 方法用于打开一个文件,并返回文件对象,在对文件进行处理过程都需要使用到这个函数,如果该文件无法被打开,会抛出 OSError。open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)open()方法的参数里file(文件)是必需的,其它参数最常用的是mode(模式)和encoding(编码)。先说说encoding,一般来说,打开文件的编码方式以操作系统的默认编码为准,中文可能会出现乱码,需要加encoding='utf-8'。In [63]: with open('test.txt','r') as f: ...: for line in f.readlines(): ...: print(line.strip())-----------------------UnicodeDecodeError Traceback (most recent call last)<ipython-input-63-731a4f9cf707> in <module>() 1 with open('test.txt','r') as f:----> 2 for line in f.readlines(): 3 print(line.strip())UnicodeDecodeError: 'gbk' codec can't decode byte 0xa4 in position 26: illegal multibyte sequenceIn [65]: with open('test.txt','r',encoding='utf-8') as f: ...: for line in f.readlines(): ...: print(line.strip())爱猫猫python is a cat.复制代码再说mode,它指定文件打开的模式。'r': 以只读模式打开(缺省模式,必须保证文件存在)'w':以只写模式打开。若文件存在,则清空文件,然后重新创建;若不存在,则新建'a':以追加模式打开。若文件存在,则会追加到文件的末尾;若文件不存在,则新建常见的mode组合'r'或'rt': 默认模式,文本读模式'w'或'wt':以文本写模式打开(打开前文件被清空)'rb': 以二进制读模式打开'ab': 以二进制追加模式打开'wb': 以二进制写模式打开(打开前文件被清空)'r+': 以文本读写模式打开,默认写的指针开始指在文件开头, 因此会覆写文件'w+': 以文本读写模式打开(打开前文件被清空)'a+': 以文本读写模式打开(只能写在文件末尾)'rb+': 以二进制读写模式打开'wb+': 以二进制读写模式打开(打开前被清空)'ab+': 以二进制读写模式打开喵喵,初看起来,模式很多,但是,它们只是相互组合罢了。建议记住最基本的w、r、a,遇到特殊场景,再翻看一下就好了。从with语句到上下文管理器基础部分讲完了,下面是进阶部分。知其然,更要知其所以然。1、with语句是初学者必会常识首先,要解释一下为啥前文直接就用了with语句。with语句是读写文件时的优雅写法,这已经默认是Python初学者必会的常识了。如果你还不会,先看看用和不用with语句的对比:# 不用with语句的正确写法try: f = open('test.txt','w') f.writelines(['python',' is',' a',' cat'])finally: if f: f.close()# 使用with语句的正确写法with open('test.txt','w') as f: f.writelines(['python',' is',' a',' cat'])复制代码因为文件对象会占用操作系统的资源,并且操作系统同一时间能打开的文件数量是有限的,所以open()方法之后一定要调用close()方法。另外,读写操作可能出现IO异常的情况,所以要加try…finally,保证无论如何,都会调用到close()方法。这样写万无一失,但是实在繁琐,一不小心还可能漏写或者写错。而with语句会保证调用close(),只需一行代码,简直不要太优雅!所以,with语句是Python初学者必会技能。2、什么是上下文管理器?下面,重头戏来了,什么是上下文管理器(context manager)? 上下文管理器是这样一个对象:它定义程序运行时需要建立的上下文,处理程序的进入和退出,实现了上下文管理协议,即在对象中定义了 __enter__() 和 __exit__() 方法。__enter__():进入运行时的上下文,返回运行时上下文相关的对象,with 语句中会将这个返回值绑定到目标对象。 __exit__(exception_type, exception_value, traceback):退出运行时的上下文,定义在块执行(或终止)之后上下文管理器应该做什么。它可以处理异常、清理现场或者处理 with 块中语句执行完成之后需要处理的动作。注意enter和exit的前后有两个下划线,Python中自带了很多类似的方法,它们是很神秘又很强大的存在,江湖人常常称其为“黑魔法”。例如,迭代器协议就实现了__iter__方法。在Python的内置类型中,很多类型都是支持上下文管理协议的,例如file,thread.LockType,threading.Lock等等。上下文管理器无法独立使用,它们要与with相结合,with语句可以在代码块运行前进入一个运行时上下文(执行_enter_ 方法),并在代码块结束后退出该上下文(执行__exit__方法)。 with 语句适用于对资源进行访问的场合,确保不管使用过程中是否发生异常都会执行必要的“清理”操作,释放资源,比如文件使用后自动关闭、线程中锁的自动获取和释放等。3、自定义上下文管理器除了Python的内置类型,任何人都可以定义自己的上下文管理器。下面是一个示例: class OpenFile(object): def __init__(self,filename,mode): self.filename=filename self.mode=mode def __enter__(self): self.f=open(self.filename,self.mode) self.f.write("enter now\n") return self.f #作为as说明符指定的变量的值 def __exit__(self,type,value,tb): self.f.write("exit now") self.f.close() return False #异常会被传递出上下文with OpenFile('test.txt','w') as f: f.write('Hello World!\n')复制代码最终写入文件的结果是:enter nowHello World!exit now上下文管理器必须同时提供 __enter__() 和 _exit_() 方法的定义,缺少任何一个都会导致 AttributeError。 上下文管理器在执行过程中可能会出现异常,_exit_() 的返回值会决定异常的处理方式:返回值等于 False,那么这个异常将被重新抛出到上层;返回值等于 True,那么这个异常就被忽略,继续执行后面的代码。__exit()__ 有三个参数(exception_type, exception_value, traceback),即是异常的相关信息。4、contextlib实现上下文管理器上例中,自定义上下文管理器的写法还是挺繁琐的,而且只能用于类级别。为了更好地辅助上下文管理,Python 内置提供了 contextlib 模块,进而可以很方便地实现函数级别的上下文管理器。 该模块本质上是通过装饰器(decorators)和生成器(generators)来实现上下文管理器,可以直接作用于函数/对象,而不用去关心 __enter__() 和 __exit()__ 方法的具体实现。 先把上面的例子改造一下,然后我们再对照着解释: from contextlib import contextmanager@contextmanagerdef open_file(name): ff = open(name, 'w') ff.write("enter now\n") try: yield ff except RuntimeError: pass ff.write("exit now") ff.close()with open_file('test.txt') as f: f.write('Hello World!\n')复制代码contextmanager是要使用的装饰器,yield关键字将普通的函数变成了生成器。yield的返回值(ff)等于上例__enter__()的返回值,也就是as语句的值(f),而yield前后的内容,分别是_enter_() 和 _exit_() 方法里的内容。使用contextlib,可以避免类定义、_enter_() 和 __exit()__方法,但是需要我们捕捉可能的异常(例如,yield只能返回一个值,否则会导致异常 RuntimeError),所以try…except语句不能忽略。
Copilot 是 Github 推出的一款人工智能编程助手,推出仅一年就受到大量开发者的追捧(据官方统计有 120 万用户)。然而,自 2022 年 6 月起,它改为了付费订阅模式(每月 10 美元或每年 100 美元)。我们暂且不讨论训练代码可能涉及的版权及授权许可问题,可以肯定的是,利用机器学习训练出智能编程 AI 模型,这会是未来的大势所趋!巧合的是,仅在 Copilot 宣布收费的几天后,Amazon 就推出了一款竞品 CodeWhisperer!相信在不久的将来,类似的产品会如雨后春笋般涌现,到那时,程序员和编程学习者们就更加有福了!作者:Brian Tarbox译者:豌豆花下猫@Python猫英文:https://blog.symops.com/2022/08/31/amazon-codewhisperer转载请保留作者&译者&来源信息代码补全最早出现在 1985 年的一个名为 Alice 的 Pascal 编辑器上。它支持自动缩进、自动补全 BEGIN/END 控制结构,甚至支持语法着色。争议也随之而来:在 Alice 的早期,人们担心代码补全使得编写软件过于简单。但它实际上只是一个语法助手。代码补全可以帮你写出语法正确的、可编译的代码,但它不能帮你写出语义正确的代码,甚至不能写出任何有用的代码。GitHub 的 CoPilot 和 Amazon 的 CodeWhisperer 改变了这一点,它们除了提供语法辅助,还能生成语义上正确的代码。它们不仅能提供 if 语句的大纲,还能创建出完整的代码样例。但在 2022 年,一个代码辅助工具到底能好到什么程度呢?本文将重点介绍 CodeWhisperer,尝试回答这个问题。试用:用 Python 从 S3 读取数据亚马逊在 2022 年 6 月发布了 CodeWhisperer 预览版,现在它支持 Python、Java 和 JavaScript。Python猫注:截至2022年9月17日,这个服务还未全面开放。若要试用,可在官网申请:https://pages.awscloud.com/codewhisperer-sign-up-form.html附官方介绍:https://aws.amazon.com/cn/blogs/compute/introducing-amazon-codewhisperer-in-the-aws-lambda-console-in-preview在 AWS 博客的一篇文章中,Mark Richman 解释说,CodeWhisperer 的模型是在“包括 Amazon 开源代码在内的各种数据源”上训练的。有了这个语料库(显然确实存在)完善 CodeWhisperer 的模型,编写从 S3 读取文件的代码应该是一个很好的测试用例。在使用 CodeWhisperer(CW)时,你需要写一个注释,描述你希望函数去做什么。注释的描述性和准确性越高,系统就越能更好地推断出你想要的逻辑。Function to open an S3 file注释以 Function 开头,让 CW 知道你想要创建一个函数。也就是说,你需要添加一个注释,作为给 CW 的提示。CW 分析注释并生成一个函数的定义。此时,你可以在生成函数体之前修改函数定义。CW 还可能提供多种函数定义供你选择。IntelliJ 集成 CodeWhisperer 的截图点击“插入代码”,你的函数就在注释的下方创建好了。注意 CodeWhisperer 不仅插入了代码,还创建了一个文档字符串。# Function to open an S3 file def open_s3_file(filename): """ :param filename: :return: """ s3 = boto3.resource('s3') return s3.Object(bucket, filename).get()['Body'].read()看起来不错!这段代码实现了你的注释所期望的功能,并且是在几秒钟内就生成了。节省了查找boto3 API 的时间,你只需要检查代码,确保语义正确。接着看看提出更多要求时,会发生什么。这有一个很有用的例子:写一个函数从 S3 的文件中返回前“n”行。# Function to get first n lines from a file in S3 def get_first_n_lines_from_s3(filename, n): """ Get the first n lines of a file in S3 :param filename: :param n: :return: """ lines = [] for line in open_s3_file(filename): lines.append(line) if len(lines) >= n: break return lines真厉害!CodeWhisperer 使用了之前创建的辅助方法 open_s3_file,并生成了正确的函数。在 CW 生成函数体之前,你可以修改参数名,提高代码的可读性。例如,如果我们输入:# Function to find common elements in listsCW 会提示:def find_common_elements(list1, list2)在生成函数体之前,我们可以修改函数定义,使其更具描述性:def find_common_elements(first, second)如果我们接受它的函数定义,CodeWhisperer 将使用函数签名和注释来构建函数体。得到的函数将使用改进后的参数名。# Function to find common elements in lists def find_common_elements(first, second): return list(set(first) & set(second))CodeWhisperer 不仅仅可以创建整个函数;它还可以在函数中插入代码片段,并在过程中推断相关变量。 # reverse the list order lines.reverse()CodeWhisperer 是一个重要的生产力助推器使用 CodeWhisperer,我能够更快地编写代码。到目前为止,最大的价值是 S3 示例所演示的功能。如果我自己写代码,大部分时间将会用来查找 boto API 文档,以确保写出正确的调用。使用 CodeWhisperer,则只需三秒钟。这让我想到了一段曾花了很多时间的烦人代码:给 CloudWatch 发送数据。让我们看看 CodeWhisperer 能否有所帮助,试试提供注释:“Function to emit a CloudWatch metric”。# Function to emit a cloudwatch metric def put_metric_data(metric_name, metric_data): """ Put a metric data into cloudwatch :param metric_name: :param metric_data: :return: """ cloudwatch = boto3.client('cloudwatch') cloudwatch.put_metric_data( Namespace='my_namespace', MetricData=[ { 'MetricName': metric_name, 'Dimensions': [ { 'Name': 'string', 'Value': 'string' }, ], 'Unit': 'Seconds', 'Value': metric_data }, ] )非常有用!CW 为我节省了许多打字和查阅 API 文档的时间。我有可能需要重构这段代码,但即使我从零开始编写它,第一步也要写出类似这样的原始逻辑。如今只花三秒钟,我节省了编写大量样板代码的时间,有了一个很好的自定义或重构的起点。那么 CodeWhisperer 是一个比我更好的程序员吗?尽管有一丝标题党,但 CW 的代码是否比我的好,这并不重要。重要的是,它可以为我节省大量的时间和精力,让我能够专注于改进、重构和测试。它通过承担一些同质化的繁重工作,让我有机会成为一个更好的程序员。上面的例子是 Amazon 工具(经过 Amazon 开源代码训练)能够表现出色的例子。当然,在大多数开发人员需要花费很多时间的地方,比如编写领域相关的逻辑时,CW 也许不会非常有用。让我们看看 CW 会出什么问题。我们可以从 Python 文档中的数据类示例开始。@dataclass class InventoryItem: """Class for keeping track of an item in inventory.""" name: str unit_price: float quantity_on_hand: int = 0 def total_cost(self) -> float: return self.unit_price * self.quantity_on_hand我想知道 CodeWhisperer 是否可以向这个类添加一个方法。让我们看看如果添加注释:" Function that return this item costs more than $10",会发生什么?@dataclass class InventoryItem: """Class for keeping track of an item in inventory.""" name: str unit_price: float quantity_on_hand: int = 0 def total_cost(self) -> float: return self.unit_price * self.quantity_on_hand # Function that returns whether this item costs more than $10 def expensive(self) -> bool: return self.unit_price > 10非常酷。值得注意的是,CW 给函数起了一个直观的名字,并包含了对 self 的引用。接着,让我们尝试用 CW 来做测试,看是否会触及它的极限。# Function to test InventoryItem class def test_inventory_item(): """ Test InventoryItem class :return: """ item = InventoryItem("Widget", 10, 5) assert item.name == "Widget" assert item.unit_price == 10 assert item.quantity_on_hand == 5 assert item.total_cost() == 50 assert not item.expensive()哇!在上面的代码中,我输入了注释,CW 自动完成了剩下的工作。测试似乎是一个极好的证明 CW 可以节省时间的例子。我不需要浪费时间去想测试的值,也不用输入所有的成员变量和方法。CodeWhisperer 的局限性它现在还处于初级阶段,还有很多问题。研究人员发现,GitHub CoPilot 生成的代码有 40% 存在安全漏洞。CodeWhisperer 还没有这类的统计数据,但 AWS 似乎强调了对安全性的关注。据我测试,在一些例子中,CW 生成的函数有 bug,或者结果没有符合我的意图。下方的例子应该返回两个文件中最长的公共行,但它只返回了第一个匹配的行:# Function to find the longest common line in two files def find_longest_common_line(file1, file2): """ Find the longest common line in two files :param file1: :param file2: :return: """ with open(file1, 'r') as f1: with open(file2, 'r') as f2: for line in f1: if line in f2: return lineCW 还出现了其它问题,原因是它没有足够的上下文来理解我的意图。经过反思,我觉得如果周围的代码结构很好的话,也是可以实现的。如果你在设计代码时用了准确表示领域的名词,那么,在给出了良好注释的前提下,很容易想象 CW 能够创建出特定于领域的逻辑。至于 bug,将来肯定会得到改善的。写在最后如果你尝试使用 CW,它可能会让你想象:可能有一天,有人会写出历史上最后一行由人类编写的代码。在那之前,CW 可以帮助你成为一个更好的程序员,这样即使世界上最后一个程序员是你,人类的最后一行代码也不会有 bug。本文首发于 Python猫 ,未经许可,请勿转载知乎:Python猫博客园:豌豆花下猫掘金:豌豆花下猫CSDN:Python猫
大家好,我是猫哥。2020年过得真快啊!总感觉这一年里还没有做成多少事,一眨眼就又到了写年度总结的时候了…… 去年1月1日的时候,我写了《我的 2019 年 Python 文章榜单》,简单列了自己比较满意的 11 篇文章。今年延续传统,我想盘点出一份 2020 年的文章榜单。 在列榜单之前,我们先来闲聊几件事,作为铺垫吧。 1、公众号订阅数破 20000 啦! 就在 2020 年结束前的两天,Python猫的订阅数终于迈上了新的台阶。从 2018 年国庆到现在,我们共花了 26 个月。 这个成绩非常非常普通,当初我第一次参加公众号互推时,认识了明哥(@Python编程时光)、小帅B(@学习Python的正确姿势)和涛哥(@涛哥聊Python),当时大家差别不太多(一起出新手村),而如今他们的订阅数达到了 4万、6万和10万,把我远远甩在了身后…… 2021年,我会花适当的精力在运营上,不敢奢望太多,争取明年达成 3.5 万目标。因此,希望能得到大家的持续支持,请帮忙分享、转载、在看,推荐Python猫给其他学习者。我在此鞠躬感谢了! 2、被评选为“优质原创号主”第2名。 在猪哥(@裸睡的猪)建的Python原创作者投稿群里,Python猫有幸被票选成了第2名!能够被众多优秀的同行号主们认可,真是难得而且荣幸! 3、有文章被国际友人翻译了。 去年7月份的时候,我偶然发现自己的 3 篇“Python为什么”系列文章被一个印度人翻译成了英文,当时写了一篇《当我发现国际友人翻译了我的文章之后…… 》说明了原由。然而,意想不到的惊喜发生了,其中一篇文章竟被发上了 PyCoder's Weekly,而且还被CSDN的作者翻译成了中文!真是太戏剧化了! 那篇文章是《Python 为什么没有 main 函数?为什么我不推荐写 main 函数?》,在国内个平台上也引发了不少的讨论。我其实是有的放矢的,但行文比较精简没有充分展开。有一些反驳声是误读,还有一些则没有驳到点上。那篇文章体现了我对优雅代码的感觉,有一种锐意思考的闪光,我个人非常满意。 4、短暂的视频UP主尝试。 我去年尝试制作了几期短视频,其实是念稿录音+PPT式图文剪辑而成的。一开始的目标是60秒短视频,但后来发现想表达的内容太多,这个时长完全不够。但是,更长的视频则意味着更大的工作量,所以我干脆暂时放弃了。已发布的视频在B站有,欢迎大家去观摩指导,地址:https://space.bilibili.com/97566624/video 5、整理了一本电子书。 我整理了过往的文章,编成了一本电子书,还美其名为《优雅的Python》(可在Python猫后台发送数字“1”领取。《耗时两年,我终于出了一本电子书!》里有内容介绍)。很大的动因是学习其他号主,用来给自己引流。但是,陆续收到了几名读者正向的反馈后,我觉得这件事还是蛮有价值的。 陆续有出版社的编辑来联系出书,我很惶恐,都婉拒了。我知道有些文章还不错,但是离出书还远着呢,不想去误人子弟。(PS.正在跟某编辑合作,但跟自己出书有所不同。以后详说。) 6、在苏州买了首套房。 去年办成了一件大事,就是在苏州园区买了房,成为了“房奴”。不用说,家庭生活的压力大了很多,而且被催生娃的压力也大增了……人的年龄到了某个阶段,家庭的责任可能促使你做出重大的抉择。我觉得在做成这件事后,自己的内心世界成熟了很多。 因此,需要给读者们打个预防针:Python猫以后“恰饭”的时候会适当变多,我觉得发挥自己写作的特长,适当地挣点钱,这件事很光荣,所以希望大家也适当地包容理解哈~~~ 闲聊先到此为止,下面是文章梳理时刻。 在过去一年里,猫哥原创及翻译了 Python 技术文 62 篇,总被转载次数达到了 500+。 我的兴趣主要集中在 Python 语法、技术原理、进阶思考、文章翻译等内容,大部分文章是比较小众的,阅读量也十分惨淡。 幸运的是,有几篇文章成为了小爆款,阅读量还挺可观的。从受众喜爱的维度看,下面这些文章的效果很好: 【01】Python 为什么推荐蛇形命名法? --(被转载32次,转载阅读量达8万+) 【02】Python 为什么要有 pass 语句? --(被转载32次,转载阅读量达5万+) 【03】Python 之父为什么嫌弃 lambda 匿名函数? -- --(被转载25次,转载阅读量达3万+) 【04】你可能不知道的 Python 技巧 --(被转载24次,转载阅读量达1.7万+) 【05】Python 为什么不支持 switch 语句? --(被转载20次,转载阅读量达1.3万+) 但是,依我个人喜爱度选择的话,我列出的TOP10榜单是这些(按时间排序): 【01】Flask 作者 Armin Ronacher:我不觉得有异步压力 【02】Python在计算内存时应该注意的问题? 【03】Python 为什么没有 main 函数?为什么我不推荐写 main 函数? 【04】Python 为什么不支持 i++ 自增语法,不提供 ++ 操作符? 【05】Python 为什么只需一条语句“a,b=b,a”,就能直接交换两个变量? 【06】Python 为什么能支持任意的真值判断? 【07】Python到底是强类型语言,还是弱类型语言? 【08】Python 之父为什么嫌弃 lambda 匿名函数? 【09】为什么继承 Python 内置类型会出问题?! 【10】Python最会变魔术的魔术方法,我觉得是它! 这里面有几篇是出自“Python为什么”系列,该系列还有一些文章也不错,全部归档在 Github 上了,大家可以去那里查阅:https://github.com/chinesehuazhou/python-whydo 在新的一年里,我写作的主体方向基本不会变,也许会增加一些偏基础向的内容,让自己更接地气一些。同时,PEP和社区好文的翻译工作,也会偶尔做做。 Flag不敢随便立,但管继续求知与分享,但求无愧于心! 最后,我把2020全年的文章罗列出来了,按照的是时间顺序: 开发者请注意:Python2 的最后版本将于 4 月发布,但它确实是在 1 月 1 日就寿命终止了! Python 打包的现状:包的三种类型 Python 打包——过去、现在与未来 Python 官方团队在打包项目中踩过的坑 Python 任务自动化工具 tox 教程 2019 年 stackoverflow 网站最受欢迎的 20 个 Python 问题 11 个最佳的 Python 编译器和解释器 Flask 作者 Armin Ronacher:我不觉得有异步压力 更好用的 Python 任务自动化工具:nox 官方教程 任务自动化工具:nox 的配置与 API 进一步学习 nox 教程,轻松掌握命令行用法 你可能不知道的 Python 技巧 强大的 Python 任务自动化工具!invoke 十分钟入门指南 如何高效地远程部署?自动化运维利器 Fabric 教程 Python在计算内存时应该注意的问题? Fabric 源码学习:如何实现批量管理远程服务器? Python 小技巧:如何实现操作系统兼容性打包? Python 3.9 新特性:任意表达式可作为装饰器! 学编程这么久,还傻傻分不清什么是方法(method),什么是函数(function)? 官宣!Python 开发者大会(PyCon US)提供在线订阅啦! 不使用 if-elif 语句,如何优雅地判断某个数字所属的等级? Python 3.9 性能优化:更快的 list()、dict() 和 range() 等内置类型 Python 如何移除旧的版本特性,如何迎接新的特性? 天大福利!世界第一科技出版公司 Springer 免费开放 400 多本电子书! Python为什么使用缩进来划分代码块? Python 的缩进是不是反人类的设计? Python的十万个为什么? Python小技巧:如何批量更新已安装的库? Python 为什么不用分号作语句终止符? Python 为什么没有 main 函数?为什么我不推荐写 main 函数? 涨见识了,在终端执行 Python 代码的 6 种方式! Python 3.9 beta2 版本发布了,看看这 7 个新的 PEP 都是什么? Python 为什么推荐蛇形命名法? Python 为什么不支持 i++ 自增语法,不提供 ++ 操作符? Python 3.10 版本采纳了首个 PEP,中文翻译即将推出 Python 3.10 的首个 PEP 诞生,内置类型 zip() 将迎来新特性 一篇文章掌握 Python 内置 zip() 的全部内容 Python 为什么只需一条语句“a,b=b,a”,就能直接交换两个变量? Python 为什么用 # 号作注释符? 当我发现国际友人翻译了我的文章之后…… Python 为什么要有 pass 语句? Python 为什么会有个奇怪的“...”对象? Python 为什么能支持任意的真值判断? Python 为什么要在 18 年前引入布尔类型?且与 C、C++ 和 Java 都不同? 一个在交流群里讨论过两轮的问题,答案竟然跟一个 PEP 有关 Python 函数为什么会默认返回 None? Python 为什么没有 void 关键字? Python到底是强类型语言,还是弱类型语言? Python中的数字到底是什么? 详解 Python 的二元算术运算,为什么说减法只是语法糖? 详解增强算术赋值:“-=”操作是怎么实现的? Python 之父为什么嫌弃 lambda 匿名函数? 耗时两年,我终于出了一本电子书! Python 为什么不支持 switch 语句? [Python 疑难问题:[] 与 list() 哪个快?为什么快?快多少呢?](https://mp.weixin.qq.com/s/-yi4HcNVI6rKBOJ25fwQDg) 为什么说 Python 内置函数并不是万能的? 如果只推荐一本 Python 书,我要 Pick 它! Python有序字典的两个小“惊喜”~~ Python 幕后解释器:一系列的学习资源 为什么继承 Python 内置类型会出问题?! Python最会变魔术的魔术方法,我觉得是它! 脑洞:如何用一个整数来表示一个列表?
原题 | Storing a list in an int (https://iantayler.com/2020/12/07/storing-a-list-in-an-int) 作者 | Computer Wit 译者 | 豌豆花下猫(“Python猫”公众号作者) 声明 | 本翻译已得到原作者授权。为便于阅读,内容略有改动。 概要 与 C、Rust 和 Go 不同,Python 默认的int 具有任意大小。[[注1]](https://iantayler.com/2020/12/07/storing-a-list-in-an-int/%23footnotes#footnotes) 、[[注2] ](https://iantayler.com/2020/12/07/storing-a-list-in-an-int/%23footnotes#footnotes) 这意味着,一个整数可以存储无限大的值,只要内存足够。 例如,你可以打开 Python3 并运行以下命令: >>> import math >>> math.factorial(2020) [number omitted] # Python猫注:此处求2020的阶乘,结果是一长串数字,所以省略 >>> math.log2(math.factorial(2020)) 19272.453841606068 >>> type(math.factorial(2020)) <class 'int'> 也就是说,在 Python 中,平常使用的 int 可以轻松地保存一个占用 19273 比特的 C 类型固定大小无符号 int 类型的值(C-style fixed-size unsigned int )。在 Python 这样的语言中,便利性高于速度和内存效率,这确实很有用。 这种无限的精度,也意味着我们可以在单个 int 中存储任意数量的信息。只要编码正确,一整本书、一整个数据库、甚至任何东西,都可以被存入一个单独的 Python int 中。 (Python猫注:这有一篇文章 ,深度剖析了 Python 整型不会溢出的实现原理,可作关联阅读) 因此,我们可以设想出一种 Python 的方言,它只有整型,需要用 int 表示其它所有的类型(字典、列表、等等)。我们还有一些特殊的函数和方法,可以将 int 视为 list 、dict 等等。 这将会是一个有趣而好玩的练习,而这就是本文想要做的事。 有一个显而易见的实现方法:所有数据结构只是内存中的位数组(bit-arrays)。最坏的情况下,它是一组相关的位数组(例如,像链表或树中的每个节点),并且它们的集合也只是位数组。位数组可以被解释为二进制数。所以我们必然能这样做。但这有点无聊。 在本博文以及本系列的后续博文中,我将介绍一些用 int 来表示复杂数据结构的方法。它们不一定是最紧凑、最合理或最有效的,其共同的目标是找到这些数据结构的有趣的表示方式。[[注3]](https://iantayler.com/2020/12/07/storing-a-list-in-an-int/%23footnotes#footnotes) 哥德尔数(Gödel numbering)简介 我们要表示的第一个数据结构是 list。我们将使用以逻辑学家 KurtGödel 命名的Gödel数。为了方便起见,我们仅处理由无符号整数(即自然数)组成的列表。 哥德尔数的原理是令每个大于 1 的自然数都用唯一的质数分解来表示。它依据的是算术的基本定理。 (Python猫注:质数分解,即 prime factorization,又译作质因数分解、素因子分解等,指的是把每个数都写成用质数相乘的形式) 看一些例子: 一个数字可以通过其质因子(prime factors )的指数列表来唯一标识(直到其最高位的非零指数)。所以,我们可以用 126 来表示列表[1, 2, 0, 1] 。列表中的第一个数字是 126 作质数分解后 2 的指数,第二个数是 3 的指数,依此类推。 再来几个例子: 如果列表末尾有 0 ,该怎么办呢?好吧,基于这样的编码,不会出现这种情况。 在我们的质数分解中,指数为 0 的质数可能有无限个,因此我们需要停在某个地方。[[注4]](https://iantayler.com/2020/12/07/storing-a-list-in-an-int/%23footnotes#footnotes) 我们选择在最后一个非零指数处停止。 当列表中包含较大的数字时,这种表示形式也会使用非常大的数字。那是因为列表中的数字表示的是指数,所以 int 的大小与它们成指数增长。例如,[50, 1000, 250] 需要使用大小为 2266 比特的数字表示。 另一方面,相比于其它用 int 编码的列表,那些包含非常多小整数的长列表,尤其是大型稀疏列表(即大部分的值都为 0),则拥有非常紧凑的表示形式。 提醒一下,将 list 编码为 int,这不是很好的编程实践,仅仅是一个好玩的实验。 Python实现 让我们看一下 Python 的实现。这里有几点注意事项: 我们会使用带有 yield 的函数,因为它极大地简化了操作。[[注5]](https://iantayler.com/2020/12/07/storing-a-list-in-an-int/%23footnotes#footnotes) 你会看到大量的 while 循环。这是因为列表生成式、range 和大多数你打算在 for 循环中使用的东西,都被禁止用在只有 int 类型的方言中。所有这些都被 while 循环替代了。 质数生成器 我们要编写的第一个函数是一个迭代器,它将按顺序生成质数。它从头到尾都很关键。这里的实现是最简单可行的版本。 我可能很快会写一篇完整的关于生成质数的算法的文章,因为这是一个很酷的话题,本身也是一个古老的研究领域。最广为人知的算法是爱拉托逊斯筛法(Sieve of Erathosthenes ),但这只是冰山一角。[[注6]](https://iantayler.com/2020/12/07/storing-a-list-in-an-int/%23footnotes#footnotes) 在这里,一个非常幼稚的实现就够了: def primes(starting: int = 2): """Yield the primes in order. Args: starting: sets the minimum number to consider. Note: `starting` can be used to get all prime numbers _larger_ than some number. By default it doesn't skip any candidate primes. """ candidate_prime = starting while True: candidate_factor = 2 is_prime = True # We'll try all the numbers between 2 and # candidate_prime / 2. If any of them divide # our candidate_prime, then it's not a prime! while candidate_factor <= candidate_prime // 2: if candidate_prime % candidate_factor == 0: is_prime = False break candidate_factor += 1 if is_prime: yield candidate_prime candidate_prime += 1 创建空列表 def empty_list() -> int: """Create a new empty list.""" # 1 is the empty list. It isn't divisible by any prime. return 1 遍历元素 def iter_list(l: int): """Yields elements in the list, from first to last.""" # We go through each prime in order. The next value of # the list is equal to the number of times the list is # divisible by the prime. for p in primes(): # We decided we will have no trailing 0s, so when # the list is 1, it's over. if l <= 1: break # Count the number of divisions until the list is # not divisible by the prime number. num_divisions = 0 while l % p == 0: num_divisions += 1 l = l // p # could be / as well yield num_divisions 访问元素 def access(l: int, i: int) -> int: """Return i-th element of l.""" # First we iterate over all primes until we get to the # ith prime. j = 0 for p in primes(): if j == i: ith_prime = p break j += 1 # Now we divide the list by the ith-prime until we # cant divide it no more. num_divisions = 0 while l % ith_prime == 0: num_divisions += 1 l = l // ith_prime return num_divisions 添加元素 def append(l: int, elem: int) -> int: # The first step is finding the largest prime factor. # We look at all primes until l. # The next prime after the last prime factor is going # to be the base we need to use to append. # E.g. if the list if 18 -> 2**1 * 3**2 -> [1, 2] # then the largest prime factor is 3, and we will # multiply by the _next_ prime factor to some power to # append to the list. last_prime_factor = 1 # Just a placeholder for p in primes(): if p > l: break if l % p == 0: last_prime_factor = p # Now get the _next_ prime after the last in the list. for p in primes(starting=last_prime_factor + 1): next_prime = p break # Now finally we append an item by multiplying the list # by the next prime to the `elem` power. return l * next_prime ** elem 试用这些函数 你可以打开一个 Python、iPython 或 bPython会话,并试试这些函数! 建议列表元素使用从 1 到 10 之间的数字。如果使用比较大的数字,则 append 和 access 可能会花费很长时间。 从某种程度上说,使用哥德尔数来表示列表并不实用,尽管可以通过优化质数生成及分解算法,来极大地扩大可用数值的范围。 In [16]: l = empty_list() In [17]: l = append(l, 2) In [18]: l = append(l, 5) In [19]: list(iter_list(l)) Out[19]: [2, 5] In [20]: access(l, 0) Out[20]: 2 In [21]: access(l, 1) Out[21]: 5 In [22]: l Out[22]: 972 # Python猫注:2^2*3^5=972 其它 int 编码 我们看到了一种将自然数列表表示为 int 的方法。还有其它更实用的方法,这些方法依赖于将数字的二进制形式细分为大小不一的块。我相信你可以提出这样的建议。 我以后可能会写其它文章,介绍更好的用于生成和分解质数的算法,以及其它复杂数据结构的 int 表示形式。 脚注 我认为在内存不足之前,程序也会出现中断,但是文档确实明确地提到它们具有无限的精度。 请注意,对于 Python3,这是正确的,但对于 Python2 则不然。对于 Python2,int 是固定大小的。我认为在 2020 年用 Python 指代 Python3 是没问题的,但我也认为这个细节值得加一条脚注。 对于用哥德尔数表示列表,这很容易被反驳说是一种糟糕的表示形式。在后续的博文中,我们会讨论有关表示形式的权衡问题。 我们可以将列表的长度存储在单独的 int 中,据此知道要在列表末尾考虑多少个 0。(猫注:还有几句话没看懂,不译)If we don’t want to have a whole separate int, we can always write the length of the list as the exponent of 2 and start the actual list with the exponent of 3. This has some redundant information, though. The way to avoid redundant information is to store the number of final 0s in the list, instead of the entire length. We won’t be worrying about any of this, though. 请注意,跟使用 return 并将状态变量作为参数相比,使用 yield 没有区别(通常足以获得最后一个返回的元素)。这有点像 Continuation Passing Style。也类似于平常的使非尾递归函数尾递归的累加器。如果你从未听说过累加器技巧,这里有一些链接[[1]](https://raganwald.com/2018/05/27/tail.html) 、[[2]](https://blog.appsignal.com/2019/03/19/elixir-alchemy-recursion.html) 。我未来可能会在没有它们的语言中,写模仿迭代器的东西。 另请参见《 The Genuine Sieve of Erathosthenes》论文,它澄清了这一算法是如何被定义的。 Python猫注: 以上是全部译文,但我最后还想补充一个有趣的内容。在《黑客与画家》中,保罗·格雷大师有一个惊人的预言,他认为在逻辑上不需要有整数类型,因为整数 n 可以用一个 n 元素的列表来表示。哈哈,这跟上文的脑洞恰好反过来了!想象一下,一个只有整数类型没有列表的编程语言,以及一个只有列表类型没有整数的编程语言,哪一个更有可能在未来出现呢?
在上篇文章中,我有一个核心的发现:Python 内置类型的特殊方法(含魔术方法与其它方法)由 C 语言独立实现,在 Python 层面不存在调用关系。 但是,文中也提到了一个例外:一个非常神秘的魔术方法。 这个方法非常不起眼,用途狭窄,我几乎从未注意过它,然而,当发现它可能是上述“定律”的唯一例外情况时,我认为值得再写一篇文章来详细审视一下它。 本文主要关注的问题有: (1) __missing__()到底是何方神圣? (2) __missing__()有什么特别之处?擅长“大变活人”魔术? (3) __missing__()是否真的是上述发现的例外?如果是的话,为什么会有这种特例? 1、有点价值的__missing__() 从普通的字典中取值时,可能会出现 key 不存在的情况: dd = {'name':'PythonCat'} dd.get('age') # 结果:None dd.get('age', 18) # 结果:18 dd['age'] # 报错 KeyError dd.__getitem__('age') # 等同于 dd['age'] 对于 get() 方法,它是有返回值的,而且可以传入第二个参数,作为 key 不存在时的返回内容,因此还可以接受。但是,另外两种写法都会报错。 为了解决后两种写法的问题,就可以用到 __missing__() 魔术方法。 现在,假设我们有一个这样的诉求:从字典中取某个 key 对应的 value,如果有值则返回值,如果没有值则插入 key,并且给它一个默认值(例如一个空列表)。 如果用原生的 dict,并不太好实现,但是,Python 提供了一个非常好用的扩展类collections.defaultdict: 如图所示,当取不存在的 key 时,没有再报 KeyError,而是默认存入到字典中。 为什么 defaultdict 可以做到这一点呢? 原因是 defaultdict 在继承了内置类型 dict 之后,还定义了一个 __missing__() 方法,当 __getitem__取不存在的值时,它就会调用入参中传入的工厂函数(上例是调用 list(),创建空列表)。 作为最典型的示例,defaultdict 在文档注释中写到: 简而言之,__missing__()的主要作用就是由__getitem__在缺失 key 时调用,从而避免出现 KeyError。 另外一个典型的使用例子是collections.Counter ,它也是 dict 的子类,在取未被统计的 key 时,返回计数 0: 2、神出鬼没的__missing__() 由上可知,__missing__()在__getitem__()取不到值时会被调用,但是,我不经意间还发现了一个细节:__getitem__()在取不到值时,并不一定会调用__missing__()。 这是因为它并非内置类型的必要属性,并没有在字典基类中被预先定义。 如果你直接从 dict 类型中取该属性值,会报属性不存在:AttributeError: type object 'object' has no attribute '__missing__' 。 使用 dir() 查看,发现确实不存在该属性: 如果从 dict 的父类即 object 中查看,也会发现同样的结果。 这是怎么回事呢?为什么在 dict 和 object 中都没有__missing__属性呢? 然而,查阅最新的官方文档,object 中分明包含这个属性: 出处:https://docs.python.org/3/reference/datamodel.html?highlight=__missing__#object.__missing__ 也就是说,理论上 object 类中会预定义__missing__,其文档证明了这一点,然而实际上它并没有被定义!文档与现实出现了偏差! 如此一来,当 dict 的子类(例如 defaultdict 和 Counter)在定义__missing__ 时,这个魔术方法事实上只属于该子类,也就是说,它是一个诞生于子类中的魔术方法! 据此,我有一个不成熟的猜想:__getitem__()会判断当前对象是否是 dict 的子类,且是否拥有__missing__(),然后才会去调用它(如果父类中也有该方法,则不会先作判断,而是直接就调用了)。 我在交流群里说出了这个猜想,有同学很快在 CPython 源码中找到验证: 而这就有意思了,在内置类型的子类上才存在的魔术方法, 纵观整个 Python 世界,恐怕再难以找出第二例。 我突然有一个联想:这神出鬼没的__missing__(),就像是一个擅长玩“大变活人”的魔术师,先让观众在外面透过玻璃看到他(即官方文档),然而揭开门时,他并不在里面(即内置类型),再变换一下道具,他又完好无损就出现了(即 dict 的子类)。 3、被施魔法的__missing__() __missing__() 的神奇之处,除了它本身会变“魔术”之外,它还需要一股强大的“魔法”才能驱动。 在上篇文章中,我发现原生的魔术方法间相互独立,它们在 C 语言界面可能有相同的核心逻辑,但是在 Python 语言界面,却并不存在着调用关系: 魔术方法的这种“老死不相往来”的表现,违背了一般的代码复用原则,也是导致内置类型的子类会出现某些奇怪表现的原因。 官方 Python 宁肯提供新的 UserString、UserList、UserDict 子类,也不愿意复用魔术方法,唯一合理的解释似乎是令魔术方法相互调用的代价太大。 但是,对于特例__missing__(),Python 却不得不妥协,不得不付出这种代价! __missing__() 是魔术方法的“二等公民 ”,它没有独立的调用入口,只能被动地由 __getitem__() 调用,即__missing__() 依赖于__getitem__()。 不同于那些“一等公民 ”,例如 __init__()、__enter__()、__len__()、__eq__() 等等,它们要么是在对象生命周期或执行过程的某个节点被触发,要么由某个内置函数或操作符触发,这些都是相对独立的事件,无所依赖。 __missing__() 依赖于__getitem__(),才能实现方法调用;而 __getitem__() 也要依赖 __missing__(),才能实现完整功能。 为了实现这一点,__getitem__()在解释器代码中开了个后门,从 C 语言界面折返回 Python 界面,去调用那个名为“__missing__”的特定方法。 而这就是真正的“魔法”了,目前为止,__missing__()似乎是唯一一个享受了此等待遇的魔术方法! 4、小结 Python 的字典提供了两种取值的内置方法,即__getitem__() 和 get(),当取值不存在时,它们的处理策略是不一样的:前者会报错KeyError,而后者会返回 None。 为什么 Python 要提供两个不同的方法呢?或者应该问,为什么 Python 要令这两个方法做出不一样的处理呢? 这可能有一个很复杂(也可能是很简单)的解释,本文暂不深究了。 不过有一点是可以确定的:即原生 dict 类型简单粗暴地抛KeyError 的做法有所不足。 为了让字典类型有更强大的表现(或者说让__getitem__()作出 get() 那样的表现),Python 让字典的子类可以定义__missing__(),供__getitem__()查找调用。 本文梳理了__missing__()的实现原理,从而揭示出它并非是一个毫不起眼的存在,恰恰相反,它是唯一一个打破了魔术方法间壁垒,支持被其它魔术方法调用的特例! Python 为了维持魔术方法的独立性,不惜煞费苦心地引入了 UserString、UserList、UserDict 这些派生类,但是对于 __missing__(),它却选择了妥协。 本文揭示出了这个魔术方法的神秘之处,不知你读后有何感想呢?欢迎留言讨论。
本文出自“Python为什么”系列,请查看全部文章 不久前,Python猫 给大家推荐了一本书《流畅的Python》(点击可跳转阅读),那篇文章有比较多的“溢美之词”,显得比较空泛…… 但是,《流畅的Python》一书值得反复回看,可以温故知新。最近我偶然翻到书中一个有点诡异的知识点,因此准备来聊一聊这个话题——子类化内置类型可能会出问题?! 1、内置类型有哪些? 在正式开始之前,我们首先要科普一下:哪些是 Python 的内置类型? 根据官方文档的分类,内置类型(Built-in Types)主要包含如下内容: 详细文档:https://docs.python.org/3/library/stdtypes.html 其中,有大家熟知的数字类型、序列类型、文本类型、映射类型等等,当然还有我们之前介绍过的布尔类型、...对象 等等。 在这么多内容里,本文只关注那些作为可调用对象(callable)的内置类型,也就是跟内置函数(built-in function)在表面上相似的那些:int、str、list、tuple、range、set、dict…… 这些类型(type)可以简单理解成其它语言中的类(class),但是 Python 在此并没有用习惯上的大驼峰命名法,因此容易让人产生一些误解。 在 Python 2.2 之后,这些内置类型可以被子类化(subclassing),也就是可以被继承(inherit)。 2、内置类型的子类化 众所周知,对于某个普通对象 x,Python 中求其长度需要用到公共的内置函数 len(x),它不像 Java 之类的面向对象语言,后者的对象一般拥有自己的 x.length() 方法。(PS:关于这两种设计风格的分析,推荐阅读 这篇文章) 现在,假设我们要定义一个列表类,希望它拥有自己的 length() 方法,同时保留普通列表该有的所有特性。 实验性的代码如下(仅作演示): # 定义一个list的子类 class MyList(list): def length(self): return len(self) 我们令 MyList这个自定义类继承 list,同时新定义一个 length() 方法。这样一来,MyList 就拥有 append()、pop() 等等方法,同时还拥有 length() 方法。 # 添加两个元素 ss = MyList() ss.append("Python") ss.append("猫") print(ss.length()) # 输出:2 前面提到的其它内置类型,也可以这样作子类化,应该不难理解。 顺便发散一下,内置类型的子类化有何好处/使用场景呢? 有一个很直观的例子,当我们在自定义的类里面,需要频繁用到一个列表对象时(给它添加/删除元素、作为一个整体传递……),这时候如果我们的类继承自 list,就可以直接写 self.append()、self.pop(),或者将 self 作为一个对象传递,从而不用额外定义一个列表对象,在写法上也会简洁一些。 还有其它的好处/使用场景么?欢迎大家留言讨论~~ 3、内置类型子类化的“问题” 终于要进入本文的正式主题了:) 通常而言,在我们教科书式的认知中,子类中的方法会覆盖父类的同名方法,也就是说,子类方法的查找优先级要高于父类方法。 下面看一个例子,父类 Cat,子类 PythonCat,都有一个 say() 方法,作用是说出当前对象的 inner_voice: # Python猫是一只猫 class Cat(): def say(self): return self.inner_voice() def inner_voice(self): return "喵" class PythonCat(Cat): def inner_voice(self): return "喵喵" 当我们创建子类 PythonCat 的对象时,它的 say() 方法会优先取到自己定义出的 inner_voice() 方法,而不是 Cat 父类的 inner_voice() 方法: my_cat = PythonCat() # 下面的结果符合预期 print(my_cat.inner_voice()) # 输出:喵喵 print(my_cat.say()) # 输出:喵喵 这是编程语言约定俗成的惯例,是一个基本原则,学过面向对象编程基础的同学都应该知道。 然而,当 Python 在实现继承时,似乎不完全会按照上述的规则运作。它分为两种情况: 符合常识:对于用 Python 实现的类,它们会遵循“子类先于父类”的原则 违背常识:对于实际是用 C 实现的类(即str、list、dict等等这些内置类型),在显式调用子类方法时,会遵循“子类先于父类”的原则;但是,在存在隐式调用时,它们似乎会遵循“父类先于子类”的原则,即通常的继承规则会在此失效 对照 PythonCat 的例子,相当于说,直接调用 my_cat.inner_voice() 时,会得到正确的“喵喵”结果,但是在调用 my_cat.say() 时,则会得到超出预期的“喵”结果。 下面是《流畅的Python》中给出的例子(12.1章节): class DoppelDict(dict): def __setitem__(self, key, value): super().__setitem__(key, [value] * 2) dd = DoppelDict(one=1) # {'one': 1} dd['two'] = 2 # {'one': 1, 'two': [2, 2]} dd.update(three=3) # {'three': 3, 'one': 1, 'two': [2, 2]} 在这个例子中,dd['two'] 会直接调用子类的__setitem__()方法,所以结果符合预期。如果其它测试也符合预期的话,最终结果会是{'three': [3, 3], 'one': [1, 1], 'two': [2, 2]}。 然而,初始化和 update() 直接调用的分别是从父类继承的__init__()和__update__(),再由它们隐式地调用__setitem__()方法,此时却并没有调用子类的方法,而是调用了父类的方法,导致结果超出预期! 官方 Python 这种实现双重规则的做法,有点违背大家的常识,如果不加以注意,搞不好就容易踩坑。 那么,为什么会出现这种例外的情况呢? 4、内置类型的方法的真面目 我们知道了内置类型不会隐式地调用子类覆盖的方法,接着,就是Python猫的刨根问底时刻:为什么它不去调用呢? 《流畅的Python》书中没有继续追问,不过,我试着胡乱猜测一下(应该能从源码中得到验证):内置类型的方法都是用 C 语言实现的,事实上它们彼此之间并不存在着相互调用,所以就不存在调用时的查找优先级问题。 也就是说,前面的“__init__()和__update__()会隐式地调用__setitem__()方法”这种说法并不准确! 这几个魔术方法其实是相互独立的!__init__()有自己的 setitem 实现,并不会调用父类的__setitem__(),当然跟子类的__setitem__()就更没有关系了。 从逻辑上理解,字典的__init__()方法中包含__setitem__()的功能,因此我们以为前者会调用后者,这是惯性思维的体现,然而实际的调用关系可能是这样的: 左侧的方法打开语言界面之门进入右侧的世界,在那里实现它的所有使命,并不会折返回原始界面查找下一步的指令(即不存在图中的红线路径)。不折返的原因很简单,即 C 语言间代码调用效率更高,实现路径更短,实现过程更简单。 同理,dict 类型的 get() 方法与__getitem__()也不存在调用关系,如果子类只覆盖了__getitem__()的话,当子类调用 get() 方法时,实际会使用到父类的 get() 方法。(PS:关于这一点,《流畅的Python》及 PyPy 文档的描述都不准确,它们误以为 get() 方法会调用__getitem__()) 也就是说,Python 内置类型的方法本身不存在调用关系,尽管它们在底层 C 语言实现时,可能存在公共的逻辑或能被复用的方法。 我想到了“Python为什么”系列曾分析过的《Python 为什么能支持任意的真值判断?》。在我们写if xxx时,它似乎会隐式地调用__bool__()和__len__()魔术方法,然而实际上程序依据 POP_JUMP_IF_FALSE 指令,会直接进入纯 C 代码的逻辑,并不存在对这俩魔术方法的调用! 因此,在意识到 C 实现的特殊方法间相互独立之后,我们再回头看内置类型的子类化,就会有新的发现: 父类的__init__()魔术方法会打破语言界面实现自己的使命,然而它跟子类的__setitem__()并不存在通路,即图中红线路径不可达。 特殊方法间各行其是,由此,我们会得出跟前文不同的结论:实际上 Python 严格遵循了“子类方法先于父类方法”继承原则,并没有破坏常识! 最后值得一提的是,__missing__()是一个特例。《流畅的Python》仅仅简单而含糊地写了一句,没有过多展开。 经过初步实验,我发现当子类定义了此方法时,get() 读取不存在的 key 时,正常返回 None;但是 __getitem__() 和 dd['xxx'] 读取不存在的 key 时,都会按子类定义的__missing__()进行处理。 我还没空深入分析,恳请知道答案的同学给我留言。 5、内置类型子类化的最佳实践 综上所述,内置类型子类化时并没有出问题,只是由于我们没有认清特殊方法(C 语言实现的方法)的真面目,才会导致结果偏差。 那么,这又召唤出了一个新的问题:如果非要继承内置类型,最佳的实践方式是什么呢? 首先,如果在继承内置类型后,并不重写(overwrite)它的特殊方法的话,子类化就不会有任何问题。 其次,如果继承后要重写特殊方法的话,记得要把所有希望改变的方法都重写一遍,例如,如果想改变 get() 方法,就要重写 get() 方法,如果想改变 __getitem__()方法,就要重写它…… 但是,如果我们只是想重写某种逻辑(即 C 语言的部分),以便所有用到该逻辑的特殊方法都发生改变的话,例如重写__setitem__()的逻辑,同时令初始化和update()等操作跟着改变,那么该怎么办呢? 我们已知特殊方法间不存在复用,也就是说单纯定义新的__setitem__()是不够的,那么,怎么才能对多个方法同时产生影响呢? PyPy 这个非官方的 Python 版本发现了这个问题,它的做法是令内置类型的特殊方法发生调用,建立它们之间的连接通路。 官方 Python 当然也意识到了这么问题,不过它并没有改变内置类型的特性,而是提供出了新的方案:UserString、UserList、UserDict…… 除了名字不一样,基本可以认为它们等同于内置类型。 这些类的基本逻辑是用 Python 实现的,相当于是把前文 C 语言界面的某些逻辑搬到了 Python 界面,在左侧建立起调用链,如此一来,就解决了某些特殊方法的复用问题。 对照前文的例子,采用新的继承方式后,结果就符合预期了: from collections import UserDict class DoppelDict(UserDict): def __setitem__(self, key, value): super().__setitem__(key, [value] * 2) dd = DoppelDict(one=1) # {'one': [1, 1]} dd['two'] = 2 # {'one': [1, 1], 'two': [2, 2]} dd.update(three=3) # {'one': [1, 1], 'two': [2, 2], 'three': [3, 3]} 显然,如果要继承 str/list/dict 的话,最佳的实践就是继承collections库提供的那几个类。 6、小结 写了这么多,是时候作 ending 了~~ 在本系列的前一篇文章中,Python猫从查找顺序与运行速度两方面,分析了“为什么内置函数/内置类型不是万能的”,本文跟它一脉相承,也是揭示了内置类型的某种神秘的看似是缺陷的行为特征。 本文虽然是从《流畅的Python》书中获得的灵感,然而在语言表象之外,我们还多追问了一个“为什么”,从而更进一步地分析出了现象背后的原理。 简而言之,内置类型的特殊方法是由 C 语言独立实现的,它们在 Python 语言界面中不存在调用关系,因此在内置类型子类化时,被重写的特殊方法只会影响该方法本身,不会影响其它特殊方法的效果。 如果我们对特殊方法间的关系有错误的认知,就可能会认为 Python 破坏了“子类方法先于父类方法”的基本继承原则。(很遗憾《流畅的Python》和 PyPy 都有此错误的认知) 为了迎合大家对内置类型的普遍预期,Python 在标准库中提供了 UserString、UserList、UserDict 这些扩展类,方便程序员来继承这些基本的数据类型。 写在最后:本文属于“Python为什么”系列(Python猫出品),该系列主要关注 Python 的语法、设计和发展等话题,以一个个“为什么”式的问题为切入点,试着展现 Python 的迷人魅力。若你有其它感兴趣的话题,欢迎填在《Python的十万个为什么? 》里的调查问卷中。
本文出自“Python为什么”系列,请查看全部文章 在Python猫的上一篇文章中,我们对比了两种创建列表的方法,即字面量用法 [] 与内置类型用法 list(),进而分析出它们在运行速度上的差异。 在分析为什么 list() 会更慢的时候,文中说到它需要经过名称查找与函数调用两个步骤,那么,这就引出了一个新的问题:list() 不是内置类型么,为什么它不能直接就调用创建列表的逻辑呢?也就是说,为什么解释器必须经过名称查找,才能“认识”到该做什么呢? 其实原因很简单:内置函数/内置类型的名称并不是关键字,它们只是解释器内置的一种便捷功能,方便开发者开箱即用而已。 PS:内置函数 built-in function 和内置类型 built-in type 很相似,但 list() 实际是一种内置类型而不是内置函数。我曾对这两种易混淆的概念做过辨析,请查看这篇文章。为了方便理解与表述,以下统称为内置函数。 1、内置函数的查找优先级最低 内置函数的名称并不属于关键字,它们是可以被重新赋值的。 比如下面这个例子: # 正常调用内置函数 list(range(3)) # 结果:[0, 1, 2] # 定义任意函数,然后赋值给 list def test(n): print("Hello World!") list = test list(range(3)) # 结果:Hello World! 在这个例子中,我们将自定义的 test 赋值给了 list,程序并没有报错。这个例子甚至还可以改成直接定义新的同名函数,即"def list(): …"。 这说明了 list 并不是 Python 限定的关键字/保留字。 查看官方文档,可以发现 Python 3.9 有 35 个关键字,明细如下: 如果我们将上例的 test 赋值给任意一个关键字,例如"pass=test",就会报错:SyntaxError: invalid syntax。 由此,我们可以从这个角度看出内置函数并不是万能的:它们的名称并不像关键字那般稳固不变,虽然它们处在系统内置作用域里,但是却可以被用户局部作用域的对象所轻松拦截掉! 因为解释器查找名称的顺序是“局部作用域->全局作用域->内置作用域”,因此内置函数其实是处在最低优先级。 对于新手来说,这有一定的可能会发生意想不到的情况(内置函数有 69 个,要全记住是有难度的)。 那么,为什么 Python 不把所有内置函数的名称都设为不可复写的关键字呢? 一方面原因是它想控制关键字的数量,另一方面可能是想留给用户更多的自由。内置函数只是解释器的推荐实现而已,开发者可以根据需要,实现出与内置函数同名的函数。 不过,这样的场景极少,而且开发者一般会定义成不同名的函数,以 Python 标准库为例,ast模块有 literal_eval() 函数(对标 eval() 内置函数)、pprint 模块有 pprint() 函数(对标 print() 内置函数)、以及itertools模块有 zip_longest() 函数(对标 zip() 内置函数)…… 2、内置函数可能不是最快的 由于内置函数的名称并非保留的关键字,以及它处于名称查找的末位顺序,所以内置函数有可能不是最快的。 上篇文章展示了 [] 比 list() 快 2~3 倍的事实,其实这还可以推广到 str()、tuple()、set()、dict() 等等内置类型中,都是字面量用法稍稍快于内置类型用法。 对于这些内置类型,当我们调用 xxx() 时,可以简单理解成正在做类的实例化。在面向对象语言中,类先实例化再使用,这是再正常不过的。 但是,这样的做法有时也显得繁琐。为了方便使用,Python 给一些常用的内置类型提供了字面量表示法,也就是""、[]、()、{} 等等,表示字符串、列表、元组和字典等数据类型。 文档出处:https://docs.python.org/3/reference/lexical_analysis.html#delimiters 一般而言,所有编程语言都必须有一些字面量表示,但基本都局限在数字类型、字符串、布尔类型以及 null 之类的基础类型。 Python 中还增加了几种数据结构类型的字面量,所以是更为方便的,同时这也解释了为什么内置函数可能不是最快的。 一般而言,同样的完备功能,内置函数总是比我们自定义的函数要快,因为解释器可以做一些底层的优化,例如 len() 内置函数肯定比用户定义的 x.len() 函数快。 有些人据此形成了“内置函数总是更快”的认识误区。 解释器内置函数相对于用户定义函数,前者接近于走后门;而字面量表示法相对于内置函数,前者是在走更快的后门。 也就是说,在有字面量表示法的情况下,某些内置函数/内置类型并不是最快的! 小结 诚然,Python 本身并不是万能的,那它的任何语法构成部分(内置函数/类型),就更不是万能的了。但是,一般我们会认为内置函数/类型总归是“高人一等”的,是受到诸多特殊优待的,显得像是“万能的”。 本文从“list() 竟然会败给 []”破题,从两个角度揭示了内置函数其实存在着某种不足:内置函数的名称并不是关键字,而内置作用域位于名称查找的最低优先级,因此在调用时,某些内置函数/类型的执行速度就明显慢于它们对应的字面量表示法。 本文对上一个“Python为什么”话题做了延展讨论,一方面充实了前面的内容,另一方面,也有助于大家理解 Python 的几个基础概念及其实现。 如果你喜欢本文,请点赞支持下吧!另外,我还写了 20+ 篇类似的话题,请关注Python猫查看,并在 Github 上给我一颗小星星吧~~ --->>>最后是福利时刻: 我把两年写作的 100 多篇精品文章集结成了一本 700 多页的《优雅的Python》电子书,诚意推荐!!请在微信关注Python猫 ,回复“优雅”两字获取~~
本文出自“Python为什么”系列,请查看全部文章 在日常使用 Python 时,我们经常需要创建一个列表,相信大家都很熟练了吧? # 方法一:使用成对的方括号语法 list_a = [] # 方法二:使用内置的 list() list_b = list() 上面的两种写法,你经常使用哪一个呢?是否思考过它们的区别呢? 让我们开门见山,直接抛出本文的问题吧:两种创建列表的 [] 与 list() 写法,哪一个更快呢,为什么它会更快呢? 注:为了简化问题,我们以创建空列表为例进行分析。关于列表的更多介绍与用法说明,可以查看这篇文章 1、 [] 是 list() 的三倍快 对于第一个问题,使用timeit模块的 timeit() 函数就能简单地测算出来: >>> import timeit >>> timeit.timeit('[]', number=10**7) >>> timeit.timeit('list()', number=10**7) 如上图所示,在各自调用一千万次的情况下,[] 创建方式只花费了 0.47 秒,而 list() 创建方式要花费 1.75 秒,所以,后者的耗时是前者的 3.7 倍! 这就回答了刚才的问题:创建空列表时,[] 要比 list() 快不少。 注:timeit() 函数的效率跟运行环境相关,每次执行结果会有微小差异。我在 Python3.8 版本实验了几次,总体上 [] 速度是 list() 的 3 倍多一点。 2、list() 比 [] 执行步骤多 那么,我们继续来分析一下第二个问题:为什么 [] 会更快呢? 这一次我们可以使用dis模块的 dis() 函数,看看两者执行的字节码有何差别: >>> from dis import dis >>> dis("[]") >>> dis("list()") 如上图所示,[] 的字节码有两条指令(BUILD_LIST 与 RETURN_VALUE),而 list() 的字节码有三条指令(LOAD_NAME、CALL_FUNCTION 与 RETURN_VALUE)。 这些指令意味着什么呢?该如何理解它们呢? 首先,对于 [],它是 Python 中的一组字面量(literal),像数字之类的字面量一样,表示确切的固定值。 也就是说,Python 在解析到它时,就知道它要表示一个列表,因此会直接调用解释器中构建列表的方法(对应 BUILD_LIST ),来创建列表,所以是一步到位。 而对于 list(),“list”只是一个普通的名称,并不是字面量,也就是说解释器一开始并不认识它。 因此,解释器的第一步是要找到这个名称(对应 LOAD_NAME)。它会按照一定的顺序,在各个作用域中逐一查找(局部作用域--全局作用域--内置作用域),直到找到为止,找不到则会抛出NameError。 解释器看到“list”之后是一对圆括号,因此第二步是把这个名称当作可调用对象来调用,即把它当成一个函数进行调用(对应 CALL_FUNCTION)。 因此,list() 在创建列表时,需要经过名称查找与函数调用两个步骤,才能真正开始创建列表(注:CALL_FUNCTION 在底层还会有一些函数调用过程,才能走到跟 BUILD_LIST 相通的逻辑,此处我们忽略不计)。 至此,我们就可以回答前面的问题了:因为 list() 涉及的执行步骤更多,因此它比 [] 要慢一些。 3、list() 的速度提升 看完前两个问题的解答过程,你也许觉得还不够过瘾,而且可能觉得就算知道了这个冷知识,也不会有多大的帮助,似乎那微弱的提升显得微不足道。 但是,我们Python猫出品的《Python为什么》系列一直秉承着孜孜不倦的求知精神,是不可能放着这个问题不去回答的。 而且,由于有发散性思考的习惯,我还想到了另外一个挺有意思的问题:list() 的速度能否提升呢? 我不久前写过一篇文章 正好讨论到这个问题,也就是在刚刚发布的 Python 3.9.0 版本中,它给 list() 实现了更快的 vectorcall 协议,因此执行速度会有一定的提升。 感兴趣的同学可以去 Python 官网下载 3.9 版本。 根据我多轮的测试结果,在新版本中运行 list() 一千万次,耗时大概在 1 秒左右,也就是 [] 运行耗时的 2 倍,相比于前面接近 4 倍的数据,当前版本总体上是提升了不少。 至此,我们已回答完一连串的疑问,如果你觉得有收获,请点赞支持!欢迎大家关注后续更多精彩内容。 本文属于“Python为什么”系列(Python猫出品),该系列主要关注 Python 的语法、设计和发展等话题,以一个个“为什么”式的问题为切入点,试着展现 Python 的迷人魅力。所有文章将会归档在 Github 上,欢迎大家给颗小星星,项目地址:https://github.com/chinesehuazhou/python-whydo
本文出自“Python为什么”系列,请查看全部文章 在这篇文章里,我们会聊一聊为什么 Python 决定不支持 switch 语句。 为什么想要聊这个话题呢? 主要是因为 switch 在其它语言中太常见了,而 Python 却不支持,这样的独特性本身就值得关注,而回答这个问题,也能更加看清 Python 在程序设计上的理念,了解 Python 在语法设计中的决策过程。 本文除了会详细分析 PEP-275 和 PEP-3103,还会介绍到 Python 最新的发展动态(PEP-622),即可能要引入的模式匹配(pattern matching)语法,相信这个话题会开阔大家的眼界,从而对 switch 语法有更为全面的认识。 1、switch 是什么? 在开始正题之前,我们需要先聊聊 switch 是什么? 有些同学可能会第一时间想到它…… 喂~喂~,麻烦收收心,别总想着游戏啦,我们要说的是编程语言中的 switch 语句。 一般而言,switch 的语法格式如下: switch(expression){ case value1: // 语句 break; // 可选 case value2: // 语句 break; // 可选 default: // 可选 // 语句 } 使用流程图来表示,大概是这样的: 它的用法不难理解:switch 语句的值满足哪一个 case 情况,就会执行对应的代码块,执行时遇到 break 就跳出,否则就继续执行下一个 case 分支;一般会在最后放一个 default 分支,作为兜底。 大多数语言都提供了 switch 语句或者极其相似的东西,例如,在 C/C++/Java /Go 等静态语言中,它们都支持 switch-case 结构;在 Ruby 中有类似的 case-when 结构,在 Shell 语言中,有相似的 case-in 结构,在 Perl 中,有 switch-case-else…… switch 语句的好处是支持“单条件多分支”的选择结构,相比 if-else 的二分选择结构,在某些时候会更为简洁清晰。 但是,在 Python 中,我们看不到 switch-case 或者相近的语法结构,这是为什么呢? 2、Python 为什么不支持 switch? 官方文档中有一篇 FAQ 包含了这个问题:Why isn’t there a switch or case statement in Python? FAQ 即 Frequently Asked Questions 的缩写,表示常见问题,官方列了 27 个常见问题,完整清单在此:https://mp.weixin.qq.com/s/zabIvt4dfu_rf7SmGZXqXg 该文档给出了几个建议,告诉了我们几个 switch/case 的替代方案: 使用 if-elif-else 条件判断语句 使用字典,将 case 值与调用的函数映射起来 使用内置 getattr() 检索特定的对象调用方法 曾有人提出过一些提案(即 PEP-275 和 PEP-3103),想给 Python 引入 switch 语法,然而,对于“是否以及如何进行靶场测试”,大家没有达成一致的共识。 靶场测试,即 range test,指的是对武器弹药的技术性能作各种测试验证,与药物的临床试验一样,都是在最终产品交付前的一项关键性测试。 官方文档对于“为什么 Python 不引入 switch”的解释,实际上来源于 Python 之父 Guido van Rossum 在 PEP-3103 中的意见: 出处:https://www.python.org/dev/peps/pep-3103/ A quick poll during my keynote presentation at PyCon 2007 shows this proposal has no popular support. I therefore reject it. 我在 PyCon 2007 的主题演讲中做了一个快速的民意调查,结果表明这个提案没有得到广泛的支持。因此,我拒绝了它。 简而言之,PEP 提案有了,语法实现也有了雏形,但是核心开发者们似乎没有达成一致意见,最终导致提案流产了。 3、PEP-275 与 PEP-3103 说了什么? PEP-3103 是在 2006 年提出的,PEP-275 则是在 2001 年提出的,它们的共同之处是提出了引入 switch 语句的某种必要性、分析了好几种备选的实现方案,然而,结局是都被拒绝了。 出处:https://www.python.org/dev/peps/pep-0275/ 那么,我们就先来回顾一下核心开发者们都做出了哪些讨论,看一看如果 Python 要实现 switch 结构,会是怎么样子的?(PS:PEP 里还涉及其它内容,本文只摘取与 switch 直接相关的部分) PEP-275 提出的语法结构如下: switch EXPR: case CONSTANT: SUITE case CONSTANT: SUITE ... else: SUITE 其中 else 分支是可选的,如果没有它,并且前面的分支都不满足的话,就什么也不做。另外 case 值 constant 支持不同类型,因为 expr 表达式的类型是动态的。 PEP-275 还提出让 switch 不支持掉落(fall-through)行为,即每个 case 分支相互独立而完整,不用像 C 语言那样需要写 break。 该 PEP 还列举了一些其它的 issue: 重用现有关键字,不引入“switch”和“case” 使用新的关键字,避免与 C 的 switch 概念混淆 支持单分支多值选择(例如:case 'a', 'b', 'c': ...) 还有建议支持范围取值判断(例如:case 10..14: ...) 除了首选方案,该 PEP 还记录了几种风格各异的语法方案: case EXPR: of CONSTANT: SUITE of CONSTANT: SUITE else: SUITE case EXPR: if CONSTANT: SUITE if CONSTANT: SUITE else: SUITE when EXPR: in CONSTANT_TUPLE: SUITE in CONSTANT_TUPLE: SUITE ... else: SUITE PEP-275 记录下了不少重要的思路和问题,为 PEP-3103 的出现做了很好的铺垫。 那么,我们再来看看由 Guido 编写的 PEP-3103 说了些什么吧。 它首先认可了 PEP-275 中的两个基础设定,例如,实现“隐式的 break”,不让 case 分支出现 fall-through 这种转移控制权的情况(其它语言似乎都要求显式地写 break);else 分支是可选的,复用 else 关键字,而不用引入“default”。 对于 PEP-275 提倡的那种风格,Guido 比较认可,但也认为它的问题是缩进层次太多,因此建议减少代码分支缩进的空格数,例如本来缩进 4 空格,改为缩进 2 空格。 PEP-3103 还列举了另外三种实现方案,分析了它们的差异以及问题,具体内容从略,这里只给大家看看它们的风格: # case 分支不缩进 switch EXPR: case EXPR: SUITE case EXPR: SUITE .... else: SUITE # switch 语句后不加冒号 switch EXPR case EXPR: SUITE case EXPR: SUITE .... else: SUITE # 省略 case 关键字 switch EXPR: EXPR: SUITE EXPR: SUITE ... else: SUITE 在基础语法之外,Guido 花了很多篇幅来讨论扩展语法(Extended Syntax),即在一个 case 分支中实现匹配多个值的复杂情况: case EXPR, EXPR, ...: # Guido 优选的 case in EXPR_LIST: case *EXPR: case [*]EXPR, [*]EXPR, ...: case *(EXPR, EXPR, ...): 他重点考虑到的问题包括:switch 中表达式的结果是元组或可迭代对象的情况、case 的值被看成元组解包的情况、在 case 分支作“*”星号操作…… 接着,Guido 又用了非常非常多的篇幅来分析该如何实现 switch,其中讨论到的主要思路有: 使用等价的 if-elif 链来定义 switch 语句(可能会做些优化) 同上,另外所有表达式都必须是可哈希的(hashable) 看作是预先计算的字典的分派(dispatch) PEP 中这部分的内容非常多,因为在每个思路上,Guido 还考虑到了好几种实现路径,这导致了他在复杂分析后的结论是:It is too early to decide( 现在做决定为时尚早)。 阅读完 PEP-3103 后,我总体的感觉是:Guido 的思路非常发散、层次丰富,但是,缺少了他在面对其它问题时那“快刀斩乱麻”式的洞察力。 也就是说,在诸多的可能性方案中,他力求面面俱到,最终无法说服自己做出一个独裁的决定。阻力主要来自于他自己,而不是其他人。 不过,之所以会出现这种情况,也许跟他的预设立场有关:他似乎认为“Python is fine without a switch statement”,因此尽管写了很长的 PEP,但只是在把问题复杂化,把议题搁置起来。 最后,他在 PyCon 上做了一个小范围调查,借此“名正言顺”地拒绝了自己发起的 PEP,试图堵住众人的悠悠之口…… 4、未来会有 switch 语句么? 归结起来,之所以 Python 没有 switch 语句,原因有:switch 的实现细节/功能点未经敲定、没有 switch 也挺好的、有其它不错的方法替代 switch、Guido 的小任性…… 但是,我们还是要追问一句:未来会有 switch 语句么?或者类似的多分支选择结构? 为什么要有此一问呢?原因是有太多语言自带 switch 语句,而且也有很多人尝试编写提供 switch 功能的库(我记得在 PyCoder's Weekly 里曾见到过两次)。 我(Python猫)本人自始至终并不喜欢 switch,几乎可以肯定地说,Python 未来也不会有 switch,但是,它很可能会引入一个类似于 switch 且更为复杂的语法结构! 2020 年 6 月,PEP-622 被提出了,它建议引入在 Scala、Erlang 和 Rust 等语言中的模式匹配语法(pattern matching)。 截至 2020 年 10 月,该 PEP 已被分解成另外三个 PEP(634-636),目前都处于草案阶段。考虑到核心开发者们的参与情况以及话题讨论的情况,这些提案极有可能会在未来版本(比如正在开发中的 3.10)中实现。 以一个求平均数的函数为例,模式匹配语法可以实现成这样: def average(*args): match args: case [x, y]: # captures the two elements of a sequence return (x + y) / 2 case [x]: # captures the only element of a sequence return x case []: return 0 case x: # captures the entire sequence return sum(x) / len(x) match-case 结构神似于 switch-case 结构,然而它基于模式(pattern)而非表达式(expression),因此有更多待考虑的细节问题,也有更为广阔的应用空间。 对此话题感兴趣的读者,建议去查阅这几个新的 PEP。 最后,让我们回到标题中的问题:Python 为什么不支持 switch 语句? 官方文档的 FAQ 对此问题有一个解答,告诉我们有几个不错的替代写法,同时也留下了一条线索:曾有 PEP 提议引入 switch,只是没有成功实现。 沿着这条线索,本文拆解了 PEP-275 和 PEP-3103 这两篇文档,带大家看到了 Python 社区里提出过的风格各异的 switch 方案,以及诸多的悬而未决的问题。 最后,我们还关注到了最新的 PEP-622 的动态,看起来 switch 的“孪生兄弟” match 语法有望引入到 Python 中!switch 话题的讨论似乎要终止了,但是另一个更大的话题正在进行中! 本文属于“Python为什么”系列(Python猫出品),该系列主要关注 Python 的语法、设计和发展等话题,以一个个“为什么”式的问题为切入点,试着展现 Python 的迷人魅力。所有文章将会归档在 Github 上,欢迎大家给颗小星星,项目地址:https://github.com/chinesehuazhou/python-whydo
某位 A 同学发了我一张截图,问为何结果中出现了负数? 看了图,我第一感觉就是数据溢出了。数据超出能表示的最大值,就会出现奇奇怪怪的结果。 然后,他继续发了张图,内容是 print(100000*208378),就是直接打印上图的 E[0]*G[0],结果是 20837800000,这是个正确的结果。 所以新的问题是:如果说上图的数据溢出了,为何直接相乘的数却没有溢出? 由于我一直忽视数据的表示规则(整型的上限是多少?),而且对 Numpy 了解不多,还错看了图中结果,误以为每一个数据都是错误的,所以就解答不出来。 最后,经过学习群里的一番讨论,我才终于明白是怎么回事,所以本文把相关知识点做个梳理。 在正式开始之前,先总结一下上图会引出的话题: Python 3 中整数的上限是多少?Python 2 呢? Numpy 中整数的上限是多少?出现整数溢出该怎么办? 关于第一个问题,先看看 Python 2,它有两种整数: 一种是短整数,也即常说的整数,用 int 表示,有个内置函数 int()。其大小有限,可通过sys.maxint() 查看(取决于平台是 32 位还是 64 位) 一种是长整数,即大小无限的整数,用 long 表示,有个内置函数 long()。写法上是在数字后面加大写字母 L 或小写的 l,如 1000L 当一个整数超出短整数范围时,它会自动采用长整数表示。举例,打印 2**100 ,结果会在末尾加字母 L 表示它是长整数。 但是到了 Python 3,情况就不同了:它仅有一种内置的整数,表示为 int,形式上是 Python 2 的短整数,但实际上它能表示的范围无限,行为上更像是长整数。无论多大的数,结尾都不需要字母 L 来作区分。 也就是说,Python 3 整合了两种整数表示法,用户不再需要自行区分,全交给底层按需处理。 理论上,Python 3 中的整数没有上限(只要不超出内存空间)。这就解释了前文中直接打印两数相乘,为什么结果会正确了。 PEP-237(Unifying Long Integers and Integers)中对这个转变作了说明。它解释这样做的 目的: 这会给新的 Python 程序员(无论他们是否是编程新手)减少一项上手前要学的功课。 Python 在语言运用层屏蔽了很多琐碎的活,比如内存分配,所以,我们在使用字符串、列表或字典等对象时,根本不用操心。整数类型的转变,也是出于这样的便利目的。(坏处是牺牲了一些效率,在此就不谈了) 回到前面的第二个话题:Numpy 中整数的上限是多少? 由于它是 C 语言实现,在整数表示上,用的是 C 语言的规则,也就是会区分整数和长整数。 有一种方式可查看: import numpy as np a = np.arange(2) type(a[0]) # 结果:numpy.int32 也就是说它默认的整数 int 是 32 位,表示范围在 -2147483648 ~ 2147483647。 对照前文的截图,里面只有两组数字相乘时没有溢出:100007*4549、100012*13264,其它数据组都溢出了,所以出现奇怪的负数结果。 Numpy 支持的数据类型要比 Python 的多,相互间的区分界限很多样: 截图来源:https://www.runoob.com/numpy/numpy-dtype.html 要解决整数溢出问题,可以通过指定 dtype 的方式: import numpy as np q = [100000] w = [500000] # 一个溢出的例子: a = np.array(q) b = np.array(w) print(a*b) # 产生溢出,结果是个奇怪的数值 # 一个解决的例子: c = np.array(q, dtype='int64') d = np.array(w, dtype='int64') print(c*d) # 没有溢出:[50000000000] 好了,前面提出的问题就回答完了。来作个结尾吧: Python 3 极大地简化了整数的表示,效果可表述为:整数就只有一种整数(int),没有其它类型的整数(long、int8、int64 之类的) Numpy 中的整数类型对应于 C 语言的数据类型,每种“整数”有自己的区间,要解决数据溢出问题,需要指定更大的数据类型(dtype)
内置函数是 Python 的一大特色,用极简的语法实现很多常用的操作。 它们预先定义在内置命名空间中,开箱即用,所见即所得。Python 被公认是一种新手友好型的语言,这种说法能够成立,内置函数在其中起到了极关键的作用。 举个例子,求字符串 x 的长度,Python 的写法是 len(x) ,而且这种写法对列表、元组和字典等对象也同样适用,只需要传入对应的参数即可。len() 函数是共用的。 这是一种极简哲学的体现:Simple is better than complex。 但是,有些语言并不是这样,例如在 Java 中,字符串类有一个求长度的方法,其它类也有自己的求长度的方法,它们无法共用。每次使用时,通过类或实例来调用。 同样是求字符串长度,Python 的写法: saying = "Hello world!" print(len(saying)) # 结果:12 而在 Java 中,写法可能如下(简化起见): String saying = "Hello world!"; System.out.println(saying.length()); // 结果:12 Python 采用的是一种前缀表达式 ,而 Java 采用的则是后缀表达式 。 除了求长度,Python 的某些内置函数也能在 Java 中找到对应的表达。例如,数值型字符串 s 转化为整型数字,Python 可以用 int(s) 函数,而 Java 可以用 Integer.parseInt(s) ;整型数字转化为字符串,Python 可以用 str(i) ,而 Java 也有 String.valueOf(i) 。 Python 的内置函数不与特定的类绑定,它们是一级对象。而 Java 的“函数”则无法脱离类而存在,它们只是附属品。 从直观角度来看,Python 的表达似乎是更优的。但是,它们并不具有可比性 ,因为这是两套语言系统,各有独特的范畴背景,并不能轻易地化约。 就好比是,不能因为拉丁字母笔画简单,就说它优于汉字,因为在表意时,字母(表音文字)是远逊于汉字(表意文字)的。同样的,日本借用了汉字的偏旁部首而造出来的文字,虽然更省笔墨,但是也完全丧失了意蕴。 以此类比,Python 的内置函数虽有简便之美,但却丢失了某些表意功能。有些人在质疑/抨击 Python 的时候,也喜欢拿这点说事,认为这是 Python 的设计缺陷。 这就引出本文最想讨论的一个问题来:为什么 Python 要设计成 len(x) 这种前缀表达,而不是 x.len() 这样的后缀表达呢? 事实上,后缀设计也是可行的,以 Python 中列表的两个方法为例: mylist = [2, 1, 3, 5, 4] mylist.sort() print(mylist) # [1, 2, 3, 4, 5] mylist.reverse() print(mylist) # [5, 4, 3, 2, 1] 它们都是通过列表对象来调用,并不是凭空从内置命名空间中拿来的。语义表达得也很清楚,就是对 mylist 做排序和逆转。 恰恰那么巧,它们还有两个同父异母的兄弟 sorted() 与 reversed(),这俩是前缀表达型。 mylist = [2, 1, 3, 5, 4] sort_list = sorted(mylist) print(sort_list) # [1, 2, 3, 4, 5] reverse_list = reversed(mylist) print(list(reverse_list)) # [4, 5, 3, 1, 2] 不同的写法,都在做同一件事(不考虑它们的副作用)。因此,后缀语法并非不可行,之所以不用,那肯定是刻意的设计。 回到前面的问题:为什么是 len(x) ,而不是 x.len(x),这根源于 Python 的什么设计思想呢? Python 之父 Guido van Rossum 曾经解释过这个问题(链接见文末),有两个原因: 对于某些操作,前缀符比后缀更好读——前缀(和中缀)表示法在数学中有着悠久的历史,其视觉效果有助于数学家思考问题。我们可以简单地把公式 x*(a + b) 重写成 x*a + x*b ,但同样的事,以原生的面向对象的方式实现,就比较笨拙。 当读到 len(x) 时,我就 知道 这是在求某对象的长度。它告诉我了两点:返回值是一个整数,参数是某种容器。但当读到 x.len() 时,我必须事先知道某种容器 x,它实现了一个接口,或者继承了一个拥有标准 len() 方法的类。我们经常会目睹到这种混乱:一个类并没有实现映射(mapping)接口,却拥有 get() 或 keys() 方法,或者某些非文件对象,却拥有一个 write() 方法。 解释完这两个原因之后,Guido 还总结成一句话说:“I see 'len' as a built-in operation ”。这已经不仅是在说 len() 更可读易懂了,而完全是在拔高 len() 的地位。 这就好比说,分数 ½ 中的横线是数学中的一个“内置”表达式,并不需要再实现什么接口之类的,它自身已经表明了“某数除以某数 ”的意思。不同类型的数(整数、浮点数、有理数、无理数...)共用同一个操作符,不必为每类数据实现一种求分数的操作。 优雅易懂是 Python 奉行的设计哲学 ,len() 函数的前缀表达方式是最好的体现。我想起在《超强汇总:学习Python列表,只需这篇文章就够了》这篇文章中,曾引述过 Guido 对“为什么索引从 0 开始 ”的解释。其最重要的原因,也正是 0-based 索引最优雅易懂。 让我们来先看看切片的用法。可能最常见的用法,就是“取前 n 位元素”或“从第i 位索引起,取后 n 位元素”(前一种用法,实际上是 i == 起始位的特殊用法)。如果这两种用法实现时可以不在表达式中出现难看的 +1 或 -1,那将会非常的优雅。 使用 0-based 的索引方式、半开区间切片和缺省匹配区间的话(Python最终采用这样的方式),上面两种情形的切片语法就变得非常漂亮:a[:n] 和 a[i:i+n],前者是 a[0:n] 的缩略写法。 所以,我们能说 len(x) 击败 x.len() ,支撑它的是一种化繁为简、纯粹却深邃的设计思想。 面向对象的编程语言自发明时起,就想模拟我们生活于其中的现实世界。可是什么类啊、接口啊、对象啊、以及它们的方法啊,这些玩意的毒,有时候蒙蔽了我们去看见世界本质的眼睛。 桌子类有桌子类的求长度方法,椅子类有椅子类的求长度方法,无穷无尽,可现实真是如此么?求长度的方法就不能是一种独立存在的对象么?它之所以存在,是因为有“对象”存在,而不是因为有某个类才存在啊。 所以,我想说,len(x) 击败 x.len(),这还体现了 Python 对世界本质的洞察 。 求某个对象的长度,这种操作独立于对象之外而存在,并不是该对象内部所有的一种属性或功能。从这个角度理解,我们能够明白,为什么 Python 要设计出内置函数? 内置函数其实是对世界本质的一种捕捉。 这些见微知著的发现,足够使我们爱上这门语言了。人生苦短,我用 Python。 关联阅读: Guido 解释 len 的由来:http://suo.im/4ImAEo Guido 解释 0 索引的由来:http://suo.im/5cr12S
近几天,很多公众号发布了 Python 官方文档的消息。然而,一个特别奇怪的现象就发生了,让人啼笑皆非。 Python 文档的中文翻译工作一直是“默默无闻”,几个月前,我还吐槽过这件事《再聊聊Python中文社区的翻译》,当时我们的进度是 10.3%,远远落后于日本和法国,甚至落后于巴西! 这次所谓的中文版,当然是未完成翻译的残品。刚查了下,整体进度是 19.7%。 有的公众号在发布消息的时候,说明了这不是官宣、不是正式发布版,还指出了中文版的访问地址是隐藏入口。这都是忠于事实的。 然而,怪异的事情就在于,还有一些公众号在发布时,不知怎么误传,这个消息变成了官方正式发布、全部翻译完成、激动人心期盼已久,至于这个隐藏入口跳转问题、下载的文档为何是英文版的问题,则完全无法解释。这带来了极大的误导。 由于曾搜集过 PEP 文档的翻译,我无意中也了解到关于翻译官方文档的一些情况。有以下几个现状吧: 1、人员分散,缺乏核心。就我所见,在V站、华蟒邮件组、简书、知乎,分别有不同的人发起过翻译召集或者咨询,然而应者无几,并没有形成过足够大的核心组织。 2、官方的翻译?Python 官方在 2017 年的 PEP-545 中推出了一种翻译模式,各国语言的翻译在协作平台Transifex 上进行。实际上,这才是官方认可的版本,也是最终发布的依据。前文说的进度,就是指在这个平台上的进度。 3、野生的翻译?所谓野生,这里指的是不在Transifex 上的翻译。网上能看到有人零星地翻译了一些部分,但成果没有合入到官方平台上。社区内的译者还是挺多的,能力也有,只是太分散了。邮件组里就有位大佬,他说翻译过 40 多个标准库以及 C 模块的文档,但懒得组织。有人尝试组织过,时间久远的不说,就在去年夏天,某位在 PHP 界知名的站长开了个 Python 社区,召集了一批译者。他们译出了 Python 3.7 官方文档的入门教程部分,然而,后续内容的计划,似乎被放弃了。 关于对待翻译的态度,似乎多数人表示:感兴趣,但是时间少,希望有人牵头组织,可以参与作贡献。我本人也怀着同样的想法。作为参与者、见证者、沾光者就好了,谁愿意花费那么多精力,承担重任,周旋策划,最后可能还讨不到好呢? 写文章是重口难调,翻译文档更是如此,碰上质疑翻译水平的,还可商榷一下,而遇到下面这种杠精,只能是破坏心情。 前面提到的那位站长,提出在他的社区维护一份长久维护的版本。事实上,他们真的做出了点实事,除了入门教程,还完成了两本经典书籍的翻译。然而,他们也招到了非议:不当的“官方文档”措辞、不合入官方使用的平台、网站的商业化运营...... 空谈的人总是有他们的理,不对事情做贡献,还无视别人的贡献。诚然,宣称“官方”中文文档,确实不妥,这只是个人/社区的行为,改正就好了;至于合入官方的途径,只需有翻译成果,也不难做到;最后,一个站点接些贴片广告,哪有什么不妥? 我所了解到的社区翻译情况,大致如上。 总体上,分裂分散现象严重,随性自由之处跟 Python 这语言倒挺像,而各怀能力各出成绩的现象,也跟为数众多的三方模块神似。 也有默默在做事的人。从 4 个月前的 10% ,增长到现在的 20%,我们的翻译进度暴涨,这背后不知有几人在持续作出贡献?而他们还不为人知。 距离官方文档全部译出,还有大步路要走,现实情况得认清。 我总体上是乐观的。所以,最后聊个题外话。 这几天,有个热得不行的话题——996.ICU ,才仅仅一周,Github star 数已经破 10 万,绝对创造纪录了。程序员发起的活动,就是有如此大的力量。 就在本文写作过程中,Python 之父也给了这个项目 star ,而且发推声援。 在官方文档的翻译事情上,或许我们是有点脱轨了,不过不要紧,在使用全球最大的同性交友平台上,我们是与国际接轨的。 还有啊,等过完了愚人节,我们还有个节日也是与国际接轨的——国际劳动节,纪念 1886 年芝加哥工人大罢工,确立每日 8 小时工作制的节日。 相关链接: 翻译进度:https://www.transifex.com/python-doc/python-newest V站话题:https://neue.v2ex.com/t/477400#reply147
Python 提供了很多内置的工具函数(Built-in Functions),在最新的 Python 3 官方文档中,它列出了 69 个。 大部分函数是我们经常使用的,例如 print()、open() 与 dir(),而有一些函数虽然不常用,但它们在某些场景下,却能发挥出不一般的作用。内置函数们能够被“提拔”出来,这就意味着它们皆有独到之处,有用武之地。 因此,掌握内置函数的用法,就成了我们应该点亮的技能。 在《Python进阶:如何将字符串常量转为变量?》这篇文章中,我提到过 eval() 和 exec() ,但对它们并不太了解。为了弥补这方面知识,我就重新学习了下。这篇文章是一份超级详细的学习记录,系统、全面而深入地辨析了这两大函数。 1、eval 的基本用法 语法:eval(expression, globals=None, locals=None) 它有三个参数,其中 expression 是一个字符串类型的表达式或代码对象,用于做运算;globals 与 locals 是可选参数,默认值是 None。 具体而言,expression 只能是单个表达式,不支持复杂的代码逻辑,例如赋值操作、循环语句等等。(PS:单个表达式并不意味着“简单无害”,参见下文第 4 节) globals 用于指定运行时的全局命名空间,类型是字典,缺省时使用的是当前模块的内置命名空间。locals 指定运行时的局部命名空间,类型是字典,缺省时使用 globals 的值。两者都缺省时,则遵循 eval 函数执行时的作用域。值得注意的是,这两者不代表真正的命名空间,只在运算时起作用,运算后则销毁。 x = 10 def func(): y = 20 a = eval('x + y') print('a: ', a) b = eval('x + y', {'x': 1, 'y': 2}) print('x: ' + str(x) + ' y: ' + str(y)) print('b: ', b) c = eval('x + y', {'x': 1, 'y': 2}, {'y': 3, 'z': 4}) print('x: ' + str(x) + ' y: ' + str(y)) print('c: ', c) func() 输出结果: a: 30 x: 10 y: 20 b: 3 x: 10 y: 20 c: 4 由此可见,当指定了命名空间的时候,变量会在对应命名空间中查找。而且,它们的值不会覆盖实际命名空间中的值。 2、exec 的基本用法 语法:exec(object[, globals[, locals]]) 在 Python2 中 exec 是个语句,而 Python3 将其改造成一个函数,就像 print 一样。exec() 与 eval() 高度相似,三个参数的意义和作用相近。 主要的区别是,exec() 的第一个参数不是表达式,而是代码块,这意味着两点:一是它不能做表达式求值并返回出去,二是它可以执行复杂的代码逻辑,相对而言功能更加强大,例如,当代码块中赋值了新的变量时,该变量可能 在函数外的命名空间中存活下来。 >>> x = 1 >>> y = exec('x = 1 + 1') >>> print(x) >>> print(y) 2 None 可以看出,exec() 内外的命名空间是相通的,变量由此传递出去,而不像 eval() 函数,需要一个变量来接收函数的执行结果。 3、一些细节辨析 两个函数都很强大,它们将字符串内容当做有效的代码执行。这是一种字符串驱动的事件 ,意义重大。然而,在实际使用过程中,存在很多微小的细节,此处就列出我所知道的几点吧。 常见用途:将字符串转成相应的对象,例如 string 转成 list ,string 转成 dict,string 转 tuple 等等。 >>> a = "[[1,2], [3,4], [5,6], [7,8], [9,0]]" >>> print(eval(a)) [[1, 2], [3, 4], [5, 6], [7, 8], [9, 0]] >>> a = "{'name': 'Python猫', 'age': 18}" >>> print(eval(a)) {'name': 'Python猫', 'age': 18} # 与 eval 略有不同 >>> a = "my_dict = {'name': 'Python猫', 'age': 18}" >>> exec(a) >>> print(my_dict) {'name': 'Python猫', 'age': 18} eval() 函数的返回值是其 expression 的执行结果,在某些情况下,它会是 None,例如当该表达式是 print() 语句,或者是列表的 append() 操作时,这类操作的结果是 None,因此 eval() 的返回值也会是 None。 >>> result = eval('[].append(2)') >>> print(result) None exec() 函数的返回值只会是 None,与执行语句的结果无关,所以,将 exec() 函数赋值出去,就没有任何必要。所执行的语句中,如果包含 return 或 yield ,它们产生的值也无法在 exec 函数的外部起作用。 >>> result = exec('1 + 1') >>> print(result) None 两个函数中的 globals 和 locals 参数,起到的是白名单的作用,通过限定命名空间的范围,防止作用域内的数据被滥用。 conpile() 函数编译后的 code 对象,可作为 eval 和 exec 的第一个参数。compile() 也是个神奇的函数,我翻译的上一篇文章《Python骚操作:动态定义函数》就演示了一个动态定义函数的操作。 吊诡的局部命名空间:前面讲到了 exec() 函数内的变量是可以改变原有命名空间的,然而也有例外。 def foo(): exec('y = 1 + 1\nprint(y)') print(locals()) print(y) foo() 按照前面的理解,预期的结果是局部变量中会存入变量 y,因此两次的打印结果都会是 2,然而实际上的结果却是: 2 {'y': 2} Traceback (most recent call last): ...(略去部分报错信息) print(y) NameError: name 'y' is not defined 明明看到了局部命名空间中有变量 y,为何会报错说它未定义呢? 原因与 Python 的编译器有关,对于以上代码,编译器会先将 foo 函数解析成一个 ast(抽象语法树),然后将所有变量节点存入栈中,此时 exec() 的参数只是一个字符串,整个就是常量,并没有作为代码执行,因此 y 还不存在。直到解析第二个 print() 时,此时第一次出现变量 y ,但因为没有完整的定义,所以 y 不会被存入局部命名空间。 在运行期,exec() 函数动态地创建了局部变量 y ,然而由于 Python 的实现机制是“运行期的局部命名空间不可改变 ”,也就是说这时的 y 始终无法成为局部命名空间的一员,当执行 print() 时也就报错了。 至于为什么 locals() 取出的结果有 y,为什么它不能代表真正的局部命名空间?为什么局部命名空间无法被动态修改?可以查看我之前分享的《Python 动态赋值的陷阱》,另外,官方的 bug 网站中也有对此问题的讨论,查看地址:https://bugs.python.org/issue4831 若想把 exec() 执行后的 y 取出来的话,可以这样:z = locals()['y'] ,然而如果不小心写成了下面的代码,则会报错: def foo(): exec('y = 1 + 1') y = locals()['y'] print(y) foo() #报错:KeyError: 'y' #把变量 y 改为其它变量则不会报错 KeyError 指的是在字典中不存在对应的 key 。本例中 y 作了声明,却因为循环引用而无法完成赋值,即 key 值对应的 value 是个无效值,因此读取不到,就报错了。 此例还有 4 个变种,我想用一套自恰的说法来解释它们,但尝试了很久,未果。留个后话吧,等我想明白,再单独写一篇文章。 4、为什么要慎用 eval() ? 很多动态的编程语言中都会有 eval() 函数,作用大同小异,但是,无一例外,人们会告诉你说,避免使用它。 为什么要慎用 eval() 呢?主要出于安全考虑,对于不可信的数据源,eval 函数很可能会招来代码注入的问题。 >>> eval("__import__('os').system('whoami')") desktop-fa4b888\pythoncat >>> eval("__import__('subprocess').getoutput('ls ~')") #结果略,内容是当前路径的文件信息 在以上例子中,我的隐私数据就被暴露了。而更可怕的是,如果将命令改为rm -rf ~ ,那当前目录的所有文件都会被删除干净。 针对以上例子,有一个限制的办法,即指定 globals 为 {'__builtins__': None} 或者 {'__builtins__': {}} 。 >>> s = {'__builtins__': None} >>> eval("__import__('os').system('whoami')", s) #报错:TypeError: 'NoneType' object is not subscriptable __builtins__ 包含了内置命名空间中的名称,在控制台中输入 dir(__builtins__) ,就能发现很多内置函数、异常和其它属性的名称。在默认情况下,eval 函数的 globals 参数会隐式地携带__builtins__ ,即使是令 globals 参数为 {} 也如此,所以如果想要禁用它,就得显式地指定它的值。 上例将它映射成 None,就意味着限定了 eval 可用的内置命名空间为 None,从而限制了表达式调用内置模块或属性的能力。 但是,这个办法还不是万无一失的,因为仍有手段可以发起攻击。 某位漏洞挖掘高手在他的博客中分享了一个思路,令人大开眼界。其核心的代码是下面这句,你可以试试执行,看看输出的是什么内容。 >>> ().__class__.__bases__[0].__subclasses__() 关于这句代码的解释,以及更进一步的利用手段,详见博客。(地址:https://www.tuicool.com/articles/jeaqe2n) 另外还有一篇博客,不仅提到了上例的手段,还提供了一种新的思路: #警告:千万不要执行如下代码,后果自负。 >>> eval('(lambda fc=(lambda n: [c 1="c" 2="in" 3="().__class__.__bases__[0" language="for"][/c].__subclasses__() if c.__name__ == n][0]):fc("function")(fc("code")(0,0,0,0,"KABOOM",(),(),(),"","",0,""),{})())()', {"__builtins__":None}) 这行代码会导致 Python 直接 crash 掉。具体分析在:https://segmentfault.com/a/1190000011532358 除了黑客的手段,简单的内容也能发起攻击。像下例这样的写法, 将在短时间内耗尽服务器的计算资源。 >>> eval("2 ** 888888888", {"__builtins__":None}, {}) 如上所述,我们直观地展示了 eval() 函数的危害性,然而,即使是 Python 高手们小心谨慎地使用,也不能保证不出错。 在官方的 dumbdbm 模块中,曾经(2014年)发现一个安全漏洞,攻击者通过伪造数据库文件,可以在调用 eval() 时发起攻击。(详情:https://bugs.python.org/issue22885) 无独有偶,在上个月(2019.02),有核心开发者针对 Python 3.8 也提出了一个安全问题,提议不在 logging.config 中使用 eval() 函数,目前该问题还是 open 状态。(详情:https://bugs.python.org/issue36022) 如此种种,足以说明为什么要慎用 eval() 了。同理可证,exec() 函数也得谨慎使用。 5、安全的替代用法 既然有种种安全隐患,为什么要创造出这两个内置方法呢?为什么要使用它们呢? 理由很简单,因为 Python 是一门灵活的动态语言。与静态语言不同,动态语言支持动态地产生代码,对于已经部署好的工程,也可以只做很小的局部修改,就实现 bug 修复。 那有什么办法可以相对安全地使用它们呢? ast 模块的 literal() 是 eval() 的安全替代,与 eval() 不做检查就执行的方式不同,ast.literal() 会先检查表达式内容是否有效合法。它所允许的字面内容如下: strings, bytes, numbers, tuples, lists, dicts, sets, booleans, 和 None 一旦内容非法,则会报错: import ast ast.literal_eval("__import__('os').system('whoami')") 报错:ValueError: malformed node or string 不过,它也有缺点:AST 编译器的栈深(stack depth)有限,解析的字符串内容太多或太复杂时,可能导致程序崩溃。 至于 exec() ,似乎还没有类似的替代方法,毕竟它本身可支持的内容是更加复杂多样的。 最后是一个建议:搞清楚它们的区别与运行细节(例如前面的局部命名空间内容),谨慎使用,限制可用的命名空间,对数据源作充分校验。 关联阅读: Python 动态赋值的陷阱 Python骚操作:动态定义函数 Python与家国天下 Python进阶:如何将字符串常量转为变量? https://docs.python.org/3/library/ast.html#ast.literal_eval
PEP原文 : https://www.python.org/dev/peps/pep-0342/ PEP标题: Coroutines via Enhanced Generators PEP作者: Guido van Rossum, Phillip J. Eby 创建日期: 2005-05-10 合入版本: 2.5 目录 简介 动机 规格摘要 规格:将值发送到生成器 新的生成器方法:send(value) 新的语法:yield 表达式 规格:异常和清理 新语法:yield 允许在try-finally中 新的生成器方法:throw(type,value = None,traceback = None) 新的标准异常:GeneratorExit 新的生成器方法:close() 新的生成器方法:__del__() 可选的扩展 扩展的 continue 表达式 未决问题 示例 参考实现 致谢 参考文献 版权 简介 这个 PEP 在生成器的 API 和语法方面,提出了一些增强功能,使得它们可以作为简单的协程使用。这基本上是将下述两个 PEP 的想法结合起来,如果它被采纳,那它们就是多余的了: PEP-288,关于生成器的属性特征与异常(Attributes and Exceptions)。当前 PEP 沿用了它的下半部分,即生成器的异常(事实上,throw() 的方法名就取自 PEP-288)。PEP-342 用 yield 表达式(这个概念来自 PEP-288 的早期版本)来替换了生成器的属性特征。 PEP-325,生成器支持释放资源。PEP-342 收紧了 PEP-325 中的一些松散的规范,使其更适用于实际的实现。 (译注:PEP-288 和 PEP-325 都没有被采纳通过,它们的核心内容被集成到了 PEP-342里。) 动机 协程是表达许多算法的自然方式,例如模拟/仿真、游戏、异步 I/O、以及其它事件驱动编程或协同的多任务处理。Python 的生成器函数几乎就是协程——但不完全是——因为它们允许暂停来生成值,但又不允许在程序恢复时传入值或异常。它们也不允许在 try-finally 结构的 try 部分作暂停,因此很难令一个异常退出的(aborted)协程来清理自己。 同样地,当其它函数在执行时,生成器不能提供控制,除非这些函数本身是生成器,并且外部生成器之所以写了去 yield,是要为了响应内部生成器所 yield 的值。这使得即使是相对简单的实现(如异步通信)也变得复杂,因为调用任意函数,要么需要生成器变堵塞(block,即无法提供控制),要么必须在每个要调用的函数的周围,添加一大堆引用循环代码(a lot of boilerplate looping code)。 但是,如果有可能在生成器挂起的点上传递进来值或者异常,那么,一个简单的协程调度器或蹦床函数(trampoline function)就能使协程相互调用且不用阻塞——对异步应用程序有巨大好处。这些应用程序可以编写协程来运行非阻塞的 socket I/O,通过给 I/O 调度器提供控制,直到数据被发送或变为可用。同时,执行 I/O 的代码只需像如下方式操作,就能暂停执行,直到 nonblocking_read() 继续产生一个值: data = (yield nonblocking_read(my_socket, nbytes)) 换句话说, 通过给语言和生成器类型增加一些相对较小的增强,Python 不需要为整个程序编写一系列回调,就能支持异步操作,并且对于本该需要数百上千个协作式的多任务伪线程的(co-operatively multitasking pseudothreads)程序,也可以不需要使用资源密集型线程。因此,这些增强功能将给标准 Python 带来 Stackless Python 的许多优点,又无需对 CPython 核心及其 API 进行任何重大的修改。此外,这些增强在任何已经支持生成器的 Python 实现(例如 Jython)上都是可落实的。 规格摘要 通过给生成器类型增加一些简单的方法,以及两个微小的语法调整,Python 开发者就能够使用生成器函数来实现协程与其它的协作式多任务。这些方法和调整是: 重定义 yield 为表达式(expression),而不是语句(statement)。当前的 yield 语句将变成一个 yield 表达式,其值将被丢弃。每当通过正常的 next() 调用来恢复生成器时,yield 表达式的返回值是 None。 为生成器(generator-iterator)添加一个新的 send() 方法,它会恢复生成器,并且 send 一个值作为当前表达式的结果。send() 方法返回的是生成器产生的 next 值,若生成器没有产生值就退出的话,则抛出 StopIteration 。 为生成器(generator-iterator)添加一个新的 throw() 方法,它在生成器暂停处抛出异常,并返回生成器产生的下一个值,若生成器没有产生值就退出的话,则抛出 StopIteration (如果生成器没有捕获传入的异常,或者它引发了其它异常,则该异常会传递给调用者。) 为生成器(generator-iterator)添加一个新的 close() 方法,它在生成器暂停处引发 GeneratorExit 。如果生成器在之后引发 StopIteration (通过正常退出,或者已经被关闭)或 GeneratorExit (通过不捕获异常),则 close() 返回给其调用者。如果生成器产生一个值,则抛出 RuntimeError。如果生成器引发任何其它异常,也会传递给调用者。如果生成器已经退出(异常退出或正常退出),则 close() 不执行任何操作。 增加了支持,确保即使在生成器被垃圾回收时,也会调用 close()。 允许 yield 在 try-finally 块中使用,因为现在允许在 finally 语句中执行垃圾回收或显式地调用 close() 。 实现了所有这些变更的原型补丁已经可用了,可作为当前 Python CVS HEAD 的 SourceForge 补丁。# 1223381 设计规格:将值发送进生成器 新的生成器方法:send(value) 为生成器提出了一种新的方法,即 send() 。它只接收一个参数,并将它发送给生成器。调用 send(None) 完全等同于调用生成器的 next() 方法。使用其它参数调用 send() 也有同样的效果,不同的是,当前生成器表达式产生的值会不一样。 因为生成器在生成器函数体的头部执行,所以在刚刚创建生成器时不会有 yield 表达式来接收值,因此,当生成器刚启动时,禁止使用非 None 参数来调用 send() ,如果调用了,就会抛出 TypeError (可能是由于某种逻辑错误)。所以,在与协程通信前,必须先调用 next() 或 send(None) ,来将程序推进到第一个 yield 表达式。 与 next() 方法一样,send() 方法也返回生成器产生的下一个值,或者抛出 StopIteration 异常(当生成器正常退出,或早已退出时)。如果生成器出现未捕获的异常,则它会传给调用者。 新语法:yield 表达式 yield 语句(yield-statement)可以被用在赋值表达式的右侧;在这种情况下,它就是 yield 表达式(yield-expression)。除非使用非 None 参数调用 send() ,否则 yield 表达式的值就是 None。见下文。 yield 表达式必须始终用括号括起来,除非它是作为顶级表达式而出现在赋值表达式的右侧。所以,下面例子都是合法的: x = yield 42 x = yield x = 12 + (yield 42) x = 12 + (yield) foo(yield 42) foo(yield) 而下面的例子则是非法的(举了一些特例的原因是,当前的 yield 12,42 是合法的): x = 12 + yield 42 x = 12 + yield foo(yield 42, 12) foo(yield, 12) 请注意,如今没有表达式的 yield-语句 和 yield-表达式是合法的。这意味着:当 next() 调用中的信息流被反转时,应该可以在不传递显式的值的情况下 yield (yield 当然就等同于 yield None)。 当调用 send(value) 时,它恢复的 yield 表达式将返回传入的值。当调用 next() 时,它恢复的 yield 表达式将返回 None。如果 yield-表达式(yield-expression)是一个 yield-语句(yield-statement),其返回值会被忽略,就类似于忽略用作语句的函数的返回值。 实际上,yield 表达式就像一个反函数调用(inverted function);它所 yield 的值实际上是当前函数返回(生成)的,而它 return 的值则是通过 send() 传入的参数。 提示:这样的拓展语法,使得它非常地接近于 Ruby。这是故意的。请注意,Python 在阻塞时,通过使用 send(EXPR) 而不是 return EXPR 来传值给生成器,并且在生成器与阻塞之间传递控制权的底层机制完全不同。Python 中的阻塞不会被编译成 thunk,相反,yield 暂停生成器的执行进度。有一些不是这样的特例,在 Python 中,你不能保存阻塞以供后续调用,并且你无法测试是否存在着阻塞。(XXX - 关于阻塞的这些东西似乎不合适,或许 Guido 会编辑下,做澄清。) 设计规格:异常和清理 让生成器对象成为通过调用生成器函数而生成的迭代器。本节中的 g 指的都是生成器对象。 新语法:yield 允许在 try-finally 里 生成器函数的语法被拓展了,允许在 try-finally 语句中使用 yield 语句。 新的生成器方法:throw(type,value = None,traceback = None) g.throw(type, value, traceback) 会使生成器在挂起的点处抛出指定的异常(即在 yield 语句中,或在其函数体的头部、且还未调用 next() 时)。如果生成器捕获了异常,并生成了新的值,则它就是 g.throw() 的返回值。如果生成器没有捕获异常,那 throw() 也会抛出同样的异常(它溜走了)。如果生成器抛出其它异常(包括返回时产生的 StopIteration),那该异常会被 throw() 抛出。总之,throw() 的行为类似于 next() 或 send(),除了它是在挂起点处抛出异常。如果生成器已经处于关闭状态,throw() 只会抛出经过它的异常,而不去执行生成器的任何代码。 抛出异常的效果完全像它所声明的那样: raise type, value, traceback 会在暂停点执行。type 参数不能是 None,且 type 与 value 的类型必须得兼容。如果 value 不是 type 的实例(instance),则按照 raise 语句创建异常实例的规则,用 value 来生成新的异常实例。如果提供了 traceback 参数,则它必须是有效的 Python 堆栈(traceback)对象,否则会抛出 TypeError 。 注释:选择 throw() 这个名称,有几个原因。Raise 是一个关键字,因此不能作为方法的名称。与 raise 不同(它在执行点处即时地抛出异常),throw() 首先恢复生成器,然后才抛出异常。单词 throw 意味着将异常抛在别处,并且跟其它语言相关联。 考虑了几个替代的方法名:resolve(), signal(), genraise(), raiseinto() 和 flush() 。没有一个像 throw() 那般合适。 新的标准异常:GeneratorExit 定义了一个新的标准异常 GeneratorExit,继承自 Exception。生成器应该继续抛出它(或者就不捕获它),或者通过抛出 StopIteration 来处理这个问题。 新的生成器方法:close() g.close() 由以下伪代码定义: def close(self): try: self.throw(GeneratorExit) except (GeneratorExit, StopIteration): pass else: raise RuntimeError("generator ignored GeneratorExit") # Other exceptions are not caught 新的生成器方法:__del__() g.__ del __() 是 g.close() 的装饰器。当生成器对象被作垃圾回收时,会调用它(在 CPython 中,则是它的引用计数变为零时)。如果 close() 引发异常, 异常的堆栈信息(traceback)会被打印到 sys.stderr 并被忽略掉;它不会退回到触发垃圾回收的地方。这与类实例在处理 __del__()的异常时的方法一样。 如果生成器对象被循环引用,则可能不会调用 g.__del__() 。这是当前 CPython 的垃圾收集器的表现。做此限制的原因是,GC 代码需要在一个任意点打破循环,以便回收它,在此之后,不允许 Python 代码“看到”形成循环的对象,因为它们可能处于无效的状态。被用于解开(hanging off)循环的对象不受此限制。 尽管实际上不太可能看到生成器被循环引用。但是,若将生成器对象存储在全局变量中,则会通过生成器框架的 f_globals 指针创建一个循环。另外,若在数据结构中存储对生成器对象的引用,且该数据结构被作为参数传递给生成器,这也会创造一个循环引用(例如,如果一个对象具有一个作为生成器的方法,并持有由该方法创建的运行中的迭代器的引用)。鉴于生成器的典型用法,这些情况都不太可能。 此外,CPython 在实现当前 PEP 时,每当由于错误或正常退出而终止执行时,会释放被生成器使用的框架对象(frame object)。这保证了那些无法被恢复的生成器不会成为无法回收的循环引用的部分。这就允许了其它代码在 try-finally 或 with 语句中使用 close() (参考 PEP-343),确保了给定的生成器会正确地完结。 可选扩展 扩展的 continue 语句 本 PEP 的早期草案提出了一种新的 continue EXPR 语法,用于 for 循环(继承自 PEP-340),将 EXPR 的值传给被遍历的迭代器。此功能暂时被撤销了,因为本 PEP 的范围已经缩小,只关注将值传给生成器迭代器(generator-iterator),而非其它类型的迭代器。Python-Dev 邮件列表中的一些人也觉得为这个特定功能添加新语法是为时过早(would be premature at best)。 未决问题 Python-Dev 邮件的讨论提出了一些未决的问题。我罗列于此,附上我推荐的解决方案与它的动机。目前编写的 PEP 也反映了这种喜好的解决方案。 当生成器产生另一个值作为对“GeneratorExit”异常的响应时,close()应该引发什么异常? 我最初选择了 TypeError ,因为它表示生成器函数发生了严重的错误行为,应该通过修改代码来修复。但是 PEP-343 中的 with_template 装饰器类使用了 RuntimeError 来进行类似处理。可以说它们都应该使用相同的异常。我宁愿不为此目的引入新的异常类,因为它不是我希望人们捕获的异常:我希望它变成一个 traceback 给程序员看到,然后进行修复。所以我觉得它们都应该抛出 RuntimeError 。有一些先例:在检测到无限递归的情况下,或者检测到未初始化的对象(由于各种各样的原因),核心 Python 代码会抛出该异常。 Oren Tirosh 建议将 send() 方法重命名为 feed() ,以便能跟 consumer 接口兼容(规范参见:http://effbot.org/zone/consumer.htm)。 然而,仔细观察 consumer 接口,似乎 feed() 所需的语义与 send() 不同,因为后者不能在刚启动的生成器上作有意义的调用。此外,当前定义的 consumer 接口不包含对 StopIteration 的处理。 因此,创建一个贴合 consumer 接口的简单的装饰器,来装饰生成器函数,似乎会更有用。举个例子,它可以用初始的 next() 调用给生成器预热(warm up),追踪 StopIteration,甚至可以通过重新调用生成器来提供 reset() 用途。 示例 一个简单的 consumer 装饰器,它使生成器函数在最初调用时,就自动地前进到第一个 yield 点: def consumer(func): def wrapper(*args,**kw): gen = func(*args, **kw) gen.next() return gen wrapper.__name__ = func.__name__ wrapper.__dict__ = func.__dict__ wrapper.__doc__ = func.__doc__ return wrapper 一个使用 consumer 装饰器创建反向生成器(reverse generator)的示例,该生成器接收图像并创建缩略图,再发送给其它 consumer。像这样的函数可以链接在一起,形成 consumer 间的高效处理流水线,且每个流水线都可以具有复杂的内部状态: @consumer def thumbnail_pager(pagesize, thumbsize, destination): while True: page = new_image(pagesize) rows, columns = pagesize / thumbsize pending = False try: for row in xrange(rows): for column in xrange(columns): thumb = create_thumbnail((yield), thumbsize) page.write( thumb, col*thumbsize.x, row*thumbsize.y ) pending = True except GeneratorExit: # close() was called, so flush any pending output if pending: destination.send(page) # then close the downstream consumer, and exit destination.close() return else: # we finished a page full of thumbnails, so send it # downstream and keep on looping destination.send(page) @consumer def jpeg_writer(dirname): fileno = 1 while True: filename = os.path.join(dirname,"page%04d.jpg" % fileno) write_jpeg((yield), filename) fileno += 1 # Put them together to make a function that makes thumbnail # pages from a list of images and other parameters. # def write_thumbnails(pagesize, thumbsize, images, output_dir): pipeline = thumbnail_pager( pagesize, thumbsize, jpeg_writer(output_dir) ) for image in images: pipeline.send(image) pipeline.close() 一个简单的协程调度器或蹦床(trampoline),它允许协程通过 yield 其它协程,来调用后者。被调用的协程所产生的非生成器的值,会被返回给调用方的协程。类似地,如果被调用的协程抛出异常,该异常也会传导给调用者。实际上,只要你用 yield 表达式来调用协程(否则会阻塞),这个例子就模拟了 Stackless Python 中使用的简单的子任务(tasklet)。这只是一个非常简单的例子,但也可以使用更复杂的调度程序。(例如,现有的 GTasklet 框架 (http://www.gnome.org/~gjc/gtasklet/gtasklets.html) 和 peak.events 框架 (http://peak.telecommunity.com/) 已经实现类似的调度功能,但大多数因为无法将值或异常传给生成器,而必须使用很尴尬的解决方法。) import collections class Trampoline: """Manage communications between coroutines""" running = False def __init__(self): self.queue = collections.deque() def add(self, coroutine): """Request that a coroutine be executed""" self.schedule(coroutine) def run(self): result = None self.running = True try: while self.running and self.queue: func = self.queue.popleft() result = func() return result finally: self.running = False def stop(self): self.running = False def schedule(self, coroutine, stack=(), val=None, *exc): def resume(): value = val try: if exc: value = coroutine.throw(value,*exc) else: value = coroutine.send(value) except: if stack: # send the error back to the "caller" self.schedule( stack[0], stack[1], *sys.exc_info() ) else: # Nothing left in this pseudothread to # handle it, let it propagate to the # run loop raise if isinstance(value, types.GeneratorType): # Yielded to a specific coroutine, push the # current one on the stack, and call the new # one with no args self.schedule(value, (coroutine,stack)) elif stack: # Yielded a result, pop the stack and send the # value to the caller self.schedule(stack[0], stack[1], value) # else: this pseudothread has ended self.queue.append(resume) 一个简单的 echo 服务器以及用蹦床原理实现的运行代码(假设存在 nonblocking_read 、nonblocking_write 和其它 I/O 协程,该例子在连接关闭时抛出 ConnectionLost ): # coroutine function that echos data back on a connected # socket # def echo_handler(sock): while True: try: data = yield nonblocking_read(sock) yield nonblocking_write(sock, data) except ConnectionLost: pass # exit normally if connection lost # coroutine function that listens for connections on a # socket, and then launches a service "handler" coroutine # to service the connection # def listen_on(trampoline, sock, handler): while True: # get the next incoming connection connected_socket = yield nonblocking_accept(sock) # start another coroutine to handle the connection trampoline.add( handler(connected_socket) ) # Create a scheduler to manage all our coroutines t = Trampoline() # Create a coroutine instance to run the echo_handler on # incoming connections # server = listen_on( t, listening_socket("localhost","echo"), echo_handler ) # Add the coroutine to the scheduler t.add(server) # loop forever, accepting connections and servicing them # "in parallel" # t.run() 参考实现 实现了本 PEP 中描述的所有功能的原型补丁已经可用,参见 SourceForge 补丁 1223381 (https://bugs.python.org/issue1223381)。 该补丁已提交到 CVS,2005年8月 01-02。 致谢 Raymond Hettinger (PEP 288) 与 Samuele Pedroni (PEP 325) 第一个正式地提出将值或异常传递给生成器的想法,以及关闭生成器的能力。Timothy Delaney 建议了本 PEP 的标题,还有 Steven Bethard 帮忙编辑了早期的版本。另见 PEP-340 的致谢部分。 参考文献 TBD. 版权 本文档已经放置在公共领域。 源文档:https://github.com/python/peps/blob/master/pep-0342.txt
我正打算写写 Python 的生成器,然而查资料时发现,引入生成器的 PEP 没人翻译过,因此就花了点时间翻译出来。如果在阅读时,你有读不懂的地方,不用怀疑,极有可能是我译得不到位。若出现这种情况,我建议你直接阅读原文,最好也能将错误处告知于我,以便做出修改。 原文:https://www.python.org/dev/peps/pep-0255 创建日期:2001-05-18 合入Python版本:2.2 译者 :豌豆花下猫(Python猫 公众号作者) PEP背景知识 :学习Python,怎能不懂点PEP呢? 摘要 这个 PEP 想在 Python 中引入生成器的概念,以及一个新的表达式,即 yield 表达式。 动机 当一个生产者函数在处理某些艰难的任务时,它可能需要维持住生产完某个值时的状态,大多数编程语言都提供不了既舒服又高效的方案,除了往参数列表中添加回调函数,然后每生产一个值时就去调用一下。 例如,标准库中的tokenize.py采用这种方法:调用者必须传一个 tokeneater 函数给 tokenize() ,当 tokenize() 找到下一个 token 时再调用。这使得 tokenize 能以自然的方式编码,但程序调用 tokenize 会变得极其复杂,因为它需要记住每次回调前最后出现的是哪个 token(s)。tabnanny.py中的 tokeneater 函数是处理得比较好的例子,它在全局变量中维护了一个状态机,用于记录已出现的 token 和预期会出现的 token 。这很难正确地工作,而且也挺难让人理解。不幸的是,它已经是最标准的解决方法了。 有一个替代方案是一次性生成 Python 程序的全部解析,并存入超大列表中。这样 tokenize 客户端可以用自然的方式,即使用局部变量和局部控制流(例如循环和嵌套的 if 语句),来跟踪其状态。然而这并不实用:程序会变得臃肿,因此不能在实现整个解析所需的内存上放置先验限制;而有些 tokenize 客户端仅仅想要查看某个特定的东西是否曾出现(例如,future 声明,或者像 IDLE 做的那样,只是首个缩进的声明),因此解析整个程序就是严重地浪费时间。 另一个替代方案是把 tokenize 变为一个迭代器【注释1】,每次调用它的 next() 方法时再传递下一个 token。这对调用者来说很便利,就像前一方案把结果存入大列表一样,同时没有内存与“想要早点退出怎么办”的缺点。然而,这个方案也把 tokenize 的负担转化成记住 next() 的调用状态,读者只要瞄一眼 tokenize.tokenize_loop() ,就会意识到这是一件多么可怕的苦差事。或者想象一下,用递归算法来生成普通树结构的节点:若把它投射成一个迭代器框架实现,就需要手动地移除递归状态并维护遍历的状态。 第四种选择是在不同的线程中运行生产者和消费者。这允许两者以自然的方式维护其状态,所以都会很舒服。实际上,Python 源代码发行版中的 Demo/threads/Generator.py 就提供了一个可用的同步通信(synchronized-communication)类,来完成一般的任务。但是,这在没有线程的平台上无法运用,而且就算可用也会很慢(与不用线程可取得的成就相比)。 最后一个选择是使用 Python 的变种 Stackless 【注释2-3】来实现,它支持轻量级的协程。它与前述的线程方案有相同的编程优势,效率还更高。然而,Stackless 在 Python 核心层存在争议,Jython 也可能不会实现相同的语义。这个 PEP 不是讨论这些问题的地方,但完全可以说生成器是 Stackless 相关功能的子集在当前 CPython 中的一种简单实现,而且可以说,其它 Python 实现起来也相对简单。 以上分析完了已有的方案。其它一些高级语言也提供了不错的解决方案,特别是 Sather 的迭代器,它受到 CLU 的迭代器启发【注释4】;Icon 的生成器,一种新颖的语言,其中每个表达式都是生成器【注释5】。它们虽有差异,但基本的思路是一致的:提供一种函数,它可以返回中间结果(“下一个值”)给它的调用者,同时还保存了函数的局部状态,以便在停止的位置恢复(译注:resum,下文也译作激活)调用。一个非常简单的例子: def fib(): a, b = 0, 1 while 1: yield b a, b = b, a+b 当 fib() 首次被调用时,它将 a 设为 0,将 b 设为 1,然后生成 b 给其调用者。调用者得到 1。当 fib 恢复时,从它的角度来看,yield 语句实际上跟 print 语句相同:fib 继续执行,且所有局部状态完好无损。然后,a 和 b 的值变为 1,并且 fib 再次循环到 yield,生成 1 给它的调用者。以此类推。 从 fib 的角度来看,它只是提供一系列结果,就像用了回调一样。但是从调用者的角度来看,fib 的调用就是一个可随时恢复的可迭代对象。跟线程一样,这允许两边以最自然的方式进行编码;但与线程方法不同,这可以在所有平台上高效完成。事实上,恢复生成器应该不比函数调用昂贵。 同样的方法适用于许多生产者/消费者函数。例如,tokenize.py 可以生成下一个 token 而不是用它作为参数调用回调函数,而且 tokenize 客户端可以以自然的方式迭代 tokens:Python 生成器是一种迭代器,但是特别强大。 设计规格:yield 引入了一种新的表达式: yield_stmt:“yield”expression_list yield 是一个新的关键字,因此需要一个 future 声明【注释8】来进行引入:在早期版本中,若想使用生成器的模块,必须在接近头部处包含以下行(详见 PEP 236): from __future__ import generators 没有引入 future 模块就使用 yield 关键字,将会告警。 在后续的版本中,yield 将是一个语言关键字,不再需要 future 语句。 yield 语句只能在函数内部使用。包含 yield 语句的函数被称为生成器函数。从各方面来看,生成器函数都只是个普通函数,但在它的代码对象的 co_flags 中设置了新的“CO_GENERATOR”标志。 当调用生成器函数时,实际参数还是绑定到函数的局部变量空间,但不会执行代码。得到的是一个 generator-iterator 对象;这符合迭代器协议【注释6】,因此可用于 for 循环。注意,在上下文无歧义的情况下,非限定名称 “generator” 既可以指生成器函数,又可以指生成器-迭代器(generator-iterator)。 每次调用 generator-iterator 的 next() 方法时,才会执行 generator-function 体中的代码,直至遇到 yield 或 return 语句(见下文),或者直接迭代到尽头。 如果执行到 yield 语句,则函数的状态会被冻结,并将 expression_list 的值返回给 next() 的调用者。“冻结”是指挂起所有本地状态,包括局部变量、指令指针和内部堆栈:保存足够的信息,以便在下次调用 next() 时,函数可以继续执行,仿佛 yield 语句只是一次普通的外部调用。 限制:yield 语句不能用于 try-finally 结构的 try 子句中。困难的是不能保证生成器会被再次激活(resum),因此无法保证 finally 语句块会被执行;这就太违背 finally 的用处了。 限制:生成器在活跃状态时无法被再次激活: >>> def g(): ... i = me.next() ... yield i >>> me = g() >>> me.next() Traceback (most recent call last): ... File "<string>", line 2, in g ValueError: generator already executing 设计规格:return 生成器函数还可以包含以下形式的return语句: return 注意,生成器主体中的 return 语句不允许使用 expression_list (然而当然,它们可以嵌套地使用在生成器里的非生成器函数中)。 当执行到 return 语句时,程序会正常 return,继续执行恰当的 finally 子句(如果存在)。然后引发一个 StopIteration 异常,表明迭代器已经耗尽。如果程序没有显式 return 而执行到生成器的末尾,也会引发 StopIteration 异常。 请注意,对于生成器函数和非生成器函数,return 意味着“我已经完成,并且没有任何有趣的东西可以返回”。 注意,return 并不一定会引发 StopIteration :关键在于如何处理封闭的 try-except 结构。 例如: >>> def f1(): ... try: ... return ... except: ... yield 1 >>> print list(f1()) [] 因为,就像在任何函数中一样,return 只是退出,但是: >>> def f2(): ... try: ... raise StopIteration ... except: ... yield 42 >>> print list(f2()) [42] 因为 StopIteration 被一个简单的 except 捕获,就像任意异常一样。 设计规格:生成器和异常传播 如果一个未捕获的异常——包括但不限于 StopIteration——由生成器函数引发或传递,则异常会以通常的方式传递给调用者,若试图重新激活生成器函数的话,则会引发 StopIteration 。 换句话说,未捕获的异常终结了生成器的使用寿命。 示例(不合语言习惯,仅作举例): >>> def f(): ... return 1/0 >>> def g(): ... yield f() # the zero division exception propagates ... yield 42 # and we'll never get here >>> k = g() >>> k.next() Traceback (most recent call last): File "<stdin>", line 1, in ? File "<stdin>", line 2, in g File "<stdin>", line 2, in f ZeroDivisionError: integer division or modulo by zero >>> k.next() # and the generator cannot be resumed Traceback (most recent call last): File "<stdin>", line 1, in ? StopIteration >>> 设计规格:Try/Exception/Finally 前面提过,yield 语句不能用于 try-finally 结构的 try 子句中。这带来的结果是生成器要非常谨慎地分配关键的资源。但是在其它地方,yield 语句并无限制,例如 finally 子句、except 子句、或者 try-except 结构的 try 子句: >>> def f(): ... try: ... yield 1 ... try: ... yield 2 ... 1/0 ... yield 3 # never get here ... except ZeroDivisionError: ... yield 4 ... yield 5 ... raise ... except: ... yield 6 ... yield 7 # the "raise" above stops this ... except: ... yield 8 ... yield 9 ... try: ... x = 12 ... finally: ... yield 10 ... yield 11 >>> print list(f()) [1, 2, 4, 5, 8, 9, 10, 11] >>> 示例 # 二叉树类 class Tree: def __init__(self, label, left=None, right=None): self.label = label self.left = left self.right = right def __repr__(self, level=0, indent=" "): s = level*indent + `self.label` if self.left: s = s + "\n" + self.left.__repr__(level+1, indent) if self.right: s = s + "\n" + self.right.__repr__(level+1, indent) return s def __iter__(self): return inorder(self) # 从列表中创建 Tree def tree(list): n = len(list) if n == 0: return [] i = n / 2 return Tree(list[i], tree(list[:i]), tree(list[i+1:])) # 递归生成器,按顺序生成树标签 def inorder(t): if t: for x in inorder(t.left): yield x yield t.label for x in inorder(t.right): yield x # 展示:创建一棵树 t = tree("ABCDEFGHIJKLMNOPQRSTUVWXYZ") # 按顺序打印树的节点 for x in t: print x, print # 非递归生成器 def inorder(node): stack = [] while node: while node.left: stack.append(node) node = node.left yield node.label while not node.right: try: node = stack.pop() except IndexError: return yield node.label node = node.right # 练习非递归生成器 for x in t: print x, print Both output blocks display: A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 问答 为什么重用 def 而不用新的关键字? 请参阅下面的 BDFL 声明部分。 为什么用新的关键字yield而非内置函数? Python 中通过关键字能更好地表达控制流,即 yield 是一个控制结构。而且为了 Jython 的高效实现,编译器需要在编译时就确定潜在的挂起点,新的关键字会使这一点变得简单。CPython 的实现也大量利用它来检测哪些函数是生成器函数(尽管一个新的关键字替代 def 就能解决 CPython 的问题,但人们问“为什么要新的关键字”问题时,并不想要新的关键字)。 为什么不是其它不带新关键字的特殊语法? 例如,为何不用下面用法而用 yield 3: return 3 and continue return and continue 3 return generating 3 continue return 3 return >> , 3 from generator return 3 return >> 3 return << 3 >> 3 << 3 * 3 我没有错过一个“眼色”吧?在数百条消息中,我算了每种替代方案有三条建议,然后总结出上面这些。不需要用新的关键字会很好,但使用 yield 会更好——我个人认为,在一堆无意义的关键字或运算符序列中,yield 更具表现力。尽管如此,如果这引起足够的兴趣,支持者应该发起一个提案,交给 Guido 裁断。 为什么允许用return,而不强制用StopIteration? “StopIteration”的机制是底层细节,就像 Python 2.1 中的“IndexError”的机制一样:实现时需要做一些预先定义好的东西,而 Python 为高级用户开放了这些机制。尽管不强制要求每个人都在这个层级工作。 “return”在任何一种函数中都意味着“我已经完成”,这很容易解读和使用。注意,return 并不总是等同于 try-except 结构中的 raise StopIteration(参见“设计规格:Return”部分)。 那为什么不允许return一个表达式? 也许有一天会允许。 在 Icon 中,return expr 意味着“我已经完成”和“但我还有最后一个有用的值可以返回,这就是它”。 在初始阶段,不强制使用return expr的情况下,使用 yield 仅仅传递值,这很简单明了。 BDFL声明 Issue 引入另一个新的关键字(比如,gen 或 generator )来代替 def ,或以其它方式改变语法,以区分生成器函数和非生成器函数。 Con 实际上(你如何看待它们),生成器是函数,但它们具有可恢复性。使它们建立起来的机制是一个相对较小的技术问题,引入新的关键字无助于强调生成器是如何启动的机制(生成器生命中至关重要却很小的部分)。 Pro 实际上(你如何看待它们),生成器函数实际上是工厂函数,它们就像施了魔法一样地生产生成器-迭代器。 在这方面,它们与非生成器函数完全不同,更像是构造函数而不是函数,因此重用 def 无疑是令人困惑的。藏在内部的 yield 语句不足以警示它们的语义是如此不同。 BDFL def 留了下来。任何一方都没有任何争论是完全令人信服的,所以我咨询了我的语言设计师的直觉。它告诉我 PEP 中提出的语法是完全正确的——不是太热,也不是太冷。但是,就像希腊神话中的 Delphi(译注:特尔斐,希腊古都) 的甲骨文一样,它并没有告诉我原因,所以我没有对反对此 PEP 语法的论点进行反驳。 我能想出的最好的(除了已经同意做出的反驳)是“FUD”(译注:缩写自 fear、uncertainty 和 doubt)。 如果这从第一天开始就是语言的一部分,我非常怀疑这早已让安德鲁·库奇林(Andrew Kuchling)的“Python Warts”页面成为可能。(译注:wart 是疣,一种难看的皮肤病。这是一个 wiki 页面,列举了对 Python 吹毛求疵的建议)。 参考实现 当前的实现(译注:2001年),处于初步状态(没有文档,但经过充分测试,可靠),是Python 的 CVS 开发树【注释9】的一部分。 使用它需要您从源代码中构建 Python。 这是衍生自 Neil Schemenauer【注释7】的早期补丁。 脚注和参考文献 [1] PEP-234, Iterators, Yee, Van Rossum http://www.python.org/dev/peps/pep-0234/ [2] http://www.stackless.com/ [3] PEP-219, Stackless Python, McMillan http://www.python.org/dev/peps/pep-0219/ [4] "Iteration Abstraction in Sather" Murer, Omohundro, Stoutamire and Szyperski http://www.icsi.berkeley.edu/~sather/Publications/toplas.html [5] http://www.cs.arizona.edu/icon/ [6] The concept of iterators is described in PEP 234. See [1] above. [7] http://python.ca/nas/python/generator.diff [8] PEP 236, Back to the __future__, Peters http://www.python.org/dev/peps/pep-0236/ [9] To experiment with this implementation, check out Python from CVS according to the instructions at http://sf.net/cvs/?group_id=5470 ,Note that the std test Lib/test/test_generators.py contains many examples, including all those in this PEP. 版权信息 本文档已经放置在公共领域。源文档:https://github.com/python/peps/blob/master/pep-0255.txt (译文完) PS:官方 PEP 有将近500个,然而保守估计,被翻译成中文的不足20个(去重的情况下)。我好奇,感兴趣将一些重要的 PEP 翻译出来的人有多少呢?现抛此问题出来探探路,欢迎留言交流。 ----------------- 本文翻译并首发于微信公众号【Python猫】,后台回复“爱学习”,免费获得20+本精选电子书。
稍微关心编程语言的使用趋势的人都知道,最近几年,国内最火的两种语言非 Python 与 Go 莫属,于是,隔三差五就会有人问:这两种语言谁更厉害/好找工作/高工资...... 对于编程语言的争论,就是猿界的生理周期,每个月都要闹上一回。到了年末,各类榜单也是特别抓人眼球,闹得更凶。 其实,它们各有对方所无法比拟的优势以及用武之地,很多争论都是没有必要的。身为一个正在努力学习 Python 的(准)中年程序员,我觉得吧,先把一门语言精进了再说。没有差劲的语言,只有差劲的程序员,等真的把语言学好了,必定是“山重水复疑无路,柳暗花明又一村”。 铺垫已了,进入今天的正题,Python 猫荐书系列之五—— Python高性能编程 本书适合已入门 Python、还想要进阶和提高的读者阅读。 所有计算机语言说到底都是在硬件层面的数据操作,所以高性能编程的一个终极目标可以说是“高性能硬件编程”。然而,Python 是一门高度抽象的计算机语言,它的一大优势是开发团队的高效,不可否认地存在这样或那样的设计缺陷,以及由于开发者的水平而造成的人为的性能缺陷。 本书的一大目的就是通过介绍各种模块和原理,来促成在快速开发 Python 的同时避免很多性能局限,既减低开发及维护成本,又收获系统的高效。 1、性能分析是基础 首先的一个关键就是性能分析,借此可以找到性能的瓶颈,使得性能调优做到事半功倍。 性能调优能够让你的代码能够跑得“足够快”以及“足够瘦”。性能分析能够让你用最小的代价做出最实用的决定。 书中介绍了几种性能分析的工具: (1)基本技术如 IPython 的 %timeit 魔法函数、time.time()、以及一个计时修饰器,使用这些技术来了解语句和函数的行为。 (2)内置工具如 cProfile,了解代码中哪些函数耗时最长,并用 runsnake 进行可视化。 (3)line_profiler 工具,对选定的函数进行逐行分析,其结果包含每行被调用的次数以及每行花费的时间百分比。 (4)memory_profiler 工具,以图的形式展示RAM的使用情况随时间的变化,解释为什么某个函数占用了比预期更多的 RAM。 (5)Guppy 项目的 heapy 工具,查看 Python 堆中对象的数量以及每个对象的大小,这对于消灭奇怪的内存泄漏特别有用。 (6)dowser 工具,通过Web浏览器界面审查一个持续运行的进程中的实时对象。 (7)dis 模块,查看 CPython 的字节码,了解基于栈的 Python 虚拟机如何运行。 (8)单元测试,在性能分析时要避免由优化手段带来的破坏性后果。 作者强调了性能分析的重要性,同时也对如何确保性能分析的成功提了醒,例如,将测试代码与主体代码分离、避免硬件条件的干扰(如在BIOS上禁用了TurboBoost、禁用了操作系统改写SpeedStep、只使用主电源等)、运行实验时禁用后台工具如备份和Dropbox、多次实验、重启并重跑实验来二次验证结果,等等。 性能分析对于高性能编程的作用,就好比复杂度分析对于算法的作用,它本身不是高性能编程的一部分,但却是最终有效的一种评判标准。 2、数据结构的影响 高性能编程最重要的事情是了解数据结构所能提供的性能保证。 高性能编程的很大一部分是了解你查询数据的方式,并选择一个能够迅速响应这个查询的数据结构。 书中主要分析了 4 种数据结构:列表和元组就类似于其它编程语言的数组,主要用于存储具有内在次序的数据;而字典和集合就类似其它编程语言的哈希表/散列集,主要用于存储无序的数据。 本书在介绍相关内容的时候很克制,所介绍的都是些影响“速度更快、开销更低”的内容,例如:内置的 Tim 排序算法、列表的 resize 操作带来的超额分配的开销、元组的内存滞留(intern机制)带来的资源优化、散列函数与嗅探函数的工作原理、散列碰撞带来的麻烦与应对、Python 命名空间的管理,等等。 理解了这些内容,就能更加了解在什么情况下使用什么数据结构,以及如何优化这些数据结构的性能。 另外,关于这 4 种数据结构,书中还得出了一些有趣的结论:对于一个拥有100 000 000个元素的大列表,实际分配的可能是112 500 007个元素;初始化一个列表比初始化一个元组慢5.1 倍;字典或集合默认的最小长度是8(也就是说,即使你只保存3个值,Python仍然会分配 8 个元素)、对于有限大小的字典不存在一个最佳的散列函数。 3、矩阵和矢量计算 矢量计算是计算机工作原理不可或缺的部分,也是在芯片层次上对程序进行加速所必须了解的部分。 然而,原生 Python 并不支持矢量操作,因为 Python 列表存储的不是实际的数据,而是对实际数据的引用。在矢量和矩阵操作时,这种存储结构会造成极大的性能下降。比如,grid[5][2] 中的两个数字其实是索引值,程序需要根据索引值进行两次查找,才能获得实际的数据。 同时,因为数据被分片存储,我们只能分别对每一片进行传输,而不是一次性传输整个块,因此,内存传输的开销也很大。 减少瓶颈最好的方法是让代码知道如何分配我们的内存以及如何使用我们的数据进行计算。 Numpy 能够将数据连续存储在内存中并支持数据的矢量操作,在数据处理方面,它是高性能编程的最佳解决方案之一。 Numpy 带来性能提升的关键在于,它使用了高度优化且特殊构建的对象,取代了通用的列表结构来处理数组,由此减少了内存碎片;此外,自动矢量化的数学操作使得矩阵计算非常高效。 Numpy 在矢量操作上的缺陷是一次只能处理一个操作。例如,当我们做 A B + C 这样的矢量操作时,先要等待 A B 操作完成,并保存数据在一个临时矢量中,然后再将这个新的矢量和 C 相加。 Numexpr 模块可以将矢量表达式编译成非常高效的代码,可以将缓存失效以及临时变量的数量最小化。另外,它还能利用多核 CPU 以及 Intel 芯片专用的指令集来将速度最大化。 书中尝试了多种优化方法的组合,通过详细的分析,展示了高性能编程所能带来的性能提升效果。 4、编译器 书中提出一个观点:让你的代码运行更快的最简单的办法就是让它做更少的工作。 编译器把代码编译成机器码,是提高性能的关键组成部分。 不同的编译器有什么优势呢,它们对于性能提升会带来多少好处呢?书中主要介绍了如下编译工具: Cython ——这是编译成C最通用的工具,覆盖了Numpy和普通的Python代码(需要一些C语言的知识)。 Shed Skin —— 一个用于非Numpy代码的,自动把Python转换成C的转换器。 Numba —— 一个专用于Numpy代码的新编译器。 Pythran —— 一个用于Numpy和非numpy代码的新编译器。 PyPy —— 一个用于非Numpy代码的,取代常规Python可执行程序的稳定的即时编译器。 书中分析了这几种编译器的工作原理、优化范围、以及适用场景等,是不错的入门介绍。此外,作者还提到了其它的编译工具,如Theano、Parakeet、PyViennaCL、ViennaCL、Nuitka 与 Pyston 等,它们各有取舍,在不同领域提供了支撑之力。 5、密集型任务 高性能编程的一个改进方向是提高密集型任务的处理效率,而这样的任务无非两大类:I/O 密集型与 CPU 密集型。 I/O 密集型任务主要是磁盘读写与网络通信任务,占用较多 I/O 时间,而对 CPU 要求较少;CPU 密集型任务恰恰相反,它们要消耗较多的 CPU 时间,进行大量的复杂的计算,例如计算圆周率与解析视频等。 改善 I/O 密集型任务的技术是异步编程 ,它使得程序在 I/O 阻塞时,并发执行其它任务,并通过“事件循环”机制来管理各项任务的运行时机,从而提升程序的执行效率。 书中介绍了三种异步编程的库:Gevent、Tornado 和 Asyncio,对三种模块的区别做了较多分析。 改善 CPU 密集型任务的主要方法是利用多核 CPU 进行多进程的运算。 Multiprocessing 模块使用基于进程和基于线程的并行处理,在队列上共享任务,以及在进程间共享数据,是处理 CPU 密集型任务的重要技术。 书中没有隐瞒它的局限性:Amdahl 定律揭示的优化限度、适应于单机多核而多机则有其它选择、全局解释锁 GIL 的束缚、以及进程间通信(同步数据和检查共享数据)的开销。针对进程间通信问题,书中还分析了多种解决方案,例如 Less Naïve Pool、Manager、Redis、RawValue、MMap 等。 6、集群与现场教训 集群是一种多服务器运行相同任务的结构,也就是说,集群中的各节点提供相同的服务,其优点是系统扩展容易、具备容灾恢复能力。 集群需要克服的挑战有:机器间信息同步的延迟、机器间配置与性能的差异、机器的损耗与维护、其它难以预料的问题。书中列举了两个惨痛的教训:华尔街公司骑士资本由于软件升级引入的错误,损失4.62亿美元;Skype 公司 24 小时全球中断的严重事故。 书中给我们重点介绍了三个集群化解决方案:Parallel Python、IPython Parallel 和 NSQ。引申也介绍了一些普遍使用的方案,如 Celery、Gearman、PyRes、SQS。 关于现场教训,它们不仅仅是一些事故或者故事而已,由成功的公司所总结出来的经验更是来之不易的智慧。书中单独用一章内容分享了六篇文章,这些文章出自几个使用 Python 的公司/大型组织,像是Adaptive Lab、RadimRehurek、Smesh、PyPy 与 Lanyrd ,这些国外组织的一线实践经验,应该也能给国内的 Python 社区带来一些启示。 7、写在最后 众所周知,Python 应用前景大、简单易学、方便开发与部署,然而与其它编程语言相比,它的性能几乎总是落于下风。如何解决这个难题呢?本期荐书的书目就是一种回应。 《Python高性能编程》全书从微观到宏观对高性能编程的方方面面做了讲解,主要包含以下主题:计算机内部结构的背景知识、列表和元组、字典和集合、迭代器和生成器、矩阵和矢量计算、编译器、并发、集群和工作队列等。这些内容为编写更快的 Python 指明了答案。 本篇文章主要以梳理书中的内容要点为主,平均而兼顾地理清了全书脉络(PS:介绍得太面面俱到了,但愿不被指责为一篇流水账的读书笔记才好......)。我认为,鉴于书中谈及的这些话题,它就足以成为我们荐书栏目的一员了。除去某些句段的糟糕翻译、成书时间比较早(2014年)而造成的过时外,这本书总体质量不错,可称为是一份优秀的高性能编程的指引手册。 关于荐书栏目,我最后多说几句。本栏目原计划两周左右出一篇,但由于其它系列文章花费了我不少时间,而要写好一篇荐书/书评也特别费劲,最后生生造成了现在两月一更的尴尬局面......这篇文章是个错误的示范,我不该试图全面通读与概括其内容的。因此,我决定今后选一些易读的书目,在写作上也尽量走短小精悍风,希望能持续地将本栏目运作下去。若你有什么建议(如书目推荐、书评推荐、写作建议、甚至是投稿),我随时欢迎,先行致谢啦。 往期荐书回顾: 第一期:《编写高质量代码改善 Python 程序的 91 个建议》第二期:《Python最佳实践指南》第三期:《黑客与画家》第四期:《Python源码剖析》
迭代器是 23 种设计模式中最常用的一种(之一),在 Python 中随处可见它的身影,我们经常用到它,但是却不一定意识到它的存在。在关于迭代器的系列文章中(链接见文末),我至少提到了 23 种生成迭代器的方法。有些方法是专门用于生成迭代器的,还有一些方法则是为了解决别的问题而“暗中”使用到迭代器。 在系统学习迭代器之前,我一直以为 range() 方法也是用于生成迭代器的,现在却突然发现,它生成的只是可迭代对象,而并不是迭代器! (PS:Python2 中 range() 生成的是列表,本文基于Python3,生成的是可迭代对象) 于是,我有了这样的疑问:为什么 range() 不生成迭代器呢?在查找答案的过程中,我发现自己对 range 类型的认识存在一些误区。因此,本文将和大家全面地认识一下 range ,期待与你共同学习进步。 1、range() 是什么? 它的语法:range(start, stop [,step]) ;start 指的是计数起始值,默认是 0;stop 指的是计数结束值,但不包括 stop ;step 是步长,默认为 1,不可以为 0 。range() 方法生成一段左闭右开的整数范围。 >>> a = range(5) # 即 range(0,5) >>> a range(0, 5) >>> len(a) 5 >>> for x in a: >>> print(x,end=" ") 0 1 2 3 4 对于 range() 函数,有几个注意点:(1)它表示的是左闭右开区间;(2)它接收的参数必须是整数,可以是负数,但不能是浮点数等其它类型;(3)它是不可变的序列类型,可以进行判断元素、查找元素、切片等操作,但不能修改元素;(4)它是可迭代对象,却不是迭代器。 # (1)左闭右开 >>> for i in range(3, 6): >>> print(i,end=" ") 3 4 5 # (2)参数类型 >>> for i in range(-8, -2, 2): >>> print(i,end=" ") -8 -6 -4 >>> range(2.2) ---------------------------- TypeError Traceback (most recent call last) ... TypeError: 'float' object cannot be interpreted as an integer # (3)序列操作 >>> b = range(1,10) >>> b[0] 1 >>> b[:-3] range(1, 7) >>> b[0] = 2 TypeError Traceback (most recent call last) ... TypeError: 'range' object does not support item assignment # (4)不是迭代器 >>> hasattr(range(3),'__iter__') True >>> hasattr(range(3),'__next__') False >>> hasattr(iter(range(3)),'__next__') True 2、 为什么range()不生产迭代器? 可以获得迭代器的内置方法很多,例如 zip() 、enumerate()、map()、filter() 和 reversed() 等等,但是像 range() 这样仅仅得到的是可迭代对象的方法就绝无仅有了(若有反例,欢迎告知)。这就是我存在知识误区的地方。 在 for-循环 遍历时,可迭代对象与迭代器的性能是一样的,即它们都是惰性求值的,在空间复杂度与时间复杂度上并无差异。我曾概括过两者的差别是“一同两不同”:相同的是都可惰性迭代,不同的是可迭代对象不支持自遍历(即next()方法),而迭代器本身不支持切片(即__getitem__() 方法)。 虽然有这些差别,但很难得出结论说它们哪个更优。现在微妙之处就在于,为什么给 5 种内置方法都设计了迭代器,偏偏给 range() 方法设计的就是可迭代对象呢?把它们都统一起来,不是更好么? 事实上,Pyhton 为了规范性就干过不少这种事,例如,Python2 中有 range() 和 xrange() 两种方法,而 Python3 就干掉了其中一种,还用了“李代桃僵”法。为什么不更规范点,令 range() 生成的是迭代器呢? 关于这个问题,我没找到官方解释,以下纯属个人观点 。 zip() 等方法都需要接收确定的可迭代对象的参数,是对它们的一种再加工的过程,因此也希望马上产出确定的结果来,所以 Python 开发者就设计了这个结果是迭代器。这样还有一个好处,即当作为参数的可迭代对象发生变化的时候,作为结果的迭代器因为是消耗型的,不会被错误地使用。 而 range() 方法就不同了,它接收的参数不是可迭代对象,本身是一种初次加工的过程,所以设计它为可迭代对象,既可以直接使用,也可以用于其它再加工用途。例如,zip() 等方法就完全可以接收 range 类型的参数。 >>> for i in zip(range(1,6,2), range(2,7,2)): >>> print(i, end="") (1, 2)(3, 4)(5, 6) 也就是说,range() 方法作为一种初级生产者,它生产的原料本身就有很大用途,早早把它变为迭代器的话,无疑是一种画蛇添足的行为。 对于这种解读,你是否觉得有道理呢?欢迎就这个话题与我探讨。 3、range 类型是什么? 以上是我对“为什么range()不产生迭代器”的一种解答。顺着这个思路,我研究了一下它产生的 range 对象,一研究就发现,这个 range 对象也并不简单。 首先奇怪的一点就是,它竟然是不可变序列!我从未注意过这一点。虽然说,我从未想过修改 range() 的值,但这一不可修改的特性还是令我惊讶。 翻看文档,官方是这样明确划分的——有三种基本的序列类型:列表、元组和范围(range)对象。(There are three basic sequence types: lists, tuples, and range objects.) 这我倒一直没注意,原来 range 类型居然跟列表和元组是一样地位的基础序列!我一直记挂着字符串是不可变的序列类型,不曾想,这里还有一位不可变的序列类型呢。 那 range 序列跟其它序列类型有什么差异呢? 普通序列都支持的操作有 12 种,在《你真的知道Python的字符串是什么吗?》这篇文章里提到过。range 序列只支持其中的 10 种,不支持进行加法拼接与乘法重复。 >>> range(2) + range(3) ----------------------------------------- TypeError Traceback (most recent call last) ... TypeError: unsupported operand type(s) for +: 'range' and 'range' >>> range(2)*2 ----------------------------------------- TypeError Traceback (most recent call last) ... TypeError: unsupported operand type(s) for *: 'range' and 'int' 那么问题来了:同样是不可变序列,为什么字符串和元组就支持上述两种操作,而偏偏 range 序列不支持呢?虽然不能直接修改不可变序列,但我们可以将它们拷贝到新的序列上进行操作啊,为何 range 对象连这都不支持呢? 且看官方文档的解释: ...due to the fact that range objects can only represent sequences that follow a strict pattern and repetition and concatenation will usually violate that pattern. 原因是 range 对象仅仅表示一个遵循着严格模式的序列,而重复与拼接通常会破坏这种模式... 问题的关键就在于 range 序列的 pattern,仔细想想,其实它表示的就是一个等差数列啊(喵,高中数学知识没忘...),拼接两个等差数列,或者重复拼接一个等差数列,想想确实不妥,这就是为啥 range 类型不支持这两个操作的原因了。由此推论,其它修改动作也会破坏等差数列结构,所以统统不给修改就是了。 4、小结 回顾全文,我得到了两个偏冷门的结论:range 是可迭代对象而不是迭代器;range 对象是不可变的等差序列。 若单纯看结论的话,你也许没有感触,或许还会说这没啥了不得啊。但如果我追问,为什么 range 不是迭代器呢,为什么 range 是不可变序列呢?对这俩问题,你是否还能答出个自圆其说的设计思想呢?(PS:我决定了,若有机会面试别人,我必要问这两个问题的嘿~) 由于 range 对象这细微而有意思的特性,我觉得这篇文章写得值了。本文是作为迭代器系列文章的一篇来写的,所以对于迭代器的基础知识介绍不多,欢迎查看之前的文章。另外,还有一种特殊的迭代器也值得单独成文,那就是生成器了,敬请期待后续推文哦~ 猜你想读: Python进阶:迭代器与迭代器切片 Python进阶:设计模式之迭代器模式 你真的知道Python的字符串是什么吗? 官方文档:http://t.cn/EGMzJt8 ----------------- 本文原创并首发于微信公众号【Python猫】,后台回复“爱学习”,免费获得20+本精选电子书。
在软件开发领域中,人们经常会用到这一个概念——“设计模式”(design pattern),它是一种针对软件设计的共性问题而提出的解决方案。在一本圣经级的书籍《设计模式:可复用面向对象软件的基础》(1991年,Design Patterns - Elements of Reusable Object-Oriented Software)中,它提出了23种设计模式。迭代器模式就是其中的一种,在各种编程语言中都得到了广泛的应用。 本文将谈谈 Python 中的迭代器模式,主要内容:什么是迭代器模式、Python 如何实现迭代器模式、itertools 模块创建迭代器的方法、其它运用迭代器的场景等等,期待与你共同学习进步。 1、什么是迭代器模式? 维基百科有如下定义: 迭代器是一种最简单也最常见的设计模式。它可以让用户透过特定的接口巡访容器中的每一个元素而不用了解底层的实现。——维基百科 简单地说,迭代器模式就是一种通用性的可以遍历容器类型(如序列类型、集合类型等)的实现方式。使用迭代器模式,可以不关心遍历的对象具体是什么(如字符串、列表、字典等等),也不需要关心遍历的实现算法是什么,它关心的是从容器中遍历/取出元素的结果。 按遍历方式划分,迭代器可分为内部迭代器与外部迭代器,它们的区别在于执行迭代动作与维持迭代状态的不同。 通常而言,迭代器是一次性的,当迭代过一轮后,再次迭代将获取不到元素。 2、Python的迭代器模式 由于迭代器模式的使用太常见了,所以大多数编程语言都给常见的容器类型实现了它,例如 Java 中的 Collection,List、Set、Map等。在 Java 中使用迭代器遍历 List 可以这么写: List<String> list = new ArrayList<>(); Iterator<String> iterator = list.iterator(); while(iterator.hasNext()){ System.out.println(iterator.next()); } ArrayList 类通过自身的 iterator() 方法获得一个迭代器 iterator,然后由该迭代器实例来落实遍历过程。 Python 当然也应用了迭代器模式,但它的实现思路跟上例却不太一样。 首先,Python 认为遍历容器类型并不一定要用到迭代器,因此设计了可迭代对象。 list = [1,2,3,4] for i in list: print(i,end=" ") # 1 2 3 4 for i in list: print(i,end=" ") # 1 2 3 4 上例中的 list 是可迭代对象(Iterable),但并不是迭代器(虽然在底层实现时用了迭代器的部分思想)。Python 抓住了迭代器模式的本质,即是“迭代”,赋予了它极高的地位。 如此设计的好处显而易见:(1)写法简便,用意直白;(2)可重复迭代,避免一次性迭代器的缺陷;(3)不需要创建迭代器,减少开销。 可迭代对象可看作是广义的迭代器,同时,Python 也设计了普通意义的狭义的迭代器。 list = [1,2,3,4] it = iter(list) for i in it: print(i,end=" ") # 1 2 3 4 for i in it: print(i,end=" ") # 无输出 上例中的 iter() 方法会将可迭代对象变成一个迭代器。从输出结果可以看出,该迭代器的迭代过程是一次性的。 由此看来,Python 其实是将“迭代器模式”一拆为二来实现:一是可迭代思想,广泛播种于容器类型的对象中,使它们都可迭代;一是迭代器,一种特殊的可迭代对象,承担普通意义上的迭代器所特有的迭代任务。 同时,它还提供了将可迭代对象转化为迭代器的简易方法,如此安排,真是将迭代器模式的效力发挥到了极致。(关于可迭代对象与迭代器的更多区别、以及它们的实现原理,请参见《Python进阶:迭代器与迭代器切片》) 3、创建迭代器 创建迭代器有如下方式:(1)iter() 方法,将可迭代对象转化成迭代器;(2)__iter__() 与 __next__() 魔术方法,定义类实现这两个魔术方法;(3)itertools 模块,使用内置模块生成迭代器;(4)其它创建方法,如 zip() 、map() 、enumerate() 等等。 四类方法各有适用场所,本节重点介绍 itertools 模块。它可以创建三类迭代器:无限迭代器、有限迭代器与组合迭代器。 3.1 无限迭代器 count(start=0, step=1) :创建一个从 start (默认值为 0) 开始,以 step (默认值为 1) 为步长的的无限整数迭代器。 cycle(iterable) :对可迭代对象的元素反复执行循环。 repeat(object [,times]) :反复生成 object 至无限,或者到给定的 times 次。 import itertools co = itertools.count() cy = itertools.cycle('ABC') re = itertools.repeat('A', 30) # 注意:请分别执行;以下写法未加终止判断,只能按 Ctrl+C 退出 for n in co: print(n,end=" ") # 0 1 2 3 4...... for n in cy: print(n,end=" ") # A B C A B C A B...... for n in re: print(n,end=" ") # A A A A A A A A....(30个) 3.2 有限迭代器 以上方法,比较常用的有:chain() 将多个可迭代对象(可以是不同类型)连接成一个大迭代器;compress() 方法根据真假过滤器筛选元素;groupby() 把迭代器中相邻的重复元素挑出来放在一起;islice() 方法返回迭代器切片(用法参见《Python进阶:迭代器与迭代器切片》);tee() 方法根据可迭代对象创建 n 个(默认2个)迭代器副本。 for c in itertools.chain('ABC', [1,2,3]): print(c,end=" ") # 输出结果:A B C 1 2 3 for c in itertools.compress('ABCDEF', [1, 1, 0, 1, 0, 1]): print(c,end=" ") # 输出结果:A B D F for key, group in itertools.groupby('aaabbbaaccd'): print(key, ':', list(group)) # 输出结果: a : ['a', 'a', 'a'] b : ['b', 'b', 'b'] a : ['a', 'a'] c : ['c', 'c'] d : ['d'] itertools.tee('abc', 3) # 输出结果:(<itertools._tee at 0x1fc72c08108>, <itertools._tee at 0x1fc73f91d08>, <itertools._tee at 0x1fc73efc248>) 3.3 组合迭代器 product() :求解多个可迭代对象的笛卡尔积。 permutations() :求解可迭代对象的元素的全排列。 combinations():求解可迭代对象的元素的组合。 for i in itertools.product('ABC', [1,2]): print(i, end=" ") # 输出结果:('A', 1) ('A', 2) ('B', 1) ('B', 2) ('C', 1) ('C', 2) for i in itertools.permutations('ABC', 2): print(i, end=" ") # 输出结果:('A', 'B') ('A', 'C') ('B', 'A') ('B', 'C') ('C', 'A') ('C', 'B') for i in itertools.combinations('ABC', 2): print(i, end=" ") # 输出结果:('A', 'B') ('A', 'C') ('B', 'C') for i in itertools.combinations('ABCD', 3): print(i, end=" ") # 输出结果:('A', 'B', 'C') ('A', 'B', 'D') ('A', 'C', 'D') ('B', 'C', 'D') 4、强大的内置迭代器方法 迭代器模式的使用场景实在太普遍了,而 Python 也为迭代器的顺利使用而提供了很多便利的条件,本节将介绍相关的几个内置方法。这些方法非常常用而且强大,是 Python 进阶的必会内容。 4.1 zip() 方法 zip() 方法可以同时迭代多个序列,并各取一个元素,生成一个可返回元组的迭代器。此迭代器的长度以较短序列的长度保持一致,若想生成较长序列的长度,需要使用 itertools 模块的 zip_longest() 方法。 import itertools a = [1, 2, 3] b = ['w', 'x', 'y', 'z'] for i in zip(a,b): print(i,end=" ") # (1, 'w') (2, 'x') (3, 'y') # 空缺值以 None 填补 for i in itertools.zip_longest(a,b): print(i,end=" ") # (1, 'w') (2, 'x') (3, 'y') (None, 'z') 4.2 enumerate() 方法 enumerate() 方法接收一个序列类型参数,生成一个可返回元组的迭代器,元组内容是下标及其对应的元素值。它还可接收一个可选参数,指定下标的起始值,默认是0 。 注意:众所周知,Python 中序列的索引值从 0 开始,但是,enumerate() 可以达到改变起始索引数值的效果。 seasons = ['Spring', 'Summer', 'Fall', 'Winter'] for i in enumerate(seasons): print(i,end=" ") #输出结果:(0, 'Spring') (1, 'Summer') (2, 'Fall') (3, 'Winter') for i in enumerate(seasons, start=7): print(i,end=" ") #输出结果:(7, 'Spring') (8, 'Summer') (9, 'Fall') (10, 'Winter') 4.3 map() 方法 map() 方法的参数是一个函数及一个或多个可迭代对象,它会将可迭代对象的元素映射到该函数中,然后迭代地运行该函数,返回结果也是一个迭代器。当存在多个可迭代对象参数时,迭代长度等于较短对象的长度。 def square(x): return x ** 2 l = map(square, [1, 2, 3, 4, 5]) print(list(l)) # 输出结果:[1, 4, 9, 16, 25] m = map(lambda x, y: x + y, [1, 3, 5, 7, 9], [2, 4, 6, 8, 10, 2]) print(list(m)) # 输出结果:[3, 7, 11, 15, 19] 4.4 filter() 方法 filter() 方法的参数是一个判断函数及一个可迭代对象,遍历可迭代对象执行判断函数,过滤下判断为True 的元素,与它相对,若想保留判断为 False 的元素,可使用 itertoole 模块的 filterfalse() 方法。 import itertools fi = filter(lambda x: x%2, range(10)) ff = itertools.filterfalse(lambda x: x%2, range(10)) for i in fi: print(i,end=" ") # 输出结果:1 3 5 7 9 for i in ff: print(i,end=" ") # 输出结果:0 2 4 6 8 5. 小结 迭代器模式几乎是 23 种设计模式中最常用的设计模式,本文主要介绍了 Python 是如何运用迭代器模式,并介绍了 itertools 模块生成迭代器的 18 种方法,以及 5 种生成迭代器的内置方法。 相关链接: itertools模块文档:http://t.cn/R6cGtfw Python进阶:迭代器与迭代器切片 Python进阶:全面解读高级特性之切片! ----------------- 本文原创并首发于微信公众号【Python猫】,后台回复“爱学习”,免费获得20+本精选电子书。
切片是 Python 中最迷人最强大最 Amazing 的语言特性(几乎没有之一),在《Python进阶:切片的误区与高级用法》中,我介绍了切片的基础用法、高级用法以及一些使用误区。这些内容都是基于原生的序列类型(如字符串、列表、元组......),那么,我们是否可以定义自己的序列类型并让它支持切片语法呢?更进一步,我们是否可以自定义其它对象(如字典)并让它支持切片呢? 1、魔术方法:__getitem__() 想要使自定义对象支持切片语法并不难,只需要在定义类的时候给它实现魔术方法 __getitem__() 即可。所以,这里就先介绍一下这个方法。 语法: object.__getitem__(self, key) 官方文档释义:Called to implement evaluation of self[key]. For sequence types, the accepted keys should be integers and slice objects. Note that the special interpretation of negative indexes (if the class wishes to emulate a sequence type) is up to the __getitem__() method. If key is of an inappropriate type, TypeError may be raised; if of a value outside the set of indexes for the sequence (after any special interpretation of negative values), IndexError should be raised. For mapping types, if key is missing (not in the container), KeyError should be raised. 概括翻译一下:__getitem__() 方法用于返回参数 key 所对应的值,这个 key 可以是整型数值和切片对象,并且支持负数索引;如果 key 不是以上两种类型,就会抛 TypeError;如果索引越界,会抛 IndexError ;如果定义的是映射类型,当 key 参数不是其对象的键值时,则会抛 KeyError 。 2、自定义序列实现切片功能 接下来,我们定义一个简单的 MyList ,并给它加上切片功能。(PS:仅作演示,不保证其它功能的完备性)。 class MyList(): def __init__(self): self.data = [] def append(self, item): self.data.append(item) def __getitem__(self, key): print("key is : " + str(key)) return self.data[key] l = MyList() l.append("My") l.append("name") l.append("is") l.append("Python猫") print(l[3]) print(l[:2]) print(l['hi']) ### 输出结果: key is : 3 Python猫 key is : slice(None, 2, None) ['My', 'name'] key is : hi Traceback (most recent call last): ... TypeError: list indices must be integers or slices, not str 从输出结果来看,自定义的 MyList 既支持按索引查找,也支持切片操作,这正是我们的目的。 特别需要说明的是,此例中的 __getitem__() 方法会根据不同的参数类型而实现不同的功能(取索引位值或切片值),也会妥当地处理异常,所以并不需要我们再去写繁琐的处理逻辑。网上有不少学习资料完全是在误人子弟,它们会教你区分参数的不同类型,然后写一大段代码来实现索引查找和切片语法,简直是画蛇添足。下面的就是一个代表性的错误示例: ###略去其它代码#### def __getitem__(self, index): cls = type(self) if isinstance(index, slice): # 如果index是个切片类型,则构造新实例 return cls(self._components[index]) elif isinstance(index, numbers.Integral): # 如果index是个数,则直接返回 return self._components[index] else: msg = "{cls.__name__} indices must be integers" raise TypeError(msg.format(cls=cls)) 3、自定义字典实现切片功能 切片是序列类型的特性,所以在上例中,我们不需要写切片的具体实现逻辑。但是,对于其它非序列类型的自定义对象,就得自己实现切片逻辑。以自定义字典为例(PS:仅作演示,不保证其它功能的完备性): class MyDict(): def __init__(self): self.data = {} def __len__(self): return len(self.data) def append(self, item): self.data[len(self)] = item def __getitem__(self, key): if isinstance(key, int): return self.data[key] if isinstance(key, slice): slicedkeys = list(self.data.keys())[key] return {k: self.data[k] for k in slicedkeys} else: raise TypeError d = MyDict() d.append("My") d.append("name") d.append("is") d.append("Python猫") print(d[2]) print(d[:2]) print(d[-4:-2]) print(d['hi']) ### 输出结果: is {0: 'My', 1: 'name'} {0: 'My', 1: 'name'} Traceback (most recent call last): ... TypeError 上例的关键点在于将字典的键值取出,并对键值的列表做切片处理,其妙处在于,不用担心索引越界和负数索引,将字典切片转换成了字典键值的切片,最终实现目的。 4、小结 最后小结一下:本文介绍了__getitem__() 魔术方法,并用于实现自定义对象(以列表类型和字典类型为例)的切片功能,希望对你有所帮助。 参考阅读: Python进阶:切片的误区与高级用法 官方文档getitem用法:http://t.cn/EbzoZyp Python切片赋值源码分析:http://t.cn/EbzSaoZ PS:本公众号(Python猫)已开通读者交流群,详情请通过菜单栏中的“交流群”来了解。 ----------------- 本文原创并首发于微信公众号【Python猫】,后台回复“爱学习”,免费获得20+本精选电子书。
正如《你真的知道Python的字符串是什么吗?》所写,Python 中字符串是由 Uniocde 编码的字符组成的不可变序列,它具备与其它序列共有的一些操作,例如判断元素是否存在、拼接序列、切片操作、求长度、求最值、求元素的索引位置及出现次数等等。 除此之外,它还有很多特有的操作,值得我们时常温故学习,所以,今天我就跟大家继续聊聊字符串。 本文主要介绍 Python 字符串特有的操作方法,比如它的拼接、拆分、替换、查找及字符判断等使用方法,辨析了一些可能的误区。最后,还做了两个扩展思考:为什么 Python 字符串不具备列表类型的某些操作呢,为什么它不具备 Java 字符串的一些操作呢?两相比较,希望能帮助你透彻地理解——Python 的字符串到底怎么用? 0. 拼接字符串 字符串的拼接操作最常用,我专门为这个话题写过一篇《详解Python拼接字符串的七种方式》,建议你回看。 在此,简单回顾一下:七种拼接方式从实现原理上划分为三类,即格式化类(%占位符、format()、template)、拼接类(+操作符、类元祖方式、join())与插值类(f-string),在使用上,我有如下建议—— 当要处理字符串列表等序列结构时,采用join()方式;拼接长度不超过20时,选用+号操作符方式;长度超过20的情况,高版本选用f-string,低版本时看情况使用format()或join()方式。 不敢说字符串就只有这七种拼接方式,但应该说它们是最常见的了。有小伙伴说,我写漏了一种,即字符串乘法 ,可以重复拼接自身。没错,从结果上看,这是第八种拼接方式,视为补充吧。 关于字符串拼接,还得补充一个建议,即在复杂场景下,尽量避免使用以上几类原生方法,而应该使用外置的强大的处理库。比如在拼接 SQL 语句的时候,经常要根据不同的条件分支,来组装不同的查询语句,而且还得插入不同的变量值,所以当面临这种复杂的场景时,传统拼接方式只会加剧代码的复杂度、降低可读性和维护性。使用 SQLAlchemy 模块,将有效解决这个问题。 1. 拆分字符串 在字符串的几种拼接方法中,join() 方法可以将列表中的字符串元素,拼接成一个长的字符串,与此相反,split() 方法可以将长字符串拆分成一个列表。前面已说过,字符串是不可变序列,所以字符串拆分过程是在拷贝的字符串上进行,并不会改变原有字符串。 split() 方法可接收两个参数,第一个参数是分隔符,即用来分隔字符串的字符,默认是所有的空字符,包括空格、换行(n)、制表符(t)等。拆分过程会消耗分隔符,所以拆分结果中不包含分隔符。 s = 'Hello world' l = '''Hi there , my name is Python猫 Do you like me ? ''' # 不传参数时,默认分隔符为所有空字符 s.split() >>> ['Hello', 'world'] s.split(' ') >>> ['Hello', 'world'] s.split(' ') >>> ['Hello world'] # 不存在两个空格符 s.split('world') >>> ['Hello', ''] # 空字符包括空格、多个空格、换行符等 l.split() >>> ['Hi', 'there', ',', 'my', 'name', 'is', 'Python猫', 'Do', 'you', 'like', 'me', '?'] split() 方法的第二个参数是一个数字,默认是缺省,缺省时全分隔,也可以用 maxsplit 来指定拆分次数。 # 按位置传参 l.split(' ',3) >>> ['Hi', 'there', ',', 'my name is Python 猫\nDo you like me ?\n'] # 指定传参 l.split(maxsplit=3) >>> ['Hi', 'there', ',', 'my name is Python 猫\nDo you like me ?\n'] # 错误用法 l.split(3) --------------- TypeError Traceback (most recent call last) <ipython-input-42-6c16d1a50bca> in <module>() ----> 1 l.split(3) TypeError: must be str or None, not int split() 方法是从左往右遍历,与之相对,rsplit() 方法是从右往左遍历,比较少用,但是会有奇效。 拆分字符串还有一种方法,即 splitlines() ,这个方法会按行拆分字符串,它接收一个参数 True 或 False ,分别决定换行符是否会被保留,默认值 False ,即不保留换行符。 # 默认不保留换行符 'ab c\n\nde fg\rkl\r\n'.splitlines() >>> ['ab c', '', 'de fg', 'kl'] 'ab c\n\nde fg\rkl\r\n'.splitlines(True) >>> ['ab c\n', '\n', 'de fg\r', 'kl\r\n'] 2. 替换字符串 替换字符串包括如下场景:大小写替换、特定符号替换、自定义片段替换...... 再次说明,字符串是不可变对象,以下操作并不会改变原有字符串。 以上这些方法都很明了,使用也简单,建议你亲自试验一下。这里只说说 strip() 方法,它比较常用,可以去除字符串前后的空格,不仅如此,它还可以删除首末位置的指定的字符。 s = '******Hello world******' s.strip('*') >>> 'Hello world' 3. 查找字符串 查找字符串中是否包含某些内容,这是挺常用的操作。Python 中有多种实现方式,例如内置的 find() 方法,但是这个方法并不常用,因为它仅仅告诉你所查找内容的索引位置,而在通常情况下,这个位置并不是我们的目的。 find() 方法与 index() 方法的效果一样,它们的最大的区别只在于,找不到内容时的返回值不同,一个返回 -1,一个抛出异常 : s = 'Hello world' s.find('cat') >>> -1 s.index('cat') >>> ValueError Traceback (most recent call last) <ipython-input-55-442007c50b6f> in <module>() ----> 1 s.index('cat') ValueError: substring not found 以上两个方法,只能用来满足最简单的查找需求。在实战中,我们常常要查找特定模式的内容,例如某种格式的日期字符串,这就得借助更强大的查找工具了。正则表达式和 re 模块就是这样的工具,正则表达式用来定制匹配规则,re 模块则提供了 match() 、find() 及 findall() 等方法,它们组合起来,可以实现复杂的查找功能。限于篇幅,今后再对这两大工具做详细介绍,这里有一个简单的例子: import re datepat = re.compile(r'\d+/\d+/\d+') text = 'Today is 11/21/2018. Tomorrow is 11/22/2018.' datepat.findall(text) >>> ['11/21/2018', '11/22/2018'] 4. 字符判断 判断字符串是否(只)包含某些字符内容,这类使用场景也很常见,例如在网站注册时,要求用户名只能包含英文字母和数字,那么,当校验输入内容时,就需要判断它是否只包含这些字符。其它常用的判断操作,详列如下: 5. 字符串不可以做的事 上文内容都是 Python 字符串特有的操作方法,相信读完之后,你更清楚知道 Python 能够做什么了。 但是,这还不足以回答本文标题的问题——你真的知道 Python 的字符串怎么用吗?这些特有的操作方法,再加上之前文章提到的序列共有的操作、字符串读写文件、字符串打印、字符串Intern机制等等内容,才差不多能够回答这个问题。 尽管如此,为了体现严谨性,我试着再聊聊“Python 字符串不可以做的事”,从相反的维度来补充回答这个问题。下面是开拓思维,进行头脑风暴的时刻: (1)受限的序列 与典型的序列类型相比,字符串不具备列表的如下操作:append()、clear()、copy()、insert()、pop()、remove(),等等。这是为什么呢? 有几个很好理解,即append()、insert()、pop() 和 remove(),它们都是对单个元素的操作,但是,字符串中的单个元素就是单个字符,通常没有任何意义,我们也不会频繁对其做增删操作,所以,字符串没有这几个方法也算合理。 列表的 clear() 方法会清空列表,用来节省内存空间,其效果等同于 anylist[:] = [] ,但是,奇怪的是,Python 并不支持清空/删除操作。 首先,字符串没有 clear() 方法,其次,它是不可变对象,不支持这种赋值操作 anystr[:] = '' ,也不支持 del anystr[:] 操作: s = 'Hello world' s[:] = '' >>> 报错:TypeError: 'str' object does not support item assignment del s[:] >>> 报错:TypeError: 'str' object does not support item deletion 当然,你也别想通过 del s 来删除字符串,因为变量名 s 只是字符串对象的引用 (挖坑,以后写写这个话题),只是一个标签,删除标签并不会直接导致对象实体的消亡。 如此看来,想要手动清空/删除 Python 字符串,似乎是无解。 最后还有一个 copy() 方法,这就是拷贝嘛,可是字符串也没有这个方法。为什么呢?难道拷贝字符串的场景不多么?在这点上,我也没想出个所以然来,搁置疑问。 通过以上几个常用列表操作的比较,我们可以看出字符串这种序列是挺受限的。列表可以看成多节车厢链接成的火车,而字符串感觉就只像多个座椅联排成的长车厢,真是同源不同相啊。 (2)比就比,谁怕谁 接下来,又到了 Python 字符串与 Java 字符串 PK 的时刻。在上一篇文章《你真的知道Python的字符串是什么吗?》中,它们已经在对象定义的角度切磋了两回合,胜利的天平倒向了 Python,这次看看会比出个啥结果吧。 Java 中有 比较字符串 的方法,即 compareTo() 方法与 equals() 方法,前一个方法逐一比较两个字符串的字符编码,返回一个整型的差值,后一个方法在整体上比较两个字符串的内容是否相等。 Python 字符串没有这两个单独的方法,但要实现类似的功能却很简便。 先看例子: myName = "Python猫" cmpName = "world" newName = myName # 直接用比较符号进行compare myName > cmpName >>> False myName == newName >>> True cmpName != newName >>> True # 比较是否同一对象 myName is cmpName >>> False myName is newName >>> True 上例中,如果把赋值的字符串换成列表或者其它对象,这些比较操作也是可以进行的。也就是说,作比较的能力 是 Python 公民们的一项基本能力,并不会因为你是字符串就给你设限,或者给你开特权。 与此类似,Python 公民们自带求自身长度的能力 ,len() 方法是内置方法,可以直接传入任意序列参数,求解长度。Java 中则要求不同的序列对象,只能调用各自的 length() 方法。说个形象的比喻,Python 中共用一把秤,三教九流之辈都能拿它称重,而Java 中有多把秤,你称你的,我称我的,大家“井水不犯河水”。 Python 中曾经有 cmp() 方法和 __cmp__() 魔术方法,但官方嫌弃它们鸡肋,所以在Python 3 中移除掉了。虽然在 operator 模块中还为它留下了一脉香火,但保不定哪天就会彻底废弃。 import operator operator.eq('hello', 'name') >>> False operator.eq('hello', 'hello') >>> True operator.gt('hello', 'name') >>> False operator.lt('hello', 'name') >>> True (3)墙上的门 在 Java 中,字符串还有一个强大的 valueOf() 方法,它可以接收多种类型的参数,如boolean、char、char数组、double、float、int等等,然后返回这些参数的字符串类型。 例如,要把 int 转为字符串,可以用 String.valueOf(anynum) 。 Python 字符串依然没有这个单独的方法,但要实现相同的功能却很简便。对Python来说,不同的数据类型转换成字符串,那是小菜一碟,例如: str(123) >>> '123' str(True) >>> 'True' str(1.22) >>> '1.22' str([1,2]) >>> '[1, 2]' str({'name':'python', 'sex':'male'}) >>> "{'name': 'python', 'sex': 'male'}" 而从字符串转换为其它类型,也不难,例如,int('123') 即可由字符串'123' 得到数字 123。对比 Java,这个操作要写成 Integer.parseInt('123') 。 在Java 的不同数据类型之间,那道分隔之墙矗立得很高,仿佛需要借助一座更高的吊桥才能沟通两边,而在灵活的 Python 里,你可以很方便地打开墙上的那扇门,来往穿越。 小结一下,跟 Java 相比,Python 字符串确实没有几项方法,但是事出有因,它们的天赋能力可不弱,所有这些操作都能简明地实现。一方面,Python 字符串做不到某些事,但是另一方面,Python 可以出色地做成这些事,孰优孰劣,高下立判。 6. 总结 写文章贵在善始善终,现在给大家总结一下:本文主要介绍 Python 字符串特有的操作方法,比如它的拼接、拆分、替换、查找及字符判断等使用方法,从正向回答,Python 字符串能做什么?最后,我们还从反向来回答了 Python 字符串不能做什么?有些不能做,实际上是 不为,是为了在其它地方更好地作为,归根到底,应该有的功能,Python 字符串全都有了。 本文中依然将 Python 与 Java 做了比较,有几项小小的差异,背后反映的其实是,两套语言系统在世界观上的差异。古人云,以铜为镜,可以正衣冠。那么,在编程语言的世界里,以另一种语言为镜,也更能看清这种语言的面貌。希望这种跨语言的思维碰撞,能为你擦出智慧的火花。 最后是福利时刻:本公众号(Python猫)由清华大学出版社赞助,将抽奖送出两本新书《深入浅出Python机器学习》,截止时间到11月29日18:18,点击 这个链接,马上参与吧。 ----------------- 本文原创并首发于微信公众号【Python猫】,后台回复“爱学习”,免费获得20+本精选电子书。 扩展阅读: 《详解Python拼接字符串的七种方式》 《你真的知道Python的字符串是什么吗?》 Java字符串比较方法: https://blog.csdn.net/barryhappy/article/details/6082823 Python3为何取消cmp方法: https://www.zhihu.com/question/47895103
或许你是一个初入门Python的小白,完全不知道PEP是什么。又或许你是个学会了Python的熟手,见过几个PEP,却不知道这玩意背后是什么。那正好,本文将系统性地介绍一下PEP,与大家一起加深对PEP的了解。 目前,国内各类教程不可胜数,虽然或多或少会提及PEP,但笼统者多、局限于某个PEP者多,能够详细而全面地介绍PEP的文章并不多。 本文的目的是:尽量全面地介绍PEP是什么,告诉大家为什么要去阅读PEP,以及列举了一些我认为是必读的PEP,最后,则是搜罗了几篇PEP的中文翻译,希望能为Python学习资料的汉化,做点抛砖引玉的贡献。 PEP是什么? PEP的全称是Python Enhancement Proposals,其中Enhancement是增强改进的意思,Proposals则可译为提案或建议书,所以合起来,比较常见的翻译是Python增强提案或Python改进建议书。 我个人倾向于前一个翻译,因为它更贴切。Python核心开发者主要通过邮件列表讨论问题、提议、计划等,PEP通常是汇总了多方信息,经过了部分核心开发者review和认可,最终形成的正式文档,起到了对外公示的作用,所以我认为翻译成“提案”更恰当。 PEP的官网是:https://www.python.org/dev/peps/,这也就是PEP 0 的地址。其它PEP的地址是将编号拼接在后面,例如:https://www.python.org/dev/peps/pep-0020/ 就是PEP 20 的链接,以此类推。 第一个PEP诞生于2000年,现在正好是18岁成年。到目前为止,它拥有478个“兄弟姐妹”。 官方将PEP分成三类: I - Informational PEP P - Process PEP S - Standards Track PEP 其含义如下: 信息类:这类PEP就是提供信息,有告知类信息,也有指导类信息等等。例如PEP 20(The Zen of Python,即著名的Python之禅)、PEP 404 (Python 2.8 Un-release Schedule,即宣告不会有Python2.8版本)。 流程类:这类PEP主要是Python本身之外的周边信息。例如PEP 1(PEP Purpose and Guidelines,即关于PEP的指南)、PEP 347(Migrating the Python CVS to Subversion,即关于迁移Python代码仓)。 标准类:这类PEP主要描述了Python的新功能和新实践(implementation),是数量最多的提案。例如我之前推文《详解Python拼接字符串的七种方式》提到过的f-string方式,它出自PEP 498(Literal String Interpolation,字面字符串插值)。 每个PEP最初都是一个草案(Draft),随后会经历一个过程,因此也就出现了不同的状态。以下是一个流程图: A – Accepted (Standards Track only) or Active proposal 已接受(仅限标准跟踪)或有效提案 D – Deferred proposal 延期提案 F – Final proposal 最终提案 P – Provisional proposal 暂定提案 R – Rejected proposal 被否决的提案 S – Superseded proposal 被取代的提案 W – Withdrawn proposal 撤回提案 在PEP 0(Index of Python Enhancement Proposals (PEPs))里,官方列举了所有的PEP,你可以按序号、按类型以及按状态进行检索。而在PEP 1(PEP Purpose and Guidelines)里,官方详细说明了PEP的意图、如何提交PEP、如何修复和更新PEP、以及PEP评审的机制等等。 为什么要读PEP? 无论你是刚入门Python的小白、有一定经验的从业人员,还是资深的黑客,都应该阅读Python增强提案。 依我之见,阅读PEP至少有如下好处: (1)了解Python有哪些特性,它们与其它语言特性的差异,为什么要设计这些特性,是怎么设计的,怎样更好地运用它们; (2)跟进社区动态,获知业内的最佳实践方案,调整学习方向,改进工作业务的内容; (3)参与热点议题讨论,或者提交新的PEP,为Python社区贡献力量。 说到底,学会用Python编程,只是掌握了皮毛。PEP提案是深入了解Python的途径,是真正掌握Python语言的一把钥匙,也是得心应手使用Python的一本指南。 哪些PEP是必读的? 如前所述,PEP提案已经累积产生了478个,我们并不需要对每个PEP都熟知,没有必要。下面,我列举了一些PEP,推荐大家一读: PEP 0 -- Index of Python Enhancement ProposalsPEP 7 -- Style Guide for C Code,C扩展PEP 8 -- Style Guide for Python Code,Python编码规范(必读)PEP 20 -- The Zen of Python,Python之禅PEP 202 -- List Comprehensions,列表生成式PEP 274 -- Dict Comprehensions,字典生成式PEP 234 -- Iterators,迭代器PEP 257 -- Docstring Conventions,文档注释规范PEP 279 -- The enumerate() built-in function,enumerate枚举PEP 282 -- A Logging System,日志模块PEP 285 -- Adding a bool type,布尔值(建议阅读《Python对象的身份迷思:从全体公民到万物皆数》)PEP 289 -- Generator Expressions,生成器表达式PEP 318 -- Decorators for Functions and Methods,装饰器PEP 342 -- Coroutines via Enhanced Generators,协程PEP 343 -- The "with" Statement,with语句PEP 380 -- Syntax for Delegating to a Subgenerator,yield from语法PEP 405 -- Python Virtual Environments,虚拟环境PEP 471 -- os.scandir() function,遍历目录PEP 484 -- Type Hints,类型约束PEP 492 -- Coroutines with async and await syntax,async/await语法PEP 498 -- Literal String Interpolation Python,字面字符串插值PEP 525 -- Asynchronous Generators,异步生成器PEP 572 -- Assignment Expressions,表达式内赋值(最具争议)PEP 3105 -- Make print a function,print改为函数PEP 3115 -- Metaclasses in Python 3000,元类PEP 3120 -- Using UTF-8 as the default source encoding,默认UTF-8PEP 3333 -- Python Web Server Gateway Interface v1.0.1,Web开发PEP 8000 -- Python Language Governance Proposal Overview,GvR老爹推出决策层后,事关新决策方案 关于PEP,知乎上有两个问题,推荐大家关注:哪些PEP值得阅读(https://dwz.cn/7CHMBlLu),如何看待PEP 572(https://dwz.cn/L46jpzMB)。 对PEP的贡献 虽无确切数据作证,我国Python开发者的数量应该比任何国家都多。然而,纵观PEP 0 里面列举的200多个PEP作者,我只看到了一个像是汉语拼音的国人名字(不排除看漏,或者使用了英文名的)。反差真是太大了。 我特别希望,国内的Python黑客们的名字,能越来越多地出现在那个列表里,出现在Python核心开发者的列表里。 此外,关于对PEP的贡献,还有一种很有效的方式,就是将PEP翻译成中文,造福国内的Python学习社区。经过一番搜索,我还没有看到系统性翻译PEP的项目,只找到了零星的对于某个PEP的翻译。 我用心搜集了几篇中文翻译成果,分享给大家: PEP8 https://dwz.cn/W01HexFDPEP257 https://dwz.cn/JLctlNLCPEP328 https://dwz.cn/4vCQJpEPPEP333 https://dwz.cn/TAXIZdzcPEP484 https://dwz.cn/dSLZgg5BPEP492 http://t.cn/EALeaL0PEP541 https://dwz.cn/ce98vc27PEP3107 http://suo.im/4xFESRPEP3333 https://dwz.cn/si3xylgw 最后,表达一下我的私心: (1)希望本文能给大家带来知识和见识的增长,激发一些小伙伴的学习热情 (2)希望有小伙伴去翻译更多的PEP,造福Python的中文学习社区 -----------------原文链接:https://mp.weixin.qq.com/s/oRoBxZ2-IyuPOf_MWyKZyw本文原创并首发于微信公众号【Python猫】,后台回复“爱学习”,免费获得20+本精选电子书。
2023年05月
2022年12月
2022年10月
2022年09月