书名:编写整洁的Python代码
ISBN:978-7-115-54802-3
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
著 [西]马里亚诺•阿那亚(Mariano Anaya)
译 包永帅 周 立
责任编辑 吴晋瑜
人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
网址 http://www.ptpress.com.cn
读者服务热线:(010)81055410
反盗版热线:(010)81055315
Copyright ©Packt Publishing 2018. First published in the English language under the title Clean Code in Python(9781788835831).
All rights reserved.
本书由英国Packt Publishing公司授权人民邮电出版社有限公司出版。未经出版者书面许可,对本书的任何部分不得以任何方式或任何手段复制和传播。
版权所有,侵权必究。
本书介绍Python软件工程的主要实践和原则,旨在帮助读者编写更易于维护和更整洁的代码。全书共10章:第1章介绍Python语言的基础知识和搭建Python开发环境所需的主要工具;第2章描述Python风格代码,介绍Python中的第一个习惯用法;第3章总结好代码的一般特征,回顾软件工程中的一般原则;第4章介绍一套面向对象软件设计的原则,即SOLID原则;第5章介绍装饰器,它是Python的最大特性之一;第6章探讨描述符,介绍如何通过描述符从对象中获取更多的信息;第7章和第8章介绍生成器以及单元测试和重构的相关内容;第9章回顾Python中最常见的设计模式;第10章再次强调代码整洁是实现良好架构的基础。
本书适合所有Python编程爱好者、对程序设计感兴趣的人,以及其他想学习更多Python知识的软件工程的从业人员。
包永帅 京东物流高级开发工程师,负责供应链产品的研发设计和开发工作。有丰富的亿级流量系统设计经验,以及丰富的大数据和分布式系统开发经验。擅长使用Python语言进行大数据分析和建模。
周立 京东供应链算法产品研发经理,目前为京东物流供应链的数据业务线负责人,负责供应链的商品布局产品。精通Python语言和R语言,主要研究方向为运筹学领域的算法建模,负责落地算法验证性测试,已为国内20余个行业头部商家提供专业的供应链算法解决方案。
这是一本关于Python软件工程原理方面的书。
关于软件工程的书有很多,关于Python的可用资源也有很多,但要将这两者结合起来,还有许多工作要做。本书正是尝试在这二者之间架起一座桥梁。
要想在一本书中涵盖关于软件工程的所有主题是不现实的,因为软件工程的领域十分广泛,而且针对某个特定的主题会有专门的图书去介绍。本书重点介绍Python软件工程的主要实践和原则,旨在帮助读者编写更易于维护的代码,同时教读者利用Python的特性来编写代码。
简而言之,对于软件工程领域的问题,解决方案通常都不止一种。一般来说,这是一个多方平衡的问题。每种解决方案有各自的优缺点,我们必须遵循一些标准来选择它们,在付出一定成本的同时也获得一定的好处。通常没有所谓单一的最佳解决方案,但是仍有一些原则需要遵循。只要遵循了这些原则,你就会走上一条风险更小的道路。本书鼓励你遵循这些原则,做出最佳选择,因为即使面临困难,如果遵循好的实践方式,仍然能得到较好的结果。
说到良好的实践,一部分可以阐释为遵循了一些既定的且经过验证的原则,另一部分是依据自己想法的原则。但这并不意味着良好的实践必须以某种特定的方式去完成。我并没有说自己在代码整洁的问题上多么权威,因为本来就不可能存在这样的命题,而是鼓励你进行重要的思考——采取什么样的方法对项目才是最有效的,并要勇于提出不同的意见。只要意见分歧能够引发具有启发性的讨论,就是值得鼓励的。
之所以编写本书,主要是为了分享学习Python的乐趣,以及我从经验中总结的一些习惯用法,并希望这些内容能够帮助你拓展Python语言专业知识。
本书通过代码示例阐释如何编写整洁的代码。这些代码示例使用的都是Python 3.7版本,当然将来的版本也是兼容的。代码不会涉及任何与某特定平台绑定的特性,因为有Python解释器的存在,所以可以在任何操作系统上测试代码示例。
在大多数示例中,为了尽可能保持代码的简单,功能实现及其测试都是使用普通Python来编写的(仅仅使用了标准库)。在某些章节中,我们需要用到一些额外的库,为了运行这些示例代码,运行说明会与requirements.txt文件一并给出。
本书所介绍Python提供的那些特性,都是为了使代码变得更好、更可读且更易于维护。我们不仅要探索Python的语言特性,还要分析在Python中如何进行软件工程实践。你可能会注意到,有些参考实现在Python中是完全不同的,某些原则或模式只是稍有变化,而另外一些原则或模式甚至可能完全不适用。能够理解各种不同的示例就意味着你有机会进一步深入了解Python语言。
本书适合所有对软件设计感兴趣以及想学习更多Python知识的软件工程的从业人员。本书假设你已经熟悉面向对象软件设计的原理,并有一定编写代码的经验。
本书适合所有不同级别的Python学习者,对于学习Python很有好处,因为本书内容是按照从简单到复杂依次排序的。这是学习Python语言中主要习惯用法、函数和实用程序的首选方法。其思想是不但要用Python解决一些问题,而且要以一种惯用的方式来解决这些问题。
有经验的程序员也可以从中受益,因为其中一些章节介绍了Python中的高级主题,例如装饰器、描述符,以及异步编程。本书将帮助你探索更多关于Python的内容,因为一些示例是从语言的内部进行分析的。
值得强调的是前文提到的“从业人员”一词。这是一本很实用的书。示例虽然仅限于研究案例所需的内容,但也旨在模拟软件项目的真实应用场景。这不是一部学术著作,因此请谨慎对待所给出的定义、评论和提出的建议。你应该批判地、务实地去看待这些内容,而不是教条式地全盘接受它们。毕竟,实用才是最重要的。
第1章 简介、代码格式和工具,介绍搭建Python开发环境所需的主要工具、Python开发人员在开始使用该语言时需要了解的基本知识,以及维护项目中代码可读性的一些指导原则,如用于静态分析、文档、类型检查和代码格式化的工具。
第2章 Python风格代码,介绍Python中的第一个习惯用法——我们在后续章节中将继续使用它。本章还会介绍一些Python的独有特性,以及如何使用它们,并且开始围绕“Python风格代码如何能够让代码质量更高”展开论述。
第3章 好代码的一般特征,回顾软件工程的一般原则,以期帮助读者编写可维护的代码。本章就这个话题展开讨论,并利用Python语言中的工具应用这些原则。
第4章 SOLID原则,介绍面向对象软件设计的SOLID原则。SOLID是软件工程领域的行业术语,即SRP、OCP、LSP、ISP和DIP。本章会展示这5项原则在Python中的应用。可以说,鉴于Python语言的性质,并非所有方法都完全适用。
第5章 用装饰器改进代码,介绍Python的最大特性之一——装饰器。在了解如何创建装饰器(用于函数和类)之后,我们将其用于代码重用、责任分离和创建更细粒度的函数。
第6章 用描述符从对象中获取更多信息,探讨Python中的描述符,它把面向对象设计提升到了一个新的层次。尽管这更多只是一个与框架和工具相关的特性,但我们可以看到如何用描述符提高代码的可读性,以及如何重用代码。
第7章 使用生成器,说明生成器可能是Python的最佳特性。事实上,迭代是Python的核心组件,这让我们认为它引申出了一种新的编程范式。一般来说,通过使用生成器和迭代器,我们可以考虑编写程序的方式。基于从生成器中吸取的知识,我们将进一步了解Python中的协同程序以及异步编程的基本知识。
第8章 单元测试和重构,讨论单元测试在任何所谓“可维护的代码库”中的重要性。本章回顾了单元测试的重要性,并探究了单元测试的主要框架(unittest和pytest)。
第9章 常见的设计模式,回顾如何在Python中实现最常见的设计模式,不是从解决问题的角度,而是通过研究如何利用更好和更易于维护的解决方案来解决问题。本章提到了Python的一些特性,这些特性使得一些设计模式不可见,并采用实用的方法实现了其中的一些设计模式。
第10章 整洁架构,强调代码整洁是实现良好架构的基础。我们在第1章中提到的所有细节,以及在此过程中重温的其他内容,在部署系统时都将在整个设计中发挥关键作用。
为了便于阅读,本书使用了一些特殊的体例格式,具体如下。
(1)黑体表示这是一个新的名词术语,或者是需要强调的内容。
(2) 表示警告或重要注释。
(3) 表示提示和小窍门。
本书由异步社区出品,社区(https://www.epubit.com/)为你提供相关资源和后续服务。
本书为读者提供源代码。读者可在异步社区本书页面中单击,跳转到下载界面,按提示进行操作即可。注意:为保证购书读者的权益,该操作会给出相关提示,要求输入提取码进行验证。
作者和编辑尽最大努力来确保书中内容的准确性,但难免还会存在疏漏。欢迎读者将发现的问题反馈给我们,帮助我们提升图书的质量。
如果读者发现错误,请登录异步社区,搜索到本书页面,单击“提交勘误”,输入相关信息,单击“提交”按钮即可。本书的作者和编辑会对读者提交的勘误进行审核,确认并接受后,将赠予读者异步社区的100积分(积分可用于在异步社区兑换优惠券,或者用于兑换样书或奖品)。
我们的联系邮箱是contact@epubit.com.cn。
如果读者对本书有任何疑问或建议,请发送邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。
如果读者有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区在线投稿(直接访问www.epubit.com/selfpublish/submission即可)。
如果读者来自学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。
如果读者在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请将怀疑有侵权行为的链接发邮件给我们。这一举动是对作者权益的保护,也是我们持续为广大读者提供有价值的内容的动力之源。
“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。
“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近40年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、人工智能、测试、前端、网络技术等。
异步社区
微信服务号
本章介绍与代码整洁相关的第一个概念——什么是代码整洁,以及代码整洁意味着什么。本章旨在让你明白,整洁的代码不是软件项目中的“好东西”或“奢侈品”,而是必需品。没有高质量的代码,项目会面临因累积的技术债务而带来的失败风险。
本章还会详细介绍代码格式化和代码注释的概念,这也许听起来有点多余,但是将来你会发现,代码格式化和代码注释在保持代码的可维护性和可操作性方面起着非常重要的作用。
接着,本章会探讨为项目采用良好编码规范的重要性。意识到维护代码与文档保持一致是一项持续的任务,我们就能明白利用自动化工具可以简化我们的工作。为此,我们快速讨论如何配置主要工具,以便让它们作为构建的一部分在项目上自动运行。
通过学习本章的内容,你将了解什么是代码整洁、代码整洁为什么如此重要、为什么格式化和文档化是编码的关键任务,以及如何实现这个过程的自动化。由此,你应该具备快速组织新项目结构的思维方式,并以获得良好的代码质量为目标。
通过学习本章的内容,你将了解以下要点。
(1)代码整洁实际上远比软件架构中的格式化重要。
(2)代码整洁很重要,但为了代码的可维护性,在软件项目中使用标准格式也是十分关键的。
(3)如何使用Python提供的特性实现代码文档的自动生成。
(4)如何配置工具才能以一致的方式安排代码的布局,以便团队成员能够将更多精力放在关注业务问题的本质上。
对于代码整洁,没有唯一的或者严格的定义,而且可能无法正式地衡量怎样才算代码整洁,因此你不能在代码仓库上运行一个可以告诉你代码是好是坏、可维护性如何的工具。当然,你可以运行检查器、代码校验器、静态分析器等工具。这些工具会给你很大的帮助。它们是必需的,但光有这些还远远不够。代码整洁与否不是机器或脚本能说了算的(到目前为止),而是作为专业人员的我们才能决定的。
几十年来,我们沿用“编程语言”这个术语,并将其视为把我们的想法传达给计算机的语言,可以让计算机运行我们的程序。但是我们错了,这仅仅是部分事实。编程语言背后的“真正语言”是将我们的想法传达给其他开发人员的语言。
这才是代码整洁的真正本质所在。它取决于其他开发人员是否能够读取和维护代码。作为专业人士,我们是唯一能够判断这一点的人。想想看,作为开发人员,我们阅读代码的时间比实际编写代码的时间要多得多。每当我们想要更改或添加新功能,首先必须阅读需要修改或扩展的代码的所有上下文内容。编程语言(Python)就是开发人员实现互相沟通的语言。
因此,本书并不会给出代码整洁的定义,而是给出所有关于Python的惯用内容,以帮助你了解好代码和坏代码之间的区别,识别好代码和好架构的特征,然后让你从自己的角度理解代码整洁的含义。读完本书,你将能够自行判断和分析代码,并将对代码整洁有更透彻的理解。不管给出什么定义,你都会知道代码整洁是什么以及它意味着什么。
为什么保持代码整洁如此重要,原因有很多。大多数原因与可维护性、减少技术债务、有效配合敏捷开发以及管理一个成功的项目的想法有关。
我们想探讨的第一个想法是关于敏捷开发和持续交付的。如果希望项目能够以稳定和可预测的速度不断成功地交付特性,那么必须有一个良好且可维护的代码库。
假设你正驾驶着一辆汽车行驶在去往某个目的地的道路上,而且想要在某个时间点之前到达那里。你必须预估自己到达目的地的时间,这样才能告知正在等你的人。如果汽车不出故障,道路十分平坦,那么你的预估不大可能有太大的偏差;相反,如果道路被破坏,你必须下车把石头移开,或者要避开裂缝,抑或每隔几千米就必须停下来检查一下发动机等,那么你不太可能确定什么时候到达(或者你是否能到达)。这个比喻明确易懂,这里的道路可以理解为代码。如果希望以稳定、恒定和可预测的速度向前推进项目,那么代码应该是可维护和可读的。
技术债务是指因妥协或所做出的不良决策而导致的软件问题。在某种程度上,我们可以从两个方面考虑技术债务问题。一是从过去到现在,如果我们当前面临的问题是由之前编写的错误代码造成的,那会怎样?二是从现在到将来,如果我们决定现在就走捷径,而不是花时间去寻找合适的解决方案,那么未来又会为自己带来什么麻烦?
“债务”这个词用得恰如其分。这是一笔债务,因为在未来代码将比现在更难以修改。产生的成本就是债务的利息。产生的技术债务意味着,明天修改代码比今天更困难,成本更高,而且后天的成本会更昂贵,等等。
一旦团队不能按时交付一些东西,并且不得不停下来去修复和重构代码,代码就要付出技术债务的代价。
技术债务最糟糕的一点是它代表了一个长期和根本的问题。这不是什么值得高度警觉的东西。相反,这是一个悄无声息的问题,这个问题分散在整个项目的各个部分,在某一天,在某一个特定的时间,这个问题会“醒来”,并成为项目推进的阻碍。
代码整洁是指根据一些标准(例如,PEP-8或由项目规范定义的自定义标准)进行的代码格式化和结构化吗?并非如此。
代码整洁远远不止编码标准、格式化、美化工具和其他有关代码布局的检查这些内容。代码整洁是关于实现高质量的软件和建立一个健壮、可维护和避免技术债务的系统的。一段代码或整个软件组件可以百分之百符合PEP-8(或任何其他准则)标准,但仍可能无法满足上述要求。
然而,不注意代码的结构会有一些危险。鉴于此,我们先来分析不良代码结构的问题以及如何解决这些问题,然后介绍如何为Python项目配置和使用工具,以便自动检查和纠正问题。
综上所述,我们可以说代码整洁与PEP-8或编码风格没有任何关系。代码整洁的意义远不止于此,除了可维护性和软件的质量,它还意味着更有意义的东西。不过,正如你将看到的,要实现高效工作,正确地格式化代码非常重要。
编码准则是项目在质量标准下开发时必须考虑的最低要求。在本节中,我们将探讨其背后的原因。接下来我们开始探讨如何通过工具自动在项目中遵循编码风格准则。
在试图考虑在代码布局中找到某种好的特性时,我们首先想到的就是一致性。我们希望代码能够具有一致的结构,以便更易阅读和理解。如果代码不正确或者结构不一致,并且团队中的每个人都以自己的方式做事,那么最终得到的将是需要额外努力和集中精力才能正确理解的代码。这样的代码很容易引起误解,并且由此引发的漏洞或微小的错误很可能被忽略。
上述情况是我们想要避免的。我们想要的是一眼就能读懂和理解的代码。
如果开发团队的成员都同意采用标准化的方式编写代码,那么所得到的代码看起来会更加熟悉。这样,你就能快速识别模式,并且记住这些模式,进而能更容易地理解内容和检测错误。例如,当某些代码出错时,你可能会在你熟悉的模式中看到一些奇怪的东西——它们会吸引你的注意,再仔细观察,就很可能发现错误!
正如经典著作Code Complete中所述的,在名为Perception in Chess(1973年)的论文中对此进行了有趣的分析,该论文提到了一项实验,以确定不同的人如何理解或记忆不同的棋局。该实验针对不同级别的棋手(新手、中级棋手和象棋高手)以及棋盘上不同位置的棋局来进行统计。他们发现,当棋子的位置是随机的时候,新手能和象棋高手表现得一样好。因为这只是一个记忆练习,任何人都可以发挥出合理的水平。但当棋子的位置遵循一个可能发生在一场真正对弈中的一些逻辑顺序(或者,遵守某种一致性,坚持某种模式时)时,那么象棋高手们的表现比其他人要好得多了。
现在我们想象一下,同样的情况也适用于软件开发。作为Python方面的软件工程师专家,我们就好比上述例子中的象棋高手。如果代码的结构是随机的,没有遵循任何逻辑或者没有遵循任何标准,我们就会像一个新手开发人员一样,很难发现错误;如果我们习惯以结构化的方式阅读代码,并且通过遵循这种模式学会从代码中快速获得想法,就会在项目开发中比其他开发人员更有优势。
就Python而言,你应该遵循的编码风格是PEP-8。你可以对其进行扩展或采用其中的一部分,以适应正在参与的项目的某种特殊性(如行的长度、字符串的注释等)。不过,我们建议,无论你使用最原始版本的PEP-8规范还是对它进行扩展,都应该坚持使用,而不是从头开始尝试另一个不同的标准。
这是因为PEP-8充分考虑了Python语法的许多特殊性(通常不适用于其他语言),并且它是由对Python语法做出贡献的核心Python开发人员创建的。因此,我们认为其他标准其实很难与PEP-8相提并论,更不用说超越它了。
尤其是,在处理代码时,PEP-8还有一些不错的改进特性。
(1)可进行grep。这就是在代码中对内容进行grep的能力,即在某些文件(以及这些文件的某个部分)中搜索所要查找的特定字符串。PEP-8引入的特性之一是区分将值赋值写入变量的方式和传递给函数的关键字参数的方式。
为了更好地理解这一点,我们用一个示例加以阐释。假设我们正在进行调试,需要找到名为location的参数值的传递位置。我们可以运行以下grep命令,获悉要查找内容所在的文件和行号。
$ grep -nr "location=" .
./core.py:13: location=current_location,
现在,我们想知道这个变量在哪里被分配这个值,则可以运行以下命令。
$ grep -nr "location =" .
./core.py:10: current_location = get_location()
PEP-8建立了这样一种约定,即当通过关键字向函数传递参数时,不使用空格,但在分配变量时使用空格。因此,我们可以调整搜索条件(第一次搜索时等号两侧没有空格,第二次搜索时等号两侧都有一个空格),从而提高搜索效率。这是遵守约定的好处之一。
(2)一致性。如果代码看起来有一种统一的格式,阅读起来就会容易得多。这对于新加入项目的人来说尤为重要,如果你希望有新的开发人员加入项目,或者为团队聘用新的(可能经验不足的)程序员,那么他们势必要熟悉代码(甚至可能由多个代码仓库组成)。如果代码格式、文档、命名约定等在所有代码仓库的所有文件中都是相同的,那么他们的工作将变得更加轻松。
(3)代码质量。以结构化的方式查看代码,你一下子就能更熟练地理解它(就像在Perception in Chess中所说的那样),并且更容易发现程序的漏洞和错误。除此之外,检查代码质量的工具也会提示潜在的错误。对代码的静态分析可能有助于降低每行代码的错误率。
本节主要介绍如何在代码内部对Python中的代码进行文档化。好的代码是自解释型的,但仍然需要有很好的文档记录。我们需要解释代码应该做什么(而不是怎么做)。
一个重要的区别是:为代码编写文档与为代码添加注释是不同的。注释是不好的,应该避免使用。通过文档,我们可以找到解释数据类型、提供数据类型示例以及注释变量的说明。
这在Python中是相关的,由于Python变量是动态类型的,很容易在函数和方法之间丢失变量或对象的值,因此声明这些信息将使代码更易读。
还有一个与注释特别相关的内容,即还可以通过Mypy等工具帮助运行一些自动检查,如类型提示,最后让添加注释变得有益。
简单来说,我们可以说文档字符串基本上是嵌在源代码中的文档。文档字符串其实就是一个普通的字符串,可以放在代码中的某个地方,目的是为了记录这一部分的逻辑。
注意,我们重点强调的“文档”这个词。这种微妙之处很重要,因为它是用来解释的,而不是用来判断的。文档字符串不是注释,它们是文档。
在代码中添加注释是一种糟糕的做法,具体原因有很多。首先,添加注释意味着我们未能在代码中表达自己的想法。如果我们真的需要解释为什么或者如何做某事,那么说明这个代码可能不够好——它不是不解自明的,还可能产生误导。相比花些时间阅读复杂的代码片段,更糟糕的是阅读了说明代码应该如何工作的注释,却发现代码实际上做了一些不同的事情。人们在更改代码时往往会忘记更新注释,因此位于刚更改的行旁边的注释就会过时,并将造成危险的误导。
有时,在个别情况下,我们不得不添加注释,例如可能因为某个第三方库有一个错误,我们必须规避。在这些情况下,放置一个描述性的小注释是可以接受的。
然而,对于文档字符串,情况就不同了。再次说明,它们不代表注释,而是代码中特定组件(模块、类、方法或函数)的文档。使用文档字符串不但是被接受的,而且是值得鼓励的。尽可能添加文档字符串是很好的实践。
文档字符串在代码中(甚至可能是必需的,这取决于项目的标准)之所以是一个好东西,是因为Python是动态类型的。这意味着函数可以将任何东西作为其任何参数的值。Python不会强制或检查类似的内容。因此,假设你在代码中找到了一个必须修改的函数,而且不巧这个函数有一个描述性名称,它的参数也有描述性名称,但是你有可能仍然不太清楚应该传递给它什么类型。在这种情况下,我们该如何使用这个函数呢?
这时,一个好的文档字符串可能会有所帮助。记录函数的预期输入和输出是不错的做法,有助于阅读相关代码的人理解这个函数应该如何工作。
我们来看标准库中下面这个非常好的例子:
In [1]: dict.update??
Docstring:
D.update([E, ]**F) -> None. Update D from dict/iterable E and F.
If E is present and has a .keys() method, then does: for k in E: D[k] =
E[k]
If E is present and lacks a .keys() method, then does: for k, v in E: D[k]
= v
In either case, this is followed by: for k in F: D[k] = F[k]
Type: method_descriptor
其中,字典update方法的文档字符串提供了有用的信息,它告诉我们可以以不同的方式使用它。
(1)可以使用.keys()方法传递某些内容(如另一个字典),它将使用每个参数传递的对象的键更新原始字典:
>>> d = {}
>>> d.update({1: "one", 2: "two"})
>>> d
{1: 'one', 2: 'two'}
(2)可以传递成对的键和值,并对它们加以解析,然后传给update方法:
>>> d.update([(3, "three"), (4, "four")])
>>> d
{1: 'one', 2: 'two', 3: 'three', 4: 'four'}
在任何情况下,字典都将使用传递给它的其余关键字参数进行更新。
这些信息对于必须学习和理解新功能如何工作以及如何利用它的人来说至关重要。
注意,在上面的示例中,我们通过在函数上使用双问号(dict.update??)获得了该函数的文档字符串。这是IPython交互式解释器的一个特性。此函数被调用时,将打印所需对象的文档字符串。现在,假设以同样的方式,我们从标准库的这个功能中获得了帮助,如果你在编写的函数上放置文档字符串,以便其他人能够以相同的方式理解它们的操作,这能使你(代码的用户)的工作轻松多少?
文档字符串不是从代码中分离出来的内容。它是代码的一部分,可供访问。当一个对象定义了一个文档字符串时,文档字符串通过__doc__属性成为对象的一部分:
>>> def my_function():
... """Run some computation"""
... return None
...
>>> my_function.__doc__
'Run some computation'
这意味着甚至可以在运行时访问它,甚至可以从源代码生成或编译文档。事实上,这是有工具可以实现的。如果运行Sphinx,即可为项目的文档创建基本的框架。通过autodoc(sphinx.ext.autodoc)扩展,该工具将从代码中获取文档字符串,并将它们放在文档功能的页面中。
一旦有了构建文档的工具,你应该将其公布出来,使其成为项目本身的一部分。对于开放源代码项目,你可以使用Read the Docs,它将根据分支或版本(可配置)自动生成文档。对于公司或项目来说,你可以在内部使用相同的工具或配置这些服务,但是不管如何决定,重要的是应该准备好文档并可供团队的所有成员使用。
不过,文档字符串有一个缺点:与所有文档一样,它需要手动和持续的维护。若代码有更改,必须对其进行更新。还有一个问题是,为了使文档字符串真正发挥作用,必须对其进行详细说明,这需要许多行文字。
虽然维护正确的文档是一个我们无法逃避的软件工程方面的挑战,但是这么做也是有意义的。之所以手工编写文档,是为了让其他人阅读,如果它是自动生成的,可能就没有多大用处了。为了使文档有价值,所有团队成员必须同意它需要人工干预,并需要为之付出努力。关键是要明白软件不仅仅是关于代码的,附带的文档也是可交付结果的一部分。因此,当有人对某个函数进行更改时,同样重要的是对刚刚更改的代码相应部分的文档也进行更新,不管它是wiki、用户手册、README文件还是多个文档字符串。
PEP-3107引入了“注解”的概念。其基本思想是向代码阅读者提示函数中参数值的期望值。这里使用“提示”这个词可能并不正式。注解支持类型提示,本章稍后将讨论这一点。
注解允许指定已定义的某些变量的预期类型。实际上,它不仅与类型有关,还与任何类型的元数据有关,这些元数据可以帮助你更好地了解该变量实际表示的内容。
考虑下面的例子:
class Point:
def __init__(self, lat, long):
self.lat = lat
self.long = long
def locate(latitude: float, longitude: float) -> Point:
"""Find an object in the map by its coordinates"""
这里,我们用float表示latitude和longitude的预期类型。这对于阅读函数的人来说只是提供了信息,以便他们了解这些预期类型。但是Python不会校验或者强制规定这些类型。
我们还可以指定函数返回值的预期类型。在这个例子中,Point是一个用户定义的类,因此返回的内容将是Point的实例。
类型或内置不是可以用作注解的唯一类型。基本上,所有在当前Python解释器范围内有效的内容都可以放在注解里,例如,解释变量意图的字符串、可作为回调或验证函数使用的调用,等等。
随着注解的引入,一个新的特殊属性也被包括进来,这就是__annotations__。这将使我们能够访问一个字典,该字典将注解的名称(作为字典中的键)与其对应的值(这些值是我们为它们定义的值)映射在一起。在示例中,该字典如下所示:
>>> locate.__annotations__
{'latitude': float, 'longitue': float, 'return': __main__.Point}
如果有必要,我们可以用它来生成文档、运行验证或在代码中强制检查。
谈到通过注解检查代码,这就是PEP-484发挥作用的时候了。它指定了类型提示的基础和通过注解检查函数类型的想法。重申一下:
“Python仍然是一种动态类型语言,笔者不希望强制使用类型提示,即使是通过约定。”
类型提示的思想是使用额外的工具(独立于解释器)检查和评估代码中的类型是否正确地使用,并在检测到任何不兼容的类型时提示用户。我们将在后续章节中详细介绍运行这些检查的工具Mypy,并将讨论如何在项目中使用和配置该工具。现在,你可以把它看作一种检查代码上所用类型的语义的linter。有时,这有助于在测试和检查运行的早期发现错误。因此,最好在项目上配置Mypy,并将其与静态分析的其他工具同时使用。
不过,类型提示不仅仅意味着检查代码类型的工具。Python自3.5版本开始,引入了新的类型模块,这显著改进了我们在Python代码中定义类型和注解的方式。
这背后的基本思想是,现在语义扩展到更有意义的概念,使我们(人类)更容易理解代码的含义,或者在某个地方所期望的东西。例如,你可能会有这样一个函数,它的某个参数是列表或元组,然后你就可以将这两种类型中的一种作为注解,甚至是解释它的字符串。但是有了新的类型模块,就可以告诉Python它需要一个可迭代的对象或一个序列。你甚至可以标识类型或其上的值,例如,它采用一个整数序列。
在编写本书时,Python对注解做了一个额外的改进,那就是从Python 3.6开始,可以直接注解变量,而不仅仅注解函数参数和返回类型。这是在PEP-526中引入的,意思是可以声明定义的某些变量的类型,而不必为它们赋值,如下所示。
class Point:
lat: float
long: float
>>> Point.__annotations__
{'lat': <class 'float'>, 'long': <class 'float'>}
这是一个很合理的问题,因为在引入注解之前很久,在旧版本的Python上,就有了通过在函数或属性上放置文档字符串记录参数类型的方法。关于如何构造文档字符串以包含函数的基本信息的格式,甚至有一些约定,包括每个参数的类型和含义、函数的类型、结果的含义,以及函数可能抛出的可能异常。
其中大部分已经通过注解以更紧凑的方式进行了处理,所以有人可能会想,使用文档字符串是否真的值得。答案是值得,因为注解和文档字符串是互补的。
确实,以前包含在文档字符串中的一部分信息现在可以移动到注解中了。但这只会为更好地记录文档字符串留下更多的空间。特别是,对于动态和嵌套数据类型,最好提供预期数据的示例,以便我们能更好地了解正在处理的内容。
考虑下面的例子。假设有一个函数,该函数需要一个用于验证某些数据的字典:
def data_from_response(response: dict) -> dict:
if response["status"] != 200:
raise ValueError
return {"data": response["payload"]}
如上述代码所示,该函数接收一个字典并返回另一个字典。如果键值"status"对应的值不是预期值,则可能抛出异常。然而,除此之外,我们没有更多关于这个函数的信息。例如,response对象的正确实例是什么样的?result的实例会是什么样的?要回答这两个问题,最好把期望由参数传入并由该函数返回的数据示例记录下来。
让我们看看能否借助文档字符串更好地解释这一点:
def data_from_response(response: dict) -> dict:
"""If the response is OK, return its payload.
- response: A dict like::
{
"status": 200, # <int>
"timestamp": "....", # ISO format string of the current
date time
"payload": { ... } # dict with the returned data
}
- Returns a dictionary like::
{"data": { .. } }
- Raises:
- ValueError if the HTTP status is != 200
"""
if response["status"] != 200:
raise ValueError
return {"data": response["payload"]}
现在,我们对这个函数预期接收和返回的内容有了更好的了解。之所以说文档字符串是有价值的输入,不仅因为它有助于理解传递的内容,还因为它是单元测试的有价值的信息来源。我们可以构造这样的数据作为输入,并且知道在测试中使用的值哪些是正确的,哪些是不正确的。实际上,这些测试也可以作为代码的可操作文档,但这需要更详细的解释。
这样做的好处是,现在我们知道了键的可能值以及它们的类型,并且对数据的结构有了更具体的印象。如前所述,我们为此付出的代价是文档字符串占用大量的行,并且是冗长且详细的,这样才能有效。
在本节中,我们将探讨如何配置一些基本工具并自动对代码运行检查,以利用部分重复的验证检查。
需要着重牢记的一点是:代码是为了让我们——人——理解的,所以只有我们能够判定什么是好的代码,什么是坏的代码。我们应该在代码评审上投入时间,思考什么是好的代码,以及什么样的代码是可读的和可理解的。当评审同行编写的代码时,你应该问这样的问题:“对于其他程序员来说,这段代码容易理解和遵循吗?”“它是否从专业的角度解决了问题?”“加入团队的新成员是否能够理解并有效地使用它?”
正如我们在前面看到的,代码格式、一致的布局和适当的缩进都是必需的,但在代码库中仅有这些还是不够的。作为有高度质量意识的工程师,我们认为这些是理所当然的事情,所以应该读写远远超出满足基本要求的高质量代码。我们不愿意把时间浪费在审核这些项目上,因此可以查看代码中的实际模式以更有效地投入时间,以理解代码的真正含义并给出有价值的结果。
所有这些检查都应该是自动的。它们应该是测试或检查项列表的一部分,而这又应该是持续集成构建的一部分。如果这些检查未通过,则会导致构建失败。这是确保代码结构始终保持连续性的唯一方法。这些检查也是可供团队参考的客观参数。不是让一些工程师或团队负责人总是在代码评审中参照PEP-8给出相同的评论,而是要让构建自动失败,使之成为客观的东西。
Mypy是Python中主要的可选静态类型检查工具。其思想是,一经安装,Mypy将分析项目中的所有文件,检查类型使用上的不一致。这是很有用的,因为在大多数情况下,它会提前检测到实际的错误(但有时会给出误报)。
可以使用pip安装Mypy,建议将其包括在项目对安装文件的依赖内。
$ pip install mypy
一旦将Mypy安装到虚拟环境中,只需运行前面的命令,它就能报告类型检查的所有结果。尽量遵循生成的报告内容,因为大多数时候,它提供的见解有助于避免错误被带到生产环境中去。但是,该工具并不是完美的,因此如果你认为报告内容不合理,则可以用以下命令作为注释来忽略它。
type_to_ignore = "something" # type: ignore
在Python中有许多用于检查代码结构的工具(基本上,这与PEP-8是一致的),例如pycodestyle(以前称为PEP-8)、Flake8,等等。这些工具都是可配置的,并且像运行它们所提供的命令一样容易使用。在这些工具中,笔者发现Pylint是最完整的(也是最严格的)。它也是可配置的。
同样,只需使用pip将其安装在虚拟环境中。
$ pip install pylint
然后,只要运行pylint命令,就可以对代码进行校验了。
可以通过名为pylintrc的配置文件配置Pylint。在此配置文件中,你可以决定要启用或禁用的规则,并将其他规则参数化(例如,更改列的最大长度)。
在UNIX开发环境中,最常见的运行自动检查的方式是使用makefile。makefile是一种功能强大的工具,允许我们配置要在项目中运行的命令,主要用于编译、运行等。除此之外,我们还可以在项目的根目录中使用makefile,通过配置一些命令来自动检查代码的格式和约定。
一个好的方法是为测试设置目标,再为每个特定的测试项目设置目标,然后再运行另一个测试,例如。
typehint:
mypy src/ tests/
test:
pytest tests/
lint:
pylint src/ tests/
checklist: lint typehint test
.PHONY: typehint test lint checklist
在这里,我们应该(在用于开发的计算机上和持续集成环境构建中)运行如下命令。
make checklist
其将按以下步骤运行所有内容:第一,检查是否符合编码准则(如PEP-8);第二,检查代码中数据类型的使用情况;第三,运行测试。
如果这些步骤中的任何一个失败,则认为整个过程失败。
除了在构建中自动配置这些检查,如果团队采用约定和自动方法构建代码,也是一个好主意。Black等工具用于自动格式化代码。有许多工具可以自动编辑代码,但是Black的有趣之处在于它以一种独特的形式进行编辑。它有统一的标准,而且具有确定性,因此代码最终总是以相同的方式排列。
例如,Black字符串始终使用双引号,参数的顺序始终遵循相同的结构。这听起来可能很死板,但这是确保代码差异最小的唯一方法。如果代码总是遵循相同的结构,那么代码中的更改只会随实际变化显示在拉取请求中,并且没有额外的修饰性修改。虽然它比PEP-8更具限制性(实际上不必担心这一点),但也很方便,因为我们通过一个工具直接格式化代码,从而可以集中精力解决手头的问题。
在写这本书的时候,唯一可以配置的就是行的长度。其他的都是根据项目的标准来修正的。
以下代码符合正确的PEP-8标准,但不遵循Black约定:
def my_function(name):
"""
>>> my_function('black')
'received Black'
"""
return 'received {0}'.format(name.title())
现在,我们可以运行以下命令来格式化文件:
black -l 79 *.py
我们可以看到工具写了什么:
def my_function(name):
"""
>>> my_function('black')
'received Black'
"""
return "received {0}".format(name.title())
在更复杂的代码上,会有更多的变化(后面有逗号等),但是可以清楚地体现这个约定的宗旨。尽管有点自以为是,但是有一个为我们处理细节的工具仍不失为一个好主意。这也是Golang社区很久以前就学到的,现在甚至有了一个标准的工具库got fmt,它可以根据语言的约定自动格式化代码。很好,Python现在有了这样的东西。
这些工具(Black、Pylint、Mypy等)可以与你选择的编辑器或IDE集成,使工作变得更加简单。配置编辑器在保存文件或通过快捷方式做这些修改是一项不错的主意。
现在,我们对“何谓代码整洁”有了最初的概念,并对其进行了可行的解释,这将作为本书其余部分的参考。
更重要的是,我们了解到代码整洁比代码的结构和布局更重要。我们必须关注如何在代码上表达这些想法,以查看它们是否正确。代码整洁与代码的可读性、可维护性有关,它将技术债务保持在最低限度,并将我们的想法有效地传达到代码中,以便其他人能够准确理解我们最初打算写的内容。
我们讨论了遵守编码样式或准则的重要性——理由有很多。我们一致认为这是一个必要但不充分的条件,而且鉴于这是每个项目应该遵守的最低要求,显然我们会把这些事情留给工具去处理。因此,自动化所有这些检查变得至关重要,在这方面,我们必须记住如何配置诸如Mypy、Pylint等工具。
在第 2 章中,我们将更加关注特定的 Python 代码,以及如何用地道的Python表达我们的想法。我们将探讨Python中的习惯用法,这些习惯用法使代码更加紧凑和高效。分析过程中,我们将看到,通常情况下,与其他语言相比,Python会用不同的想法或方法完成任务。