python编写函数
在Python中 ,像大多数现代编程语言一样,该函数是抽象和封装的主要方法。 作为开发人员,您可能已经编写了数百个函数。 但是,并非所有功能都是平等创建的。 编写“不良”函数将直接影响代码的可读性和可维护性。 那么,“坏”功能是什么,更重要的是,什么才是“好”功能?
快速刷新
虽然函数可能会让人记忆犹新,但我们可能不记得它们了,所以让我们回想一下每个人最喜欢的主题:微积分。 您可能还记得看过以下公式f(x) = 2x + 3
。 这是一个名为f
的函数,它带有一个参数x
,并“返回”两次x
+3。尽管它看起来可能不像我们在Python中使用的函数,但它直接类似于以下代码:
def f(x):
return 2*x + 3
函数早已存在于数学中,但在计算机科学中却具有更强大的功能。 但是,有了这种能力,就会遇到各种陷阱。 现在,让我们讨论什么使“好”功能成为可能,并警告可能需要重构的功能。
良好功能的关键
是什么将“好的” Python函数与笨拙的Python函数区分开? 您会惊讶于可以使用多少种“好”定义。 出于我们的目的,如果Python函数可以勾选此清单中的大多数项目(有些并不总是可能),我将认为它是“好”的:
- 明智地命名
- 负有单一责任
- 包含文档字符串
- 返回一个值
- 不超过50行
- 是幂等的 ,如果可能的话是纯净的
对于您中的许多人而言,此列表似乎过于严苛。 但是,我向您保证,如果您的函数遵循这些规则,那么您的代码将是如此美丽,它将使独角兽哭泣。 在下面,我将为每个项目专门设置一个部分,然后将它们与它们如何和谐地结合起来以创建“良好”功能。
命名
关于这个主题,我有一个最喜欢的说法,通常是误称唐纳德·克努斯(Donald Knuth),但实际上来自菲尔·卡尔顿 ( Phil Karlton) :
There are only two hard things in Computer Science: cache invalidation and naming things. -- Phil Karlton
听起来很愚蠢, 很难给事物命名。 这是一个“错误的”函数名称的示例:
def get_knn_from_df(df):
现在,我几乎到处都看到过恶名,但是这个例子来自数据科学(实际上是机器学习),在那里它的从业人员通常在Jupyter笔记本中编写代码,然后尝试将这些不同的单元转换为可理解的程序。
该函数名称的第一个问题是其首字母缩写词/缩写的使用。 最好使用完整的英语单词而不是缩写词和普遍使用的首字母缩写词。 可能会缩写单词的唯一原因是保存输入,但是每个现代编辑器都具有autocomplete ,因此您只需输入该全名一次。 缩写是一个问题,因为它们通常是特定于域的。 在上面的代码中, knn
指的是“ K最近邻居”,而df
指的是“ DataFrame”,即无处不在的熊猫数据结构。 如果另一个不熟悉这些首字母缩写词的程序员正在阅读代码,那么对她的名字几乎一无所知。
该函数的名称还有另外两个小缺点:“ get”一词是多余的。 对于大多数命名良好的函数,很明显,该函数正在返回某些内容,其名称将反映出来。 from_df
位也是不必要的。 如果函数的docstring或(如果位于边缘)类型注释(如果尚未通过参数名将其清除 )将描述该参数的类型。
那么我们如何重命名该功能呢? 简单:
def k_nearest_neighbors(dataframe):
现在,即使是外行人也很清楚此函数计算的内容,并且参数的名称( dataframe
)使得应该向该参数传递什么类型的参数。
单一责任
与“叔叔”鲍勃·马丁一样,“ 单一责任原则”与类和模块一样,在功能上同样适用(马丁先生的最初目标)。 它指出(在我们的情况下)功能应具有单一责任 。 也就是说,它应该做一件事而只能做一件事。 一个重要的原因是,如果每个函数都只做一件事情,那么更改它的理由就只有一个:如果必须改变它的做事方式。 当可以删除某个功能时,也会变得很清楚:如果在其他地方进行更改时,很明显不再需要该功能的单一职责,只需将其删除即可。
一个例子会有所帮助。 这是一个功能,可以执行多个“操作”:
def calculate_and print_stats(list_of_numbers):
sum = sum(list_of_numbers)
mean = statistics.mean(list_of_numbers)
median = statistics.median(list_of_numbers)
mode = statistics.mode(list_of_numbers)
print('-----------------Stats-----------------')
print('SUM: {}'.format(sum) print('MEAN: {}'.format(mean)
print('MEDIAN: {}'.format(median)
print('MODE: {}'.format(mode)
此函数有两件事:它计算一组关于数字列表的统计信息, 并将其打印到STDOUT
。 该功能违反了只有一个理由才能更改功能的规则。 有两个明显的原因需要更改此函数:可能需要计算新的或不同的统计信息,或者可能需要更改输出的格式。 最好将此函数编写为两个独立的函数:一个执行并返回计算结果,另一个将这些结果打印出来。 功能名称中 的单词 和 单词是功能具有多重职责的一个死角 。
这种分离还可以更轻松地测试功能的行为,还可以将这两个部分不仅分离为同一模块中的两个功能,而且还可以视情况完全位于不同的模块中。 这也导致更清洁的测试和更容易的维护。
找到一个仅能做两件事的功能实际上很少。 很多时候,您会发现执行许多其他功能的函数。 同样,出于可读性和可测试性的目的,这些千篇一律的功能应分解为较小的功能,每个功能封装一个工作单元。
Docstrings
虽然每个人似乎都知道PEP-8 (定义了Python的样式指南),但似乎很少有人知道PEP-257 ,它对文档字符串也是如此。 与其简单地重新打包PEP-257的内容,不如随意阅读它。 但是,主要的收获是:
- 每个函数都需要一个文档字符串
- 使用适当的语法和标点符号; 用完整的句子写
- 从函数功能的一句摘要开始
- 使用说明性而不是描述性语言
编写函数时,这很容易被选中。 请养成始终编写文档字符串的习惯,并在编写函数代码之前尝试编写它们。 如果您无法编写清晰的文档字符串来描述函数将要执行的操作,则表明您需要更多地考虑为什么首先要编写函数。
返回值
可以(并且应该 )将功能视为很少的独立程序。 它们以参数的形式接受一些输入并返回一些结果。 参数当然是可选的。 但是,从Python内部观点来看,返回值不是可选的。 即使您尝试创建不返回值的函数,也不会这样做。 如果一个函数否则不会返回值,则Python解释器“强制其”返回None
。 不相信我吗 自己测试以下内容:
❯ python3
Python 3.7.0 (default, Jul 23 2018, 20:22:55)
[Clang 9.1.0 (clang-902.0.39.2)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> def add(a, b):
... print(a + b)
...
>>> b = add(1, 2)
3
>>> b
>>> b is None
True
您会看到b
的值实际上是None
。 因此,即使您编写的函数没有return
语句,它仍然会返回某些内容 。 它应该返回一些东西。 毕竟,这是一个小程序,对。 不产生输出的程序(包括它们是否正确执行)有多有用? 但是最重要的是,您将如何测试这样的程序?
我什至会做出以下声明:每个函数都应返回一个有用的值,即使仅出于可测试性目的。 您编写的代码应该经过测试(这不值得争论)。 试想一下如何对上面的add
函数进行繁琐的测试(提示:您必须重定向I / O,然后事情才能从那里快速发展)。 同样,返回值允许方法链接,这个概念使我们可以编写如下代码:
with open('foo.txt', 'r') as input_file:
for line in input_file:
if line.strip().lower().endswith('cat'):
# ... do something useful with these lines
该行if line.strip().lower().endswith('cat'):
之所以起作用是因为每个字符串方法( strip(), lower(), endswith()
)都返回一个字符串作为调用函数的结果。
人们在被问到为什么他们编写的给定函数不返回值时会给出以下一些常见原因:
“它所做的全部是[一些与I / O相关的事情,例如将值保存到数据库中。 我无法退还任何有用的东西。”
我不同意。 如果操作成功完成,该函数可以返回True
。
“我们像参考参数一样使用它来修改其中一个参数。”””
这里有两点。 首先,尽力避免这种做法。 对于其他人而言,在某些情况下提供某些东西作为您的函数的参数仅是发现它已被更改可能是令人惊讶的,而在最坏的情况下则是彻头彻尾的危险。 取而代之的是,就像字符串方法一样,它更喜欢返回参数的新实例并应用更改。 即使这样做不可行,因为复制某些参数的成本过高,您仍然可以退回到原来的“如果操作成功完成,则返回True
”的建议。
“我需要返回多个值。 我没有可以返回的唯一值。”
这有点像个稻草人的论点,但我已经听到了。 答案当然是准确地执行作者想做的事情,但不知道该怎么做: 使用元组返回多个值。
始终返回有用值的最引人注目的论点是,调用者始终可以随意忽略它们。 简而言之,从函数返回值几乎肯定是个好主意,即使在现有的代码库中,也几乎不可能破坏任何内容。
功能长度
我已经说过很多次我很傻了。 我一次只能把大约三样东西放在脑海中。 如果让我阅读200行功能并询问该功能,则大约10秒钟后我的眼睛可能会呆滞。 函数的长度直接影响可读性,从而影响可维护性。 因此,请保持简短的功能。 50行是一个完全任意的数字,对我来说似乎是合理的。 您编写的大多数函数(希望)会短很多。
如果某个功能遵循“单一职责原则”,则它可能很短。 如果它是纯的或幂等的(在下面讨论),它也可能很短。 这些构想共同产生了良好的,干净的代码。
那么,如果函数太长怎么办? 推荐人! 重构是您可能一直都在做的事情,即使您对这个术语不熟悉。 它只是意味着更改程序的结构而不更改其行为 。 因此,从一个长函数中提取几行代码并将其变成自己的函数是一种重构 。 它也恰巧是最快有效地缩短长功能的方式。 而且,由于为所有这些新函数赋予了适当的名称,因此生成的代码更容易读取 。 我可以写一本关于重构的完整书(实际上已经做过很多次了),这里不再赘述。 只知道如果您的函数过长,则可以通过重构来解决它。
幂等性和功能纯度
本小节的标题听起来有些吓人,但概念很简单。 的幂函数,总是返回无论它是如何调用的次数给出了相同的参数集相同的值。 结果不取决于非局部变量,参数的可变性或任何I / O流中的数据。 以下add_three(number)
函数是幂等的:
def add_three(number):
"""Return *number* + 3."""
return number + 3
无论一个调用add_three(7)
多少次,答案始终是10
。 这是非幂等函数的另一种说法:
def add_three():
"""Return 3 + the number entered by the user."""
number = int(input('Enter a number: '))
return number + 3
这个公认的示例并不是幂等的,因为函数的返回值取决于I / O,即用户输入的数字。 显然,对add_three()
每次调用都返回相同的值是不正确的。 如果调用两次,则用户可以第一次输入3
,第二次输入7
,对add_three()
的调用分别返回6
和10
。
幂等的真实示例是按下电梯前面的“向上”按钮。 第一次按下时,会“通知”您要上电梯。 因为按下按钮是幂等的,所以一遍又一遍地按下它是无害的。 结果总是一样的。
幂等为什么重要?
可测试性和可维护性。 幂等函数易于测试,因为可以保证当使用相同的参数调用它们时始终返回相同的结果。 测试仅是检查由函数的各种不同调用返回的值是否返回期望值的问题。 而且,这些测试将是快速的 ,这是单元测试中一个经常被忽视的重要问题。 在处理幂等函数时进行重构很容易。 无论您如何在函数外更改代码,使用相同参数调用它的结果将始终相同。
什么是“纯”功能?
在函数编程,一个功能被认为纯如果它既是幂等的,并具有没有可观察到的副作用 。 请记住,如果函数始终为给定的参数集返回相同的值,则它是幂等的 。 函数外部的任何内容都不能用于计算该值。 但是,这并不意味着该函数不会影响非局部变量或I / O流之类的东西。 例如,如果上面的add_three(number)
的幂等版本在返回结果之前先打印了结果,则仍将其视为幂等的,因为在访问I / O流时,该访问与从函数返回的值无关。 对print()
的调用只是一个副作用 :除了返回值之外,还与程序的其余部分或系统本身进行了某些交互。
让我们将add_three(number)
示例更进一步。 我们可以编写以下代码片段以确定add_three(number)
被调用了多少次:
add_three_calls = 0
def add_three(number):
"""Return *number* + 3."""
global add_three_calls
print(f'Returning {number + 3}')
add_three_calls += 1
return number + 3
def num_calls():
"""Return the number of times *add_three* was called."""
return add_three_calls
现在,我们正在打印到控制台(一个副作用) 并修改一个非局部变量(另一个副作用),但是由于这两个都不影响该函数返回的值,因此它仍然是幂等的。
纯函数没有副作用。 它不仅不使用任何“外部数据”来计算其值,而且除了计算并返回所述值外,它与系统/程序的其余部分没有任何交互。 因此,尽管我们新的add_three(number)
定义仍然是幂等的,但它不再是纯净的。
纯函数没有日志记录语句或print()
调用。 他们不使用数据库或Internet连接。 他们不访问或修改非局部变量。 而且它们不会调用任何其他非纯函数。
简而言之,他们无法实现爱因斯坦所说的“远距离的诡异动作”(在计算机科学领域)。 他们不会以任何方式修改程序或系统的其余部分。 在命令式编程 (编写Python代码时所采用的方式)中,它们是所有人中最安全的功能。 它们具有出色的可测试性和可维护性,甚至比仅是幂等功能更重要,因此保证它们的测试基本上与执行它们一样快。 而且测试本身很简单:无需模拟数据库连接或其他外部资源,不需要安装代码,此后也无需清理。
需要明确的是,幂等性和纯度是理想的 ,而不是必需的。 也就是说,由于提到的好处,我们只想编写纯函数或幂等函数,但这并不总是可能的。 但是,关键是我们自然开始整理代码以隔离副作用和外部依赖性。 这具有使我们编写的每一行代码更易于测试的效果,即使我们并非总是编写纯函数或幂等函数。
加起来
就是这样了。 编写好的函数的秘诀根本不是秘密。 它仅涉及遵循一些既定的最佳实践和经验法则。 希望本文对您有所帮助。 现在,去告诉你的朋友们! 我们都同意在所有情况下都始终编写出色的代码:)。 或者至少尽我们最大的努力不向世界投放更多的“不良”代码。 我可以忍受...
发表于十月11,2018作者Jeff Knupp
最初于 2018 年10月11日 发布在 jeffknupp.com 。
python编写函数