编写坚不可摧的 Python
本文将向您展示如何编写不抛出运行时异常的 Python 代码。这将帮助您编写出几乎不会出错且大部分功能都能正常运行的代码。我们将通过学习如何将函数式编程应用于 Python 来实现这一目标。本文将涵盖以下内容:
- 通过学习纯函数来确保函数始终有效。
- 返回 Maybe 值可以避免运行时错误。
- 使用 Lenses 避免处理深度嵌套数据时出现运行时错误
- 通过返回结果来避免运行时错误
- 利用流水线编程从纯函数创建纯程序
为什么要关心?
编写不会出错的代码就像保持牙齿健康一样重要。两者都有行之有效的预防方法,但人们仍然会不负责任地行事,要么吃不健康的食物而不刷牙,要么在编程中被“用已有的知识快速实现功能”的诱惑所蒙蔽。
例如,为了预防蛀牙,你应该避免食用高糖食物,饮用含氟自来水,并每天刷牙两次。至少在美国,许多人仍然存在牙齿问题。口腔卫生服务的可及性问题可能与经济因素有关。而对于另一些人来说,他们只是缺乏责任感,没有重视口腔健康。
对于编程方面的问题,我可以提供帮助。本文将提供一些行之有效的方法,帮助你掌握自助编程所需的知识。
然而,就责任而言,我深知这更难。我会教你技巧,但你必须练习。就像每天刷牙一样,你可以很快掌握。但在非函数式编程语言中运用函数式编程技巧则需要更多的练习和努力。
勤刷牙会让你的牙齿感谢你。同样,代码持续稳定运行也会感谢你。
“永不分离”究竟是什么意思?
“永不中断”意味着该函数始终有效,始终返回值,永远不会抛出异常,也不会退出 Python 进程(除非您主动要求)。无论您以何种顺序调用这些函数或程序,调用多少次,它们的行为始终可预测且值得信赖。
“大部分工作”究竟是什么意思?
即使你的函数一直运行正常,也不代表你的软件就一定没问题。比如,Docker 容器传递了错误的环境变量、下游 API 宕机,或者仅仅是计算错误。你仍然需要单元测试、模糊测试、功能测试,如果条件允许,还需要形式化测试,以及传统的手动测试来验证软件是否正常工作。不必担心软件会随机崩溃,你就可以全身心投入到这些方面。
此外,再多的函数式编程也无法避免糟糕的代码格式。你仍然需要像 PyLint 这样的工具来确保你编写的print("Sup")是规范的 .py 代码,而不是规范的 .py 代码。print("Sup)
如果这如此显而易见,那么我什么时候不应该这样做呢?
按优先级排序:
- 当你探索想法的时候。
- 当你和那些没有接受过函数式编程训练的人一起编写代码时。
- 当你面临截止日期时。
- 当你在修改没有单元测试的遗留代码时。
Python 的魅力在于它的动态特性。虽然它不像 JavaScript、Lua 或 Ruby 那样宽容,但它仍然提供了很大的自由度,让你可以尝试各种想法、使用各种数据类型,并提供多种方式在不同的基础设施架构上高效运行代码。由于类型(通常)只在运行时强制执行,你可以尝试各种想法,快速运行它们,并在运行后纠正发现的错误,然后重复这个过程,直到最终确定一个实现方案。虽然你可以使用函数式编程的概念来实现这一点,但如果你还在学习阶段,它们可能会拖慢你的速度。不过,在其他情况下,这倒是一个学习的好时机。
将函数式编程 (FP) 代码提交到 GitHub 仓库,而其他人不熟悉 FP,或者不明白你为什么编写一些看起来不符合 PEP 规范的代码,这真的会引发很多问题。通常,团队会采用自己的规则、模式和风格……而且他们这样做并不总是有合理的理由。最好的办法是了解他们为什么这样编码,并采纳这些标准。如果你有能力教导团队,那当然很好,但 FP 对他们来说相当陌生,而且它本身就以晦涩难懂和糟糕的布道者而闻名。所以,要谨慎行事。破坏团队信任是导致软件永远无法正常运行的途径之一。糟糕的工作关系会导致糟糕的软件。
如果你时间紧迫,学习任何新东西都可能拖慢你的进度,甚至导致无法按时完成任务。但另一方面,这也是确保你快速掌握新知识的可靠方法,呵呵。
无论是否采用函数式编程,如果你负责维护的大型代码库中缺少单元测试,就不应该添加或修改代码。否则,你可能要花几天甚至几周的时间才能发现问题所在,根本无法判断是否破坏了某些功能。应该先添加测试,然后再进行重构。
始终有效的函数:编写纯函数
纯函数总是有效的。它们总是有效的原因是它们总是返回值,不会抛出异常。从技术上讲,它们不应该有副作用。之所以需要纯函数,是因为它们总是返回相同的值,这与数学类似,你可以依赖它们的结果或“答案”。此外,对纯函数进行单元测试也不需要模拟对象(mock),只需要桩对象(stub)。
这两条官方规则如下:
- 相同的输入,相同的输出
- 无副作用
返回值None是可以的。大多数 Python 开发者接触到的第一个函数,print看起来似乎没有返回值,很像console.logJavaScript。然而,它实际上是返回值的None:
result = print("Sup")
print(result == None) # True
通常,不返回任何值的函数,或者None在 Python 中,被称为“空操作”函数,简称“noop”(读作 no-op)。Noop 函数通常意味着该函数会产生副作用。Noop 函数不是纯函数。我们知道它print确实会产生副作用;调用它的目的就是为了产生向标准输出写入内容的副作用,以便我们能够看到代码的运行情况。
然而,对于类来说,情况就比较微妙了。现在你了解了规则,就会明白问题所在。以下是如何停止使用类包装 REST 调用并验证 SSL 证书的方法urllib:
import request.rest
import ssl
req = request.rest()
req.disable_ssl()
res = req.get("https://some.server.com")
注意这个disable_ssl()类方法。它不接受任何参数,也不返回任何值。为什么呢?可能是因为像大多数类一样,它在类实例内部更改了一个设置,关闭了 SSL 相关功能,这样下一个发起 REST 调用的人就不需要验证证书了。
在函数式编程中,你的做法恰恰相反。不过,在这种情况下,disable_ssl()多次调用可能没什么问题。但像这样的情况get就比较棘手了。
所以,不纯函数:
ssl = enabled
def get(url):
return requests.get(url, ssl_enabled=ssl)
以及一个纯函数:
def get(ssl, url):
return requests.get(url, ssl_enabled=ssl)
而且,它更加纯粹,也更容易进行单元测试:
def get(requests, ssl, url):
return requests.get(url, ssl_enabled=ssl)
你能用 Python 以合理的方式编写出的最纯粹的函数:
def get(requests, ssl, url):
try:
result = requests.get(url, ssl_enabled=ssl)
return result
except Exception as e:
return e
像这样编写函数,你就离理解Golang 的编写方式不远了。
避免使用“无”,而选择“可能”。
Python 提供的保证不多,所以喜欢冒险的人才会用它。如果你在编写软件,你可能不想承担风险。调用函数时,风险主要来自以下三个方面:
- 从字典中获取数据(因为你现在使用的是字典,而不是类,对吧?)
- 从列表中获取数据
- 从 Python 外部获取数据
更安全的字典:也许元组
我们来聊聊字典吧。
字典可以发挥作用:
person = { firstName: "Jesse" }
print(person["firstName"]) # Jesse
字典也可能出错:
print(person["lastName"])
# KeyError: 'lastName'
怎么办!?
你需要从两个方面改变你对 Python 的思考方式。首先,要改变你安全地访问对象的方式,例如使用 ` key in dictionarylen` 或 `or` 函数。其次,要改变你从函数返回值的方式,例如使用 Go 语言的语法,通过返回多个值来判断函数是否成功,或者使用 `Maybe` 或 `Result` 类型。
您可以通过创建 getter 函数安全地访问字典:
def get_last_name(object):
if "lastName" in object:
return (True, object["lastName"], None)
return (False, None, f"lastName does not exist in {object}")
这个函数是纯函数,安全可靠,可以处理任何数据而不会崩溃。它还利用了 PythonTuple在函数返回只读列表时的一个巧妙技巧:你可以解构它,用简洁的语法获取三个变量,使其看起来像是返回了多个值。我们选择了类似于 Go 语言的语法,Go 语言返回 `List` value, error,而我们返回 `List` didItWork, value, error。如果你愿意,也可以使用 Go 语言的语法,但我个人不喜欢写成 `List` if error != None。
ok, lastName, error = get_last_name(person)
if ok == False:
return (False, None, f"Failed to get lastName from {person}")
这是你的第一个Maybe底层代码。它Tuple包含函数是否接收到你的数据,如果接收到了,则包含数据内容,如果没有接收到,则包含原因。注意,如果ok代码为真False,则你的程序可能已经完成了。
Exceptions开发者通常被鼓励为所有非异常情况都创建异常raise处理,以便其他人能够捕获这些异常或不同类型的异常,并做出相应的反应,通常是在函数链的更高层级。这样做的问题在于,由于错误可能来自完全不同的文件,因此你无法轻松地阅读代码并发现错误。而使用 `maybe` 语句则可以非常清晰地指出哪个函数失败了,你也不必为了“以防万一”而将代码包裹在 `try/catch` 语句中。
更安全的词典:也许类型
元组虽然可以,但比较冗长。更简洁的选择是 Maybe 类型。我们将使用PyMonad 的版本,因为他们已经为我们做了很多工作。首先导入它:
from pymonad.Maybe import *
然后,我们将创建一个getLastName函数,使其返回一个Maybe类型而不是像以前那样返回一个元组:
def get_last_name(object):
if "lastName" in object:
return Just(object["lastName"])
return Nothing
我说的是“类型”这个词,但在 Python 中,它感觉更像是一个函数。(True, data, None)用 `type`替换Just(data)`type`,(False, None, Exception('reason'))用 `function`替换Nothing`function`。然后你就可以使用它了:
lastNameMaybe = get_last_name(person)
你的第一反应可能是“酷,如果是加密的Just,我该如何导出数据?”。但实际上,你无法导出。
“什么!?”
相信我,我们会在下面的管道编程部分详细介绍这一点。现在,你只需要知道这个函数永远不会失败,并且你总是会得到一个Maybe返回值,从而确保你的代码不会抛出错误,并且更具可预测性和可测试性。
说到这里,这里是Pytest:
def test_get_last_name_happy():
result = get_last_name({'lastName': 'cow'})
assert result == Just('cow')
“什么……”
😁 好的,在你更适应之前,试试这个:
def test_get_last_name_happy():
result = get_last_name({'lastName': 'cow'})
assert result.value == 'cow'
更安全的列表:也许类型
ListsPython 也同样如此。
people = [{'firstName': 'Jesse'}]
first = people[0]
凉爽的。
people = []
first = people[0]
# IndexError: list index out of range
太不酷了。有很多方法可以解决这个问题;这里介绍的这个快速解决方法,你最终会反复使用:
def get_first_person(list):
try:
result = list[0]
return Ok(result)
except Exception:
return Nothing
你会看到它以一种更易于重用的方式实现nth,只是它不会返回None,而是返回一个Maybe:
def nth(list, index);
try:
result = list[index]
return OK(result)
except Exception:
return Nothing
使用透镜实现纯粹的深度嵌套数据
你知道如何通过编写纯函数来创建不可破坏的函数。你知道如何访问Dictionaries和Lists安全地使用它们Maybes。
在实际应用中,我们通常会遇到嵌套的大型数据结构。这是如何实现的呢?以下是一些示例数据,包含两个人的地址信息:
people = [
{ 'firstName': 'Jesse', 'address': { skreet: '007 Cow Lane' } },
{ 'firstName': 'Bruce', 'address': { skreet: 'Klaatu Barada Dr' } }
]
让我们使用以下方法安全地获取第二个人的地址Maybe:
def get_second_street(list):
second_person_maybe = nth(list, 1)
if isinstance(second_person_maybe, Just):
address_maybe = get_address( second_person_maybe.value)
if isinstance(address_maybe, Just):
street_maybe = get_street(address_maybe.value)
return street_maybe
return Nothing
呃……不,太糟糕了。很多人已经花时间用更好用、更易用的 API 完成了这项工作。PyDash 就有一个get方法:
from pydash import get
def get_second_street(list):
return get(list, '[1].address.skreet')
很棒吧?适用于字典、列表,以及两者合并后的类型。
不过……有个小问题。如果找不到任何内容,它会返回一个空值None。返回 None 会导致运行时异常。你可以将默认值作为第三个参数提供。我们会用一个 ` Maybe;` 将其包裹起来;虽然看起来不太美观,但更强大、更纯粹。
def get_second_street(list):
result = get(list, '[1].address.skreet')
if result is None:
return Nothing
return Just(result)
返回错误而不是引发异常
字典和数组没有数据是可以接受的,但有时确实会出现问题或无法正常工作……如果没有异常处理该怎么办?我们可以返回一个异常Result。有两种方法可以实现这一点。你可以使用Tuple我们上面展示的方法,以 Golang 风格来实现:
def ping():
try:
result = requests.get('https://google.com')
if result.status_code == 200:
return (True, "pong", None)
return (False, None, Exception(f"Ping failed, status code: {result.status_code}")
except Exception as e:
return (False, None, e)
然而,使用真正的类型是有优势的,我们将在后面的管道编程部分详细介绍。PyMonad 有一个常用的类型叫做 `Result` Either,但是`Ok`Left和`Error`Right听起来毫无意义,所以我Result根据 JavaScript Folktale 的 Result 类型创建了自己的类型。因为“Ok”和“Error”是人们能够理解的词,并且与函数的运行或失败联系在一起。而 `Left` 和 `Right` 则像是……开车……或者你的手臂……或者跳舞……或者玩游戏……或者任何与编程无关的事情。
def ping():
try:
result = requests.get('https://google.com')
if result.status_code == 200:
return Ok("pong")
return Error(Exception(f"Ping failed, status code: {result.status_code}"))
except Exception as e:
return Error(e)
你不一定要把异常放进去Error;直接放个字符串就行了。我喜欢用异常,因为它们包含一些有用的方法、信息、堆栈跟踪信息等等。
流水线式编程:打造坚不可摧的程序
你知道如何构建不会出错的纯函数。你可以使用Maybe和Lenses安全地获取无法保证存在的数据。你可以通过返回Results安全地调用会产生副作用的函数,例如 HTTP 请求、读取文件或解析用户输入字符串。你已经掌握了函数式编程的所有核心工具……那么,你该如何构建函数式软件呢?
通过组合函数来实现。有很多方法可以纯粹地做到这一点。纯函数不会出错。你可以构建更大的纯函数,这些函数又会调用这些纯函数。如此反复,直到你的软件最终完成。
等等……你说的“创作”是什么意思?
如果你有面向对象编程的背景,你可能会认为组合是继承的反义词;它指的是在另一个类中使用一个类的实例。但我们这里指的并非如此。
让我们来解析一些 JSON 数据!目标是从一个庞大的字典列表中格式化人名。在这个过程中,你将学习如何组合函数。虽然这些函数并非纯函数,但概念是相同的。
请看,我们的 JSON 字符串:
peopleString = """[
{
"firstName": "jesse",
"lastName": "warden",
"type": "Human"
},
{
"firstName": "albus",
"lastName": "dumbledog",
"type": "Dog"
},
{
"firstName": "brandy",
"lastName": "fortune",
"type": "Human"
}
]"""
首先,我们必须解析JSON:
def parse_people(json_string):
return json.loads(json_string)
接下来,我们需要筛选出其中的人类List,不包括狗。
def filter_human(animal):
return animal['type'] == 'Human'
既然我们有了谓词List,我们将在filterPyDash 的函数中使用该谓词:
def filter_humans(animals):
return filter_(animals, filter_human)
接下来,我们需要提取名称:
def format_name(person):
return f'{person["firstName"]} {person["lastName"]}'
然后对所有项目执行此操作List;我们将使用mapPyDash 中的相应工具:
def format_names(people):
return map_(people, format_name)
最后,我们需要将所有名称转换为大写,所以我们map再次使用PyDash 中的start_case 函数:
def uppercase_names(people):
return map_(people, start_case)
太好了,这么多函数,怎么才能把它们结合起来使用呢?
嵌套
筑巢是最常见的。
def parse_people_names(str):
return uppercase_names(
format_names(
filter_humans(
parse_people(str)
)
)
)
哎……这就是为什么你经常会听到“鸟巢”这个词带有贬义,用来形容代码。
流动
虽然 PyDash 和 Lodash 称之为流程,但这是一种更常见的通过组合较小的函数来构建较大函数的方法,它能让你初步了解“管道”风格的编程。
parse_people = flow(parse_people, filter_humans, format_names, uppercase_names)
Pipeline:PyMonad 版本
Flow 现在确实很不错,但希望你已经发现了一些问题。具体来说,这些函数都不是完全纯函数。没错,它们有相同的输入、相同的输出,也没有副作用……但如果其中一个函数返回一个 `a` 呢Nothing?如果你在做一些危险的操作,而其中一个函数返回的 `a`Result包含的是一个Error`a` 而不是一个 `b` ,又会发生什么呢Ok?
嗯,这些类型都是为了方便管道连接而设计的。你已经看到了我编写的函数是如何flow协同工作的;它们只需要遵循 3 条规则:
- 成为主要纯函数
- 只有一个输入
- 返回输出
它们Maybe也Result可以连接在一起,但它们有一些额外的特殊功能。本文我们只关注以下 4 个功能:
- 如果函数 a
Maybe接收到一个值Just,它会智能地获取该值Just(thing).value并将其传递给下一个函数。这与解包该值并将其传递给下一个函数的过程Result相同。Ok - 每个函数都期望你返回相同类型的值。如果你
Maybe像在 `<string>` 函数中那样将多个函数链接在一起flow,那么期望你返回你的 `<string>Just(thing)` 或 `<string>` 类型Nothing。 - 两者都处理错误情况。如果一个函数链
Maybe突然出现异常Nothing,整个函数链都会报错Nothing。如果你连接在一起的任何函数出现异常,并且链中的Result某个函数突然也出现异常,那么整个函数链都会报错。ErrorError - 它们已经
flow内置了;但是你没有调用它,而是使用奇怪的、新的、非 Python 符号来迷惑你,让你看起来很厉害,并且尽管增加了大脑活动,但代码却感觉不那么冗长。
太多了,不用管它。直接看例子:
def parse_people_names(str):
return parse_people(str) \
>> filter_humans \
>> format_names \
>> uppercase_names
再见!👋🏼
如果这听起来有点陌生和奇怪,那是因为:
- 你居然用面向对象+命令式语言进行函数式编程;太棒了!
- >> 既不符合 Pythonic 规范,也不符合 PEP 规范®
- 大多数 Python 开发人员看到反斜杠 (\) 就会想:“哎呀,这段代码太长了……”
- “……等等,你把函数放在那里,却不调用它们,也不给它们传递参数,这到底是怎么回事……”
这就是为什么很多函数式程序员即使不怎么热衷于推广,人也都很友善的原因。很多非函数式编程爱好者看到这类代码会感到不适,然后离开。这让很多函数式程序员感到孤独,因此,当编程社区的人和他们交流时,他们非常乐意保持友好和礼貌。
手动管道
还记得在学习 Lenses 之前获取深度嵌套属性的那个糟糕例子吗?让我们用一个管道来代替它Maybe;这将让你更好地理解这些东西是如何连接在一起的,就像flow上面那样。
def get_second_street(list):
second_person_maybe = nth(list, 1)
if isinstance(second_person_maybe, Just):
address_maybe = get_address( second_person_maybe.value)
if isinstance(address_maybe, Just):
street_maybe = get_street(address_maybe.value)
return street_maybe
return Nothing
真恶心。好吧,我们从头开始,首先我们需要lastName(顺便说一句,我很高兴你无视了“再见”并且仍然在这里,你具备所需的素质,哦耶):
def get_second_person(object):
return nth(object, 1)
好的,接下来,获取地址:
def get_address(object):
if 'address' in object:
return Just(object['address'])
return Nothing
最后,得到那个skreet skreet!
def get_street(object):
if 'skreet' in object:
return Just(object['skreet']
return Nothing
现在让我们把它们组合起来。
def get_second_street(object):
second_person_maybe = get_second_person(object)
if isinstance(second_person_maybe, Nothing):
return Nothing
address_maybe = get_address(second_person_maybe.value)
if isinstance(address_maybe, Nothing):
return Nothing
street_maybe = get_street(address_maybe)
if isinstance(street_maybe, Nothing)
return Nothing
return street_maybe
呃……好吧,我们来测试一下她:
second_street_maybe = get_second_street(people)
注意几点。每次调用函数后,都要检查返回值是否为 `int` Nothing。如果是,则立即返回该值并停止执行后续所有操作。否则,调用链中的下一个函数并解包该值。这里我们手动执行了这一操作maybe.value。另外,return street_maybe末尾的 `int` 语句有点多余;无需检查返回值Nothing,直接返回即可,但我希望你能看到重复三次的模式。
这种模式正是它>>为你所做的:检查Nothing并提前中止,否则解包该值并将其传递给链中的函数。使用该绑定运算符重写如下:
def get_second_street(object):
return get_second_person(object) \
>> second_person_maybe \
>> get_address \
>> get_street
很容易忘记反斜杠(\)。这是因为 Python 对空格的处理非常严格,只有极少数情况下才允许在代码中使用换行符。如果你不想使用换行符,那就把所有内容都放在一行里:
def get_second_street(object):
return get_second_person(object) >> second_person_maybe >> get_address >> get_street
手动结果流程
让我们来做点危险的事。我们将从 AWS 加载加密密钥,然后获取 OAuth 令牌,接着从一个需要该令牌才能工作的 API 加载贷款产品列表,最后只提取 ID。
危险物品?潜在例外情况?一份工作Result……
获取数据密钥:
def get_kms_secret():
try:
result = client.generate_data_key(
KeyId='dev/cow/example'
)
key = base64.b64decode(result['Plaintext']).decode('ascii')
return Ok(key)
except Exception as e:
return Error(e)
如果您不知道KMS是什么,别担心,它只是亚马逊的加密技术,会给您提供只有您才能获取的密钥。如果您没有权限,该功能将无法运行。它只是以文本形式提供一个临时的私钥。我们将使用该私钥来获取 OAuth 令牌。
接下来,通过requests库获取我们的 OAuth 令牌:
def get_oauth_token(key):
try:
response = requests.post(oauth_url, json={'key': key})
if response['status_code'] == 200:
try:
token_data = response.json()
return Ok(token_data['oauth_token'])
except Exception as parse_error:
return Error(parse_error)
return Error(Exception(response.text))
except Exception as e:
return Error(e)
这里你可以选择一种风格。如上所示,有四种潜在的失败情况:状态码不是 200、JSON 解析失败、在解析的 JSON 中找不到 OAuth 令牌,或者网络错误。你可以像我上面那样,“把所有问题都放在同一个函数里处理”。这样做的目的是为了判断“我是否获取到了令牌?”。但是,如果你不喜欢嵌套的方式ifs,trys你可以把它拆分成四个函数,每个函数都接受一个参数,然后Result按顺序将它们连接起来。
现在我们有了令牌,让我们调用最后一个 API 来获取贷款产品列表。我们可能会得到一个列表,但我们只需要 ID,所以我们将使用映射来提取它们:
def get_loan_ids(token):
try:
auth_header = {'authentication': f'Bearer {token}'}
response = requests.get(loan_url, headers=auth_header)
if response.status_code == 200:
try:
loans = response.json()
ids = map_(loans, lambda loan: get(loan, 'id', '???'))
return Ok(ids)
except Exception as e:
return Error(e)
return Error(Exception(response.text))
exception Exception as overall_error:
return Error(overall_error)
如果一切顺利,你会得到一个字符串列表。否则,会返回错误信息。让我们把这三个组件连接起来:
def get_loan_ids():
return get_kms_secret() \
>> get_oauth_token \
>> get_loan_ids
当你去的时候:
loan_ids_result = get_load_ids()
要么它运行正常,这loan_ids_result是一个成功Ok;要么它在某个地方失败了,这是一个Error包含异常或错误文本的异常。
……现在,我演示一下我用的作弊方法,你可以随意组合Maybes搭配Results使用。你看,当我们尝试获取贷款ID的时候?
get(loan, 'id', '???')
第三个参数是属性不存在或为空时的默认值None。正确的做法是使用 ` Maybeis` 属性。如果你愿意,可以这样务实,但要注意,这类情况你经常会遇到“代码没有错误但就是运行不了”的情况🤪。此外,我通常Errors更喜欢Nothing在这些场景中使用 `is` 属性,因为它能让你有机会提供异常信息或包含大量上下文的文本,解释错误原因。对于程序员来说,了解错误原因至关重要,尤其是在你接近错误源,并且知道它可能是在一个可能出错的大量函数中失败的原因时。
结论
函数本身没问题,但异常处理会导致很多间接调用。也就是说,你以为你知道一个函数甚至整个程序可能出错的三种方式,但随后某个深层嵌套的函数又抛出了意想不到的异常。完全移除这些异常,并锁定那些你已知或未知的、会导致程序崩溃的情况,使用try/catch纯函数就能解决这个问题。现在,你可以确切地知道函数执行的路径。这就是纯函数的强大之处。
然而,这并不能避免使用空值(即 null None)数据。你会在那些期望接收有效数据的函数中遇到各种运行时异常。如果你强制所有函数都使用Maybe来处理这种情况,那么就不会出现空指针异常。这包括使用Lenses访问深层嵌套的数据。
一旦通过副作用(例如发起 REST 调用、读取文件或解析数据)跳出程序,就可能出现问题。错误是正常的,而且是有益的,还能带来学习机会。让函数返回一个Result而不是抛出异常Exception,是确保函数纯函数性的关键。如果出现问题,你也可以尽早中止程序,避免级联效应导致其他函数的数据状态异常。此外,与Golang和Rust类似,你还可以记录错误发生的位置,并提供有用的上下文信息,而不是仅仅依靠冗长的堆栈跟踪信息进行猜测。
最后,一旦你编写了纯函数,你就可以构建更大的纯函数,将它们组合在一起。这就是通过流水线编程(也称为铁路编程,一种流式编程风格等)构建纯软件的方法。
Python 提供了PyDash库,让你掌握纯函数处理数据和列表的基础知识。PyMonad则提供了创建Maybe结果Either(我称之为 Result)的基础知识。如果你感兴趣,Coconut 编程语言可以与 Python 集成并编译,让你能够以更函数式的编程风格编写代码。以下是重写上面示例的一个例子:
def get_loan_ids():
return get_kms_secret()
|> get_oauth_token
|> get_loans
|> map( .id )
如果这一切看起来让人不知所措,别担心。函数式编程是一种完全不同的思维方式,Python 也不是函数式语言,而且这些都是非常高级的概念,我只是讲了些基础知识。你只需要练习纯函数并编写测试用例。一旦你掌握了这些基础知识,你就会像蜘蛛侠的直觉一样,开始感知哪些代码是不纯的,哪些代码会产生副作用。一开始你会很喜欢 Maybe 类型,但当你开始负责任地使用它们时,你会意识到为了避免空指针,你需要做多少额外的工作。Either 类型和 Results 类型一开始可能会比较难调试,因为你不会去查看堆栈跟踪信息,你需要学习如何编写最佳的错误信息,以及如何捕获各种错误。坚持下去,即使你只用一点点也没关系;即使是一点点,也能让你的代码更加健壮。
文章来源:https://dev.to/jesterxl/write-unbreakable-python-20ck