尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Python 工匠(one-python-craftsman):让函数返回结果的 7 个实战技巧

Python 工匠(one-python-craftsman):让函数返回结果的 7 个实战技巧 技术博客教程文档【免费下载链接】one-python-craftsman来自一位 Pythonista 的编程经验分享内容涵盖编码技巧、最佳实践与思维模式等方面。项目地址https://gitcode.com/gh_mirrors/on/one-python-craftsman点击查看免费下载函数是 Python 语言里最重要的概念之一而绝大多数函数都以「返回结果」作为结束——返回结果的手法直接决定了调用方的使用体验。本篇基于 one-python-craftsman 仓库「Python 工匠」系列第 5 篇《让函数返回结果的技巧》系统讲解 7 个经过实战检验的编程建议从稳定返回值、functools.partial、异常代替错误返回到 None 的三种使用场景、空对象模式、生成器函数与递归限制。读完你将掌握一套可立即落地的函数返回设计准则并能在仓库系列文章中看到这些准则在真实场景文件分块读取、循环解耦、边界处理中的延伸应用。函数返回的三种基本方式在深入技巧之前先厘清 Python 函数返回结果的底层语法返回单个值使用return value函数执行到该语句时立即返回value。返回多个值使用return value1, value2实际返回的是一个元组调用方可以用x, y f()的方式一次解包。隐式返回 None如果函数体内没有任何return语句函数默认返回None。除了通过return语句返回内容函数内还可以通过抛出异常raise Exception来「返回结果」——这是后面建议 3、建议 4 的核心讨论对象。可以这样理解return返回的是「正常结果」而raise返回的是「异常结果」两者共同构成了函数与调用方之间的契约。建议一单个函数不要返回多种类型Python 语言非常灵活可以轻松让一个函数同时返回不同类型的结果从而实现看似实用的「多功能函数」。比如下面这个例子def get_users(user_idNone): if user_id is not None: return User.get(user_id) else: return User.filter(is_activeTrue) # 返回单个用户 get_users(user_id1) # 返回多个用户 get_users()当需要获取单个用户时传user_id否则不传参数拿到所有活跃用户列表——一切都由一个get_users搞定。看似合理但这正是典型的「瑞士军刀型函数」。好的函数一定是「单一职责Single responsibility」的一个函数只做好一件事目的明确也更不容易在未来因需求变更而被修改。而返回多种类型的函数必然违反单一职责原则。好的函数应该总是提供稳定的返回值把调用方的处理成本降到最低。正确的做法是拆成两个独立函数def get_user_by_id(user_id): 按 ID 获取单个用户 ... def get_active_users(): 获取所有活跃用户 ...调用方从此无需记忆「传不传参数会得到什么类型」函数签名的含义与返回值一一对应。建议二使用 functools.partial 构造新函数假设代码里有一个参数很多的函数A适用性很强而另一个函数B完全通过调用A来完成工作相当于一个快捷方式。比如def multiply(x, y): return x * y def double(value): # 返回另一个函数调用结果 return multiply(2, value)double完全通过multiply完成计算。对于这种场景可以用functools模块里的partial()函数来简化。partial(func, *args, **kwargs)基于传入的函数与可变位置/关键字参数构造一个新函数。所有对新函数的调用都会在合并了当前调用参数与构造参数后代理给原始函数处理。利用它上面的double可以改写为单行表达式import functools double functools.partial(multiply, 2)之后调用double(10)等价于multiply(2, 10)更简洁也更直接。partial不只是简化「快捷函数」定义它还常常与其他内建函数组合出精妙写法在仓库系列第 11 篇《高效操作文件的三个建议》里作者就用iter(partial(file.read, block_size), )两行实现了一个可复用的分块文件读取生成器——partial将file.read(block_size)变成无参可调用对象配合iter(callable, sentinel)的哨兵终止模式彻底消除了循环内的手动break判断见 分块读取生成器实现。这是「构造新函数」思想在真实 I/O 场景中的直接落地。建议三抛出异常而不是返回结果与错误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(namename), def create_from_input(): name input() item, err_msg create_item(name) if err_msg: print(fcreate item failed: {err_msg}) else: print(fitem{name} created)create_item利用多返回值特性把错误信息作为第二个结果返回。乍看很自然尤其对有 Go 语言经验的人来说更是如此。但在 Python 世界里这并非最佳做法——它会显著增加调用方的错误处理成本尤其是当很多函数都遵循这个规范且存在多层调用时。Python 具备完善的异常机制并且在某种程度上鼓励使用异常官方术语 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(namename) def create_for_input(): name input() try: item create_item(name) except CreateItemError as e: print(fcreate item failed: {e}) else: print(fitem{name} created)用「抛出异常」替代「返回结果, 错误信息」后错误流程处理看似变化不大实际上有诸多不同新版本函数拥有更稳定的返回值类型它永远只返回Item类型或是抛出异常异常总是会无法避免地让人感到惊讶所以最好在函数文档里说明可能抛出的异常类型如上例的:raises:注释异常不同于返回值它在被捕获前会不断向调用栈上层汇报。create_item的一级调用方完全可以省略异常处理、交由上层处理。这给了我们更多灵活性但也带来了更大的风险。补充如何在编程语言里处理错误是一个至今仍存在争议的主题。上面不推荐的多返回值方式正是缺乏异常的 Go 语言中最核心的错误处理机制而即使异常机制本身不同语言之间也存在差别。异常或是不异常都是由语言设计者多方取舍后的结果不存在绝对性的优劣。但单就 Python 语言而言用异常表达错误更符合 Python 哲学。这一建议与仓库系列第 6 篇《异常处理的三个好习惯》一脉相承那里进一步提出「只做最精确的异常捕获」主张永远只捕获可能抛异常的语句块、尽量捕获精确的异常类型而非模糊的Exception见 精确捕获示例同时还讨论了异常抽象层级一致性问题——底层模块应抛出与自身抽象层级一致的异常再在贴近高层的视图处做异常包装与转换。这两篇文章合在一起构成了完整的「Python 异常返回」方法论。建议四谨慎使用 None 返回值None通常被用来表示「某个应该存在但是缺失的东西」它类似 JavaScript 的null、Go 的nil。因为其独特的「虚无」气质它经常被作为函数返回值使用。使用 None 作为返回值时通常是下面 3 种情况。场景 1作为操作类函数的默认返回值当某个操作类函数不需要任何返回值时通常就返回 None。同时None 也是不带任何return语句函数的默认返回值。对这种函数使用 None 没有任何问题标准库里的list.append()、os.chdir()均属此类——调用它们的目的就是「执行操作」操作完成后返回 None 完全符合预期。场景 2作为某些「意料之中」的可能没有的值有一些函数其目的是尝试性地做某件事视情况不同可能结果也可能没有结果。对调用方来说「没有结果」完全是意料之中的事情此时用 None 表示「没结果」是合理的。Python 标准库中正则模块re下的re.search、re.match均属此类找到匹配时返回re.Match对象找不到时返回None。「搜索」这种行为本身就允许没有结果。场景 3作为调用失败时代表「错误结果」的值有时 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 不合法时函数返回 None。但在这个场景下这样做并不好。你可能会觉得它和场景 2 非常相似——如何区分这两种情形关键在于函数签名名称与参数与 None 返回值之间是否存在一种「意料之中」的暗示。每当你让函数返回 None 值时请仔细阅读函数名然后问自己假如我是该函数的使用者从这个名字来看「拿不到任何结果」是否是该函数名称含义里的一部分用两个函数对比re.search()search代表从目标字符串里去搜索匹配结果搜索行为一向是可能有没有结果的所以该函数适合返回 Nonecreate_user_from_name()名字代表「基于一个名字来构建用户」读不出「可能返回、可能不返回」的含义所以不适合返回 None。对于无法从函数名读出 None 暗示的函数有两种修改方式第一种坚持用 None但修改函数名例如把create_user_from_name()改名为create_user_or_none()让「可能没有结果」成为名称含义的一部分。第二种更常见用抛出异常来代替 None 返回值。因为如果返回不了正常结果并非函数意义里的一部分就代表出现了「意料以外的状况」而这正是异常所掌管的领域class UnableToCreateUser(Exception): 当无法创建用户时抛出 def create_user_from_name(username): 通过用户名创建一个 User 实例 :raises: 当无法创建用户时抛出 UnableToCreateUser if validate_username(username): return User.from_username(username) else: raise UnableToCreateUser(funable to create user from {username}) try: user create_user_from_name(username) except UnableToCreateUser: # Error handling else: user.do_something()与 None 返回值相比抛出异常还有一个额外优势可以在异常信息里提供出现意料之外结果的原因如unable to create user from {username}这是只返回一个 None 值做不到的。这一「用异常携带失败原因」的思想正是仓库系列第 15 篇《在边界处思考》里 EAFPEasier to Ask for Forgiveness than Permission获取原谅比许可简单风格的源头那里用计数器的try/except KeyError改写演示了 Python 社区对「请求原谅」风格的偏爱见 EAFP 风格示例。建议五合理使用「空对象模式」用 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(usernameusername, balancebalance) 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」来改善控制流——使用一个符合正常结果接口的「空类型」来替代空值返回/抛出异常以此降低调用方处理结果的成本。引入「空对象模式」后示例改为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(usernameusername, balancebalance) 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 对象失败时返回 balance 为 0 的NullAccount整个计算逻辑被大幅简化。注意NullAccount也实现了from_string类方法但抛NotImplementedError说明它保持了与正常结果一致的接口外形——这正是「空对象」的设计要点接口一致、行为无害。「空对象模式」在 Python 世界里并不少见Django 框架里的AnonymousUser就是一个典型代表。仓库系列第 15 篇《在边界处思考》在总结边界处理思路时也专门把「空对象模式」列为面向对象多态之外的重要补充手段见 空对象模式引用可见这一模式在本系列中的分量。建议六使用生成器函数代替返回列表在函数里返回列表特别常见先初始化results []在循环体内results.append(item)填充最后在函数末尾返回。对于这类模式可以用生成器函数简化——粗暴点说就是用yield item替代append语句def foo_func(items): for item in items: # ... 处理 item 后直接使用 yield 返回 yield item使用生成器的函数通常更简洁、也更具通用性——调用方拿到的是一次性可迭代对象既可以直接for遍历也可以按需消费不必一次性在内存中构建完整列表。这一模式在仓库系列中贯穿始终系列第 4 篇《容器的门道》的「写扩展性更好的代码」小节把强依赖 list 的add_ellipsis函数改写成基于typing.Iterable的生成器版本add_ellipsis_gen让同一个函数既能处理列表、元组也能直接处理文件对象正是「用 yield 替代 append、面向接口编程」的经典演示见 生成器版本 add_ellipsis_gen系列第 7 篇《编写地道循环的两个建议》进一步提出「用生成器函数解耦循环体」把「挑选周末时间戳」从奖励积分的循环里抽成gen_weekend_ts_ranges()生成器后旧需求「发积分」和新需求「发通知」都能直接复用见 生成器解耦循环体系列第 11 篇《高效操作文件的三个建议》中作者把「分块读文件」抽象为chunked_file_reader生成器让统计函数的主循环只负责计数并把 5GB 单行大文件的统计内存占用从约 2GB 降到约 7MB见 分块读取生成器。可以总结一条规律凡是「循环 append」的返回列表模式都值得先想想能不能用 yield 改写。建议七限制递归的使用当函数返回自身调用时递归就发生了。递归在特定场景下是非常有用的技巧但坏消息是Python 语言对递归的支持非常有限。这种「有限的支持」体现在很多方面。首先Python 语言不支持「尾递归优化」——即使你的递归调用出现在函数末尾也不会被编译器优化成迭代。另外Python 对最大递归层级数有着严格的限制。所以建议尽量少写递归。如果你想用递归解决问题先想想它是不是能方便地用循环替代如果答案是肯定的就用循环改写。如果迫不得已必须使用递归请考虑下面几点函数输入数据规模是否稳定是否一定不会超过sys.getrecursionlimit()规定的最大层数限制是否可以通过使用类似functools.lru_cache的缓存工具函数来降低递归层数缓存中间结果可以剪掉大量重复的递归分支例如经典斐波那契递归。这里的核心仍然与「返回结果」有关递归函数的返回路径高度依赖输入规模与调用深度属于「返回值稳定性」最难以保证的函数形态之一因此从函数设计角度也应该被谨慎对待。总结最后再总结一下本篇文章的要点让函数拥有稳定的返回值一个函数只做好一件事使用functools.partial定义快捷函数抛出异常也是返回结果的一种方式使用它来替代返回错误信息函数是否适合返回 None由函数签名的「含义」所决定使用「空对象模式」可以简化调用方的错误处理逻辑多使用生成器函数尽量用循环替代递归。判断一段函数返回设计是否优雅可以回到一个朴素的检验标准调用方拿到返回值时需不需要再写额外防御代码如果答案是「需要很多」那通常意味着返回契约本身可以设计得更好。延伸阅读本文为 one-python-craftsman 仓库「Python 工匠」系列的第 5 篇。该系列围绕 Python 编码技巧、最佳实践与思维模式展开全部文章索引见 README。与本篇主题紧密相关的系列文章上一篇Python 工匠容器的门道生成器替代列表、面向接口编程的底层原理下一篇Python 工匠异常处理的三个好习惯如何精确捕获、包装与简化异常处理Python 工匠编写地道循环的两个建议用生成器函数解耦循环体Python 工匠高效操作文件的三个建议partial与生成器在分块读文件中的实战组合Python 工匠在边界处思考EAFP 风格与空对象模式在边界处理中的延续应用。赞分享技术博客教程文档【免费下载链接】one-python-craftsman来自一位 Pythonista 的编程经验分享内容涵盖编码技巧、最佳实践与思维模式等方面。项目地址https://gitcode.com/gh_mirrors/on/one-python-craftsman点击查看免费下载相关推荐Python 工匠数字与字符串的进阶实战技巧one-python-craftsman 系列第 3 篇Python 工匠数字与字符串的进阶实战技巧one python craftsman 系列第 3 篇 本文是「Python 工匠」系列的第 3 篇文章源技术博客教程文档3大技术突破OpenCore Legacy Patcher让老Mac重获新生的终极方案3大技术突破OpenCore Legacy Patcher让老Mac重获新生的终极方案 当你的MacBook Pro 2015在App Store中看到此更操作系统固件驱动开发Python 工匠善用变量来改善代码质量one-python-craftsman 系列Python 工匠善用变量来改善代码质量one python craftsman 系列 本篇是开源仓库 one python craftsman 中『Py技术博客教程文档创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表