序言
这是“Python 工匠”系列的第 5 篇文章。
毫无疑问,函数是 Python 语言里最重要的概念之一。在编程时,咱们将真实世界里的大问题合成为小问题,而后通过一个个函数交出答案。函数即是反复代码的克星,也是反抗代码复杂度的最佳武器。
如同大部分故事都会有终局,绝大多数函数也都是以返回后果作为完结。函数返回后果的手法,决定了调用它时的体验。所以,理解如何优雅的让函数返回后果,是编写好函数的必备常识。
Python 的函数返回形式
Python
函数通过调用 return
语句来返回后果。应用 returnvalue
能够返回单个值,用 returnvalue1
,value2
则能让函数同时返回多个值。
如果一个函数体内没有任何 return
语句,那么这个函数的返回值默认为 None
。除了通过 return
语句返回内容,在函数内还能够应用抛出异样(raise Exception)的形式来“返回后果”。
接下来,我将列举一些与函数返回相干的罕用编程倡议。
编程倡议
1. 单个函数不要返回多种类型
Python 语言非常灵活,咱们能用它轻松实现一些在其余语言里很难做到的事件。比方:让一个函数同时返回不同类型的后果。从而实现一种看起来十分实用的“多功能函数”。
就像上面这样:
当咱们须要获取单个用户时,就传递 user_id
参数,否则就不传参数拿到所有沉闷用户列表。所有都由一个函数 get_users
来搞定。这样的设计仿佛很正当。
然而在函数的世界里,以编写具备“多功能”的瑞士军刀型函数为荣不是一件坏事。这是因为好的函数肯定是“繁多职责(Single responsibility)”的。繁多职责意味着一个函数只做好一件事,目标明确。这样的函数也更不容易在将来因为需要变更而被批改。
而返回多种类型的函数肯定是违反“繁多职责”准则的,好的函数应该总是提供稳固的返回值,把调用方的解决老本降到最低 。像下面的例子,咱们应该编写两个独立的函数 get_user_by_id(user_id)
、get_active_users()
来代替。
2. 应用 partial 结构新函数
假如这么一个场景,在你的代码里有一个参数很多的函数 A,适用性很强。而另一个函数 B 则是齐全通过调用 A 来实现工作,是一种相似快捷方式的存在。
比如在这个例子里,double
函数就是齐全通过 multiply
来实现计算的:
对于下面这种场景,咱们能够应用 functools
模块里的 partial()
函数来简化它。
partial(func,*args,**kwargs)
基于传入的函数与可变(地位 / 关键字)参数来结构一个新函数。所 有对新函数的调用,都会在合并了以后调用参数与结构参数后,代理给原始函数解决。
利用 partial
函数,下面的 double
函数定义能够被批改为单行表达式,更简洁也更间接。
倡议浏览:partial 函数官网文档
3. 抛出异样,而不是返回后果与谬误
我在后面提过,Python 里的函数能够返回多个值。基于这个能力,咱们能够编写一类非凡的函数:同时返回后果与错误信息的函数。
在示例中,create_item
函数的作用是创立新的 Item 对象。同时,为了在出错时给调用方提供谬误详情,它利用了多返回值个性,把错误信息作为第二个后果返回。
乍看上去,这样的做法很天然。尤其是对那些有 Go 语言编程教训的人来说更是如此。然而在 Python 世界里,这并非解决此类问题的最佳方法。因为这种做法会减少调用方进行错误处理的老本,尤其是当很多函数都遵循这个标准而且存在多层调用时。
Python 具备欠缺的异样(Exception)机制,并且在某种程度上激励咱们应用异样(官网文档对于 EAFP 的阐明)。所以,应用异样来进行谬误流程解决才是更纯粹的做法。
引入自定义异样后,下面的代码能够被改写成这样:
应用“抛出异样”代替“返回 (后果, 错误信息)”后,整个谬误流程解决乍看上去变动不大,但实际上有着十分多不同,一些细节:
- 新版本函数领有更稳固的返回值类型,它永远只会返回
Item
类型或是抛出异样 - 尽管我在这里激励应用异样,但“异样”总是会无奈防止的让人 感到诧异,所以,最好在函数文档里阐明可能抛出的异样类型
-
异样不同于返回值,它在被捕捉前会一直往调用栈下层汇报。所以
create_item
的一级调用方齐全能够省略异样解决,交由下层解决。这个特点给了咱们更多的灵活性,但同时也带来了更大的危险。Hint:如何在编程语言里处理错误,是一个至今依然存在争议的主题。比方像下面不举荐的多返回值形式,正是不足异样的 Go 语言中最外围的错误处理机制。另外,即便是异样机制自身,不同编程语言之间也存在着差异。
异样,或是不异样,都是由语言设计者进行多方取舍后的后果,更多时候不存在绝对性的优劣之分。然而,单就 Python 语言而言,应用异样来表白谬误无疑是更合乎 Python 哲学,更应该受到推崇的。
4. 审慎应用 None 返回值
None
值通常被用来示意“某个应该存在然而缺失的货色”,它在 Python 里是举世无双的存在。很多编程语言里都有与 None 相似的设计,比方 JavaScript 里的 null
、Go 里的 nil
等。因为 None 所领有的独特 虚无 气质,它常常被作为函数返回值应用。
当咱们应用 None 作为函数返回值时,通常是上面 3 种状况。
4.1. 作为操作类函数的默认返回值
当某个操作类函数不须要任何返回值时,通常就会返回 None
。同时,None
也是不带任何 return
语句函数的默认返回值。
对于这种函数,应用 None
是没有任何问题的,规范库里的 list.append()
、os.chdir()
均属此类。
4.2. 作为某些“意料之中”的可能没有的值
有一些函数,它们的目标通常是去尝试性的做某件事情。视状况不同,最终可能有后果,也可能没有后果。而对调用方来说,“没有后果”齐全是意料之中的事件。对这类函数来说,应用 None
作为“没后果”时的返回值也是正当的。
在 Python 规范库里,正则表达式模块 re
下的 re.search
、re.match
函数均属于此类,这两个函数在能够找到匹配后果时返回 re.Match
对象,找不到时则返回 None
。
4.3. 作为调用失败时代表“谬误后果”的值
有时,None
也会常常被咱们用来作为函数调用失败时的默认返回值,比方上面这个函数:
当 username
不非法时,函数 create_user_from_name
将会返回 None
。但在这个场景下,这样做其实并不好。
不过你兴许会感觉这个函数齐全荒诞不经,甚至你会感觉它和咱们提到的上一个“没有后果”时的用法十分类似。那么如何辨别这两种不同情景呢?关键在于:函数签名(名称与参数)与 None 返回值之间是否存在一种“意料之中”的暗示。
让我解释一下,每当你让函数返回 None
值时,请仔细阅读函数名,而后问本人一个问题:如果我是该函数的使用者,从这个名字来看,“拿不到任何后果”是否是该函数名称含意里的一部分?
别离用这两个函数来举例:
re.search():
从函数名来看,search
,代表着从指标字符串里去搜寻匹配后果,而搜寻行为,一贯是可能有也可能没有后果的,所以该函数适宜返回None
create_user_from_name():
从函数名来看,代表基于一个名字来构建用户,并不能读出一种 可能返回、可能不返回的含意。所以不适宜返回None
对于那些不能从函数名里读出 None
值暗示的函数来说,有两种批改形式。第一种,如果你保持应用 None
返回值,那么请批改函数的名称。比方能够将函数 create_user_from_name()
改名为 create_user_or_none()
。
第二种形式则更常见的多:用抛出异样 (raise Exception) 来代替 None
返回值。因为,如果返回不了失常后果并非函数意义里的一部分,这就代表着函数呈现了“预料以外的情况”,而这正是 Exceptions 异样 所主持的畛域。
应用异样改写后的例子:
与 None 返回值相比,抛出异样除了领有咱们在上个场景提到的那些特点外,还有一个额定的劣势:能够在异样信息里提供呈现意料之外后果的起因,这是只返回一个 None 值做不到的。
5. 正当应用“空对象模式”
我在后面提到函数能够用 None
值或异样来返回谬误后果,但这两种形式都有一个独特的毛病。那就是所有须要应用函数返回值的中央,都必须加上一个 if
或 try/except
进攻语句,来判断后果是否失常。
让咱们看一个可运行的残缺示例:
补充图中显示不到的为:{BALANCE}” ‘)
在这个例子里,每当咱们调用 Account.from_string
时,都必须应用 try/except
来捕捉可能产生的异样。如果我的项目里须要调用很屡次该函数,这部分工作就变得十分繁琐了。针对这种状况,能够应用“空对象模式(Null object pattern)”来改善这个控制流。
Martin Fowler 在他的经典著作《重构》中用一个章节具体阐明过这个模式。简略来说,就是应用一个合乎失常后果接口的“空类型”来代替空值返回 / 抛出异样,以此来升高调用方处理结果的老本。
引入“空对象模式”后,下面的示例能够被批改成这样:
在新版代码里,我定义了 NullAccount
这个新类型,用来作为 from_string
失败时的谬误后果返回。这样批改后的最大变动体现在 caculate_total_balance
局部:
调整之后,调用方不用再显式应用 try
语句来处理错误,而是能够假如 Account.from_string
函数总是会返回一个非法的 Account
对象,从而大大简化整个计算逻辑。
Hint:在 Python 世界里,“空对象模式”并不少见,比方赫赫有名的 Django 框架里的 AnonymousUser 就是一个典型的 null object。
6. 应用生成器函数代替返回列表
在函数里返回列表特地常见,通常,咱们会先初始化一个列表 results=[]
,而后在循环体内应用 results.append(item)
函数填充它,最初在函数的开端返回。
对于这类模式,咱们能够用生成器函数来简化它。粗犷点说,就是用 yielditem
代替 append
语句。应用生成器的函数通常更简洁、也更具通用性。
我在 系列第 4 篇文章“容器的门道”里详细分析过这个模式,更多细节能够拜访文章,搜寻“写扩展性更好的代码”查看。
7. 限度递归的应用
当函数返回本身调用时,也就是 递归 产生时。递归是一种在特定场景下十分有用的编程技巧,但坏消息是:Python 语言对递归反对的十分无限。
这份“无限的反对”体现在很多方面。首先,Python 语言不反对“尾递归优化”。另外 Python 对最大递归层级数也有着严格的限度。
所以我倡议:尽量少写递归。如果你想用递归解决问题,先想想它是不是能不便的用循环来代替。如果答案是必定的,那么就用循环来改写吧。如果无可奈何,肯定须要应用递归时,请思考上面几个点:
- 函数输出数据规模是否稳固,是否肯定不会超过
sys.getrecursionlimit()
规定的最大层数限度 - 是否能够通过应用相似
functools.lru_cache
的缓存工具函数来升高递归层数
总结
在这篇文章中,我虚构了一些与 Python 函数返回无关的场景,并针对每个场景提供了我的优化倡议。最初再总结一下要点:
- 让函数领有稳固的返回值,一个函数只做好一件事
- 应用
functools.partial
定义快捷函数 - 抛出异样也是返回后果的一种形式,应用它来代替返回错误信息
- 函数是否适宜返回 None,由函数签名的“含意”所决定
- 应用“空对象模式”能够简化调用方的错误处理逻辑
- 多应用生成器函数,尽量用循环代替递归
看完文章的你,有没有什么想吐槽的?请留言或者在 我的项目 Github Issues 通知我吧。
附录
题图起源: Dominik Scythe on Unsplash
更多系列文章地址:https://github.com/piglei/one…
蓝鲸智云
本文由腾讯蓝鲸智云编辑公布,腾讯蓝鲸智云(简称蓝鲸)软件体系是一套基于 PaaS 的技术解决方案,致力于打造行业当先的一站式自动化运维平台。目前曾经推出社区版、企业版,欢送体验。
- 官网:https://bk.tencent.com/
- 下载链接:https://bk.tencent.com/download/
- 社区:https://bk.tencent.com/s-mart…