这是 “Python 工匠”系列的第 5 篇文章。[查看系列全部文章]html
毫无疑问,函数是 Python 语言里最重要的概念之一。在编程时,咱们将真实世界里的大问题分解为小问题,而后经过一个个函数交出答案。函数便是重复代码的克星,也是对抗代码复杂度的最佳武器。python
如同大部分故事都会有结局,绝大多数函数也都是以返回结果做为结束。函数返回结果的手法,决定了调用它时的体验。因此,了解如何优雅的让函数返回结果,是编写好函数的必备知识。git
Python 函数经过调用 return
语句来返回结果。使用 return value
能够返回单个值,用 return value1, value2
则能让函数同时返回多个值。github
若是一个函数体内没有任何 return
语句,那么这个函数的返回值默认为 None
。除了经过 return
语句返回内容,在函数内还可使用抛出异常*(raise Exception)*的方式来“返回结果”。正则表达式
接下来,我将列举一些与函数返回相关的经常使用编程建议。django
Python 语言很是灵活,咱们能用它轻松完成一些在其余语言里很难作到的事情。好比:*让一个函数同时返回不一样类型的结果。*从而实现一种看起来很是实用的“多功能函数”。编程
就像下面这样:缓存
def get_users(user_id=None):
if user_id is None:
return User.get(user_id)
else:
return User.filter(is_active=True)
# 返回单个用户
get_users(user_id=1)
# 返回多个用户
get_users()
复制代码
当咱们须要获取单个用户时,就传递 user_id
参数,不然就不传参数拿到全部活跃用户列表。一切都由一个函数 get_users
来搞定。这样的设计彷佛很合理。app
然而在函数的世界里,以编写具有“多功能”的瑞士军刀型函数为荣不是一件好事。这是由于好的函数必定是 “单一职责(Single responsibility)” 的。**单一职责意味着一个函数只作好一件事,目的明确。**这样的函数也更不容易在将来由于需求变动而被修改。框架
而返回多种类型的函数必定是违反“单一职责”原则的,**好的函数应该老是提供稳定的返回值,把调用方的处理成本降到最低。**像上面的例子,咱们应该编写两个独立的函数 get_user_by_id(user_id)
、get_active_users()
来替代。
假设这么一个场景,在你的代码里有一个参数不少的函数 A
,适用性很强。而另外一个函数 B
则是彻底经过调用 A
来完成工做,是一种相似快捷方式的存在。
比方在这个例子里, double
函数就是彻底经过 multiply
来完成计算的:
def multiply(x, y):
return x * y
def double(value):
# 返回另外一个函数调用结果
return multiply(2, value)
复制代码
对于上面这种场景,咱们可使用 functools
模块里的 partial()
函数来简化它。
partial(func, *args, **kwargs)
基于传入的函数与可变(位置/关键字)参数来构造一个新函数。全部对新函数的调用,都会在合并了当前调用参数与构造参数后,代理给原始函数处理。
利用 partial
函数,上面的 double
函数定义能够被修改成单行表达式,更简洁也更直接。
import functools
double = functools.partial(multiply, 2)
复制代码
建议阅读:partial 函数官方文档
我在前面提过,Python 里的函数能够返回多个值。基于这个能力,咱们能够编写一类特殊的函数:同时返回结果与错误信息的函数。
def create_item(name):
if len(name) > MAX_LENGTH_OF_NAME:
return None, 'name of item is too long'
if len(CURRENT_ITEMS) > MAX_ITEMS_QUOTA:
return None, 'items is full'
return Item(name=name), ''
def create_from_input():
name = input()
item, err_msg = create_item(name)
if err_msg:
print(f'create item failed: {err_msg}')
else:
print(f'item<{name}> created')
复制代码
在示例中,create_item
函数的做用是建立新的 Item 对象。同时,为了在出错时给调用方提供错误详情,它利用了多返回值特性,把错误信息做为第二个结果返回。
乍看上去,这样的作法很天然。尤为是对那些有 Go
语言编程经验的人来讲更是如此。可是在 Python 世界里,这并不是解决此类问题的最佳办法。由于这种作法会增长调用方进行错误处理的成本,尤为是当不少函数都遵循这个规范并且存在多层调用时。
Python 具有完善的*异常(Exception)*机制,而且在某种程度上鼓励咱们使用异常(官方文档关于 EAFP 的说明)。因此,使用异常来进行错误流程处理才是更地道的作法。
引入自定义异常后,上面的代码能够被改写成这样:
class CreateItemError(Exception):
"""建立 Item 失败时抛出的异常"""
def create_item(name):
"""建立一个新的 Item :raises: 当没法建立时抛出 CreateItemError """
if len(name) > MAX_LENGTH_OF_NAME:
raise CreateItemError('name of item is too long')
if len(CURRENT_ITEMS) > MAX_ITEMS_QUOTA:
raise CreateItemError('items is full')
return Item(name=name)
def create_for_input():
name = input()
try:
item = create_item(name)
except CreateItemError as e:
print(f'create item failed: {err_msg}')
else:
print(f'item<{name}> created')
复制代码
使用“抛出异常”替代“返回 (结果, 错误信息)”后,整个错误流程处理乍看上去变化不大,但实际上有着很是多不一样,一些细节:
Item
类型或是抛出异常create_item
的一级调用方彻底能够省略异常处理,交由上层处理。这个特色给了咱们更多的灵活性,但同时也带来了更大的风险。Hint:如何在编程语言里处理错误,是一个至今仍然存在争议的主题。好比像上面不推荐的多返回值方式,正是缺少异常的 Go 语言中最核心的错误处理机制。另外,即便是异常机制自己,不一样编程语言之间也存在着差异。
异常,或是不异常,都是由语言设计者进行多方取舍后的结果,更多时候不存在绝对性的优劣之分。可是,单就 Python 语言而言,使用异常来表达错误无疑是更符合 Python 哲学,更应该受到推崇的。
None
值一般被用来表示**“某个应该存在可是缺失的东西”**,它在 Python 里是独一无二的存在。不少编程语言里都有与 None 相似的设计,好比 JavaScript 里的 null
、Go 里的 nil
等。由于 None 所拥有的独特 虚无 气质,它常常被做为函数返回值使用。
当咱们使用 None 做为函数返回值时,一般是下面 3 种状况。
当某个操做类函数不须要任何返回值时,一般就会返回 None。同时,None 也是不带任何 return
语句函数的默认返回值。
对于这种函数,使用 None 是没有任何问题的,标准库里的 list.append()
、os.chdir()
均属此类。
有一些函数,它们的目的一般是去尝试性的作某件事情。视状况不一样,最终可能有结果,也可能没有结果。而对调用方来讲,“没有结果”彻底是意料之中的事情。对这类函数来讲,使用 None 做为“没结果”时的返回值也是合理的。
在 Python 标准库里,正则表达式模块 re
下的 re.search
、re.match
函数均属于此类,这两个函数在能够找到匹配结果时返回 re.Match
对象,找不到时则返回 None
。
有时,None
也会常常被咱们用来做为函数调用失败时的默认返回值,好比下面这个函数:
def create_user_from_name(username):
"""经过用户名建立一个 User 实例"""
if validate_username(username):
return User.from_username(username)
else:
return None
user = create_user_from_name(username)
if user:
user.do_something()
复制代码
当 username 不合法时,函数 create_user_from_name
将会返回 None。但在这个场景下,这样作其实并很差。
不过你也许会以为这个函数彻底合情合理,甚至你会以为它和咱们提到的上一个“没有结果”时的用法很是类似。那么如何区分这两种不一样情形呢?关键在于:函数签名(名称与参数)与 None 返回值之间是否存在一种“意料之中”的暗示。
让我解释一下,每当你让函数返回 None 值时,请仔细阅读函数名,而后问本身一个问题:假如我是该函数的使用者,从这个名字来看,“拿不到任何结果”是不是该函数名称含义里的一部分?
分别用这两个函数来举例:
re.search()
:从函数名来看,search
,表明着从目标字符串里去搜索匹配结果,而搜索行为,一贯是可能有也可能没有结果的,因此该函数适合返回 Nonecreate_user_from_name()
:从函数名来看,表明基于一个名字来构建用户,并不能读出一种可能返回、可能不返回
的含义。因此不适合返回 None对于那些不能从函数名里读出 None 值暗示的函数来讲,有两种修改方式。第一种,若是你坚持使用 None 返回值,那么请修改函数的名称。好比能够将函数 create_user_from_name()
更名为 create_user_or_none()
。
第二种方式则更常见的多:用抛出异常*(raise Exception)来代替 None 返回值。由于,若是返回不了正常结果并不是函数意义里的一部分,这就表明着函数出现了“意料之外的情况”*,而这正是 Exceptions 异常 所掌管的领域。
使用异常改写后的例子:
class UnableToCreateUser(Exception):
"""当没法建立用户时抛出"""
def create_user_from_name(username):
""经过用户名建立一个 User 实例"
:raises: 当没法建立用户时抛出 UnableToCreateUser
"""
if validate_username(username):
return User.from_username(username)
else:
raise UnableToCreateUser(f'unable to create user from {username}')
try:
user = create_user_from_name(username)
except UnableToCreateUser:
# Error handling
else:
user.do_something()
复制代码
与 None 返回值相比,抛出异常除了拥有咱们在上个场景提到的那些特色外,还有一个额外的优点:能够在异常信息里提供出现意料以外结果的缘由,这是只返回一个 None 值作不到的。
我在前面提到函数能够用 None
值或异常来返回错误结果,但这两种方式都有一个共同的缺点。那就是全部须要使用函数返回值的地方,都必须加上一个 if
或 try/except
防护语句,来判断结果是否正常。
让咱们看一个可运行的完整示例:
import decimal
class CreateAccountError(Exception):
"""Unable to create a account error"""
class Account:
"""一个虚拟的银行帐号"""
def __init__(self, username, balance):
self.username = username
self.balance = balance
@classmethod
def from_string(cls, s):
"""从字符串初始化一个帐号"""
try:
username, balance = s.split()
balance = decimal.Decimal(float(balance))
except ValueError:
raise CreateAccountError('input must follow pattern "{ACCOUNT_NAME} {BALANCE}"')
if balance < 0:
raise CreateAccountError('balance can not be negative')
return cls(username=username, balance=balance)
def caculate_total_balance(accounts_data):
"""计算全部帐号的总余额 """
result = 0
for account_string in accounts_data:
try:
user = Account.from_string(account_string)
except CreateAccountError:
pass
else:
result += user.balance
return result
accounts_data = [
'piglei 96.5',
'cotton 21',
'invalid_data',
'roland $invalid_balance',
'alfred -3',
]
print(caculate_total_balance(accounts_data))
复制代码
在这个例子里,每当咱们调用 Account.from_string
时,都必须使用 try/except
来捕获可能发生的异常。若是项目里须要调用不少次该函数,这部分工做就变得很是繁琐了。针对这种状况,可使用“空对象模式(Null object pattern)”来改善这个控制流。
Martin Fowler 在他的经典著做《重构》 中用一个章节详细说明过这个模式。简单来讲,就是使用一个符合正常结果接口的“空类型”来替代空值返回/抛出异常,以此来下降调用方处理结果的成本。
引入“空对象模式”后,上面的示例能够被修改为这样:
class Account:
# def __init__ 已省略... ...
@classmethod
def from_string(cls, s):
"""从字符串初始化一个帐号 :returns: 若是输入合法,返回 Account object,不然返回 NullAccount """
try:
username, balance = s.split()
balance = decimal.Decimal(float(balance))
except ValueError:
return NullAccount()
if balance < 0:
return NullAccount()
return cls(username=username, balance=balance)
class NullAccount:
username = ''
balance = 0
@classmethod
def from_string(cls, s):
raise NotImplementedError
复制代码
在新版代码里,我定义了 NullAccount
这个新类型,用来做为 from_string
失败时的错误结果返回。这样修改后的最大变化体如今 caculate_total_balance
部分:
def caculate_total_balance(accounts_data):
"""计算全部帐号的总余额 """
return sum(Account.from_string(s).balance for s in accounts_data)
复制代码
调整以后,调用方没必要再显式使用 try 语句来处理错误,而是能够假设 Account.from_string
函数老是会返回一个合法的 Account 对象,从而大大简化整个计算逻辑。
Hint:在 Python 世界里,“空对象模式”并很多见,好比大名鼎鼎的 Django 框架里的 AnonymousUser 就是一个典型的 null object。
在函数里返回列表特别常见,一般,咱们会先初始化一个列表 results = []
,而后在循环体内使用 results.append(item)
函数填充它,最后在函数的末尾返回。
对于这类模式,咱们能够用生成器函数来简化它。粗暴点说,就是用 yield item
替代 append
语句。使用生成器的函数一般更简洁、也更具通用性。
def foo_func(items):
for item in items:
# ... 处理 item 后直接使用 yield 返回
yield item
复制代码
我在 系列第 4 篇文章“容器的门道” 里详细分析过这个模式,更多细节能够访问文章,搜索 “写扩展性更好的代码” 查看。
当函数返回自身调用时,也就是 递归
发生时。递归是一种在特定场景下很是有用的编程技巧,但坏消息是:Python 语言对递归支持的很是有限。
这份“有限的支持”体如今不少方面。首先,Python 语言不支持“尾递归优化”。另外 Python 对最大递归层级数也有着严格的限制。
因此我建议:尽可能少写递归。若是你想用递归解决问题,先想一想它是否是能方便的用循环来替代。若是答案是确定的,那么就用循环来改写吧。若是无可奈何,必定须要使用递归时,请考虑下面几个点:
sys.getrecursionlimit()
规定的最大层数限制在这篇文章中,我虚拟了一些与 Python 函数返回有关的场景,并针对每一个场景提供了个人优化建议。最后再总结一下要点:
functools.partial
定义快捷函数看完文章的你,有没有什么想吐槽的?请留言或者在 项目 Github Issues 告诉我吧。
系列其余文章: