你能写出漂亮的Python函数吗?与大多数现代编程语言一样,在Python中,函数是抽象和封装的基本方法之一。您可能在开发过程中编写了数百个函数,但并非每个函数都生而平等。编写“糟糕”的函数会直接影响代码的可读性和可维护性。那么,什么样的函数是“坏”函数呢?更重要的是,如何编写一个“好的”函数?简单回顾一下,数学充满了函数,尽管我们可能不记得它们。让我们首先回顾一下大家最喜欢的话题——微积分。您可能还记得这个等式:f(x)=2x+3。这是一个名为“f”的函数,它接受一个未知的x,并“返回”2*x+3。这个函数可能和我们在Python中看到的不太一样,但是基本思想和计算机语言中的函数是一样的。函数在数学中有着悠久的历史,但在计算机科学中更为强大。不过,函数也有一些缺点。接下来我们将讨论什么是“好”函数,以及我们需要重构函数的症状是什么。好的Python函数和糟糕的Python函数有什么区别?“好”功能的定义如此之多,令人惊讶。出于我们的目的,我将一个好的Python函数定义为符合以下列表中的大多数规则的函数(有些更难以实现):名字好有一个函数包含文档注释返回一个值50行是幂等的或更少尽可能纯函数这个列表对很多人来说可能有点过于严格。但我保证,如果您的函数遵循这些规则,您的代码将看起来很漂亮。下面我将逐条解释每条规则,然后总结这些规则是如何做出“好”功能的。命名关于这个主题我最喜欢的引述之一(PhilKarlton的,总是被误认为是DonaldKnuth)是这样的:计算机科学中只有两个难题:缓存失效和命名问题。听起来有点古怪,但整个漂亮的命名真的很难。这是一个糟糕的函数名称:defget_knn(from_df):我基本上到处都看到了糟糕的名字,但这个例子来自数据科学(或者更确切地说,机器学习),从业者总是在Jupyternotebooks中编写代码,然后尝试将这些不同的单元组成一个可理解的程序。函数命名的第一个问题是首字母缩略词/首字母缩略词的使用。完整的英文单词比不广泛使用的缩写词和首字母缩略词更好。使用缩写的唯一原因是节省打字时间,但现代编辑器具有自动完成功能,因此您只需输入全名一次。缩写之所以成为问题,是因为它们通常只在某些领域可用。在上面的代码中,knn指的是“K-最近邻”,df指的是“DataFrame”——无处不在的Pandas数据结构。如果另外一个不熟悉这些缩写的程序员在看代码,那个TA会一头雾水。这个函数名还有另外两个小问题:“get”这个词无关紧要。对于大多数命名良好的函数,很明显该函数会返回一些东西,并且名称反映了这一点。from_df也是不必要的。如果参数名称不够明确,函数的文档注释或类型注解会描述参数类型。那么我们如何重命名这个函数呢?例如:defk_nearest_neighbors(dataframe):现在,即使是外行也知道这个函数在计算什么,而且参数(dataframe)的名称也清楚地告诉我们应该传递什么类型的参数。TheSingleFunctionalityPrinciple“单一功能原则”出自BobMartin“叔叔”的一本书,它不仅适用于类和模块,也适用于函数(Martin的最初目标)。该原则强调函数应该具有“单一功能”。也就是说,一个函数应该只做一件事。这样做的一个重要原因是:如果每个函数只做一件事,那么只有当函数做那件事的方式必须改变时,函数才需要改变。当一个函数可以被删除时,事情就很简单了:如果其他东西发生了变化并且不再需要该函数的单一功能,那么就删除它。让我用一个例子来解释。这是一个做不止一件“事情”的函数:defcalculate_andprint_stats(list_of_numbers):sumsum=sum(list_of_numbers)mean=statistics.mean(list_of_numbers)median=statistics.median(list_of_numbers)mode=statistics.mode(list_of_numbers)print('----------------统计数据----------------')print('SUM:{}'.format(sum)print('MEAN:{}'.format(mean)print('MEDIAN:{}'.format(median)print('MODE:{}'.format(mode)这个函数做了两件事:计算一个集合统计关于数字列表并将它们打印到STDOUT。这个函数违反了函数更改的原因只有一个的原则。显然这个函数进行更改有两个原因:需要计算新的或不同的数据或者输出的格式需要改变。最好把这个函数写成两个独立的函数:一个用于执行和返回计算结果;名字中有“and”一词也简化了函数行为的测试,并且他们不仅分开在一个模块中分为两个功能,但可能存在于适当的不同模块中。这使得测试更容易更清洁且更易于维护。只做两件事的函数实际上非常少见。更多时候,一个函数负责很多很多的任务。同样,为了可读性和可测试性,我们应该称这些“多面手”为函数,将函数分成小函数,每个小函数只负责一项任务。文档注释许多Python开发人员都知道PEP-8,它定义了Python编程的风格指南,但很少有人理解文档注释样式的定义。PEP-257。PEP-257这里不做详细介绍,读者可以详细阅读本指南约定的文档注释样式。PEP-8:https://www.python.org/dev/peps/pep-0008/PEP-257:https://www.python.org/dev/peps/pep-0257/定义了第一个文档注释模块、函数、类或方法的第一个字符串声明。这个字符串应该清楚地描述函数的功能、输入参数和返回参数。PEP-257的主要信息如下:每个函数都需要一个文档描述;使用适当的语法和标点符号,写出完整的句子;功能的主要功能需要在开头用一句话概括;使用说明性语言而不是描述性语言。编写函数时很容易遵循这些规则。我们只需要养成编写文档注释的习惯,并在实际编写函数体之前完成它们。如果你不能清楚地描述这个函数是干什么的,那么你就需要多想想你为什么要写它。返回值的函数可以而且应该被视为独立的小程序。他们将一些输入作为参数并返回一些输出值。当然参数是可选的,但是从Python内部机制来看,返回值是不可选的。即使你试图创建一个不返回值的函数,我们也不能选择在内部不使用返回值,因为Python的解释器会强制返回一个None。不信的读者可以用下面的代码测试一下:?python3Python3.7.0(default,Jul232018,20:22:55)[Clang9.1.0(clang-902.0.39.2)]ondarwinType"help","copyright""credits"or"license"*for*moreinformation.>>>defadd(a,b):...print(a+b)...>>>b=add(1,2)3>>>b>>>bisNoneTrue运行上面的代码,你会看到b的值确实是None。所以即使我们写了一个不包含return语句的函数,它仍然会返回一些东西。但是函数也应该返回一些东西,因为它也是一个小程序。没有输出的程序有多有用,我们如何测试它?我什至想声明每个函数都应该返回一个有用的值,即使该值仅对测试有用。我们写的代码是要测试的,没有返回值的函数是很难测试正确性的。以上函数可能需要重定向I/O进行测试。此外,返回值可以更改方法调用。下面的代码展示了这个概念:withopen('foo.txt','r')asinput_file:forlineininput_file:ifline.strip().lower().endswith('cat'):#...dosomethingusefulwiththeselinesThelineifline。strip().lower().endswith('cat')起作用是因为字符串方法(strip()、lower()、endswith())返回一个字符串以用作调用该函数的结果。以下是人们在被问及为什么要编写不返回值的函数时给出的一些常见原因:“执行类似I/O的函数,例如将值保存到数据库,不能返回有用的输出。”我不同意这个观点,因为函数可以在操作成功完成时返回True。“我需要返回多个值,因为只返回一个值不代表任何东西。”当然,也可以返回包含多个值的元组。简而言之,即使在现有代码库中,从函数返回值绝对是个好主意,而且不太可能破坏任何东西。函数长度函数的长度直接影响可读性,从而影响可维护性。所以确保你的函数长度足够短。50行的函数对我来说是一个合理的长度。如果函数遵循单一函数原则,它的长度一般会很短。如果函数是纯函数或幂等函数(下面讨论),它也会更短。这些想法有助于构建干净的代码。那么如果函数太长怎么办?代码重构!代码重构可能是您在编写代码时一直在做的事情,即使您不熟悉该术语也是如此。它的含义是:改变程序的结构而不改变程序的行为。所以从一个长函数中抽取出几行代码,转换成属于那个函数的函数,也是一种代码重构。这也是缩短长函数的最快和最常用的方法。只要适当地命名这些新函数,代码就会更易于阅读。幂等性和函数纯度幂等函数在给定相同的变量参数集时返回相同的值,无论它被调用多少次。该函数的结果不依赖于非局部变量、参数的易变性或来自任何I/O流的数据。以下add_three(number)函数是幂等的:defadd_three(number):"""Return*number*+3."""returnnumber+3每当调用add_three(7)时,返回值为10。下面显示一个示例一个非幂等函数的:defadd_three():"""Return3+thenumberenteredbytheuser."""number=int(input('Enteranumber:'))returnnumber+3这个函数不是幂等的,因为函数返回值依赖于I/O,即用户输入的数字。每次调用此函数时,它可能会返回不同的值。如果它被调用两次,用户可以第一次输入3,第二次输入7,导致对add_three()的调用分别返回6和10。为什么幂等性很重要?可测试性和可维护性。幂等函数很容易测试,因为它们在给定相同参数的情况下返回相同的结果。测试是检查对函数的不同调用是否按预期返回值。此外,幂等函数的测试速度很快,这在单元测试中非常重要但经常被忽视。重构幂等函数也很简单。无论您如何更改函数外部的代码,使用相同的参数调用函数都会返回相同的值。什么是“纯”功能?在函数式编程中,如果函数是幂等的并且没有明显的副作用,那么它就是纯函数。请记住,幂等函数意味着函数在给定参数集的情况下始终返回相同的结果,并且不能使用任何外部因素来计算结果。然而,这并不意味着幂等函数不能影响非局部变量或I/O流等。例如,如果上面的add_three(number)的幂等版本在返回结果之前打印了结果,它仍然是幂等的,因为它访问了I/O流,这不会影响函数的返回值。调用print()是一个副作用:除了返回值之外,还与程序或系统的其余部分进行交互。让我们扩展add_three(number)示例。我们可以使用下面的代码片段来查看add_three(number)函数被调用的次数:add_three_calls=0defadd_three(number):"""Return*number*+3."""globaladd_three_callsprint(f'Returning{number+3}')add_three_calls+=1returnnumber+3defnum_calls():"""Returnthenumberoftimes*add_three*wascalled."""returnadd_three_calls现在我们将结果输出到控制台(副作用),并修改非局部变量(另一面effect),但是由于这些Sideeffects不影响函数的返回值,所以函数仍然是幂等的。纯函数没有副作用。它不仅不使用任何“外部数据”来计算值,而且除了计算和返回值外,它也不与系统/程序的其他部分交互。因此,虽然我们新定义的add_three(number)仍然是幂等的,但它不再是纯粹的。纯函数不记录语句或print()调用,不使用数据库或互联网连接,也不访问或修改非局部变量。他们不调用任何其他不纯函数。简而言之,纯函数不能(在计算机科学背景下)做爱因斯坦所说的“远距离幽灵般的动作”。他们不会以任何方式修改程序或系统的其余部分。在命令式编程中(写Python代码就是命令式编程),它们是最安全的函数。它们经过了很好的测试和维护,在这方面甚至优于纯粹的幂等函数。测试纯函数几乎和执行它们一样快。而且测试很简单:不需要数据库连接或其他外部资源,不需要设置代码,测试后也不需要清理任何东西。显然,幂等和纯函数很好,但不是必需的。即,由于上述优点,我们喜欢编写纯函数或幂等函数,但不可能一直写。关键是我们本能地开始部署代码以消除副作用和外部依赖性。这使得我们编写的每一行代码都更易于测试,甚至无需编写纯函数或幂等函数。总结编写好的函数的秘诀不再是秘密。只需遵循一些公认的最佳实践和经验法则。希望这篇文章能帮到你。原文链接:https://hackernoon.com/write-better-python-functions-c3a9a36382a6【本文为微信《机器之心》专栏原文翻译公众号《机器之心》(id:almosthuman2014)"]戳这里,阅读更多本作者的好文
