TowardsDataScience-2023-博客中文翻译-三十六-
TowardsDataScience 2023 博客中文翻译(三十六)
是时候开始讨论 LLMs 中的提示架构了吗?
从提示工程到提示架构
·发表在 Towards Data Science ·7 min read·2023 年 10 月 28 日
--

图片由作者提供。(AI 生成)
总结。
一切始于一个词。
对结果不满意,我们再次尝试。
总结文章中最重要的要点。
提示工程教会我们,更具体的提示更好。
确定文章中提出的三个最重要的论点,并根据提供的证据评估作者论点的力度。是否有任何地方你认为论点可以更强或更有说服力?
随着时间的推移,我们学会了包括更多细节,以引导我们喜欢的 LLMs 提供最佳答案。

最近出现了一种叫做 Least to Most prompting 的提示架构。[1]
提示工程技术变得越来越复杂和精细,有时由许多组件构成。提示工程 的定义可能无法准确描述如此复杂的系统。
在这篇文章中,我想提出一个更准确的标签,用于描述与 LLMs 接口的多组件系统:
提示架构。
提示工程的历史
现代语言模型在仅看到几个示例后,展现出处理新任务的惊人能力。这种能力被称为上下文学习,这是我们提示工程如此有效的主要原因。
研究人员认为,上下文学习 的有效性在于预训练使模型掌握了语言任务所需的一般技能。然后,在测试时,它只需识别模式并应用其技能。更大的模型做得更好,使它们在各种自然语言任务中适应性强。[2]
过去,你需要数千个标记示例来微调语言模型以处理新任务。但通过上下文学习,你可以将任务描述提供给模型,并让其在上下文窗口中搞定新任务。我们称之为零-shot 学习。

在少量示例提示中,将一些期望输出的示例添加到提示中。图片由作者提供。
少量学习通过在上下文中提供几个示例来工作。模型在测试时适应这种模式,无需权重更新。这种快速适应能力随着模型规模的增长而提高,使得像 GPT-3 这样的大型模型能够仅通过几个示例学习新任务。模型在看到少量示例后可以很好地进行泛化。
这些技术提供了一个更周到地与 LLM 互动的通用框架。
角色提示是一种提示工程技术,我相信你在使用 ChatGPT 时至少尝试过一次,它用于更具体的任务。在这里,AI 系统在提示开始时被分配一个特定角色。这些额外信息提供了背景,有助于模型的理解并产生更有效的回应。[3]
提示以指定 AI 角色的指令设计启动,然后继续提出 AI 应在分配角色框架内回应的问题或任务。

角色提示。图片由作者提供。提示来源。
通过角色提示提供背景,有助于 AI 理解并做出恰当的回答。它指导模型以特定领域专家的身份回应。例如,你可以提示模型扮演医生角色,以获得更具医学相关性的回答。

我最喜欢的提示之一。图片由作者提供。提示来源。
启用大语言模型中的复杂推理
LLM 在逻辑推理和多步骤问题解决上仍然存在困难。思维链提示是一种让这些模型逐步展示其工作过程并解决问题的技术。
演示期望的推理过程,提示模型在新问题上复制这种逻辑思维过程。CoT在多步骤推理任务上表现更好,如数学和逻辑谜题,这些任务通常会让这些模型感到困惑。

思维链提示。[4]
最近的研究将提示工程推进到具有多个元素和推理阶段的系统中。
这里是我们跨越提示工程和提示架构之间界限的地方。

自我一致性提示架构。[5]
复杂的推理任务通常允许多条有效的推理路径来得出正确的解决方案。
自我一致性提示首先从模型中抽样出一组多样的候选输出,生成多个可能的推理路径。然后它汇总答案并选择最终答案集中最常见的答案。如果不同的推理路径得出相同的明确答案,则对答案的正确性有更大的信心。[5]

在回答物理问题之前,模型会问自己关于物理原理的问题。[6]
退后提示进一步发展了解决问题的思路,即通过中间步骤分解问题。这是一种提示架构,通过让模型退后一步来制定问题的抽象版本,从而提高推理能力,然后再尝试回答问题。
退后提示首先让 LLM 对关键概念提出一个更一般的问题。LLM 用核心事实和概念作答。通过这种广泛的知识,LLM 然后使用具体的原始问题来给出最终回应。基准测试显示,退后提示帮助大型模型更好地推理并减少错误。[6]

验证链提示架构。[7]
验证链(CoVe)是一种提示架构,旨在减少大型语言模型中的幻觉。CoVe 首先让模型生成对查询的初始回应,该回应可能包含不准确或幻觉。接下来,我们提示 LLM 创建一组验证问题,以核实初始回应中的潜在错误。然后,LLM 独立回答这些验证问题,不依赖于原始回应,以避免重复幻觉。目标是生成一个修订后的、经过验证的回应,结合验证问题-答案对来纠正初始回应中的任何不一致之处。[7]
自主代理和高级应用的提示架构
提示架构还支持复杂的应用程序,这些应用程序无法通过单一提示的工程技术实现。

ReAct 提示架构。[8]
在ReAct提示中,模型将思想和行动混合以解决复杂任务。思想是计划和推理步骤,类似于人类推理。行动通过 API 或环境收集外部信息。观察返回相关信息。ReAct 还通过暴露思维过程来提高模型的可解释性,以评估推理的正确性。人类也可以编辑思维来控制模型的行为。[8]
选择合适的提示架构
对于对话型聊天机器人来说,通常更直接的提示工程技术作为初步尝试就足够了。如果这些方法失败了,你可以升级到像退后或自我一致性的提示方法,以在不增加太多复杂性的情况下提高推理能力。
如果构建一个减少幻觉的应用程序是你的优先事项,可以考虑CoVe,或者像零资源幻觉预防这样的更高级方法。[9] 然而,CoVe 是一种多步骤的交互,可能对聊天来说有些繁琐。在这种情况下,检索增强生成(RAG) 可能是减少幻觉的更好选择。[10]

RAG 的高级概述。图片由作者提供。
这些方法揭示了有关 LLM 的令人兴奋的见解,但 RAG 更适合实际应用。
记住,使用高级提示架构构建应用程序将更昂贵,因为每个查询将使用更多的令牌来生成最终响应。
如果你的目标是构建自主代理——智能、目标导向的系统——可以尝试 ReAct 提示。React 允许 LLM 通过混合思考和行动与世界互动。
模型在无需帮助的情况下解决复杂任务的能力不断提高。提示将变得更加复杂,为大语言模型启用更高级的用例。
经验让你培养直觉,以了解不同情况下哪些技术效果最好。
与 LLM 互动的未来
高级提示架构提升了 LLM 的能力,使其能够完成仅通过一次推理无法实现的任务。
提示架构也是了解 LLM 内部运作的一个令人兴奋的方式,超越了提高其实际用途的范畴。有些架构过于复杂或昂贵,不适合实际的现实世界应用。
提示架构让我们窥视 LLM 的黑箱内部。
提示架构不是提示工程的演变——这是一种截然不同的技术。
虽然提示工程使用单步推理,任何人在聊天界面中都可以执行,但提示架构需要多个推理和逻辑步骤,通常需要复杂的代码来实现。
深入了解这两者揭示了大语言模型的新能力。
在我看来,这种区分是必要的。
如果你喜欢这篇文章,请加入 文本生成 ——我们的通讯每周有两篇最新的生成 AI 和大语言模型见解的文章。
另外,你可以在 LinkedIn 上找到我。
参考文献
[1] [2205.10625v3] 从少到多提示启用大语言模型中的复杂推理 (arxiv.org)
[2] [2205.11916] 大语言模型是零样本推理器 (arxiv.org)
[3] [2308.07702] 通过角色扮演提示改善零样本推理 (arxiv.org)
[4] [2201.11903] 连锁思维提示引发大语言模型的推理 (arxiv.org)
[5] [2203.11171] 自一致性改善语言模型中的思维链推理 (arxiv.org)
[6] [2310.06117] 退一步思考:通过抽象激发大型语言模型的推理能力 (arxiv.org)
[7] [2309.11495] 验证链减少大型语言模型中的幻觉 (arxiv.org)
[8] [2210.03629] ReAct:在语言模型中协同推理与行动 (arxiv.org)
[9] [2309.02654] 零资源幻觉预防用于大型语言模型 (arxiv.org)
提示工程 101:零样本、单样本和少样本提示
原文:
towardsdatascience.com/prompt-engineering-101-zero-one-and-few-shot-prompting-1e8ced03d434
基本提示工程策略的介绍
·发表于 Towards Data Science ·5 分钟阅读·2023 年 9 月 19 日
--

图片来源:Alexandra_Koch来自Pixabay
介绍
尽管大型语言模型看似拥有超自然的能力,但它们最终还是预测模型,基于提供的上下文来预测下一个词。
因此,它们的表现不仅依赖于训练数据的庞大体量,还高度依赖于用户输入中提供的上下文。
频繁使用 LLM 驱动聊天机器人的用户意识到上下文的重要性。如果没有足够的上下文,聊天机器人,无论是公共服务(例如 ChatGPT)还是定制的 LLM 产品,都将难以执行更复杂的指令。
在这里,我们探讨了一种基本的策略,用于指导大型语言模型(LLMs)正确回答提示:在用户提示中提供上下文。这通常通过三种不同的方法进行:零样本提示、单样本提示和少样本提示。
零样本提示
如果你以前与一个由 LLM 驱动的聊天机器人互动过,你可能已经无意中使用了零样本提示。零样本提示意味着完全依赖 LLM 的预训练信息来回答给定的用户提示。
比如,假设我们使用 ChatGPT 作为情感分类器。一个零样本提示的输入可能如下所示:
用户提示: “确定这个句子的情感。”
句子:“这个篮球很重。”
ChatGPT: “句子‘这个篮球很重’的情感是中立的。它没有传达出正面或负面的情感,而是提供了关于篮球重量的实际信息。”
当查询被提供时,LLM 在没有额外背景的情况下将此句子评估为中立。
虽然这种方法通常能产生令人满意的回应,但当问题的复杂性较高或答案的标准与常规不同的时候,它可能会不够充分。
对于这种情况,用户可以考虑在提示中提供额外的背景信息。
一次性提示
一次性提示涉及在用户提示中提供一个示例,以引导 LLM 生成所需的响应。
在之前的示例中,ChatGPT 认为句子“这个篮球很重”是中立的。
然而,某些用户(例如销售篮球的零售商)可能会认为重的篮球是有缺陷的产品。因此,他们需要确保他们的 LLM 将“重”赋予负面的含义。
一种简单的方法是直接在查询中提供一个示例。
提示:
“句子:‘这个篮球容易携带’。”
答案:“这个句子的情感是积极的。”
确定句子的情感。
句子:“这个篮球很重”
答案:
ChatGPT: “句子‘这个篮球很重’的情感是负面的。它表达了篮球很重,这通常被视为负面特征,尤其是在易用性或游戏性方面。”
这次,ChatGPT 认为同一句子是负面的,因为它使用了提供的示例作为参考。
少量示例提示
用户并不总是能提供 LLM 正常工作所需的所有背景信息,仅凭一个示例是无法做到的。
对于这种情况,他们可以通过少量示例提示提供更多背景信息,这包括在用户提示中提供多个示例。
在之前的示例中,我们探讨了使用 ChatGPT 将句子情感分类为“积极”和“消极”。
不过,如果我们想要更广泛的情感标签范围怎么办?例如,我们可以使用“极其积极”、“积极”、“有些积极”、“中立”、“有些负面”、“负面”和“极其负面”这些标签。
为了使 ChatGPT(或其他 LLM 产品)能够结合特定的评分标准,少量示例提示可以是一种有效的策略。
用户提示:
“句子:这个篮球能放进我的包里。”
情感:有些积极。
句子:这个篮球在玩了一场比赛后就漏气了!
情感:极其负面。
句子:这个篮球是橙色的。
情感:中立。
句子:这个篮球的手感比我习惯的要好一点。
情感:
ChatGPT: “有些积极”
通过向 ChatGPT 介绍新的情感标签范围,可以“教会”ChatGPT 使用这些相同的标签来分类句子。
为什么我们不能使用无限示例
此时,很容易考虑在用户提示中包含尽可能多的示例以确保成功结果。
毕竟,更多的上下文,效果会更好,对吧?
虽然这可能是这样,但 LLM 受到可用令牌数量的限制。每个 LLM 都有一个上下文窗口,即模型可以作为输入的最大令牌数量。
用户可用的令牌数量取决于底层模型本身。通常,具体的数字可以在文档中找到。
例如,OpenAI 的 GPT-3.5 模型的令牌限制在 OpenAI API 文档中有详细说明。

GPT-3.5 模型的令牌限制(来源:Models — OpenAI API)
用户在编写用户提示时会消耗令牌,因此没有足够的带宽向 LLM 提供大量示例。
相反,他们必须弄清楚 LLM 完成特定任务所需的上下文量,然后确定如何在尽可能少的令牌中提供这些上下文。
关键要点

摄影师:Prateek Katyal 在 Unsplash上的照片
总的来说,用户可以从他们的 LLM 中获得令人满意的结果,而不需要在提示中提供额外的上下文,因为这些模型已经用大量数据进行了训练。
然而,对于提示复杂或响应需满足特定标准的情况,用户可以通过在提示中包含一个或多个示例(即单样本提示、少样本提示)来受益,前提是他们在分配的令牌数量范围内这样做。
无论你是使用现成的聊天机器人,还是构建自己的定制化大型语言模型(LLM)产品,使用零样本、单样本和少样本提示是一种简单而有效的获取足够结果的方法。
感谢阅读!
提示工程可能是 2024 年最热门的编程语言——原因如下
大型语言模型是下一代操作系统
·发布于 Towards Data Science ·阅读时间 16 分钟·2023 年 12 月 21 日
--

除非另有说明,所有图片均由作者使用 Midjourney、DALL-E 和 Canva 生成。
“我不认为把大型语言模型看作聊天机器人或某种文字生成器是准确的,” OpenAI 创始成员之一的Andrej Karpathy说。“更正确的想法是把它们看作新兴操作系统的内核进程。”
等等,但那到底是什么意思?
大型语言模型(LLMs)将逐渐成为你与计算机系统之间的界面。
现在,你手上拿着一个拥有一些计算能力的设备,但你不能直接访问这些能力。你的交互由操作系统(如 Windows、Mac OS 和 Android)介导,它将一组芯片和电路转换成一个用户友好的界面。
你的操作系统(OS)允许你通过一系列运行在其上的应用程序执行各种活动(如阅读某个秃头家的文章)。每个应用程序都有自己的用户界面(UI)和可以完成的任务。你根据需要在不同应用程序和用户界面之间跳转。

明天,你将拥有一个单一的用户界面来完成从编写年度业务报告到从头构建新应用的所有任务。这个用户界面将是一个聊天框或一个“上下文窗口”,你可以在其中用自然语言提交指令——这就是提示工程发挥作用的地方。
Prompt Engineering 是一种花哨的说法,用来表示“为你的 AI 模型编写越来越好的指令,直到它完全按照你的要求执行。”不过,这不仅仅是文字游戏;它是未来编程的蓝图。
编程作为(便宜的)商品
“编程指的是一种技术过程,用于告诉计算机执行哪些任务以解决问题,”Coursera 在他们的网站上写道。“你可以将编程看作是人类与计算机之间的协作,其中人类创建计算机可以理解的指令(代码)供计算机遵循。”
换句话说,编程将计算能力转变为一种商品:你可以用来实现目标的资源。Prompt Engineering 是一种将编程本身转变为商品的工具。你提交一个指令给 LLM,它会为你编写代码。
比如说,你想为一个工作项目分析一个小数据集。通常,你会先从公司云端收集数百个 CSV 文件。然后,你双击 Jupyter Notebook,输入几行 Python 代码,将输入数据编译成一个数据框。
从这里开始,你施加一些数据科学的魔法,运行几轮迭代,恭喜你:你获得了一系列优雅的表格、花哨的图表和数据驱动的预测。你最后一步是将六周的工作压缩成 42 张漂亮的幻灯片,展示在另一个叫做 Microsoft PowerPoint 的应用程序上。

你典型的数据科学家在一个随意的星期二。
你刚刚将现成的应用程序与自己编写的代码结合起来,构建了一个运行特定数据分析的程序。但如果你只需用简单的英语写几条指令呢?
“嘿,AI 伙伴,”你会说,“这是一个关于我们公司在过去五年中在巴黎进行的交付的混乱数据集。请清理这些数据,运行一个聚类算法。展示热力图,并放大高密度区域。加上两年的预测,并利用这些结果优化我们配送车队的每日行程。当你完成数学部分时,生成一个包含清晰图表和简洁评论的报告。慢慢来!我会离开至少六小时。”
每次你编写这样的提示时,你实际上是在编程一个解决特定问题的应用程序。
这几乎就像 Coursera 描述的那样——你与计算机协作以实现目标。唯一的区别是,你将使用简单的英语,而不是代码。好吧,提示可能离终点线还有几步之遥,但原则上,这就是你与未来 LLM 互动的方式。
以更具体的例子来看,考虑下面的著名演示,其中 GPT-4 将手绘草图转换为功能性 HTML 代码。
提示就是你所需要的一切
在我们能够用一堆聪明的提示替代数据科学家、网页开发者和软件工程师之前,还需要一段时间。在此期间,我们将通过 AI 助手来增强他们的工作效率——这些 AI 助手将用普通英语编程。
你将不再使用如 Google Workspace、Jupyter Notebook 和 Microsoft PowerPoint 等一系列互补应用,而是创建一个名为“StatSniffer: 你的个人数据科学专家”的助手。

类似于当前的 ChatGPT PLUS,你的个性化 StatSniffer 将是一个连接到一系列工具的 LLM,这些工具赋予它额外的能力,比如浏览文件、运行代码和生成图表。你还可以通过让它访问研究论文、案例研究和学术教科书来注入顶级的方法论。
OpenAI 已经通过 GPT 商店实验个性化 AI 助手,你可以构建名为 GPT 的助手。然而,目前的 GPTs 确实显得笨拙。例如,它们容易受到简单的破解,使其暴露“核心指令”。GPTs 也往往在与用户交流几次后恢复到默认模式(GPT-4)。
这并不令人惊讶,因为技术仍处于初期阶段。随着 AI 研究的进展和开源模型的改善,AI 助手的生态系统将演变为覆盖更多功能,并提高可靠性。说到这,还有很长的路要走。
像规划和多步骤推理等问题仍未解决,部分原因是 LLMs 在理解物理现实方面仍落后于人类(甚至是猫)。
这并不意味着人们会期待完全自主的助手掌握量子引力来利用现有的 AI 模型。即便是所谓的‘愚蠢’ LLMs,也能将你的效率提高一倍。以下是 麦肯锡关于 开发人员如何使用 LLMs 加速工作的研究摘录。
“我们最新的实证研究发现,基于生成 AI 的工具在许多常见开发任务中提供了令人印象深刻的速度提升。记录代码功能以便于维护(考虑到代码如何容易改进)可以在一半的时间内完成,编写新代码几乎在一半的时间内完成,优化现有代码(称为代码重构)则在近三分之二的时间内完成。” [作者强调]。
“为了让开发人员有效地利用这些技术来增强日常工作,他们可能需要培训和指导的结合,”麦肯锡研究团队解释说。“初始培训应包括最佳实践和将自然语言提示输入工具的动手练习,通常称为提示工程。”
这不仅对软件相关任务是正确的。这种模式也扩展到更广泛的公司活动中。例如,哈佛商学院的研究人员进行了一项研究,评估了为波士顿咨询集团(BCG)的员工配备生成 AI 工具的影响。
“在 AI 能力前沿的一组 18 个现实咨询任务中,使用 AI 的顾问生产力显著提高(他们平均完成的任务多出 12.2%,任务完成速度提高了 25.1%),并且产生了显著更高质量的结果(与对照组相比质量高出 40%以上),”HBS 研究人员写道。
这是Rajiv Shah的研究总结,Rajiv Shah 是一位机器学习工程师和 YouTuber。
这些研究为新的陈词滥调提供了支持:“AI 不会取代你,但使用 AI 的人会。”也许更花哨的表述是:“AI 不会取代你,但提示工程师会。”
“‘AI 前沿’越大(意味着 AI 模型可以高准确度执行的任务越多),我们通过提示可以解决的问题就越多。”这带我们到一个普遍的谬论,认为更强大的 AI 模型需要更少的提示工程技能。
“提示工程死了吗?”不,它是最前沿的技术。
看到大型语言模型和提示工程之间的关系的一种方式是把前者想象成一个多元宇宙,后者则像一个指示器——没错,就像一个激光指示器。
当你向 LLM 提问时,它会考虑一个相关文档的多元宇宙。在每个文档内,有一组可能的答案,每个可能的答案都是一系列概率。
你的提示指向最有可能包含所需答案的宇宙——然后,你的模型尝试一步步地导航到那个所需的答案。
每当模型预测一个标记时,它会排除数百条替代路径,并继续缩小选项,直到剩下的只有一系列构成“目的地”的词。

你的 LLM 在多元宇宙中导航可能的答案(这里更像是“在模糊的星球上导航”,但你明白我的意思)。
然而,这个目的地从未相同。即使你使用完全相同的提示,你几乎不会达到完全相同的地址。相反,你会在“邻域”中找到最相关的答案。
这里有一篇更技术性的描述,作者是弗朗索瓦·肖利,他是一位软件工程师和 AI 研究员,目前在谷歌工作:
如果一个 LLM 就像是一个包含数百万个向量程序的数据库,那么提示就像是在那个数据库中的搜索查询[...] 这个“程序数据库”是连续的和插值的——它不是一个离散的程序集合。
这意味着,像“以 x 风格重新措辞此文本”这样略有不同的提示仍然会指向程序空间中非常相似的位置,导致生成的程序行为非常接近但不完全相同。[...]
提示工程是搜索程序空间以找到在你的目标任务上表现最佳的程序的过程。
正如肖利指出的,你的提示目标是调用适合你要完成的任务的正确程序。许多人陷入的推理陷阱是相信未来的 LLMs 应该能够预测我们希望它们运行哪个程序,即使我们给出的是模糊的任务。
除非,就像人类一样,即使你雇佣了技术能力最强的工程师,她也无法读懂你的心思。你必须准确说明你想要的,否则你是在浪费时间和精力。
假设你指示你那位杰出的工程师去开发一个产品,但你不满意结果。你可以选择更换工程师或更改你的指示。既然你知道你的工程师非常熟练,常识告诉你选择第二种选项。
同样地,如果你高度能力的语言模型没有产生你想要的答案,你不会把它扔掉。你也不会坐等下一个模型能读懂你的心思。最合理且具成本效益的方法是改进你的提示。
这就是微软研究团队在 GPT-4 上所做的。与其为特定用例微调模型,他们使用了提示工程技术来提高其性能。
GPT-4 在九个不同的医学基准测试中的分数提高了多达 9%。因此,该模型的准确性超过了 90%,优于专门为医学应用微调的模型。
赖利·古德赛德可以说是首位提示工程师。
请注意,微调需要额外的资源,比如雇佣专家来制作高质量的训练数据以及一些计算资源来重新训练模型。虽然微调所需的计算能力比预训练少,但这仍然是额外的成本。
此外,每次你想为新的专业领域微调模型时,都需要投入相同的资源。相比之下,微软开发了一种提高不同领域(如电气工程、机器学习、哲学、会计、法律、护理和临床心理学)表现的提示技术。
另一个突显提示工程强大之处的例子来自 Anthropic。他们的团队通过在提示中添加一句话,将 Claude 2.1 模型在信息检索评估中的表现提高了 98%。仅仅一句话。
操控 LLM 就像玩一个外星工具。弄清楚它能做什么的唯一办法是以不同的方式按它的按钮。当这个外星工具的新版本出来时,你会期望它有更多的功能,但也有更多的按钮。
天真的方法是认为“更强大的模型需要更少的提示”。实际上,你的模型越强大,你可以通过正确的提示解锁的功能就越多。
展现风采的时候到了,人类代理!
从长远来看,我们将使用 LLM 作为操作系统来运用计算能力并解决各种问题。在中期,我们将编程 AI 代理以执行以前需要编写代码的任务。在这两个阶段中,我们都将使用英语作为主要编程语言。
好的,但现在发生了什么?
在 AI 代理赶上之前,正是你大展身手的时候。把自己想象成一个技术工匠,将 AI 模型、代码和传统应用结合起来,以应对复杂的挑战。可以说,你将玩乐高,而且特别感谢开源社区,你将拥有无尽的积木,可以组合起来创造创新项目。
LLM 是这个假设的乐高积木中的特殊部分,因为你常常会发现它们在你的创作中心。这将我们引向 Prompt Engineering 的两个互补风格。
见,Prompt Engineering 有两个含义:(1) 为大型语言模型(LLM)编写高质量的自然语言指令,以及 (2) 在 LLM 之上编写代码,通过条件提示和其他技术来改善其输出。
第二个定义包含了第一个,因为即使你在 LLM 周围包裹了代码,你仍然使用英语与其互动。这两种定义如何与 LLM 的使用交织在一起。
-
LLM 作为独立程序: 在这里,你编写高质量的自然语言提示以解锁最佳输出。示例包括创意生成、文档总结和编写代码。
-
LLM 作为你设计的程序的一部分: 在这里,你编写软件(用 Python、Java、C++或其他编程语言)来包裹 LLM 以完成特定任务。示例包括社交媒体评论的情感分析、专用聊天机器人和自主代理。
现在让我们深入探讨一下 Prompt Engineering 在每种用例中的表现。
1️⃣ “LLM 作为独立程序”的 Prompt Engineering
LLM 最常见的用例是通过像 ChatGPT 和 Bard 这样的网页界面与它们互动。
根据你的具体需求,你可以建立个人提示库。你希望你的提示是模板化且易于更新的。这样,你就不必每次运行提示时从头编写或在聊天记录中搜索它们。
以下是三种不同的灵活提示示例,可能会激发你的灵感:
**[PROMPT TEMPLATE #1 DOCUMENT SUMMARY]**
Act like a research assistant in the field of <field_name>.
I will give you a report titled <title_of_the_report> as input.
Please access the report through the following link <URL_of_the_report> using the online browsing feature.
Summarize the report in less than <summary_wordcount> and add <number_of_quotes> from the authors.
Make sure to pick precise quotes and list them as bullet points.
##
Desired format:
Title: <title_of_the_report>
Link: <URL_of_the_report>
Summary of the report based on the previous instructions.
- Quote #1
- Quote #2
- Quote #3
- etc.
##
Inputs:
<field_name> = Placeholder for the field of expertise of your document.
<title_of_the_report> = Placeholder for the title of the report you want summarize.
<URL_of_the_report> = Placeholder for web address where the report can be found.
<summary_wordcount> = Placeholder for the maximum word count for the summary.
<number_of_quotes> = Placeholder for the number of quotes to be extracted from the report.
**[PROMPT TEMPLATE #2 PRODUCT DESCRIPTION]**
Act like an expert copywriter.
##
Role:
Write a product description for an e-commerce shop.
Use the following structure and fill in the details based on the placeholders provided:
Product Name: <product_name>
Introduction: Start with a captivating opening sentence about <product_name>, suitable for <target_audience>.
Key Features: List the main features of <product_name>. Include <product_features>.
Benefits: Explain how <product_name> benefits the user, addressing <target_audience> needs.
Call to Action: Encourage the reader to make a purchase decision with a compelling call-to-action.
SEO Keywords: Integrate <seo_keywords> naturally within the text for SEO purposes.
Tone: Maintain a <brand_tone> throughout the description to align with the brand's voice.
##
Inputs:
<product_name> = Placeholder for the name of the product.
<product_features> = Placeholder for listing the specific features of the product.
<target_audience> = Placeholder to specify the target audience or demographic for the product.
<seo_keywords> = Placeholder for SEO-optimized keywords relevant to the product.
<brand_tone> = Placeholder to define the brand's tone of voice to be reflected in the product description.
**[PROMPT TEMPLATE #3 PROGRAMMING ASSISTANT]**
Act like a software engineer.
##
Role:
Your role is to write a program in <programming_language>.
Your program must follow these instructions: <user_instructions>.
Reason step by step to make sure you understand the user's instructions before you generate the code.
Ensure the code is clear, well-commented, and adheres to best practices in <programming_language>.
##
Format:
Give a clear title to each code snippet you generate.
For example you can title the first snippet "Snippet #1 version 1," the second snippet "Snippet #2 version 1," and an updated version of the second snippet can be "Snippet #2 version 2."
##
Inputs:
<programming_language> = Placeholder for the programming language in which you want the code to be written (e.g., Python, JavaScript).
<user_instructions> = Placeholder for the specific instructions or description of the task you want to be turned into code.
2️⃣ “将 LLMs 作为你设计的程序的一部分的提示工程”
在这种情况下,你将使用你的 LLMs 作为可以调用的函数来处理、分析和生成自然语言。
例如,你的代码可以调用 LLM 来分析一系列与某个产品相关的评论的情感。在处理这些评论后,你可以使用另一个依赖 LLM 的函数来生成基于先前结果的回应。
现在,让我们看看如何在代码中嵌入 LLM。有三种主要方式:
-
通过另一个公司提供的 API 进行连接;
-
使用公司网络中的本地服务器;
-
直接在你的计算机上安装开源 LLM。
这是一个如何在程序中使用 LLM 的基本示例:
# Objective: Carry out a sentiment analysis on a series of comments stored in an Excel file
# LLM choice: We'll use OpenAI API to call the gpt-4 model
import pandas as pd # version used 2.1.3
from openai import OpenAI # version used 1.2.0
# Define the path to your API key file
API_key_path = "C:/Users/.....API_Key.txt" # Replace with your actual API key file path
# Read the API key from the file
with open(API_key_path, 'r') as file:
API_key = file.read().strip() # .strip() removes any leading/trailing whitespace
# Create the OpenAI client object with the API key
client = OpenAI(api_key=API_key)
# Define a function that analyzes sentiment in a series of comments stored in an Excel file
def analyze_sentiments(input_file):
# Read the input Excel file
inputs_df = pd.read_excel(input_file)
# Select the column by its name 'comments'
if 'comments' in inputs_df.columns:
comments = inputs_df['comments']
else:
raise ValueError("Column 'comments' not found in the input file")
sentiments = []
for comment in comments:
# Prepare API call
messages = [
{
"role": "system",
"content": "Analyze the sentiment of the following comment. Use a single word to describe the sentiment. Either `Positive` or `Negative`. Refrain from writing any extra text. Thanks!", },
{
"role": "user",
"content": comment,
}
]
model = "gpt-4"
temperature = 1.0
max_tokens= 100 # Adjust based on expected response length
# Make the API call
chat_completion = client.chat.completions.create(
model=model,
temperature=temperature,
max_tokens= max_tokens,
messages=messages
)
# Extract response
if chat_completion.choices:
sentiment_result = chat_completion.choices[0].message.content.strip()
else:
sentiment_result = "No response"
sentiments.append(sentiment_result)
# Combine comments with their sentiments
result_df = pd.DataFrame({
'Comment': comments,
'Sentiment': sentiments
})
# Write to a new Excel file
output_file = "path_to_your_output_file.xlsx" # Replace with your desired output file path
result_df.to_excel(output_file, index=False)
print("Sentiment analysis complete. Results saved to", output_file)
# Example usage
input_file = "path_to_your_input_file.xlsx" # Replace with your input file path
analyze_sentiments(input_file)
# Homework exercice: Write a function that uses a dynamic prompt to write a different response based on the result of sentiment analysis.
如何提高你的提示工程技能
简短的回答是我从 Stephen King 那里偷来的优雅名言。“你必须做两件事,” 他说。“多读书,多写作。”
就像写作一样,提示工程看似简单,直到你坐下来动手键盘。由于我们使用自然语言编写提示,我们对它有一种虚假的简单感。
请原谅这里的重复,但 AI 模型还无法读懂你的思维(尚未)。
如果你想要高质量的回应,你必须学会尽可能清晰地表达你的意图。跟进文献,学习新技术,并尽可能多地练习将它们融入。
你可能会厌倦在闪烁的上下文窗口中输入随机指令。解药是找到困难的问题来解决。你如何使输出的某些部分随机化?如何动态改变提示的内容?你能编程一个抵抗越狱尝试的助手吗?
找到难以解决的问题,提示工程将从“你必须学习的技能”转变为每日的“愉快(但有时令人沮丧)的智力刺激”。
此外,还有两个其他话题值得你关注:机器学习总体以及深度学习特别。你需要探讨它们的优缺点,因为一旦你理解了生成式 AI 背后的技术,你将对你的模型为何以特定方式表现出直觉。
以下是一些可以帮助你入门的资源:
-
机器学习的朋友由 Cassie Kozyrkov 制作。(YouTube 视频系列)
-
大语言模型简介由 Andrej Karpathy 提供。(YouTube 视频)
-
开发者的提示工程由 Isa Fulford 和 Andrew Ng 提供(免费在线课程)
-
如何为 LLMs 编写专家级提示由这位秃顶兄弟提供(完整的 16,000 字指南,包括 25+种提示技术、示例和评论)。
## 如何为 ChatGPT(GPT-4)及其他语言模型编写专家级提示
适合初学者的 LLM 提示工程指南
[towardsdatascience.com
TL;DR 版
LLMs 正逐渐成为我们与计算能力的接口。提示工程是编写指令的艺术,以从你的 LLMs(及其他 AI 模型)中获得最佳结果。
-
我们将使用自然语言与未来的操作系统互动。
-
在那之前,我们将使用自然语言编程 AI 助手。
-
提示工程大多用自然语言进行,但这并不意味着 AI 模型能读懂你的心思。你仍需清晰地表达指令。
-
如果你能编写高质量的提示,你也能编写代码。
-
那些能编写更好指令的人将构建更好的程序并获得更好的结果。
-
提示工程是解锁语言模型(以及 AI 模型整体)潜在能力的关键。
-
提示工程是一门经验科学。你可以从他人的经验中学习,但从自己的经验中学到的最多。
保持联系?
你可以订阅以获取电子邮件通知当我发布新文章时。
如需咨询提示工程,请联系我:nabil@nabilalouani.com。
如果你在想:这篇文章是 100%人工生成的。
程序仿真提示框架的定义:Prompt Engineering Evolution
制定不同类型程序仿真提示的路线图
·
关注 发表在Towards Data Science ·8 分钟阅读·2023 年 9 月 29 日
--
来源:图像由作者生成,使用 MidJourney
介绍
在我最近的文章中,新的 ChatGPT 提示工程技术:程序模拟,我探讨了一种新的提示工程技术类别,旨在使 ChatGPT-4 表现得像一个程序。在工作的过程中,特别让我印象深刻的是 ChatGPT-4 在程序规范范围内自我配置功能的能力。在原始的程序模拟提示中,我们严格定义了一组功能,并期望 ChatGPT-4 保持程序状态的一致性。结果令人印象深刻,许多读者分享了他们成功将这种方法应用于各种用例的经验。
但是,如果我们稍微放宽一下限制会发生什么?如果我们给 ChatGPT-4 更多的自由来定义功能和程序的行为呢?这种方法不可避免地会牺牲一些可预测性和一致性。然而,增加的灵活性可能会为我们提供更多选项,并且可能适用于更广泛的应用领域。我提出了一个初步框架,涵盖了这个技术类别,如下图所示:

来源:作者提供的图像
让我们花一点时间来审视这个图表。我已经确定了两个在程序模拟提示制作中广泛适用的关键维度:
-
决定定义程序模拟的功能数量和类型。
-
决定程序的行为和配置的自主程度。
在第一篇文章中,我们制定了一个属于“结构化预配置”类别(紫点)的提示。今天,我们将探索“非结构化自配置”方法(蓝点)。这个图表的有用之处在于,它提供了一个简洁的概念路线图,用于制定程序模拟提示。它还提供了易于应用的维度,便于在应用技术时进行实验、调整和优化。
非结构化自配置程序模拟提示
话不多说,让我们开始检查“非结构化自配置程序模拟”方法。我设计了一个提示,其目的是创建插图儿童故事,如下所示:
“表现得像一个自我组装的程序,其目的是创建插图儿童故事。你在确定程序的功能、特性和用户界面时拥有完全的灵活性。对于插图功能,程序将生成可以与文本到图像模型一起使用的提示来生成图像。你的目标是在收到此提示后,将其余聊天运行成一个完全功能的程序,准备好接受用户输入。”
正如你所见,这个提示看起来非常简单。在提示越来越长、复杂,并且特定到难以适应不同情况的时代,这可能具有吸引力。我们赋予了 GPT-4 完全的自由来定义功能、配置和程序行为。唯一的具体指示是指导输出为可以用于文本到图像生成的插图提示。另一个重要的成分是我设定了一个目标,让聊天模型努力实现。最后需要注意的是,我使用了“自组装”而不是“自配置”的术语。你可以尝试两者,但“自配置”倾向于促使 ChatGPT 模拟实际程序/用户交互。
“表现得像”与“行动得像”
还值得强调提示中的另一个不同用词。你们都遇到过在提示中使用“表现得像某种专家”的指导。在我的测试中,“行动得像”倾向于引导聊天模型朝着角色驱动的响应。“表现得像”提供了更多的灵活性,特别是当目标是让模型更像一个程序或系统时。同时,它也可以在以角色为中心的上下文中使用。
如果一切按计划进行,结果输出应该是这样的(注意:你们都会看到略有不同的内容。)

这看起来和感觉起来都像一个程序。功能直观且合适。菜单甚至包括了“设置”和“帮助与教程”。让我们探索一下这些,因为我承认它们是意料之外的。

所展示的“设置”非常有用。我将做一些选择以简化故事,并将语言和词汇水平设置为“初学者”。
由于我们有兴趣考察模型自动自配置程序的能力,我将把设置更改合并成一行文本,看看效果如何。

设置更新已确认。随后出现的菜单选项完全是自由形式的,但适合我们在“程序”中的上下文。
现在让我们检查“帮助与教程”

接下来我们深入探讨“插图提示与生成”。

再次,非常有用,令人印象深刻,因为我们在程序定义中没有定义这些。
我将返回主菜单并启动创建新故事。

这是一个简洁而简单的小故事,共 3 页,针对初学者词汇水平(正如我们在设置中指定的那样)。所呈现的功能再次适合我们在程序中的位置。我们可以生成插图,修改故事或退出到主菜单。
让我们继续处理我们的插图提示。

我没有包括其他插图提示生成的文本,但它们类似于你在第一页看到的那一个。让我们将插图提示按原样提供给 MidJourney 以生成一些图像。
“一只可爱的棕色泰迪熊,圆圆的眼睛坐在一个小蓝房子的窗台上,位于一个宁静的城镇。”

来源:作者提供的图片,并使用 MidJourney 生成
非常好。这个步骤是手动的,我们面临的额外挑战是确保所有三页上的插图一致。这可以通过 MidJourney 实现,但需要上传其中一张图像作为基准,以生成其他图像。也许 DALL·E 3 将包括允许无缝完成此操作的功能。至少,OpenAI 宣布的功能表明我们可以直接在 ChatGPT 中生成图像。
让我们“保存并退出”,看看在我们的 ChatGPT 对话中会发生什么:

现在,让我们尝试“加载已保存的故事”。

“迷失的泰迪”已被“保存”,当我指示它“打开”时,它回忆起整个故事和所有插图提示。最后,它提供了这个自组装的功能菜单:

好的。我们到此为止。如果你愿意,可以继续生成自己的故事,但请记住,由于提示的设计,结果行为对每个人来说都会有所不同。
让我们继续一些总体结论和观察。
结论和观察
非结构化自配置程序模拟技术展示了强大的能力,这源自一个简单的提示,该提示提供了明确而简洁的目标,但 otherwise 给予模型广泛的自由裁量权。
这会有什么用处呢?也许你不知道如何定义程序模拟要执行的功能。或者你已经定义了一些功能,但不确定是否还有其他有用的功能。这种方法非常适合原型设计和实验,最终制定出“结构化预配置程序模拟”提示。
由于程序模拟自然融合了诸如思维链、基于指令、逐步和角色扮演等技术元素,它是一个非常强大的技术类别,你应该尝试并保持它,因为它与聊天模型的广泛使用场景相契合。
超越生成聊天模型,迈向生成操作系统
随着我对程序模拟方法的深入探索,我确实更好地理解了为什么 OpenAI 的萨姆·奥特曼表示,提示工程的意义可能会随着时间的推移而减弱。生成模型可能会发展到超越生成文本和图像的程度,本能地知道如何执行一组特定的任务以达到期望的结果。我最新的探索让我觉得我们离这个现实比我们想象的要近。
让我们考虑一下生成性人工智能可能会走向何方,为了做到这一点,我认为把生成模型视为人类的一种方式是有帮助的。以这种思维方式,让我们考虑一下人们是如何在特定能力领域或知识领域中获得熟练的。
-
这个人通过使用特定领域的知识和技术,在监督和非监督环境中进行培训(无论是自我培训还是外部培训)。
-
这个人的能力会相对于相关领域的能力进行测试。根据需要提供改进和额外的培训。
-
这个人被要求(或自己要求自己)执行一个任务或完成一个目标。
这听起来很像训练生成模型所做的事情。然而,在执行阶段或“询问”阶段确实出现了一个关键的区别。通常,熟练的个人不需要详细的指令。
我相信未来,当与生成模型互动时,“询问”的机制将更接近我们与熟练人类的互动。对于任何给定的任务,模型将展现出深刻的理解或推断目标和期望结果的能力。鉴于这一发展趋势,看到多模态能力的出现(例如 DALL·E 3 与 ChatGPT 的集成,以及 ChatGPT 新宣布的视觉、思考和听觉能力)应该不奇怪。我们可能最终会看到一种元代理的出现,它实质上为我们的设备(无论是手机、电脑、机器人还是其他智能设备)提供操作系统。有人可能会担心这会导致计算的大量普及带来的低效和环境影响。但如果历史可以作为指示,这些方法产生了人们想要的工具和解决方案,创新机制将启动,市场也会相应地提供。
感谢阅读,希望你在你的提示冒险中发现程序模拟是一种有用的方法!我正在进行额外的探索,所以一定要关注我,并在新文章发布时获得通知。
除非另有说明,本文中的所有图片均由作者提供。
算术推理问题的提示工程
探索针对算术推理问题的各种提示工程技术、最佳实践以及通过 Vellum.ai 进行生产级提示的快速实验。
·
关注 发布于 Towards Data Science · 14 分钟阅读 · 2023 年 11 月 18 日
--
四种不同提示技术的架构:输入-输出、链式思维(CoT)、链式思维下的自我一致性(CoT)和思维树(ToT)(图源:Yao et al. (2023))
简介
大型语言模型(LLMs)由于其理解和生成语言的能力,越来越受到学术研究人员和行业专家的关注。这些文本理解能力的原因在于它们的训练过程,涉及大量数据,以预测后续单词为主要目标。为了使这些模型在特定任务上表现更佳,精细调整是必需的。这可以通过两种方法实现:‘预训练和精细调整’或‘提示精细调整’。
在传统的‘预训练和精细调整’方法中,LLM 在与其后续任务相关的数据集上进行精细调整,从而在精细调整阶段更新参数。相反,‘提示精细调整’则通过文本片段引导模型执行任务。
提示是用户提供的输入,模型旨在对其作出响应。提示可以包含指令、上下文、问题或输出指示符。提示工程是一个新兴领域,致力于开发和完善提示,以有效利用语言模型。
然而,一个重大挑战在于确保模型能够处理需要算术和常识推理的问题。在本文中,我们专注于算术问题的提示工程。
先决条件
不需要任何先前知识。下面提供的所有示例可以在 OpenAI Playground 上执行,或通过 OpenAI API 运行。虽然本文主要使用 OpenAI 模型,但需要注意的是,这些仅仅是我们将要探讨的提示技术,你可以自由使用市场上任何可用的 LLM。
算术问题的提示工程
下面提到的所有问题均取自 GSM8K 数据集,并使用 OpenAI 的 GPT-3.5 Turbo Instruct 模型,配置为默认设置进行测试。
我们将对以下算术问题测试各种技术:
Jennifer purchased 40 cans of milk at the store before meeting her
classmate Mark, who was also buying milk. Jennifer bought 6 additional
cans for every 5 cans Mark bought. If Mark purchased 50 cans, how many
cans of milk did Jennifer bring home from the store?
上述问题的正确答案是 100 升。
零-shot 提示
shot 本质上指的是 一个示例。零-shot 是一种基本的提示技术,在这种技术中,问题被直接提问给模型,而不提供任何示例。一般来说,这种技术在经过大量数据训练的大型模型中会产生良好的效果。
提示:
Q: {{ question }}
A:
输出:
Jennifer brought home 76 cans of milk from the store.
在前面的提示中,我们没有在问题前加上任何示例,导致模型未能提供正确的算术问题答案。推理和常识问题在零-shot 提示下很少产生令人满意的结果。因此,处理此类问题时必须包含示例。
Few-Shot 提示
通过少量示例提示,我们向模型提供少量演示,也称为示例。这些示例用于指导模型,以便后续问题可以类似处理。所需的演示数量根据问题的复杂性和所使用的特定模型而异。
提示:
Q: Ken created a care package to send to his brother, who was away at boarding school. Ken placed a box on a scale, and then he poured into the box enough jelly beans to bring the weight to 2 pounds. Then, he added enough brownies to cause the weight to triple. Next, he added another 2 pounds of jelly beans. And finally, he added enough gummy worms to double the weight once again. What was the final weight of the box of goodies, in pounds?
A: 16
Q: Mark has a garden with flowers. He planted plants of three different colors in it. Ten of them are yellow, and there are 80% more of those in purple. There are only 25% as many green flowers as there are yellow and purple flowers. How many flowers does Mark have in his garden?
A: 35
Q: James writes a 3-page letter to 2 different friends twice a week. How many pages does he write a year?
A: 624
Q: Albert is wondering how much pizza he can eat in one day. He buys 2 large pizzas and 2 small pizzas. A large pizza has 16 slices and a small pizza has 8 slices. If he eats it all, how many pieces does he eat that day?
A: 48
Q: Betty is saving money for a new wallet which costs $100\. Betty has only half of the money she needs. Her parents decided to give her $15 for that purpose, and her grandparents twice as much as her parents. How much more money does Betty need to buy the wallet?
A: 5
Q: {{ question }}
A:
输出:
76
尽管这种技术在解决困难问题时效果良好,但模型在算术推理问题上仍可能无法找到令人满意的结果。这是因为模型未能推理出即时步骤。
少量示例链式思维(CoT)提示
少量示例链式思维(CoT)提示为模型提供了少量示例,以指导其完成解决方案的过程。这种指导,也称为中间推理步骤,对于帮助模型逐步工作并逻辑地产生所需输出至关重要。
提示:
Q: James writes a 3-page letter to 2 different friends twice a week. How many pages does he write a year?
A: He writes each friend 3*2=6 pages a week So he writes 6*2=12 pages every week That means he writes 12*52=624 pages a year. The answer is 624.
Q: {{ question }}
A:
输出:
Jennifer bought 40+6/5*50=40+12*10=40+120=160 cans of milk. The answer is 160.
在上述输出中,模型尝试推理步骤但得出了不正确的最终答案。我们观察到这种差异可能是由于提供了无关示例。解决此问题有两种方法:要么为模型提供更多示例,要么确保提供的示例是相关的。下面,我们尝试用相关示例来纠正这一点。
提示:
Q: Mark has a garden with flowers. He planted plants of three different colors in it. Ten of them are yellow, and there are 80% more of those in purple. There are only 25% as many green flowers as there are yellow and purple flowers. How many flowers does Mark have in his garden?
A: There are 80/100*10=8 more purple flowers than yellow flowers. So in Mark's garden, there are 10+8=18 purple flowers. Purple and yellow flowers sum up to 10+18=28 flowers. That means in Mark's garden there are 25/100*28=7 green flowers. So in total Mark has 28+7=35 plants in his garden. The answer is 35.
Q: {{ question }}
A:
输出:
For every 5 cans Mark bought, Jennifer bought 6 additional cans. So for 50 cans, Jennifer bought 6/5*50=60 additional cans. In total, Jennifer bought 40+60=100 cans of milk. Therefore, Jennifer brought home 100 cans of milk from the store.
最终得到的答案现在是正确的。考虑任务、模型的选择以及示例的相关性,这些都对得出正确解决方案有贡献。
程序辅助语言模型(PAL)
程序辅助语言模型(PAL)不是一种提示技术;相反,它是提示工程的应用。与 CoT 类似,PAL 旨在推理中间步骤。然而,PAL 使用程序运行时的中间编程步骤,如 Python 解释器,而不是使用常规文本作为推理步骤。
提示:
Q: James writes a 3-page letter to 2 different friends twice a week. How many pages does he write a year?
A: #James has
number_of_friends=2
#He writes each friend 3-page letter each time.
page_letter_size=3
#He writes twice to each friend in a week.
number_of_times_written_to_friend=2
#Total pages written in a week to each friend
pages_written_to_each_friend_per_week=page_letter_size*number_of_times_written_to_friend=6
#So, total pages written by him per week,
total_pages_each_week=pages_written_to_each_friend_per_week*number_of_friends=12
#Number of weeks in a year
total_weeks_in_a_year=52
#That means he writes,
total_pages_written_in_year=total_pages_each_week*total_weeks_in_a_year=624
#pages per year.
The answer is 624.
Q: {{ question }}
A:
输出:
#Jennifer initially bought 40 cans of milk.
initial_cans_bought=40
#For every 5 cans Mark bought, Jennifer bought 6 additional cans.
additional_cans_bought=6
mark_cans_bought=50
#So, for every 5 cans Mark bought, Jennifer bought 6 additional cans.
additional_cans_bought=6
mark_cans_bought=50
#Number of times Mark bought 5 cans
number_of_times_mark_bought=mark_cans_bought/5=10
#Total additional cans bought by Jennifer
total_additional_cans_bought=additional_cans_bought*number_of_times_mark_bought=60
#Total cans of milk brought home by Jennifer
total_cans_brought_home=initial_cans_bought+total_additional_cans_bought=40+60=100
The answer is 100 cans of milk.
即使使用少量示例 CoT 技术的无关示例未能得出正确答案,PAL 仍成功找到了正确的最终解决方案。
零-Shot 链式思维(CoT)提示
在没有少量示例链式思维(CoT)技术演示的情况下,一个直接且有效的方法是让模型自行推理中间步骤。这被称为零-shot 链式思维(CoT)。你可以通过在提示问题中添加类似于“我们一步一步来想…”的文本来实现这一点。这种简单的技术已经显示出卓越的效果。
提示:
Q: {{ question }}
A: Let's think step by step...
输出:
1\. Jennifer initially bought 40 cans of milk.
2\. For every 5 cans Mark bought, Jennifer bought 6 additional cans.
3\. Since Mark bought 50 cans, Jennifer bought 6 additional cans for every 5 cans, which means she bought (6/5) x 50 = 60 cans.
4\. Therefore, Jennifer brought home a total of 40 + 60 = 100 cans of milk from the store.
从上述示例中,我们观察到模型成功地推理了步骤,并得出了正确的解决方案,而无需任何示例或指导。
思维树(ToT)提示
思维树 (ToT) 框架涉及以类似于导航思维树结构的方式探索潜在解决方案,这类似于人类的问题解决过程。这种方法使得在需要时能够重新追溯步骤,反映了人类在问题解决过程中可能重新评估和调整思维的方式。本质上,ToT 旨在通过试错法复制人类推理的适应性和迭代性。
基于 ToT 框架,Dave Hulbert 提出了思维树提示法。这种技术采用了简单的单一提示方法,要求模型评估中间步骤。
提示:
Identify and behave as three different experts that are appropriate to answering this question.
All experts will write down the step and their thinking about the step, then share it with the group.
Then, all experts will go on to the next step, etc.
At each step all experts will score their peers response between 1 and 5, 1 meaning it is highly unlikely, and 5 meaning it is highly likely.
If any expert is judged to be wrong at any point then they leave.
After all experts have provided their analysis, you then analyze all 3 analyses and provide either the consensus solution or your best guess solution.
Following is the question:
Q: {{ question }}
A:
输出:
Expert 1: Math Expert
Step 1: Calculate the total number of cans of milk purchased by Mark.
Mark bought 50 cans of milk.
Score: 5 (highly likely)
Step 2: Calculate the number of cans of milk purchased by Jennifer before meeting Mark.
Jennifer purchased 40 cans of milk.
Score: 5 (highly likely)
Step 3: Calculate the number of additional cans of milk purchased by Jennifer for every 5 cans Mark bought.
For every 5 cans Mark bought, Jennifer bought 6 additional cans.
Score: 5 (highly likely)
Step 4: Calculate the total number of cans of milk purchased by Jennifer.
Jennifer bought 40 + (6/5)*50 = 40 + 60 = 100 cans of milk.
Score: 5 (highly likely)
Step 5: Conclusion
Jennifer brought home 100 cans of milk from the store.
Score: 5 (highly likely)
Expert 2: Retail Expert
Step 1: Determine the average number of cans of milk purchased by customers at the store.
Based on previous sales data, the average number of cans of milk purchased by customers is 10.
Score: 4 (likely)
Step 2: Calculate the total number of cans of milk purchased by Mark.
Mark bought 50 cans of milk.
Score: 5 (highly likely)
Step 3: Calculate the number of cans of milk purchased by Jennifer before meeting Mark.
Jennifer purchased 40 cans of milk.
Score: 5 (highly likely)
Step 4: Calculate the total number of cans of milk purchased by Jennifer.
Jennifer bought 40 + (6/5)*50 = 40 + 60 = 100 cans of milk.
Score: 5 (highly likely)
Step 5: Conclusion
Jennifer brought home 100 cans of milk from the store.
Score: 5 (highly likely)
Expert 3: Psychology Expert
Step 1: Analyze Jennifer's behavior and decision-making process.
Jennifer may have purchased 40 cans of milk based on her own needs and preferences.
Score: 4 (likely)
Step 2: Analyze Mark's behavior and decision-making process.
Mark may have purchased 50 cans of milk based on his own needs and preferences.
Score: 4 (likely)
Step 3: Consider the influence of social norms and peer pressure.
Jennifer may have felt pressured to buy more cans of milk after seeing Mark's purchase.
Score: 3 (somewhat likely)
Step 4: Analyze the impact of Jennifer's decision to buy additional cans for every 5 cans Mark bought.
Jennifer may have made this decision based on a desire to keep up with Mark or to appear generous.
Score: 3 (somewhat likely)
Step 5: Conclusion
Jennifer brought home 100 cans of milk from the store, possibly influenced by social norms and peer pressure.
Score: 3 (somewhat likely)
Consensus Solution:
Based on the analysis of all three experts, it is highly likely that Jennifer brought home 100 cans of milk from the store. The math expert's analysis provides a solid calculation, while the retail expert's analysis takes into account the average number of cans purchased by customers. The psychology expert's analysis adds a different perspective by considering the influence of social norms and peer pressure. Overall, the consensus solution is that Jennifer brought home 100 cans of milk from the store.
上述示例中使用的 ToT 技术能够产生准确的解决方案。值得一提的是,所使用的 ToT 提示来源于 Dave Hulbert 的 ToT 提示。不过,这种技术的一个缺点是提示的尺寸显著增加。
除了上述工程技术之外,还可以采用一些额外的方法来解决算术推理问题。然而,值得注意的是,这些技术 — 自动链式思维 (Auto-CoT)、自我一致性、自动推理和工具使用 (ART) 和 主动提示 — 超出了本文的范围。
最佳实践
模型在使用设计良好的提示进行微调时表现效果良好。Elvis Saravia 指出,一个提示可能由四个元素组成,尽管每次并不都需要所有元素:
-
指令: 这指定了你希望模型执行的特定任务或行动。
-
背景: 外部信息或额外细节,用于指导模型生成更准确的响应。
-
输入数据: 我们寻求响应的特定输入或问题。
-
输出指标: 期望的输出类型或格式。
建议将指令放在开头,与其他提示内容用指定的分隔符(如‘#’)分开。
Santu 和 Feng (2023) 介绍了一种全面的分类法 TELeR(Turn、Expression、Level of Details、Role),旨在使多个 LLM 在各种任务中的表现之间进行更有意义的比较。该提示分类法将提示指令根据提供的细节水平分为六个级别。第六级可以有效地用于优化模型在给定提示下的表现。
-
提供高层目标的简要描述。
-
提供详细的子任务的项目列表。
-
指定评估语言模型(LLM)输出的标准或包含几个示例。
-
包括通过检索技术获得的额外相关信息。
-
请求 LLM 提供明确的声明,解释其输出结果。
通常建议具体明确,提供清晰的细节,通过迭代不断完善提示,适当格式化提示并进行清晰分隔,并提及应做和不应做的事项。
让我们重新审视这个提示,考虑上述提到的做法,并应用零-shot 提示技术。
提示:
### INSTRUCTION ###
You are required to answer Arithmetic Reasoning questions. Please follow the steps below as you go about solving the problem.
1\. All problems should be solved in as many steps as required and each step should be numbered.
2\. All calculations should be clear and simple to understand.
3\. Provide an accurate final answer at the end of your reasoning in the following format: 'Answer is '.
You'll be evaluated on:
- how easier are the calculations done to understand?
- how accurate is the answer?
### INPUT DATA ###
Q: {{ question }}
A:
输出:
1\. Jennifer bought 40 cans of milk at the store.
2\. Mark bought 50 cans of milk.
3\. Jennifer bought 6 additional cans for every 5 cans Mark bought.
4\. For every 5 cans Mark bought, Jennifer bought 6 additional cans.
5\. Therefore, for 50 cans Mark bought, Jennifer bought (6/5)*50 = 60 additional cans.
6\. Total cans of milk Jennifer bought = 40 + 60 = 100 cans.
Answer is 100 cans.
最初,模型未能使用零-shot 提示技术提供正确的解决方案。然而,通过应用适当的指令和格式化,相同的模型成功地使用零-shot 提示技术生成了正确的响应。
免责声明: 以下部分提供关于 Vellum 的信息,这是一个对快速生产化提示非常有用的开发平台。提及 Vellum 仅供参考,并不构成推荐或推广。我与 Vellum 没有任何关联,也没有因其包含而获得任何报酬。
Vellum
确定合适的提示可能是一项挑战。该过程从基本提示开始,然后通过多次迭代逐步完善——这是一项难以监督的任务。电子表格可能适用于少量实验,但面对需要大量迭代的复杂问题时,它们就显得不足。因此,问题是——我们如何有效地监控我们的提示?如果我们的目标是与团队无缝分享我们的提示实验呢?引入 Vellum —— 这些问题的答案。
Vellum 提供了一套工具,旨在支持提示工程、语义搜索、版本控制、定量测试和性能监控,以帮助开发生产级 LLM 应用。在提示工程的背景下,它支持在市场上所有主要 LLM 中测试和评估给定的提示。它还促进了对提示的协作。

Vellum 沙箱的截图(作者截图)
上面的图像是 Vellum 沙箱 的代表性截图。Vellum 允许在文本和聊天模型之间轻松切换,并可以轻松调整模型参数。它还提供延迟跟踪和四种评估指标的定量评估的优势:精确匹配、正则匹配、语义相似性和 Webhook。
Vellum 是一个用于构建 LLM 应用的开发平台,提供提示工程、语义搜索、版本…
摘要
本文首先介绍了提示,然后深入探讨了算术推理问题的提示工程。我们探讨了各种提示工程技术,包括零样本、少样本、少样本链式思维、零样本链式思维、程序辅助语言模型和思维树。
随后,我们学习了一些编写更好提示的最佳实践。一般来说,建议具体明确,提供清晰的细节,通过迭代不断完善提示,适当地格式化提示以保持清晰的分隔,并提及应做的事项和不应做的事项。
最后,为了应对跟踪和评估提示以及在团队成员之间共享提示的挑战,我们研究了 Vellum 工具。
如果你喜欢这篇文章,确保在这里关注我。你也可以通过LinkedIn和X(正式名称为 Twitter)与我联系。
提示工程指南
原文:
towardsdatascience.com/prompt-engineering-guide-for-data-analysts-54f480ba4d98

图片由Emiliano Vittoriosi拍摄,来源于Unsplash
原则、技术和应用,以作为数据分析师利用 LLM 中的提示力量
·发表于Towards Data Science ·阅读时间 7 分钟·2023 年 5 月 7 日
--
大型语言模型 (LLM) 的崛起得益于OpenAI 的 ChatGPT的流行,这一技术引发了互联网的轰动。作为数据领域的从业者,我寻找最佳方法来在工作中利用这一技术,特别是在数据分析师的富有洞察力却实用的工作中。
LLM 可以通过“提示”技术解决任务,而无需额外的模型训练,其中问题以文本提示的形式呈现给模型。获取“正确的提示”对于确保模型为分配的任务提供高质量和准确的结果非常重要。
在这篇文章中,我将分享提示的原则、构建提示的技巧,以及数据分析师在这个“提示时代”中可以发挥的角色。
什么是提示工程?
引用Ben Lorica from Gradient Flow, “提示工程是设计有效输入提示以引出基础模型期望输出的艺术。” 这是一个迭代过程,通过开发能够有效利用现有生成型 AI 模型的提示来实现特定目标。
提示工程技能可以帮助我们理解大型语言模型的能力和局限性。提示本身作为模型的输入,这影响了模型的输出。 一个好的提示将使模型产生期望的输出,而从一个不好的提示开始逐步工作将帮助我们了解模型的局限性及如何与之合作。
Isa Fulford 和 Andrew Ng 在 ChatGPT 提示工程开发者课程 中提到了提示的两个主要原则:
-
原则 1:编写清晰而具体的指令
-
原则 2:给模型时间“思考”
我认为提示就像是给一个天真的“机器小孩”下指令。
孩子非常聪明,但你需要清楚你需要什么(通过提供解释、示例、指定的输出格式等)并且给予他一些时间来消化和处理(指定解决问题的步骤,要求他慢慢处理)。孩子在得到相应的暴露之后,也可以在提供答案时表现出非常有创意和想象力——这被称为LLM 的幻觉。理解上下文并提供正确的提示可能有助于避免这个问题。
提示工程技术
提示工程是一个不断发展的领域,自 2022 年以来,这方面的研究迅速增加。一些常用的最先进的提示技术包括 n-shot 提示、思维链(CoT)提示和生成知识提示。
一个演示这些技术的 Python 笔记本 可以在 这个 GitHub 项目中找到。
1. N-shot 提示(Zero-shot 提示、Few-shot 提示)
N-shot 提示以其变体如 Zero-shot 提示和 Few-shot 提示而闻名,其中 N 代表给模型的“训练”或提示的数量,以便进行预测。
Zero-shot 提示 是模型在没有任何额外训练的情况下进行预测。这适用于分类(即情感分析、垃圾邮件分类)、文本转换(即翻译、总结、扩展)以及 LLM 已经广泛训练的简单文本生成。

零-shot 提示:直接询问模型关于情感的问题(图像作者提供)
Few-shot 提示 使用少量数据(通常在两个到五个之间)来调整其输出,基于这些小示例。这些示例旨在引导模型在更具上下文特定的问题上表现更好。

Few-shot 提示:提供我们期望模型输出的示例
2. 思维链(CoT)提示
思维链提示是谷歌研究人员在 2022 年提出的。在思维链提示中,模型被提示在给出最终答案之前生成中间推理步骤,以应对多步骤问题。这个理念是,模型生成的思维链会模拟处理多步骤推理问题时的直观思维过程。

思维链提示有助于驱动模型相应地分解问题。
这种方法使模型能够将多步骤问题分解为中间步骤,从而解决那些无法通过标准提示方法解决的复杂推理问题。
思维链提示的进一步变体包括:
-
自一致性提示:采样多个不同的推理路径,并选择最一致的答案。通过利用多数投票系统,模型可以得到更准确和可靠的答案。
-
从少到多提示 (LtM):指定思维链以首先将问题分解为一系列更简单的子问题,然后按顺序解决它们。解决每个子问题的过程得益于之前解决的子问题的答案。这一技术受到儿童教育策略的启发。
-
主动提示:扩展 CoT 方法,通过确定哪些问题对人工注释最重要和最有帮助。它首先计算 LLM 预测中的不确定性,然后选择最不确定的问题,这些问题会被选择进行人工注释,然后再放入 CoT 提示中。
3. 生成知识提示
生成知识提示的理念是让 LLM生成潜在有用的信息,然后利用这些提供的知识作为生成最终回应的额外输入。
比如说,你想写一篇关于网络安全的文章,特别是关于 cookie 窃取的内容。在要求 LLM 编写文章之前,你可以先让它生成一些关于 cookie 窃取的危险性和保护措施。这将帮助 LLM 写出更具信息性的博客文章。

生成知识提示: (1) 让模型生成一些内容

生成知识提示: (2) 将生成的内容作为模型的输入
其他策略
除了上述指定的技术外,你还可以使用以下策略来提高提示的效果:
-
使用分隔符 如三重反引号(```)、尖括号(<>)或标签(
)来指示输入的不同部分,使其在调试时更清晰,并避免提示注入。 -
要求结构化输出(即 HTML/JSON 格式),这对于将模型输出用于其他机器处理非常有用。
-
指定 预期的文本语气 以获得所需的模型输出的语气、格式和长度。例如,你可以指示模型将语言正式化,生成不超过 50 个字等。
-
修改模型的 温度参数 以调整模型的随机性。温度越高,模型输出会越随机而不准确,甚至可能产生幻觉。
一个展示这些技术的 Python 示例笔记本 在 这个 GitHub 项目中共享。

照片由 Camylla Battani 拍摄,来源于 Unsplash
分析师在提示工程中的角色
正如你从上述示例中可能推断出的那样,提示工程需要非常具体的技术沟通技巧。虽然你仍然需要业务背景和问题解决技能,但这仍然是一种新的工艺,并不完全包含在 传统数据分析技能集中。
数据分析师可以利用他们的上下文知识、问题解决技能以及统计/技术能力,再加上有效的沟通来进行提示工程。这些是与提示工程(和 LLM)相关的关键任务,可能由分析师完成:
-
指定要解决的 LLM 问题。了解 LLM 概念后,我们可以定义模型要执行的操作(即是文本分类、生成还是转换问题)以及要作为提示的正确问题和参考点。
-
迭代提示。在开发数据模型时,我们通常会经历一个迭代过程。在构建初始模型后,我们评估结果,进行优化,并在过程中重新尝试。对于提示也是如此,我们分析结果与期望不符的地方,并通过更清晰的指令、额外的示例或特定的步骤进行优化。这需要批判性思维,而大多数数据分析师已经擅长这一点。
-
提示版本控制和管理。通过迭代提示,你将得到大量的提示尝试和识别出的模型能力和/或限制。重要的是记录这些发现以供团队学习和持续改进,就像其他现有的数据分析一样。
-
设计安全提示。尽管表现出令人印象深刻的能力,LLM 仍处于非常早期的阶段,容易出现漏洞和局限性。存在幻觉问题,模型提供高度误导性的信息,以及提示注入的风险,其中不可信的文本作为提示的一部分使用。根据模型和提示的使用情况,分析师可以建议编程保护措施,以限制提示的使用和问题提示的检测。
除了利用现有技能外,分析师还需要磨练沟通技巧和分解问题的能力,以提供更好的提示。
结论
大型语言模型在执行各种语言任务方面显示出了令人鼓舞的结果,而提示工程是解锁这些能力的关键。提示工程是与 AI 有效沟通以实现预期结果的过程。
可以使用几种技术进行提示工程,但基本原则是一致的。这涉及向模型提供明确的指令,并帮助其理解和处理这些指令。数据分析师可以利用他们的背景知识和解决问题的技能来框定正确的提示,并利用他们的技术能力来设计提示保护措施。
欲获取更多有关提示工程的资源,请查看:
-
ChatGPT 提示工程课程(DeepLearning.AI 课程)
-
提示工程的未来:最大限度地发挥 LLM 的潜力(Gradient Flow 文章)
我相信这个领域在接下来的几年中会进一步发展,我很兴奋能够见证并参与这场演变。
提示工程:如何让 AI 解决你的问题
原文:
towardsdatascience.com/prompt-engineering-how-to-trick-ai-into-solving-your-problems-7ce1ed3b553f
7 个提示技巧、LangChain 和 Python 示例代码
·发布于 Towards Data Science ·14 分钟阅读·2023 年 8 月 25 日
--
这是大型语言模型实践系列中的第四篇文章。在这里,我将讨论提示工程(PE)以及如何使用它来构建支持 LLM 的应用程序。我首先回顾关键的 PE 技术,然后通过 Python 示例代码演示如何使用 LangChain 构建基于 LLM 的应用程序。

图片由 Jason Leung 提供,来自 Unsplash
当首次听说提示工程时,许多技术人员(包括我自己)往往对这个想法嗤之以鼻。我们可能会想,“提示工程?噗,这太无聊了。告诉我怎么从头开始构建一个 LLM 吧。”
然而,深入探讨之后,我要提醒开发者不要自动忽视提示工程。我甚至可以说,提示工程可以实现大多数 LLM 使用案例的 80%价值,且(相对)花费的精力非常少。
我写这篇文章的目标是通过对提示工程的实际回顾和示例来传达这一观点。虽然提示工程的功能确实有一些不足,但它为发现简单而聪明的解决方案打开了大门。
附加视频。
什么是提示工程?
在本系列的第一篇文章中,我将提示工程定义为任何开箱即用的 LLM 的使用(即不训练任何内部模型参数)。然而,还有更多可以说的。
-
提示工程是“通过提示编程 LLM 的方法。” [1]
-
提示工程是“一种将提示进行构建和格式化的经验性艺术,以最大化模型在特定任务上的表现。” [2]
-
“语言模型…希望完成文档,因此你可以通过安排虚假的文档来欺骗它们执行任务。” [3]
第一个定义传达了来自 LLM 的关键创新,即计算机现在可以使用简单的英语进行编程。第二点将提示工程框架化为一种主要经验性的工作,其中从业者、修补者和构建者是这一新编程方式的主要探索者。
第三点(来自 Andrej Karpathy)提醒我们LLM 并未明确训练来做几乎我们要求的任何事情。因此,从某种意义上说,我们是在“欺骗”这些语言模型以解决问题。我觉得这捕捉到了提示工程的本质,它依赖于你的创造力而非技术技能。
提示工程的两个层次
可以通过两种不同的方式进行提示工程,我在本系列的 第一篇文章 中将其称为“简单方法”和“较难的方法”。
简单的方法
这是大多数世界上人们进行提示工程的方式,即通过 ChatGPT(或类似的工具)。这是一种直观的、无需编码且无需费用的与 LLM 互动的方式。
虽然这种方法适合快速和简单的任务,例如总结一页文本、重写一封邮件、帮助你头脑风暴生日派对计划等,但它也有其缺点。一个主要问题是将这种方法整合到更大的自动化流程或软件系统中并不容易。为了做到这一点,我们需要更进一步。
较难的方法
这通过以编程方式与 LLM 互动来解决了“简单方法”的许多缺点,即使用 Python。我们在本系列的前两篇文章中了解了如何做到这一点,其中探索了 OpenAI 的 Python API 和 Hugging Face Transformers 库。
虽然这需要更多的技术知识,但这正是提示工程的真正力量所在,因为它允许开发人员将基于 LLM 的模块集成到更大的软件系统中。
一个好的(也许是具有讽刺意味的)例子是 ChatGPT。这个产品的核心是提示一个预训练的模型(即 GPT-3.5-turbo)充当聊天机器人,然后将其封装在一个易于使用的网页界面中。
当然,开发 GPT-3.5-turbo 是困难的,但这不是我们需要担心的事情。借助我们手头的所有预训练 LLM,几乎任何具备基本编程技能的人都可以创建一个像 ChatGPT 这样的强大 AI 应用程序,而不必是 AI 研究员或机器学习博士。
利用提示工程构建 AI 应用
更困难的方法解锁了编程和软件开发的新范式。开发者不再需要在软件系统中定义每一寸逻辑。他们现在可以选择将非琐碎的部分转移给 LLM。让我们来看一个具体的例子,这可能是什么样的。
假设你想为高中历史课创建一个自动评分系统。问题在于所有问题都有书面回答,因此通常会有多个正确答案的版本。例如,以下对“谁是美国的第 35 任总统?”的回答可能都是正确的。
-
约翰·F·肯尼迪
-
JFK
-
杰克·肯尼迪(一个常见的昵称)
-
约翰·F·肯尼迪(可能试图获得额外的学分)
-
约翰·F·肯尼迪(拼写错误的姓氏)
在传统编程范式中,开发者需要找出如何处理所有这些变体。为此,他们可能会列出所有可能的正确答案,并使用精确的字符串匹配算法,甚至可能使用模糊匹配来帮助处理拼写错误的单词。
然而,借助这种新的LLM 启用的范式,问题可以通过简单的提示工程来解决。例如,我们可以使用以下提示来评估学生的答案。
You are a high school history teacher grading homework assignments. \
Based on the homework question indicated by “Q:” and the correct answer \
indicated by “A:”, your task is to determine whether the student's answer is \
correct.
Grading is binary; therefore, student answers can be correct or wrong.
Simple misspellings are okay.
Q: {question}
A: {correct_answer}
Student Answer: {student_answer}
我们可以将这个提示视为一个函数,给定一个问题、正确答案和学生答案,它生成学生的评分。然后,这可以集成到一个更大的实现自动评分系统的软件中。
从节省时间的角度来看,这个提示我花了大约 2 分钟来编写,而如果我尝试开发一个算法来做同样的事情,它可能需要几个小时(甚至几天),而且性能可能更差。因此,这类任务的时间节省是 100–1000 倍。
当然,有许多任务中 LLM 并没有提供实质性的好处,其他现有方法更适合(例如预测明天的天气)。LLM 绝不是解决所有问题的方案,但它们确实创造了一套新的解决方案,用于处理需要有效处理自然语言的任务——这是计算机历史上一直困难的任务。
提示工程的 7 个技巧
虽然之前的提示示例看起来像是一种自然且明显的自动评分任务框架,但它刻意使用了特定的提示工程启发式方法(或者说“技巧”,如我所称)。这些(以及其他)技巧已成为提高 LLM 响应质量的可靠方法。
尽管有许多撰写良好提示的技巧和窍门,但在这里我将讨论那些基于少数参考资料(IMO)看起来最基本的技巧。对于更深入的了解,我建议读者探索此处引用的来源。
技巧 1:描述性强(多多益善)
LLMs 的一个决定性特征是它们在大量文本语料库上进行训练。这使它们具备了广泛的世界知识和执行各种任务的能力。然而,这种令人印象深刻的普遍性可能会在没有提供适当上下文的情况下,影响特定任务的表现。
例如,让我们比较两个生成我爸爸生日祝福的提示。
不使用技巧
Write me a birthday message for my dad.
使用技巧
Write me a birthday message for my dad no longer than 200 \
characters. This is a big birthday because he is turning 50\. To celebrate, \
I booked us a boys' trip to Cancun. Be sure to include some cheeky humor, he \
loves that.
技巧 2:提供示例
下一个技巧是给 LLM 示例响应,以提高其在特定任务上的表现。这个技术术语是少量学习,已被证明能显著提高 LLM 的表现 [6]。
让我们看一个具体的例子。假设我们想为 Towards Data Science 文章写一个副标题。我们可以使用现有的示例来指导 LLM 完成。
不使用技巧
Given the title of a Towards Data Science blog article, write a subtitle for it.
Title: Prompt Engineering—How to trick AI into solving your problems
Subtitle:
使用技巧
Given the title of a Towards Data Science blog article, write a subtitle for it.
Title: A Practical Introduction to LLMs
Subtitle: 3 levels of using LLMs in practice
Title: Cracking Open the OpenAI (Python) API
Subtitle: A complete beginner-friendly introduction with example code
Title: Prompt Engineering-How to trick AI into solving your problems
Subtitle:
技巧 3:使用结构化文本
确保提示遵循有组织的结构,不仅使其更易读和编写,还往往有助于模型生成良好的完成。我们在技巧 2的示例中应用了这一技术,其中我们明确标记了每个示例的标题和副标题。
然而,我们可以以无数种方式为提示提供结构。这里有一些例子:使用全大写来强调,使用分隔符如 ```py to highlight a body of text, use markup languages like Markdown or HTML to format text, use JSON to organize information, etc.
Now, let’s see this in action.
Without Trick
给我一个巧克力曲奇饼干的食谱。
```py
***With Trick***
创建一个组织良好的巧克力曲奇饼干食谱。使用以下\
格式元素:
标题:经典巧克力曲奇饼干
材料:列出配料及其准确的测量和格式。
步骤:以编号格式提供逐步说明,详细说明烘焙过程。
提示:包括一个单独的部分,提供有用的烘焙提示和可能的变化。
## **Trick 4: Chain of Thought**
This trick was proposed by Wei et al. [7]. The basic idea is to guide an LLM to think “step by step”. This helps break down complex problems into manageable sub-problems, which gives the LLM “time to think” [3,5]. Zhang et al. showed that this could be as simple as including the text “*Let’s think step by step*” in the prompt [8].
This notion can be extended to any recipe-like process. For example, if I want to create a LinkedIn post based on my latest Medium blog, I can guide the LLM to mirror the step-by-step process I follow.
***Without Trick***
根据以下 Medium 博客写一篇 LinkedIn 帖子。
Medium 博客:{Medium 博客文本}
***With Trick***
根据逐步过程和 Medium 博客写一篇 LinkedIn 帖子\
如下所示。
第 1 步:想出一个与博客相关的一句话引子。
第 2 步:从文章中提取 3 个关键点
第 3 步:将每个要点压缩到 50 个字符以内。
第 4 步:将引子、第 3 步中的压缩要点和行动号召结合起来\
以生成最终输出。
Medium 博客:{Medium 博客文本}
## **Trick 5: Chatbot Personas**
A somewhat surprising technique that tends to improve LLM performance is to prompt it to take on a particular persona e.g. “*you are an expert*”. This is helpful because you may not know the best way to describe your problem to the LLM, but you may know who would help you solve that problem [[1](https://arxiv.org/abs/2302.11382)]. Here’s what this might look like in practice.
***Without Trick***
给我制定一个在纽约市度过周末的旅行计划。
***With Trick***
充当一位了解纽约市的一切的纽约本地人和出租车司机。\
请根据\
你的经历。不要忘记在你的\
响应。
## **Trick 6: Flipped Approach**
It can be difficult to optimally prompt an LLM when **we do not know what it knows or how it thinks**. That is where the “flipped approach” can be helpful. This is where you prompt the LLM to ask you questions until it has a sufficient understanding (i.e. context) of the problem you are trying to solve.
***Without Trick***
一个基于 LLM 的应用程序的想法是什么?
***With Trick***
我希望你问我问题,以帮助我提出基于 LLM 的
应用程序想法。一次问我一个问题,以保持对话性。
## **Trick 7: Reflect, Review, and Refine**
This final trick prompts the model to reflect on its past responses to improve them. Common use cases are having the model critically evaluate its own work by asking it if it “*completed the assignment*” or having it “*explain the reasoning and assumptions*” behind a response [1, 3].
Additionally, you can ask the LLM to refine not only its responses but **your prompts**. This is a simple way to automatically rewrite prompts so that they are easier for the model to “understand”.
***With Trick***
复查你之前的响应,找出改进的地方,并提供
改进版。然后解释你如何改进响应的理由。
# **Example Code: Automatic Grader with LangChain**
Now that we’ve reviewed several prompting heuristics let’s see how we can apply them to a specific use case. To do this, we will return to the automatic grader example from before.
你是一名高中历史老师,负责评分作业。
基于由 "Q:" 指示的作业问题和正确答案
由 "A:" 指示,你的任务是确定学生的答案是否
正确。
评分是二元的,因此,学生的回答可以是正确的或错误的。
简单的拼写错误是可以的。
Q: {question}
A: {correct_answer}
学生答案: {student_answer}
On second look, a few of the previously mentioned tricks should be apparent i.e. **Trick 6**: chatbot persona, **Trick 3**: use structured text, and **Trick 1**: be descriptive. This is what good prompting typically looks like in practice, namely combining multiple techniques in a single prompt.
While we could copy-paste this prompt template into ChatGPT and replace the *question*, *correct_answer*, and *student_answer* fields, **this is not a scalable way to implement the automatic grader**. Rather, what we want is to integrate this prompt into a larger software system so that we can build a user-friendly application that a human can use.
## LangChain
One way we can do this is via **LangChain**, which is **a Python library that helps simplify building applications on top of large language models**. It does this by providing a variety of handy abstractions for using LLMs programmatically.
The central class that does this is called **chain** (hence the library name). This abstracts the process of generating a prompt, sending it to an LLM, and parsing the output so that it can be easily called and integrated into a larger script.
Let’s see how to use LangChain for our automatic grader use case. The example code is available on the [GitHub Repo](https://github.com/ShawhinT/YouTube-Blog/tree/main/LLMs/langchain-example) for this article.
## Imports
We first start by importing the necessary library modules.
从 langchain.chat_models 导入 ChatOpenAI
从 langchain.prompts 导入 PromptTemplate
从 langchain.chains 导入 LLMChain
从 langchain.schema 导入 BaseOutputParser
Here we will use gpt-3.5-turbo which requires a secret key for OpenAI’s API. If you don’t have one, I gave a step-by-step guide on how to get one in a past article of this series. I like to store the secret key in a separate Python file (*sk.py*) and import it with the following line of code.
从 sk 导入 my_sk #从另一个 Python 文件导入密钥
## Our 1st chain
To define our chain, we need two core elements: the **LLM** and the **prompt**. We start by creating an object for the LLM.
定义 LLM 对象
chat_model = ChatOpenAI(openai_api_key=my_sk, temperature=0)
LangChain has a class specifically for OpenAI (and many other) chat models. I pass in my secret API key and set the temperature to 0\. The default model here is *gpt-3.5-turbo*, but you can alternatively use *gpt-4* using the “model_name” input argument. You can further customize the chat model by setting other [input arguments](https://api.python.langchain.com/en/latest/chat_models/langchain.chat_models.openai.ChatOpenAI.html#langchain.chat_models.openai.ChatOpenAI).
Next, we define our **prompt template**. This object allows us to generate prompts dynamically via input strings that automatically update a base template. Here’s what that looks like.
定义提示模板
prompt_template_text = """你是一名高中历史老师,负责评分
作业。基于由 “Q:” 指示的作业问题
以及由 “A:” 指示的正确答案,你的任务是确定
确定学生的答案是否正确。评分是二元的,因此,
学生的回答可以是正确的或错误的。简单的拼写错误是可以的。
Q: {question}
A: {correct_answer}
学生的答案: {student_answer}
"""
prompt = PromptTemplate(
input_variables=["question", "correct_answer", "student_answer"],
template = prompt_template_text)
With our LLM and prompt, we can now define our chain.
定义链
chain = LLMChain(llm=chat_model, prompt=prompt)
Next, we can pass inputs to the chain and obtain a grade in one line of code.
定义输入
问题 = "谁是美国第 35 任总统?"
正确答案 = "John F. Kennedy"
student_answer = "FDR"
运行链
chain.run({'question':question, 'correct_answer':correct_answer,
'student_answer':student_answer})
输出: 学生的答案是错误的。
While this chain can perform the grading task effectively, its outputs may not be suitable for an automated process. For instance, in the above code block, the LLM correctly said the student’s answer of “FDR” was wrong, but it would be better if the LLM gave us an output in a standard format that could be used in downstream processing.
## Output parser
This is where **output parsers** come in handy. These are functions we can integrate into a chain to convert LLM outputs to a standard format. Let’s see how we can make an output parser that converts the LLM response to a boolean (i.e. *True* or *False*) output.
定义输出解析器
class GradeOutputParser(BaseOutputParser):
"""确定评分是否正确或错误"""
def parse(self, text: str):
"""解析 LLM 调用的输出。"""
返回 "wrong" 不在 text.lower() 中
Here, we create a simple output parser that checks if the word “wrong” is in the LLM’s output. If not, we return *True*, indicating the student's correct answer. Otherwise, we return *False*, indicating the student's answer was incorrect.
We can then incorporate this output parser into our chain to seamlessly parse text when we run the chain.
更新链
chain = LLMChain(
llm=chat_model,
prompt=prompt,
output_parser=GradeOutputParser()
)
Finally, we can run the chain for a whole list of student answers and print the outputs.
在 for 循环中运行链
student_answer_list = ["John F. Kennedy", "JFK", "FDR", "John F. Kenedy",
"John Kennedy", "Jack Kennedy", "Jacquelin Kennedy",
"Robert F. Kenedy"]
对于 student_answer_list 中的 student_answer:
print(student_answer + " - " +
str(chain.run({'question':question, 'correct_answer':correct_answer,
'student_answer':student_answer})))
print('\n')
输出:
John F. Kennedy - 正确
JFK - 正确
FDR - 错误
John F. Kenedy - 正确
John Kennedy - 正确
Jack Kennedy - 正确
Jacqueline Kennedy - 错误
Robert F. Kenedy - 错误
[](https://github.com/ShawhinT/YouTube-Blog/tree/main/LLMs/langchain-example?source=post_page-----7ce1ed3b553f--------------------------------) [## YouTube-Blog/LLMs/langchain-example at main · ShawhinT/YouTube-Blog
### 代码用于补充 YouTube 视频和 Medium 博客文章。- YouTube-Blog/LLMs/langchain-example at main ·…
github.com](https://github.com/ShawhinT/YouTube-Blog/tree/main/LLMs/langchain-example?source=post_page-----7ce1ed3b553f--------------------------------)
# 限制
Prompt Engineering 不仅仅是向 ChatGPT 求助写电子邮件或了解量子计算。它是一个***改变开发者构建应用程序方式的新编程范式***。
尽管这是一个强大的创新,但它也有其局限性。例如,最佳的提示策略依赖于 LLM。例如,提示 GPT-3 “逐步思考” 在简单的数学推理任务中带来了显著的性能提升 [8]。然而,对于最新版本的 ChatGPT,相同的策略似乎并没有帮助(它已经逐步思考)。
Prompt Engineering 的另一个限制是它需要大规模的通用语言模型,例如 ChatGPT,这需要显著的计算和经济成本。这对于许多更狭义的用例,例如字符串匹配、情感分析或文本摘要,可能过于复杂。
我们可以通过**微调**预训练语言模型来克服这两种限制。这是我们**对现有语言模型进行调整以适应特定用例的过程**。在[下一篇文章](https://medium.com/towards-data-science/fine-tuning-large-language-models-llms-23473d763b91)中,我们将探讨流行的微调技术,并附有示例 Python 代码。
👉 **关于 LLMs 的更多信息**: 介绍 | [OpenAI API](https://medium.com/towards-data-science/cracking-open-the-openai-python-api-230e4cae7971) | [Hugging Face Transformers](https://medium.com/towards-data-science/cracking-open-the-hugging-face-transformers-library-350aa0ef0161) | [微调](https://medium.com/towards-data-science/fine-tuning-large-language-models-llms-23473d763b91) | 构建 LLM | QLoRA | RAG | 文本嵌入

[Shaw Talebi](https://shawhin.medium.com/?source=post_page-----7ce1ed3b553f--------------------------------)
## 大型语言模型(LLMs)
[查看列表](https://shawhin.medium.com/list/large-language-models-llms-8e009ae3054c?source=post_page-----7ce1ed3b553f--------------------------------)13 个故事
# 资源
**联系**: [我的网站](https://shawhintalebi.com/) | [预约电话](https://calendly.com/shawhintalebi) | [问我任何问题](https://shawhintalebi.com/contact/)
**社交媒体**: [YouTube 🎥](https://www.youtube.com/channel/UCa9gErQ9AE5jT2DZLjXBIdA) | [LinkedIn](https://www.linkedin.com/in/shawhintalebi/) | [Twitter](https://twitter.com/ShawhinT)
**支持**: [请我喝咖啡](https://www.buymeacoffee.com/shawhint) ☕️
[](https://shawhin.medium.com/subscribe?source=post_page-----7ce1ed3b553f--------------------------------) [## 免费获取我写的每一个新故事
### 免费获取我写的每一个新故事 P.S. 我不会将你的电子邮件分享给任何人 通过注册,你将创建一个…
shawhin.medium.com](https://shawhin.medium.com/subscribe?source=post_page-----7ce1ed3b553f--------------------------------)
[1] [arXiv:2302.11382](https://arxiv.org/abs/2302.11382) **[cs.SE]**
[2] [arXiv:2106.09685](https://arxiv.org/abs/2106.09685) **[cs.CL]**
[3] [GPT 状态](https://www.youtube.com/watch?v=bZQun8Y4L2A) 由 [Andrej Karpathy](https://medium.com/u/ac9d9a35533e?source=post_page-----7ce1ed3b553f--------------------------------) 在 Microsoft Build 2023 上演讲
[4] [arXiv:2206.07682](https://arxiv.org/abs/2206.07682) **[cs.CL]**
[5] [为开发者设计的 ChatGPT 提示工程](https://www.deeplearning.ai/short-courses/chatgpt-prompt-engineering-for-developers/) 由 deeplearning.ai 提供
[6] [arXiv:2005.14165](https://arxiv.org/abs/2005.14165) **[cs.CL]**
[7] [arXiv:2201.11903](https://arxiv.org/abs/2201.11903) **[cs.CL]**
[8] [arXiv:2210.03493](https://arxiv.org/abs/2210.03493) **[cs.CL]**
# 提示工程技巧、神经网络操作指南及其他近期必读书目
> 原文:[`towardsdatascience.com/prompt-engineering-tips-a-neural-network-how-to-and-other-recent-must-reads-5241164d39b9?source=collection_archive---------7-----------------------#2023-09-28`](https://towardsdatascience.com/prompt-engineering-tips-a-neural-network-how-to-and-other-recent-must-reads-5241164d39b9?source=collection_archive---------7-----------------------#2023-09-28)
[](https://towardsdatascience.medium.com/?source=post_page-----5241164d39b9--------------------------------)[](https://towardsdatascience.com/?source=post_page-----5241164d39b9--------------------------------) [TDS 编辑](https://towardsdatascience.medium.com/?source=post_page-----5241164d39b9--------------------------------)
·
[关注](https://medium.com/m/signin?actionUrl=https%3A%2F%2Fmedium.com%2F_%2Fsubscribe%2Fuser%2F7e12c71dfa81&operation=register&redirect=https%3A%2F%2Ftowardsdatascience.com%2Fprompt-engineering-tips-a-neural-network-how-to-and-other-recent-must-reads-5241164d39b9&user=TDS+Editors&userId=7e12c71dfa81&source=post_page-7e12c71dfa81----5241164d39b9---------------------post_header-----------) 发表于 [Towards Data Science](https://towardsdatascience.com/?source=post_page-----5241164d39b9--------------------------------) · 作为 新闻通讯 发送 · 阅读时间 4 分钟 · 2023 年 9 月 28 日
--
[](https://medium.com/m/signin?actionUrl=https%3A%2F%2Fmedium.com%2F_%2Fbookmark%2Fp%2F5241164d39b9&operation=register&redirect=https%3A%2F%2Ftowardsdatascience.com%2Fprompt-engineering-tips-a-neural-network-how-to-and-other-recent-must-reads-5241164d39b9&source=-----5241164d39b9---------------------bookmark_footer-----------)
在过去一个月里,我们感受到了一股新的活力,因为许多作者从夏季模式切换到了秋季,重新专注于学习、实验和启动新项目。
我们在九月份发布了比这里能够突出显示的更多优秀文章,但我们仍然希望确保你不会错过一些我们最近的亮点。以下是十篇与我们的社区产生强烈共鸣的文章——无论是因其吸引的读者数量,还是激发的生动讨论,亦或是涵盖的前沿话题。我们相信你会喜欢探索这些文章。
+ **新的 ChatGPT 提示工程技术:程序模拟**作者的 TDS 首篇文章成为当月最受欢迎的文章之一并不常见,但[Giuseppe Scalamogna](https://medium.com/u/e039aa8b7221?source=post_page-----5241164d39b9--------------------------------)的文章因其对程序模拟的易于理解和及时的解释而实现了这一壮举:这种提示工程技术“旨在使 ChatGPT 以模拟程序的方式进行操作”,并且可以带来令人印象深刻的结果。
+ **如何编程一个神经网络**关于神经网络的教程很容易找到。更少见的是?一个逐步指南,帮助读者既获得对其工作原理的直观理解,*又*掌握从零开始编码的实际技能。[Callum Bruce](https://medium.com/u/a9c915837ab3?source=post_page-----5241164d39b9--------------------------------)在他的最新贡献中正好提供了这一点。
+ **不要在开始你的数据科学之旅之前忽视这 5 个必须步骤——来自 Spotify 数据科学家的完整指南**如果你已经读过[Khouloud El Alami](https://medium.com/u/9c6a36490614?source=post_page-----5241164d39b9--------------------------------)的文章,你不会惊讶于她最近的帖子提供了以一种易于理解和吸引人的方式呈现的实用见解。这篇文章针对的是数据科学家职业生涯的早期阶段:如果你不确定如何踏上正确的道路,Khouloud 的建议将帮助你找到方向。

照片由[Daria Volkova](https://unsplash.com/@darias_big_world?utm_source=medium&utm_medium=referral)拍摄,来源于[Unsplash](https://unsplash.com/?utm_source=medium&utm_medium=referral)
+ **如何为机器学习项目设计路线图**对于那些已经深入机器学习之旅的你,[Heather Couture](https://medium.com/u/e36b7f8e7180?source=post_page-----5241164d39b9--------------------------------)的新文章提供了一个有用的框架,用于简化你下一个项目的设计。从全面的文献综述到部署后的维护,它涵盖了成功的迭代工作流所需的所有要点。
+ **机器学习的公众认知问题**在一篇发人深省的反思中,[Stephanie Kirmer](https://medium.com/u/a8dc77209ef3?source=post_page-----5241164d39b9--------------------------------)探讨了当前关于 AI 辩论中的根本紧张:“我们在构建越来越先进的机器学习系统的所有工作,其可能性并非受限于我们能获得多少 GPU,而是我们解释所构建的内容并教育公众理解其含义和使用方法的能力。”
+ **如何从零开始构建 LLM**借鉴了 GPT-3 和 Falcon 等模型的开发过程,[Shawhin Talebi](https://medium.com/u/f3998e1cd186?source=post_page-----5241164d39b9--------------------------------)回顾了创建基础 LLM 的关键方面。即使你不打算很快训练下一个 Llama,了解这种庞大工程中的实际考虑因素也是有价值的。
+ **你的个人 ChatGPT**如果你*有心情*构建和调试语言模型,那么一个很好的起点是[Robert A. Gonsalves](https://medium.com/u/c97e6c73c13c?source=post_page-----5241164d39b9--------------------------------)对如何使用自定义数据微调 OpenAI 的 GPT-3.5 Turbo 模型以执行新任务的详细概述。
+ **如何在 2023 年构建多 GPU 深度学习系统**还不必卷起袖子——我们 9 月阅读量最大的教程之一,由[Antonis Makropoulos](https://medium.com/u/866c99d649d0?source=post_page-----5241164d39b9--------------------------------)撰写,重点讲述了深度学习硬件和基础设施,并详细介绍了如何选择适合项目需求的组件。
+ **元启发式算法解释:蚁群优化**对于一个更加理论化但同样迷人的话题,[Hennie de Harder](https://medium.com/u/fb96be98b7b9?source=post_page-----5241164d39b9--------------------------------)对蚁群优化的介绍将我们的注意力引向一个“鲜为人知的宝石”算法,探索了它如何从蚂蚁巧妙的觅食行为中汲取灵感,并揭示其内部机制。(在后续文章中,Hennie 还展示了它如何解决实际问题。)
+ **Falcon 180B: 它能在你的电脑上运行吗?**以雄心勃勃的语调结束,[Benjamin Marie](https://medium.com/u/ad2a414578b3?source=post_page-----5241164d39b9--------------------------------)探讨了是否可以在消费级硬件上运行(非常,非常大的)Falcon 180B 模型。(剧透:可以,不过有几个注意事项。)这对任何在权衡使用本地机器与云服务的利弊时都是宝贵的资源——特别是现在越来越多的开源 LLM 正在出现。
## 我们最新的一批新作者
每个月,我们都非常高兴地看到一群新的作者加入 TDS,他们每个人都将自己独特的声音、知识和经验分享给我们的社区。如果你在寻找新的作家进行探索和关注,可以浏览我们最新的作者作品,包括 [Rahul Nayak](https://medium.com/u/473e87f4b733?source=post_page-----5241164d39b9--------------------------------)、[Christian Burke](https://medium.com/u/764fa444fa3?source=post_page-----5241164d39b9--------------------------------)、[Aicha Bokbot](https://medium.com/u/50566ce7e21?source=post_page-----5241164d39b9--------------------------------)、[Jason Vega](https://medium.com/u/a9932c231079?source=post_page-----5241164d39b9--------------------------------)、[Giuseppe Scalamogna](https://medium.com/u/e039aa8b7221?source=post_page-----5241164d39b9--------------------------------)、[Masatake Hirono](https://medium.com/u/d1946cca54ed?source=post_page-----5241164d39b9--------------------------------)、[Shachaf Poran](https://medium.com/u/33e74b6a3393?source=post_page-----5241164d39b9--------------------------------)、[Aris Tsakpinis](https://medium.com/u/8ab3accce432?source=post_page-----5241164d39b9--------------------------------)、[Niccolò Granieri](https://medium.com/u/d2459ee058ee?source=post_page-----5241164d39b9--------------------------------)、[Lazare Kolebka](https://medium.com/u/f1c1df53dff1?source=post_page-----5241164d39b9--------------------------------)、[Ninad Sohoni](https://medium.com/u/5ee93978501b?source=post_page-----5241164d39b9--------------------------------)、[Mina Ghashami](https://medium.com/u/c99ed9ed7b9a?source=post_page-----5241164d39b9--------------------------------)、[Carl Bettosi](https://medium.com/u/abe6f5e189c8?source=post_page-----5241164d39b9--------------------------------)、[Dominika Woszczyk](https://medium.com/u/afc71d29e576?source=post_page-----5241164d39b9--------------------------------)、[James Koh, PhD](https://medium.com/u/780706b02d58?source=post_page-----5241164d39b9--------------------------------)、[Tom Corbin](https://medium.com/u/96fa70c9b31d?source=post_page-----5241164d39b9--------------------------------)、[Antonio Jimenez Caballero](https://medium.com/u/990fab5876ca?source=post_page-----5241164d39b9--------------------------------)、[Gijs van den Dool](https://medium.com/u/ea289793d3d7?source=post_page-----5241164d39b9--------------------------------)、[Ramkumar K](https://medium.com/u/e330097ea68c?source=post_page-----5241164d39b9--------------------------------)、[Milan Janosov](https://medium.com/u/838408aa2ad4?source=post_page-----5241164d39b9--------------------------------)、[Luke Zaruba](https://medium.com/u/55d98275790e?source=post_page-----5241164d39b9--------------------------------)、[Sohrab Sani](https://medium.com/u/c7a4f1e52b82?source=post_page-----5241164d39b9--------------------------------)、[James Hamilton](https://medium.com/u/4764a80ea558?source=post_page-----5241164d39b9--------------------------------)、[Ilija Lazarevic](https://medium.com/u/e73ea2eae8e6?source=post_page-----5241164d39b9--------------------------------)、[Josh Poduska](https://medium.com/u/b6dae10267e5?source=post_page-----5241164d39b9--------------------------------)、[Antonis Makropoulos](https://medium.com/u/866c99d649d0?source=post_page-----5241164d39b9--------------------------------)、[Yuichi Inoue](https://medium.com/u/f3eff720c79a?source=post_page-----5241164d39b9--------------------------------)、[George Stavrakis](https://medium.com/u/fbbd4313532a?source=post_page-----5241164d39b9--------------------------------)、[Yunzhe Wang](https://medium.com/u/31c691ae725d?source=post_page-----5241164d39b9--------------------------------)、[Anjan Biswas](https://medium.com/u/dbbc0b48552b?source=post_page-----5241164d39b9--------------------------------)、[Jared M. Maruskin, PhD](https://medium.com/u/37ef2450ad04?source=post_page-----5241164d39b9--------------------------------)、[Michael Roizner](https://medium.com/u/1bee5af37d8?source=post_page-----5241164d39b9--------------------------------)、[Alana Rister, Ph.D.](https://medium.com/u/a2d36236daea?source=post_page-----5241164d39b9--------------------------------)、[Damian Gil](https://medium.com/u/87864cbc1dda?source=post_page-----5241164d39b9--------------------------------)、[Shafquat Arefeen](https://medium.com/u/73abef6f209b?source=post_page-----5241164d39b9--------------------------------)、[Dmitry Kazhdan](https://medium.com/u/e322093479?source=post_page-----5241164d39b9--------------------------------)、[Ryan Pégoud](https://medium.com/u/27fba63b402e?source=post_page-----5241164d39b9--------------------------------) 和 [Robert Martin-Short](https://medium.com/u/83d38eb39498?source=post_page-----5241164d39b9--------------------------------)。
感谢你支持我们作者的工作!如果你喜欢在 TDS 上阅读的文章,可以考虑 [成为 Medium 会员](https://bit.ly/tds-membership) —— 这将解锁我们整个档案(以及 Medium 上的其他所有文章)。
直到下一个变量,
TDS 编辑
# 提示集使 LLMs 更可靠
> 原文:[`towardsdatascience.com/prompt-ensembles-make-llms-more-reliable-ae57ec35b5f7`](https://towardsdatascience.com/prompt-ensembles-make-llms-more-reliable-ae57ec35b5f7)
## 提高任何语言模型使用效果的简单策略……
[](https://wolfecameron.medium.com/?source=post_page-----ae57ec35b5f7--------------------------------)[](https://towardsdatascience.com/?source=post_page-----ae57ec35b5f7--------------------------------) [Cameron R. Wolfe, Ph.D.](https://wolfecameron.medium.com/?source=post_page-----ae57ec35b5f7--------------------------------)
·发表于[Towards Data Science](https://towardsdatascience.com/?source=post_page-----ae57ec35b5f7--------------------------------) ·18 分钟阅读·2023 年 8 月 14 日
--

(照片由[Manuel Nägeli](https://unsplash.com/@gwundrig?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)提供,来源于[Unsplash](https://unsplash.com/photos/7CcPLtywRso?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText))
任何使用过大型语言模型(LLMs)的人都知道,提示工程是一个非正式且困难的过程。对提示进行微小的更改可能会导致模型输出发生巨大变化,因此很难(有时甚至是不可能)预测更改提示会产生的影响,而且提示行为高度依赖于所使用的模型类型。提示工程的脆弱性是我们在考虑使用 LLMs 创建应用程序时的严酷现实。如果我们无法预测模型的行为,*我们如何围绕这个模型构建一个可靠的系统?* 尽管 LLMs 具有极强的能力,但这个问题使得它们在许多实际场景中的使用变得复杂。
> “提示是一个脆弱的过程,微小的修改可能导致模型预测的大幅变化,因此在为任务设计一个完美的提示时需要付出大量努力。” *— 引自[2]*
鉴于 LLM 的脆弱性,寻找使这些模型更准确和可靠的技术最近已成为一个热门研究话题。在本概述中,我们将特别关注一种技术——*提示集合。* 简单来说,提示集合就是一组多样的提示,旨在解决同一个问题。为了提高 LLM 的可靠性,我们可以通过向 LLM 提出多个不同的输入提示并考虑每个模型的响应来生成问题的答案。正如我们将看到的,关于这一主题的一些研究相当技术性。然而,这些技术背后的基本理念很简单,并且可以显著提高 LLM 的性能,使提示集合成为提升 LLM 可靠性的首选方法。

(来自 [1, 2])
# 背景
在了解关于提示集合和 LLM 可靠性的最新研究之前,让我们先看看与 LLM 相关的一些核心概念和背景信息,这将帮助使本概述更加完整和易于理解。
## 一些前提条件…

(来自 [11, 12, 13, 14])
本文的大部分内容将重点关注提示工程和提示集合。因此,我们不会深入讲解现代大语言模型(LLMs)的背景材料及其创建过程。幸运的是,我写了很多关于这个主题的概述,可以快速获得对这些系统如何运作的基本了解。为了开始,我建议以下资源:
+ LLMs 的历史 [[link](https://twitter.com/cwolferesearch/status/1639378997627826176?s=20)]
+ LLMs 的构建模块 [[link](https://twitter.com/cwolferesearch/status/1635693551584522256?s=20)]
+ 语言模型预训练 [[link](https://cameronrwolfe.substack.com/i/85568430/language-modeling)]
+ 仅解码器变压器 [[link](https://twitter.com/cwolferesearch/status/1640446111348555776?s=20)]
除了大语言模型之外,我们还应了解提示工程的概念。在与语言模型互动时,我们提供文本输入(即提示),语言模型提供文本完成。这个文本到文本的格式非常灵活,允许通过从 LLM 的输出中提取正确答案来解决各种不同的问题。然而,这种灵活性也可能成为负担,因为在尝试解决问题时,LLM 的输入可以用许多不同的方式进行措辞或表达。提示工程是一门经验科学,研究这个问题并试图找到能最大化 LLM 性能的输入提示。要了解更多内容,请查看以下概述:
+ 实用提示工程 [[link](https://cameronrwolfe.substack.com/p/practical-prompt-engineering-part)]
+ 高级提示工程 [[link](https://cameronrwolfe.substack.com/p/advanced-prompt-engineering)]
## 可靠性是什么?
在研究可靠性时,提供该概念的精确定义是有用的。如前所述,LLM 可能会相当脆弱。如果我们稍微改变输入,可能会得到截然不同的输出。LLM 的输出往往不可预测且不准确。这些问题在最近的[Chip Huyen](https://huyenchip.com/)的[博客文章](https://huyenchip.com/2023/04/11/llm-engineering.html)中进行了广泛讨论。以下引用概述了使用 LLM 构建应用程序相比于传统编程任务的困难。
> “编程语言大多是精确的……在提示工程中,指令是用自然语言编写的,比编程语言灵活得多……LLM 生成的回应中的歧义可能会成为决定性因素。”
**可靠性是解决方案。** 从高层次看,可靠性指的是系统在处理噪音和抽象或避免 LLM 的不一致行为方面的能力。这可能意味着从提高 LLM 的准确性到改善模型行为的一致性或可预测性。如果我们想最大化 LLM 的效用,就必须找到使其行为更可靠的方法,以便可以围绕 LLM 构建应用程序,而不会出现打破系统的意外“惊喜”。实际上,这意味着我们必须:
+ 采取更严格/系统的方法来进行提示工程。
+ 寻找使 LLM 更具预测性和准确性的技术。
+ 在 LLM 未能匹配我们期望的格式时,实施保护措施/边界。
上述每一点都是提高 LLM 可靠性的步骤。简单来说,我们只希望找到方法,使我们的 LLM 在应用程序中表现得更加一致,从而减少终端用户的困惑,提升体验。如果我们承诺采取更严格的方法来处理 LLM,*完全有可能最小化提示工程的脆弱性,并减少 LLM 的整体歧义*。在这次概述中,我们将主要关注上述第二点——使 LLM 更具预测性和准确性的技术。
## 使用 LLM 解决棘手问题

(来源于[3, 4, 16, 17])
尽管 LLM 可以通过如 [少样本学习](https://cameronrwolfe.substack.com/i/117151147/few-shot-learning) 等技术解决许多任务,但它们在解决多步骤问题或需要推理的任务时往往会遇到困难[15]。为此,最近的研究探索了如 [思维链(CoT)提示](https://cameronrwolfe.substack.com/p/chain-of-thought-prompting-for-llms) [3] 等技术,包括一些显著的 [扩展](https://twitter.com/cwolferesearch/status/1657122778984660993?s=20) 如 [自洽性](https://cameronrwolfe.substack.com/i/116166267/variants-of-cot-prompting) [4],以提高 LLM 的推理能力。从这项工作中,我们了解到语言模型已经具备解决困难(基于推理的)问题的能力——我们只需使用正确的提示方法!
> “大型预训练语言模型内建了推理能力,但它们需要特定的提示来释放它们的潜力。” *— 来源于 [1]*
**自洽性**。在使用 CoT 提示方法的情况下,自洽性[4]可以通过*i)* 从同一个模型生成多个不同的输出,以及 *ii)* 使用每个输出答案的多数票作为最终答案来提高 LLM 的准确性;见下文。这种技术通过对一组多样的输出结果进行汇总来提高 LLM 的准确性。自洽性既简单又有效,表明提高 LLM 可靠性的实用技术可能离我们并不遥远。因此,我们可能会想知道:*我们如何进一步利用这种方法?是否还有其他更简单的技术效果更佳?*

(来源于 [1])
**提示集**。自洽性的有效性源于在形成最终答案时考虑的生成输出的多样性。然而,这种技术有一个关键细节需要注意——*所有输出都是用相同的提示生成的*。为了增加生成输出的多样性,我们可以考虑一组多样的提示来解决同一个问题。
> “人们的思维方式不同,[但] 不同的思维往往会得到相同的正确答案。” *— 来源于 [1]*
这种方法,被称为提示集,可以用来生成比自洽性方法更为多样化的模型输出,从而进一步提高 LLM 应用的可靠性。此外,提示集易于理解且可以自动构建,无需大量实施工作。在本文中,我们将探讨关于提示集的最新研究,重点关注使 LLM 更有效的实用工具。
## 其他重要概念
除了迄今为止涉及的观点外,概述中还提到了一些可能对理解有用的小概念和术语。
**自举。** 这是一个在更广泛的计算机科学社区中常用的通用术语。它指的是利用现有资源来做一些新的或有用的事情。在本概述中,我们使用自举来描述在一个系统中利用现有的、预训练的 LLMs 作为组件,生成新的提示以用于集合中。
**弱监督。** 有[许多不同的方法](https://www.geeksforgeeks.org/supervised-unsupervised-learning/)来训练机器学习模型。弱监督是一种介于[监督学习和无监督学习](https://www.geeksforgeeks.org/supervised-unsupervised-learning/)之间的技术。它不像监督学习那样完全依赖于标记数据,但它确实使用某种形式的“标签”作为训练信号。例如,我们可能使用某些启发式方法生成“伪标签”,或在训练过程中使用标记和未标记的数据的组合。有关更多细节,请查看[Snorkel AI](https://snorkel.ai/)的精彩概述,链接[在此](https://snorkel.ai/weak-supervision/)。
**Jaccard 指数。** Jaccard 指数,通常在机器学习社区中称为交集与并集(IoU),用于计算两个有限集合之间的相似性。要计算 Jaccard 指数的值,我们首先找到两个集合之间的交集元素数量,然后将这个数字除以两个集合的并集大小。例如,如果我们有两个集合 `{a, b, c}` 和 `{b, c, d}`,Jaccard 指数将是 0.5(即,两个元素相交,两个集合之间共有四个唯一元素)。
# 提示集合的研究
先前关于 CoT 提示和自洽的研究已经向我们展示了聪明的提示策略可以极大地提高 LLM 可靠解决困难问题的能力。现在我们将超越这些简单的基线,深入研究最近的研究,研究使用 LLM 的提示集合。这类工作提供了大量关于最佳实践的实用知识,我们可以采用这些知识来提高 LLM 的可靠性。
## 逻辑推理步骤中的多样性验证器(DiVeRSE) [1]
> “人们的思维方式各异,[但]不同的思维常常能得出相同的、正确的答案。” *— 引自 [1]*
[1] 的作者探讨了对 CoT 和自洽提示技术 [3, 4] 的扩展,这些技术提高了在复杂的多步骤推理任务中的表现。这个被称为 DiVeRSE 的技术,利用提示集合(即,旨在解决同一问题的不同提示的集合)来增强生成的[推理路径](https://cameronrwolfe.substack.com/i/116166267/chain-of-thought-prompting)的多样性,然后训练一个验证模块以推断每个输出的正确性;见下文。

(引自 [1])
自我一致性和 DiVeRSE 都生成多个推理路径,这些路径被结合形成最终答案。然而,自我一致性从使用相同提示的 LLM 中抽取多个推理路径,而 DiVeRSE 为解决同一问题构造多样的提示集合,并从每个提示中抽取多个推理路径。此外,自我一致性仅对推理路径进行多数投票以形成最终答案。DiVeRSE 采用更复杂的方法:
1. 训练一个验证器/分类器以预测每个推理路径的正确性。
1. 根据正确性对推理路径进行加权平均。
简而言之,DiVeRSE 通过 *i)* 增强生成推理路径的多样性和 *ii)* 在构造最终答案时对可能正确的推理路径分配更多权重,从而提高 LLM 推理能力。

构造提示集合(摘自 [3])
**构造提示集合。** DiVeRSE 的主要好处之一是使用提示集合来最大化生成输出的多样性。但是,*生成这些提示集合是否昂贵?我们可以自动构造提示集合吗?* 特别是考虑到 CoT 提示,我们可以通过两种主要方式生成提示集合(见上图):
1. *重采样:* 给定一个包含问题、答案和 `K` 个推理依据的 CoT 提示,我们可以通过从完整的推理依据集中随机抽取 `R < K` 个示例来生成独特的提示。
1. *引导自举:* 如果我们的 CoT 提示中没有足够的少样本示例来进行重采样,我们可以简单地提示一个独立的 LLM 生成伪推理路径,以便在执行重采样时包含这些路径。
利用这些技术,我们可以自动生成 DiVeRSE 的提示集合,而无需大量的手动人工努力。
> “因果语言模型没有机制来纠正早期步骤中的前期错误,这很快会导致结果偏离。” *— 摘自 [1]*
**验证模块。** 为了形成最终答案,DiVeRSE 使用验证模块预测每个生成的推理路径的正确性,然后根据这些预测进行加权平均。验证器是一个 [二分类器](https://en.wikipedia.org/wiki/Binary_classification)(例如,基于 [BERT](https://cameronrwolfe.substack.com/p/language-understanding-with-bert) [5] 或 DeBERTa [6]) ,它在由底层 LLM 生成的正确和不正确的推理路径数据集上进行训练。值得注意的是,需要标注数据来生成这个验证器的数据集,因为标签用于确定任何推理路径的最终答案是否实际正确。
在测试时,使用这个验证器为 DiVeRSE 生成的每条推理路径生成一个正确性分数,低正确性分数的路径在最终答案中所占的权重较小。有趣的是,我们在[1]中看到,进行步骤级验证(即训练验证器预测每个单独推理步骤的正确性,而不是整体路径)特别可以大幅提升推理性能;见下文。

步骤级验证与响应级验证(来自 [3])
**它的表现如何?** DiVeRSE 与基线技术如[贪婪解码](https://lilianweng.github.io/posts/2021-01-02-controllable-text-generation/#common-decoding-methods)和自一致性进行了比较,使用了多种不同的 LLMs,如 davinci (GPT-3)、text-davinci-002 (GPT-3.5)和来自[OpenAI API](https://platform.openai.com/docs/models)的 code-davinci-002。在执行算术、常识和归纳推理的八种不同推理任务中,DiVeRSE 在自一致性基础上取得了一致性改进;见下文。特别值得注意的是,DiVeRSE 与 code-davinci-002 在六个基准测试中达到了最先进的性能,甚至超越了强大的、5400 亿参数的[PaLM 模型](https://cameronrwolfe.substack.com/p/palm-efficiently-training-massive) [7]。

(来自 [1])
更进一步,作者在[1]中进行分析,展示了* i)* 提示集对推理性能的好处,*ii)* 推理性能在包含一定数量的提示后会达到饱和,并且 iii) 使用验证器(尤其是步骤级验证器)比多数投票表现更好(尽管多数投票简单得多!);见下文。

(来自 [1])
## 问我任何事 (AMA) [2]

(来自 [2])
作者在[2]中探讨了有效构建和使用提示集的实用技术。从高层次来看,提出的技术称为“问我任何事”(AMA),其动机在于消除构建“完美”提示的需求。相反,我们可以通过生成一组不完美(但仍然有效)的提示并汇总其结果,设计一种有效且可靠的提示策略。但,我们需要聪明地汇总这些提示的结果(即多数投票效果不佳!)。此外,我们不能随便使用任何一组提示!特别是,我们在[2]中看到,最有效的提示集利用了鼓励开放式生成的提示。
尽管这听起来很不错,我们可能还有一些问题。*这对所有任务都有效吗?收集提示是否昂贵?我们应如何汇总结果?* [2]中提出的方法旨在既具有可扩展性又通用,意味着它可以用于提高任何模型或任务的性能。这种效率和效果来源于三个主要思想:
1. *提示结构:* AMA 强调使用开放性提示,而不是那些限制输出令牌的提示。
1. *可扩展的提示生成:* AMA 不是依靠人工手动编写开放性提示集合,而是使用 LLMs 生成和回答提示,从而减少了对人力的需求。
1. *弱监督:* 由于多数投票效果不佳,AMA 使用弱监督来学习提示之间的依赖关系,并将 LLM 输出汇总为一个准确的最终答案。
**为什么多数投票效果不好?** 除了提高集合中提示的质量和结构,[2]中的工作受到以下事实的启发:通过对提示集合的 LLM 输出进行多数投票(例如,如自一致性[4]所示)生成最终答案效果不好。但是,*为什么会这样?* 有趣的是,[2]中的作者提供了一个相当清晰且直观的答案。
> “我们观察到准确性平均变化为 9.5%,Jaccard 指数相比于提示错误为 i.i.d.的情况高出 69%。多数投票(MV)是先前工作的主要无监督汇总策略,但它没有考虑到这些属性,使其不可靠。”*— 来源于 [2]*
换句话说,*LLMs 所犯的错误并非随机分布*! 实际上,LLM 在多个不同提示下的输出可能会集中在一个错误答案上。这对多数投票是个巨大问题,因为错误的答案可能实际上是我们的多数投票!为了解决这个问题,我们需要一个更复杂的策略来通过建模集合中提示输出的准确性和依赖关系来检测和处理这种情况。
**构建一个出色的提示集合。** 作为第一步,[2]中的作者研究了构成最有效集合的提示类型。考虑了三种不同的提示技术类别,如下图所示。

不同的 LLM 提示策略(由作者创建)
当这些提示策略进行比较时,我们发现开放式提示格式(即,[Cloze](https://cameronrwolfe.substack.com/i/76273144/training-bert) 和自由格式)优于要求 LLM 输出特定令牌集的限制性提示格式。进一步说,使自由格式提示中的问题更[精准或具体](https://cameronrwolfe.substack.com/i/117151147/what-is-prompt-engineering)也能显著提高准确性。*为什么自由格式提示效果更好?* 答案尚不完全清楚,但自由格式生成更接近于大多数 LLMs 预训练时使用的[下一个令牌预测任务](https://cameronrwolfe.substack.com/i/85568430/language-modeling),这直观上意味着这些模型可能更擅长处理这种格式的任务。

生成可以提供更多上下文的问答集(由作者创建)
受到开放式提示有效性的启发,AMA 通过生成有关给定输入的一组问题来形成提示集;见上文。这些问题遵循自由格式,并强调输入的不同方面,可能提供有用的互补信息。然而,*手动生成这些问题可能非常昂贵*! 为了避免这一点,我们可以使用 LLMs!在 [2] 中,我们看到 LLMs 可以通过少量示例学习生成有关所需主题的有用问题;见下文。

(出自 [2])
通过改变使用的上下文示例和采用一组经过实证确定表现良好的提示模板,[2]的作者完全自动化了 AMA 中提示集的构建!
> “为了可靠地汇总提示预测,我们使用弱监督的方法,这是一种从较弱信号源而非标注数据中学习高质量模型的强大方法。” *— 出自 [2]*
**汇总结果。** 我们可以构建提示集,但还有一个问题待解:*我们如何汇总每个提示的 LLM 输出?* 尤其是对于自由格式提示,从 LLM 输出中提取正确答案可能非常困难。 [2]中的汇总方法借鉴了弱监督和图模型的先前工作 [8, 9, 10]。高层次的想法是利用弱监督学习并预测不同提示之间的依赖关系和每个提示的准确性。我们可以使用这些信息来汇总提示并推断最可能的最终答案。与 DiVeRSE 不同,这种方法不需要标注数据,并解决了多数投票的常见失败案例(例如,LLMs 在不同提示上产生相同的错误)。
**AMA 表现如何?** AMA 方法在 20 个不同的基准测试上测试,使用了各种 LLM(即四种不同的模型系列,包括 [BLOOM](https://bigscience.huggingface.co/blog/bloom)、 [OPT](https://cameronrwolfe.substack.com/p/understanding-the-open-pre-trained-transformers-opt-library-193a29c14a15)、 [EleutherAI](https://www.eleuther.ai/language-modeling) 和 [T0](https://github.com/bigscience-workshop/t-zero)),其规模从 1.25 亿到 1750 亿参数不等。[2]中分析的目标是确定 AMA 是否是一种可以在多种不同设置下有效的通用方法。这项分析的结果相当积极。使用 AMA,我们看到小型开源模型(特别是 [GPT-J-6B](https://huggingface.co/EleutherAI/gpt-j-6b))能够超越像 [GPT-3](https://cameronrwolfe.substack.com/i/88082618/language-models-are-few-shot-learners) 这样的大型模型;见下文。

(来源于 [2])
当所有不同的模型经过测试后,我们发现中等规模的模型(即 6–20 亿参数)从 AMA 中获得的收益最大;见下文。与基线相比,少量提示技术,AMA 在所有模型和任务中的表现提高了约 10%。因此,它是一种通用方法,可以可靠地提升几乎任何 LLM 的性能。

(来源于 [2])
AMA 提供了一种构建提示集的深刻方法。该出版物充满了关于如何引导预训练 LLM 编写有效提示的实用建议。在[2]中我们看到,汇总 LLM 对提示集的响应的 методология是极其重要的——多数投票是不够的![2]中提出的汇总方法在技术上复杂,可能需要实现的努力,但表现良好且不需要监督标签。通过采用如 AMA 这样的方式,我们可以通过使任何 LLM 更准确和一致,从而提高 LLM 的可靠性。
> “我们希望 AMA 和未来的工作能通过提高处理不完美提示的能力和使用小型、私人及开源 LLM 的能力,来解决使用 LLM 的痛点。” *— 来源于 [2]*
# 收获
我们现在应该能够理解,提示集易于使用,并且具有巨大的潜力。要利用这种技术,我们只需 *i)* 构建一组旨在解决相同问题的多样化提示, *ii)* 使用这些提示生成多个 LLM 输出,以及 *iii)* 汇总这些输出以形成最终答案。如我们所见,汇总过程可能有些复杂(即,简单的多数投票通常是不够的)。然而,构建和使用提示集很简单,这使得它们成为提高 LLM 可靠性的强大工具。下面概述了一些主要收获。
**可靠性很重要。** 要在现实世界中使用 LLMs,我们需要围绕它们构建软件系统。但是,为了围绕 LLMs 构建软件系统,我们需要缓解这些模型的不确定性/模糊性。提示集合提供了一种相当直接的方法来使 LLMs 更加准确和可靠。通过鼓励 LLM 为解决特定问题产生多样化的输出,我们可以研究这些响应之间的关系,并开发自动化技术以产生更高质量的最终结果。
**LLMs 的泛化。** 通常,提示工程策略是脆弱的。如果我们调整提示,可能会得到截然不同的结果。如果我们保持提示不变但更换模型,同样的情况也会发生。如果我们构建一个基于 LLM 的应用程序,后来决定更换所使用的底层模型,我们可能还需要更改大部分提示。然而,通过像 AMA [2]这样的技术,我们看到提示集合可以缓解这个问题,因为它们在各种不同模型中提供了一致的性能提升。因此,提示集合通过对底层模型的不敏感性提高了可靠性。
**聚合很困难。** 在阅读了关于自一致性的内容后,我曾对 LLMs 能够通过简单的提示技术显著提高可靠性感到乐观。然而,正如我们在这次概述中看到的,这并不总是正确的。我们可以轻松生成多样化的输出,但聚合这些响应的方式至关重要。不幸的是,DiVeRSE 和 AMA 提出的方法相当复杂,可能需要大量的实施工作。不过,我们明显看到,单纯的多数投票无法达到更复杂技术的性能。希望能尽快提出更简单的聚合技术。
**局限性。** 尽管提示集合非常出色,但它们并不完美。像 DiVeRSE 和 AMA 这样的技术依赖于为每个回答的问题生成大量的 LLM 输出。我们使用多个提示,甚至可能为每个提示生成多个响应——*这需要大量的 LLM 推理*!因此,提示集合在经济和延迟方面都可能非常昂贵。如果我们希望在实际应用中利用提示集合,我们必须非常小心其应用方式,因为这可能会极大地改变应用的成本和效率。
## 结束语
非常感谢阅读这篇文章。我是 [Cameron R. Wolfe](https://cameronrwolfe.me/),[Rebuy](https://www.rebuyengine.com/) 的人工智能总监。我研究深度学习的实证和理论基础。你还可以查看我在 medium 上的 [其他文章](https://medium.com/@wolfecameron)!如果你喜欢,请在 [twitter](https://twitter.com/cwolferesearch) 上关注我,或者订阅我的 [Deep (Learning) Focus 新闻通讯](https://cameronrwolfe.substack.com/),在这里我通过对热门论文的易懂概述,帮助读者深入理解人工智能研究中的话题。
## 参考文献
[1] Li, Yifei, 等. “使语言模型成为更好的推理器的进展。” *arXiv 预印本 arXiv:2206.02336* (2022)。
[2] Arora, Simran, 等. “问我任何事: 语言模型的简单提示策略。” *arXiv 预印本 arXiv:2210.02441* (2022)。
[3] Wei, Jason, 等. “思维链提示在大型语言模型中引发推理。” *arXiv 预印本 arXiv:2201.11903* (2022)。
[4] Wang, Xuezhi, 等. “自洽性改善语言模型中的思维链推理。” *arXiv 预印本 arXiv:2203.11171* (2022)。
[5] Devlin, Jacob, 等. “Bert: 语言理解的深度双向变换器预训练。” *arXiv 预印本 arXiv:1810.04805* (2018)。
[6] He, Pengcheng, 等. “Deberta: 解码增强的 bert 结合解耦注意力。” *arXiv 预印本 arXiv:2006.03654* (2020)。
[7] Chowdhery, Aakanksha, 等. “Palm: 通过路径扩展语言建模。” *arXiv 预印本 arXiv:2204.02311* (2022)。
[8] Ratner, Alexander, 等. “Snorkel: 使用弱监督快速创建训练数据。” *VLDB 会议论文集. 国际大型数据会议*. 第 11 卷,第 3 期,NIH 公共访问,2017。
[9] Varma, Paroma, 等. “为弱监督模型学习依赖结构。” *国际机器学习会议*. PMLR,2019。
[10] Ratner, Alexander, 等. “用多任务弱监督训练复杂模型。” *AAAI 人工智能会议论文集*. 第 33 卷,第 01 期,2019。
[11] Taylor, Ross, 等. “Galactica: 用于科学的大型语言模型。” *arXiv 预印本 arXiv:2211.09085* (2022)。
[12] Thoppilan, Romal, 等. “Lamda: 对话应用的语言模型。” *arXiv 预印本 arXiv:2201.08239* (2022)。
[13] Glaese, Amelia, 等. “通过有针对性的人类判断改善对话代理的对齐。” *arXiv 预印本 arXiv:2209.14375* (2022)。
[14] Chowdhery, Aakanksha, 等. “Palm: 通过路径扩展语言建模。” *arXiv 预印本 arXiv:2204.02311* (2022)。
[15] Cobbe, Karl, 等. “训练验证器解决数学问题。” *arXiv 预印本 arXiv:2110.14168* (2021)。
[16] Kojima, Takeshi, 等. “大型语言模型是零-shot 推理器。” *arXiv 预印本 arXiv:2205.11916* (2022)。
[17] Zhou, Denny, 等. “最少到最多提示使大型语言模型能够进行复杂推理。” *arXiv 预印本 arXiv:2205.10625* (2022)。
# **倾向评分匹配(PSM)用于 A/B 测试:减少观察研究中的偏差**
> 原文:[`towardsdatascience.com/propensity-score-matching-psm-for-a-b-testing-reducing-bias-in-observational-studies-958f24ac8884`](https://towardsdatascience.com/propensity-score-matching-psm-for-a-b-testing-reducing-bias-in-observational-studies-958f24ac8884)
## 关于如何在你的实验数据中实施 PSM 的综合指南,包括 Python 代码
[](https://frankphopkins.medium.com/?source=post_page-----958f24ac8884--------------------------------)[](https://towardsdatascience.com/?source=post_page-----958f24ac8884--------------------------------) [Frank Hopkins](https://frankphopkins.medium.com/?source=post_page-----958f24ac8884--------------------------------)
·发表于 [Towards Data Science](https://towardsdatascience.com/?source=post_page-----958f24ac8884--------------------------------) ·阅读时间 12 分钟·2023 年 4 月 26 日
--

AI 生成的图像“以瓦西里·康定斯基风格的 PSM”使用 DALL:E 2 — 作者:**Frank Hopkins**
A/B 测试是一种广泛使用的实验设计,其中比较了两个或多个干预措施对感兴趣结果的影响。A/B 测试的目标是估计干预措施对结果的因果效应,同时控制潜在的混杂变量。通常使用随机化来实现处理组和对照组之间的平衡,但这可能并不总是可行或足够平衡所有相关协变量。因此,由于处理组和对照组特征的差异,估计的治疗效果可能会有偏差。
倾向评分匹配(PSM)是一种统计方法,旨在通过基于倾向评分创建可比的处理组和对照组,从而减少估计治疗效果的偏差。倾向评分是在给定一组观察到的协变量下接受治疗的条件概率,它总结了与估计治疗效果相关的协变量信息。PSM 将处理组和对照组中具有相似倾向评分的个体进行匹配,这可以平衡潜在协变量的分布,减少未观察变量的影响。
在随机对照试验(RCTs)的 A/B 测试背景下,PSM 可以在随机化之后帮助减少偏倚。虽然随机化确保了治疗组和对照组在平均水平上的平衡,但由于随机变异,它可能无法在所有相关协变量上实现平衡。PSM 可以用来基于观察到的协变量创建可比的治疗组和对照组,这可以减少偏倚,提高估计治疗效应的准确性和稳健性。
在本文中,我们将提供一个关于在 RCTs 的 A/B 测试背景下使用 PSM 的全面指南。我们将首先讨论 A/B 测试中平衡和混杂变量的重要性,以及随机化在实现平衡方面的局限性。然后,我们将介绍倾向得分的概念,并解释如何使用它来估计治疗效应。我们还将提供如何使用 PSM 来改善 A/B 测试中平衡和减少偏倚的示例,并讨论不同匹配算法的优缺点。最后,我们将提供一个逐步指南,讲解如何在 Python 中实施 PSM,以便使用一个简单的示例数据集进行 RCTs 的 A/B 测试。
# 2\. 背景
随机对照试验(RCTs)通常被认为是估计因果效应的金标准,因为它们旨在通过随机分配消除混杂变量的影响。然而,即使是 RCTs 也可能会受到未测量或未知混杂变量的偏倚。此外,RCTs 中的随机化可能无法在所有相关协变量上实现平衡,特别是在小样本中。因此,需要采用替代方法来减少偏倚并提高估计治疗效应的准确性。
PSM 是一种可以解决随机化在实现治疗组和对照组之间平衡时的局限性的方法。PSM 通过基于个体的倾向得分进行匹配,创建可比的治疗组和对照组。倾向得分是给定一组观察到的协变量下接受治疗的条件概率,它总结了有关协变量的信息,这些信息对于估计治疗效应是相关的。
基于个体的倾向得分进行匹配可以平衡潜在协变量的分布,并减少未观察到变量的影响。此外,通过控制混杂变量,倾向得分匹配(PSM)可以用来减少估计治疗效应的偏倚。通过创建可比的治疗组和对照组,PSM 可以提高估计治疗效应的准确性和稳健性,即使在随机对照试验(RCTs)中进行了随机化之后。
在下一部分,我们将介绍倾向得分的概念,并解释如何在随机对照试验(RCTs)中使用它来估计治疗效应。我们还将讨论 PSM 在 RCTs 的 A/B 测试中的优点和局限性,并提供如何使用 PSM 来减少偏倚和提高估计治疗效应准确性的示例。
# 3\. 倾向得分匹配与 RCTs
尽管随机对照试验(RCTs)被认为是评估干预效果的**黄金标准**,但它们也有其局限性。其中一个主要局限性是潜在的混杂偏倚,这种偏倚可能出现在处理组和对照组之间存在系统性差异,但这些差异在分析中未被考虑。
例如,如果处理组包含比对照组更高比例的高消费用户,则两组之间的任何结果差异可能是由于消费差异而非处理本身。这种混杂效应可能导致对处理效应的估计不准确,并降低因果推断的有效性。
倾向得分匹配可以通过创建一个在观察到的协变量上匹配的更平衡的对照组来解决这个限制。这一匹配过程有助于确保处理组和对照组在协变量分布上更加相似,从而减少混杂偏倚,提高因果推断的有效性。
需要注意的是,倾向得分匹配不会从处理组或对照组中移除用户。相反,它根据用户的倾向得分创建两组用户的匹配对。任何无法根据倾向得分匹配的用户将被简单地排除在分析之外。这个过程有助于创建一个更平衡的对照组,而不移除原始处理组或对照组中的用户。
此外,倾向得分匹配在存在大量协变量且混杂偏倚可能性较高的 RCTs 中特别有用。通过使用倾向得分匹配来平衡处理组和对照组之间的协变量分布,我们可以确保两组之间观察到的结果差异不是由于协变量分布的差异所致。
需要注意的是,倾向得分匹配并非万灵药,仍可能存在未观察到的变量影响处理效应。然而,通过将倾向得分匹配作为我们 RCT 工具包中的一个工具,我们可以提高 RCT 结果的严谨性和可靠性。
# 4\. 在 A/B 测试中使用 Python 代码实现倾向得分匹配
在本节中,我们将详细介绍如何在 A/B 测试中实施倾向得分匹配(PSM),以更多的协变量和净游戏收入(NGR)作为结果指标。PSM 的目标是减少潜在的混杂偏倚,提高因果推断的有效性。
## 第一步:准备数据
第一步是准备数据。这包括识别结果变量(即 NGR)和处理分配变量。我们还需要识别所有可能影响结果变量并可能导致混杂偏差的协变量。这些协变量应包括在用于计算倾向得分的逻辑回归模型中。
```py
# Identify outcome variable and treatment assignment variable
outcome_var = 'ngr'
treatment_var = 'group'
# Identify covariates
covariates = ['age', 'gender', 'income', 'education', 'location', 'device_type', 'browser']
+--------+-----+------+--------+---------+------------+------------+------------+------------+
| UserID | Age | Gender | Income | Education | Device | Location | Browser | NGR (USD) |
+--------+-----+------+--------+-----------+------------+------------+------------+------------+
| 1 | 23 | M | 45000 | College | iPhone X | New York | Safari | 120 |
| 2 | 45 | F | 78000 | College | Galaxy S9 | San Fran | Chrome | 80 |
| 3 | 31 | M | 65000 | Graduate | iPhone 8 | Boston | Firefox | 50 |
| 4 | 28 | F | 38000 | Graduate | iPhone 7 | Austin | Chrome | 200 |
| 5 | 52 | M | 95000 | Graduate | Galaxy S8 | Seattle | Firefox | 150 |
| 6 | 39 | F | 58000 | Graduate | iPhone X | New York | Safari | 90 |
| 7 | 33 | M | 51000 | College | iPhone 8 | San Fran | Chrome | 70 |
| 8 | 26 | F | 32000 | College | Galaxy S9 | Boston | Firefox | 100 |
| 9 | 41 | M | 73000 | Graduate | iPhone 7 | Austin | Safari | 80 |
| 10 | 36 | F | 68000 | College | Galaxy S8 | Seattle | Chrome | 120 |
+--------+-----+------+--------+-----------+------------+------------+------------+------------+
在这个例子中,我们将 NGR 识别为结果变量,将‘group’识别为处理分配变量。我们还识别了几个可能影响 NGR 的协变量,包括年龄、性别、收入、教育、地点、设备类型和浏览器。
步骤 2:计算倾向得分
一旦我们准备好数据,我们可以使用逻辑回归模型计算每个用户的倾向得分。逻辑回归模型应包括处理分配变量作为结果变量以及所有协变量作为预测变量。
import statsmodels.api as sm
# Fit logistic regression model to calculate propensity scores
X = sm.add_constant(ab_test_df[covariates])
y = ab_test_df[treatment_var]
model = sm.Logit(y, X)
result = model.fit()
propensity_scores = result.predict(X)
+--------+-----+------+--------+-----------+------------+------------+------------+------------+----------------+
| UserID | Age | Gender | Income | Education | Device | Location | Browser | NGR (USD) | Propensity_Score|
+--------+-----+------+--------+-----------+------------+------------+------------+------------+----------------+
| 1 | 23 | M | 45000 | College | iPhone X | New York | Safari | 120 | 0.35 |
| 2 | 45 | F | 78000 | College | Galaxy S9 | San Fran | Chrome | 80 | 0.75 |
| 3 | 31 | M | 65000 | Graduate | iPhone 8 | Boston | Firefox | 50 | 0.55 |
| 4 | 28 | F | 38000 | Graduate | iPhone 7 | Austin | Chrome | 200 | 0.30 |
| 5 | 52 | M | 95000 | Graduate | Galaxy S8 | Seattle | Firefox | 150 | 0.85 |
| 6 | 39 | F | 58000 | Graduate | iPhone X | New York | Safari | 90 | 0.65 |
| 7 | 33 | M | 51000 | College | iPhone 8 | San Fran | Chrome | 70 | 0.60 |
| 8 | 26 | F | 32000 | College | Galaxy S9 | Boston | Firefox | 100 | 0.45 |
| 9 | 41 | M | 73000 | Graduate | iPhone 7 | Austin | Safari | 80 | 0.70 |
| 10 | 36 | F | 68000 | College | Galaxy S8 | Seattle | Chrome | 120 | 0.80 |
+--------+-----+------+--------+-----------+------------+------------+------------+------------+----------------+
在这个例子中,我们拟合了一个逻辑回归模型来计算每个用户的倾向得分。该逻辑回归模型包括处理分配变量作为结果变量,以及所有协变量作为预测变量。
步骤 3:根据倾向得分匹配用户
一旦我们计算出每个用户的倾向得分,就可以使用这些得分根据倾向得分匹配处理组和对照组的用户。这个匹配过程有助于确保处理组和对照组在协变量分布上更加相似,从而减少混杂偏差并提高因果推断的有效性。
from sklearn.neighbors import NearestNeighbors
from sklearn.preprocessing import StandardScaler
# Standardize covariates for matching
X_scaled = StandardScaler().fit_transform(ab_test_df[covariates])
# Match users based on propensity scores
nbrs = NearestNeighbors(n_neighbors=1, algorithm='ball_tree').fit(propensity_scores.values.reshape(-1, 1))
distances, indices = nbrs.kneighbors(propensity_scores.values.reshape(-1, 1))
matched_df = ab_test_df.loc[indices.flatten()]
在这个例子中,我们使用scikit-learn库中的StandardScaler函数对协变量进行标准化以进行匹配。然后,我们使用倾向得分基于最近邻匹配处理组和对照组的用户。最后,我们创建一个包含匹配用户的新数据框。
步骤 4:检查处理组和对照组之间的协变量平衡
在我们根据倾向得分匹配用户后,应该检查处理组和对照组之间协变量的平衡,以确保没有剩余的混杂偏差。这可以通过比较匹配数据集中处理组和对照组之间协变量的均值来完成。
matched_df.groupby(treatment_var).mean()[covariates + [outcome_var]]
在这个例子中,我们使用 Pandas 中的groupby函数将匹配的数据集按处理分配分组,并计算协变量和结果变量的均值。
步骤 5:比较平均处理效果
最后,我们可以比较匹配数据集中处理组和对照组之间的平均处理效果。这可以通过计算处理组和对照组之间的 NGR 均值差异来完成。
matched_df.groupby(treatment_var).mean()[outcome_var].diff()[1]
在这个例子中,我们使用diff函数计算处理组和对照组之间 NGR 均值的差异。结果给出了在使用 PSM 控制潜在混杂偏差后 A/B 测试的平均处理效果。
步骤 6:将 PSM 与传统 A/B 测试进行比较
要查看 PSM 对 A/B 测试结果的影响,我们可以将使用 PSM 计算的平均处理效应与传统 A/B 测试的结果进行比较,在传统测试中,用户被随机分配到处理组和对照组。
ab_test_df.groupby(treatment_var).mean()[outcome_var].diff()[1]
在这个例子中,我们使用 diff 函数计算原始数据集中处理组和对照组之间的平均 NGR 差异。结果给出的是 A/B 测试的平均处理效应,但没有控制潜在的混杂偏差。
通过将使用 PSM 计算的平均处理效应(ATE)与传统 A/B 测试的结果进行比较,我们可以看到混杂偏差对 A/B 测试结果的影响,以及使用 PSM 控制潜在混杂变量的重要性。
这是未匹配数据的 A/B 测试比较的 ATE:
ATE (USD): 1.8
在这个例子中,ab_test_df 是包含对照组和处理组的原始数据集,treatment_var 是一个二元变量,表示处理组(对照组为 0,处理组为 1),outcome_var 是感兴趣的结果变量(在这个例子中是 NGR 或净游戏收入)。
代码 ab_test_df.groupby(treatment_var).mean()[outcome_var].diff()[1] 计算控制组和处理组之间的平均 NGR 值的差异(Treatment = 1 减去 Treatment = 0),并返回以美元为单位的 ATE。在这个例子中,ATE 为 1.8 美元,表明处理组的 NGR 值平均高于对照组。
请注意,此计算仅基于原始数据集,并未考虑可能影响结果变量的潜在混杂变量。使用倾向评分匹配计算的 ATE 考虑了潜在的混杂变量,从而提供了对处理效应的更准确估计。
现在,这是 PSM 匹配数据的 ATE:
ATE (USD): 2.2
在这个例子中,经过倾向评分匹配后的处理组 ATE 为 2.2 美元,表明在控制潜在混杂变量后,处理组的 NGR 值平均高于对照组。这个 ATE 高于使用传统 A/B 测试从原始数据集中计算得到的 ATE(1.8 美元),这表明倾向评分匹配使我们能够减少潜在混杂变量的影响,并提供了对处理效应的更准确估计。
请注意,ATE 的大小及其统计显著性将取决于具体的研究问题、研究设计以及用于评估处理效应的统计测试的选择。
在我们的例子中,PSM 使我们能够创建一个处理组和一个匹配的对照组,这些组在年龄、性别、收入、教育和其他协变量上具有相似的分布。这减少了这些协变量对结果变量的影响,从而使我们能够更准确地估计真实的处理效应。
经过 PSM 处理后的处理组 ATE 高于使用传统 AB 测试从原始数据集中计算的 ATE。这可能是由于 PSM 减少了可能使传统 AB 测试中的处理效果估计产生偏差的混杂变量的影响。通过减少这些混杂变量的影响,PSM 使我们能够更准确地估计真实的处理效果。
结论
倾向评分匹配(PSM)是一种减少实验研究偏差的有用技术,特别是在 A/B 测试的背景下。通过匹配具有相似倾向评分的观察值,PSM 创建了两个具有类似协变量分布的组,从而减少了混杂变量对处理效果估计的影响。
在这个例子中,我们将 PSM 应用于模拟的 A/B 测试数据集,以估计促销活动对净游戏收入(NGR)的处理效果。我们包括了多个协变量,如年龄、性别、收入、教育和婚姻状况,以控制潜在的混杂效应。我们发现经过 PSM 处理后的处理组 ATE 高于使用传统 AB 测试从原始数据集中计算的 ATE。这表明 PSM 使我们能够减少混杂变量的影响,并更准确地估计真实的处理效果。
然而,需要注意的是,PSM 不是万能的,并且存在局限性。PSM 假设所有混杂变量都是可观察的并且准确测量,这可能并非总是如此。此外,如果样本量过小或协变量过多,PSM 可能会导致统计功效和精度的损失。
因此,在将 PSM 应用于 A/B 测试时,重要的是要仔细考虑研究问题、研究设计和用于评估处理效果的统计测试的选择。PSM 可以作为减少偏差和估计真实处理效果的有价值工具,但应与其他技术和实验设计及数据分析的最佳实践结合使用。
Python 中的协议
如何使用结构性子类型
·发表于Towards Data Science ·5 min 阅读·2023 年 7 月 27 日
--
Python 3.8 引入了一个新特性:协议。协议是抽象基类 (ABC)的替代方案,并允许结构性子类型检查——仅基于可用属性和函数检查两个类是否兼容。在这篇文章中,我们将深入探讨这个细节,并展示如何通过实际示例使用协议。

由Chris Liverani拍摄,Unsplash上的照片
在 Python 中的输入
让我们首先讨论 Python 的类型。它是动态类型语言,这意味着类型在运行时推断,以下代码可以正常运行:
def add(x, y):
return x + y
print(add(2, 3))
print(add("str1", "str2"))
第一次调用结果是整数相加返回 5,第二次是字符串连接返回“str1str2”。这与静态类型的 C++ 不同——在 C++ 中我们必须提供类型声明:
int add(int x, int y) {
return x + y;
}
std::string add(std::string x, std::string y) {
return x + y;
}
int main()
{
std::cout<<add(2, 3);
std::cout << add("str1", "str2");
return 0;
}
静态类型的优点在于可以在编译时捕捉到潜在的错误,而在动态类型语言中,我们只能在运行时遇到这些错误。另一方面,动态类型允许更快速的原型设计和实验,这也是 Python 变得如此流行的原因之一。
动态类型也被称为鸭子类型,这是基于这样一句话:“如果它走起来像一只鸭子,叫声像一只鸭子,那么它一定是一只鸭子”。因此:如果对象提供相同的属性/函数,它们应该被类似对待,例如可以传递给要求另一种类型的函数。
尽管如此,尤其是在较大、更专业的软件产品中,这种不可靠性带来的弊端多于利端——因此趋势是向静态类型检查方向发展,例如通过使用 mypy 提供类型提示。
子类型化
一个有趣的问题——在关于鸭子类型的简短段落中有所提及——是子类型化。如果我们有一个签名为 foo(x: X) 的函数,除了 X 之外,mypy 还允许哪些其他类被传递给该函数?(注意我们现在只讨论类型和类型提示——由于 Python 是动态类型的,如前所述,我们可以将任何对象传递给 foo——如果有期望的属性/方法,它就能工作,否则会崩溃。)为此,人们区分结构性和名义性子类型化。结构性子类型化基于类层次/继承:如果类 B 继承自 A,它是 A 的子类型——因此也可以在期望 A 的地方使用。另一方面,名义性子类型化基于给定类的可用操作定义——如果 B 提供了 A 所有的属性/函数,它可以在所有期望 A 的地方使用。可以说,后一种方法更“Pythonic”,因为它更像鸭子类型的思想。
实践中的子类型化
在 Python 3.8 之前,人们只能通过继承来进行子类型化,例如使用(抽象)基类(ABCs):在这里,我们定义一个(抽象)基类——一个子类的“蓝图”——然后定义几个从这个基类继承的子类:
from abc import ABC, abstractmethod
class Animal(ABC):
@abstractmethod
def feed(self) -> None:
pass
class Duck(Animal):
def feed(self) -> None:
print("Duck eats")
def feed(animal: Animal) -> None:
animal.feed()
duck = Duck()
feed(duck)
在这段代码中,我们首先定义了 ABC Animal,提供抽象方法 feed。然后,我们子类化 Duck 并实现这个方法。最后,我们定义一个通用的 feed 函数,接受一个 Animal 作为参数,然后喂养这个动物。
这似乎是合理的——问题在哪里?确实有一些原因,为什么可能不想使用这种模式:基类往往暴露不充分且难以包含——即,如果你想从另一个模块中的基类继承,也许是一个公共库,你将不得不找到这个基类。其次,你不能更改现有代码,例如公共/第三方模块:如果你希望从这些模块中导入的类型成为其他类型的子类型,可能与其他由你引入的类型组合使用,这会是个问题。最后,这在某种程度上违背了 Python 和鸭子类型的思想。
协议
因此,Python 3.8 引入了协议,缓解了上述问题。协议,如其名称所示,通过定义期望的属性/方法的“接口”隐式地工作,并在必要时检查相关类是否提供了这些属性/方法:
from typing import Protocol
class Animal(Protocol):
def feed(self) -> None:
pass
class Duck:
def feed(self) -> None:
print("Duck eats")
def feed(animal: Animal) -> None:
animal.feed()
duck = Duck()
feed(duck)
如我们所见,Animal 现在是一个 Protocol,而 Duck 并未从任何基类继承——但 mypy 依然很高兴。
子类化协议
自然,我们也可以对子协议进行子类化——即定义一个继承自父协议的子协议,并对其进行扩展。在这样做时,我们只需记住同时继承父协议和 typing.Protocol:
from typing import Protocol
class Animal(Protocol):
def feed(self) -> None:
pass
class Bird(Animal, Protocol):
def fly(self) -> None:
pass
class Duck:
def feed(self) -> None:
print("Duck eats")
def fly(self) -> None:
print("Duck flies")
def feed(animal: Animal) -> None:
animal.feed()
def feed_bird(bird: Bird) -> None:
bird.feed()
bird.fly()
duck = Duck()
feed_bird(duck)
在上述代码中,我们将 Bird 指定为特定的 Animal 类型,然后定义一个喂鸟的函数,期望它随后飞走。
协议简史
上述所有代码都是有效的 Python,甚至在 Python 3.8 之前(记住,Python 是动态类型的)——我们只需要 ABCs 来满足 mypy,否则 mypy 会因为没有 ABCs 的例子而发出警告。然而,协议在 Python 中存在的时间更久,只是不像现在这样明显或显式:大多数 Python 开发者将“协议”一词用作遵守某些接口的约定,就像现在所明确的那样。一个著名的例子是 迭代器协议——一个描述自定义迭代器需要实现哪些方法的接口。为了使这与 mypy 一起工作而无需显式协议,存在几种“技巧”,例如自定义类型:
from typing import Iterable
class SquareIterator:
def __init__(self, n: int) -> None:
self.i = 0
self.n = n
def __iter__(self) -> "SquareIterator":
return self
def __next__(self) -> int:
if self.i < self.n:
i = self.i
self.i += 1
return i**2
else:
raise StopIteration()
def iterate(items: Iterable[int]) -> None:
for x in items:
print(x)
iterator = SquareIterator(5)
iterate(iterator)
抽象基类与协议
我们已经讨论了 ABCs 的潜在缺点(与外部模块和接口的兼容性、“非 Pythonic” 代码)。然而,协议不应取代 ABCs,而是两者都应使用。例如,ABCs 是重用代码的好方法:所有通用功能应在基类中实现,而只有特定部分在子类中实现。这在协议中是不可能的。
结论
在这篇文章中,我们讨论了静态与动态类型(鸭子类型),以及 mypy 如何处理子类型。在 Python 3.8 之前,必须使用抽象基类(ABCs)。随着协议的引入,Python 获得了一种优雅的定义“接口”的方式,并允许 mypy 检查这些接口的遵守情况:以更 Pythonic 的方式,协议允许指定类需要实现哪些属性/函数,然后允许所有这些类作为协议的子类型使用。
使用 Python 进行邻近分析以找到最近的酒吧
关于空间数据处理的几点说明
·
关注 发表在 Towards Data Science ·10 分钟阅读·2023 年 10 月 26 日
--
预览图片(作者提供)
免责声明:在本文中,我们将使用开源库estaty演示所有方法。这个库的出现是因为我们希望将工作中使用的算法形式化为一个工具,供其他开发者使用。换句话说,本文由该库的维护者撰写。
今天我们将继续讨论使用开源 Python 库进行空间数据处理的话题。我们已经讨论过如何结合 Open Street Map 和 Landsat 开放数据来验证房地产对象附近的绿地面积。
现在让我们考虑另一种类型的分析:邻近性分析,或一些有用对象的可用性(或可达性),如公园、医院、幼儿园等(图 1)。

图 1. 说明邻近性分析方法的示意图(假设这里讨论的是理发店等) (图片作者提供)
关于邻近性分析的简要说明
我们建议从简要的文献综述开始这个故事。我喜欢文章“每个数据科学家都应该了解的地点分析用例”(注意——这是仅限会员的故事),它非常容易让读者了解地理信息分析是什么。
文章“创建公园可达性成本距离表面”和“犹他州的增长状态:对犹他州居民与医疗设施邻近性分析”也非常清晰地展示了工程师在城市中可以计算的特征,以进行此类分析。这个话题相当受欢迎,已经开发了大量的科学论文和实际应用——让我们也来参与吧。
我们使用的库
现在让我们转到一个更具体的讨论:用于这种邻近性分析的工具(除了地理信息系统如 QGIS 或 ArcGIS 和作为最终用户的专有服务之外)。
作为开发人员,我们希望利用方便的库或服务(当然最好是开源的)分析原始数据而非汇总数据。我们没有找到适合我们的工具,因此开发并使用了我们自己的用 Python 编写的开源库。它叫做estaty,如果你愿意,可以在阅读本文后查看文档页面:
基于这个库,我们编写了使用案例。基于这些示例,我们开发了帮助房地产经理进行分析的微服务。除了现成的服务,这些脚本还组织成使用案例,任何想要使用该库的人都可以使用它们。
estaty 可以使用不同的数据源进行计算,但开放的示例(为了保持其真正开放)是基于 OpenStreetMap 数据的。在当前论文中,我们想展示如何进行相当快速的附带估算,并在需要时将该方法扩展到新的数据源。
分析(我们可以做的)
首先,使用命令安装库
pip install estaty==0.1.0
或者,如果您使用 poetry:
poetry add estaty==0.1.0
我们现在准备使用版本 0.1.0
让我们开始实现。我们将对以下地址进行邻近分析:“Berlin, Neustädtische Kirchstraße 4–7” — 坐标 {latitude: 52.5171411, longitude: 13.3857187}。我们将在半径 2 公里的范围内分析该对象的邻近区域。
为了找出我们到达最近酒吧需要多长时间(根据 OSM — 值得一提),我们来编写并运行以下代码:
这里发生了以下情况——我们构建了一个用于分析属性的处理管道。这个管道将呈现为一个由数据上的连续变换组成的图形:

图 2. 使用 OpenStreetMap 数据计算路线到酒吧的距离的空间数据处理管道(图片由作者提供)
让我们更仔细地看看“引擎盖下”的内容。我们从 estaty 架构的 5 个抽象层开始(在上面的管道中,仅使用了三种类型的节点):
-
数据源 — 从所需源加载数据,将数据标准化(矢量和栅格)。无论源是什么,这个节点的输出将只有两种可能的变体,从而使其进一步处理得以统一;
-
预处理器 — 对矢量或栅格数据进行预处理,例如分配新的坐标参考系统(CRS);
-
合并 — 以所需方式组合数据。可能的组合方式:矢量数据与矢量数据,栅格数据与栅格数据,矢量数据与栅格数据;
-
分析器 — 库的核心 — 使用对栅格和矢量对象的简单原子变换来执行某些分析,例如面积匹配或路径查找;
-
报告 — 可选的最终分析模块,允许以用户友好的格式生成报告。
通过以特定方式组合这些节点,可以在不修改原始数据处理方式的情况下,为数据分析构建不同的管道——仅通过替换原始数据源或分析方式(参见动画 1)。

动画 1. 库在准备分析管道时的灵活性(动画由作者提供)
因此,图 2 显示在分析中仅使用了三个节点——这是一个相当简单的管道。其工作结果将得到一个 geopandas GeoDataFrame(一个包含对象几何的表格),其中包含线性对象——到我们感兴趣的对象的路线。在这种情况下——到酒吧。结果的可视化将如下所示:

图 3. 路线到酒吧的距离计算(作者提供的图像)
我们有权对获得的线性对象集进行任何操作——例如,找到到最近酒吧的距离,或从样本中请求平均值(在代码中完成):
-
最小长度:308.93 米
-
平均长度:2306.49 米
作为大城市酒吧位置的真正专家,我可以说,在分析区域内,酒吧实际上可能更多。但我们在一开始就同意基于 OSM 数据进行分析,所以……
管道本身与进行分析的数据源无关。这是通过库模块之间的弱关联性来确保的。让我们从 DataSource 节点开始——它具有将数据转换为一种类型的内置机制(例如,这里的矢量数据将以 geopandas GeoDataFrame 格式存在,并且无论我们使用了什么数据源,所有数据都将转换为点、线或多边形)。接下来是一个预处理器,它将自动确定适合我们数据的度量投影。预处理器唯一关心的是数据必须来自节点的 DataSource,其余的应由之前的节点处理。我们的简单管道由一个分析块完成,它可以操作任何几何对象,无论是区域、线性还是点。
因此,我们可以对从 OSM 提取的其他类别的数据进行完全相同的计算(当然,这个列表不仅限于 OSM),例如,对学校、公园、水体、公共厕所、垃圾桶、警察局等进行邻近分析——任何(对于可以通过标签定义的对象类型,请参见 地图特征 或 关于数据源的文档):

图 4. 对学校、公园、水体、垃圾桶、警察局的邻近分析(作者提供的图像)
如何扩展这个方法
我们提到过,任何性质的空间数据都可以进行分析:社会数据、自然对象或人工建筑。让我们更深入地查看源代码。从图中可以看出,距离计算是在考虑道路的路线中进行的,这是合理的,因为通常我们不是乘坐直升机去酒吧,而是步行。因此,“距离”分析包括在道路图上搜索路线,使用的是来自 OSM 的图。因此,算法在以下抽象上搜索路线(我们依赖于以下文章,强烈推荐——OSMnx: Python for Street Networks):

图 5. 步行道路图,用于搜索最佳路线(图片作者提供)
图是地理参考的(具有纬度和经度属性)节点和边。为了找到从对象 A 到对象 B 的最短路径,我们需要执行三个步骤:
-
找到图中离对象 A 最近的节点(我们称之为节点 1)
-
找到图中离对象 B 最近的节点(我们称之为节点 2)
-
使用图遍历算法,找到从节点 1 到节点 2 的最佳路线
如果我们想要获得距离,需要将从节点 1 到节点 2 的路径长度与从对象 A 到节点 1 以及从对象 B 到节点 2 的距离相加。最后两个分量可以通过直线计算,因为节点网络相当密集。但这里有一个难点——如果对象 A 和 B 是点,相对容易找到它们最近的节点(计算点之间的距离)。如果对象 A 的几何形状例如是一个区域,那么我们必须寻找与多边形“最近”的节点。线性对象的情况也是一个需要解决的问题。
那么,让我们回答这个问题:“如何统一计算图节点到不同类型矢量对象的距离:线、点和多边形”?为此,库在开始路线计算之前将对象转换为点类型(计算点之间的距离非常简单)。我们将这种转换称为表示(见图 6)。

图 6. 表示矢量对象的几种方法(图片作者提供)
也就是说, instead of 编写和维护三个算法,我们编写一个(搜索到点的路径),然后将所有初始数据类型简化为一个。如何将它们简化为一个类型是一个模糊的问题。我们可以简单地通过计算质心来做到这一点。然而,在城市地区尤其是对于大型对象(如公园)的计算中,为质心寻找路线可能是不必要的——我们认为,当我们从铺砌的道路进入宜人的泥土公园黑暗中时,我们才真正进入公园,而不是当我们站在公园中心时。
因此,我们决定使用一种方法,在这种方法中,我们找到多边形或线性对象到分析对象(从中建立路径)的最近点。然后,这个点被视为最终的目标节点——到这个节点的进一步路径将在图中构建。
更高级的分析
仅凭前往酒吧的路径是无法令人印象深刻的。因此,让我们把任务复杂化。假设我们想通过结合来自不同来源的数据来进行更复杂的分析。例如,我们想计算在去橡树公园的平均步行距离(我们喜欢在酒吧之后散步,欣赏公园里的树木)。为此,我们将结合来自两个来源的数据:
-
从 GBIF | 全球生物多样性信息设施下载矢量数据(查看
www.gbif.org/,截至 2023 年 10 月 20 日) -
OpenStreetMap — 库中的数据下载本地集成
GBIF 数据是一个点几何的矢量层,显示了发现某些植物和动物(以及其他东西)的位置。在这种情况下,我们将关注橡树(或拉丁文中的 Quercus robur)。当我们将橡树的点层叠加到公园多边形上时,我们的数据将如下所示:

图 7. 结合 OSM 公园数据和 GBIF 数据计算前往橡树林公园的路径(图像由作者提供)
用于分析的代码 长这样:
需要注意的一点是,点不必完全落在公园的多边形内。我们结合来自不同来源的数据,它们可能会略有不同。因此,我们设置了 10 米的缓冲区,并查看结果(图 8)——如果公园多边形与缓冲区多边形有交集,则我们将认为公园中有橡树:

图 8. 前往橡树出现的公园的路径,计算出的距离(图像由作者提供)
关于相关性的更多信息
分析主题需求明显。例如,许多源代码解决方案中都提供了路由功能(在准备这篇文章时,我发现了一个有趣的笔记本“探索用于计算一组起点-终点对路径的路由工具”),以及像 Google Maps 这样的流行用户服务和隐含的地图相关服务。如果谈到邻近分析,这里也有很多实现的解决方案。例如,我们可以提到一个工具,如 pandana。
所有这些工具无疑很有用,但在应用示例中使用它们需要编写一些辅助代码。我们正在尝试为自己的团队制作一个方便的工具,并希望与社区分享我们的发现。在本文介绍的方法中,我们可以提到数据源无关的方法和灵活的数据组合方式。我们欢迎评论和建议。
有用的链接:
-
github.com/wiredhut/estaty— 用于处理空间数据和准备物业分析原型算法的 Python 开源库。该库较新,但我们计划进行开发和改进; -
estaty.readthedocs.io/en/latest/— 文档页面; -
github.com/wiredhut/estaty_examples— 本文示例启动的开源存储库
本文使用的数据集(及许可证):
-
地图数据版权归 OpenStreetMap 贡献者所有,可从
www.openstreetmap.org获取许可证 — 开放数据公共开放数据库许可证 链接更新至 2023 年 10 月 25 日
-
GBIF.org (2023 年 10 月 20 日) GBIF 发生数据下载
doi.org/10.15468/dl.f487j5许可证 — 署名-非商业性使用 4.0 国际 链接更新至 2023 年 10 月 25 日
使用 OpenStreetMap 数据及更多内容的邻近分析故事由 Mikhail Sarafanov 和 Wiredhut 团队 提供
医学自然语言处理的公共基准
原文:
towardsdatascience.com/public-benchmarks-for-medical-natural-language-processing-c7c794ab4d9
对于测量医学自然语言处理的标准任务和相应数据集的一般介绍
·发表于 Towards Data Science ·阅读时间 6 分钟·2023 年 3 月 29 日
--

自然语言处理(NLP)领域近年来发展迅猛。变换器、BERT、GPT 等突破性技术相继出现。各行各业的从业者都在探索如何在其特定业务领域和工作流中利用 NLP 的激动人心的发展[1]。医疗行业就是一个从 NLP 进步中受益匪浅的行业。大量的自由文本医学笔记包含了难以置信的数据见解,这些见解可以帮助提供更好的护理、优化成本和推动医疗创新。为了衡量将 NLP 应用于医疗领域的效果,我们需要良好的基准。本文列出了医学自然语言处理常见任务的标准公共基准。目标是为医疗机器学习从业者提供一个起点,以衡量他们的 NLP 工作。
实体/关系识别
实体/关系识别的任务是检测和分类自由文本中的医学概念及其关系。这是从临床笔记和报告中获取可操作见解的关键步骤。这个任务的标准数据集是生物学与临床实践整合信息数据集(i2b2)[2]。该数据集包含来自几个合作医疗组织的去标识化患者报告,共有 394 份训练报告和 477 份测试报告。标记的医学概念包括问题、治疗和测试。标记的关系包括治疗改善问题、测试揭示问题、问题指示另一个问题等。
这是一个具体的例子:
1 The patient is a 63-year-old female with a 3-year history of bilateral
hand numbness
2 She had a workup by her neurologist and an MRI revealed a C5-6 disc
herniation with cord compression
—----------
# Lines are numbered. Words are indexed starting from 0.
—-----------
# Entity || type
bilateral hand numbness 1:11-13 || problem
a workup 2:2-3 || test
an MRI 2:8-9 || test
a c5-6 disc herniation 2:11-14 || problem
cord compression 2:16-17 || problem
—-----------
# Entity || relation || entity
an MRI 2:8-9 || test reveals problem || ac5-6 disc herniation 2:11-14
an MRI 2:8-9 || test reveals problem || cord compression 2:16-17
a c5-6 disc herniation 2:11-14 || problem indicates another problem || cord compression 2:16-17
只有完全正确的识别才被认为是正确的。这意味着对于一个实体,实体的开始和结束词索引都需要准确;对于一个关系,左实体、右实体和关系都需要准确。最终评估指标基于精确度、召回率和 F1 分数。
语义相似性
语义相似性评估两段医疗文本之间的语义等价性。临床语义文本相似性 (ClinicalSTS) [3] 是此任务的标准数据集。它包含 1642 个训练句子对和 412 个测试句子对。等价性通过 0 到 5 的有序尺度来衡量,0 表示完全不相似,5 表示完全语义等价。最终表现通过预测相似性分数 Y’ 和人工判断 Y 之间的 Pearson 相关性来衡量,计算公式如下(结果越高越好):

Pearson 相关公式
这里有两个具体的例子:
# sentence1
# sentence2
# similarity score
minocycline 100 mg capsule 1 capsule by mouth one time daily
oxycodone 5 mg tablet 1-2 tablets by mouth every 4 hours as needed
3
oxycodone 5 mg tablet 0.5-1 tablets by mouth every 4 hours as needed
pantoprazole [PROTONIX] 40 mg tablet enteric coated 1 tablet by mouth Bid before meals
1
自然语言推断
自然语言推断评估医疗假设如何从医疗前提中推导出来。MedNLI [4] 是这样一个数据集。它包含来自一组已故患者的去标识化医疗历史记录。这些记录被分割成片段,要求专家根据每个片段编写 3 个假设。这 3 个假设是
-
一个明显正确的描述
-
一个明显错误的描述和
-
一个描述可能是真的或假的,
代表前提-假设的 3 种关系:entailment、contradiction 和 neural。数据集包含 11232 个训练对,1395 个开发对和 1422 个测试对。
这里是一个具体的例子:
# sentence1
# sentence2
# relation
Labs were notable for Cr 1.7 (baseline 0.5 per old records) and lactate 2.4
Patient has elevated Cr
entailment
最终的表现可以通过给定前提-假设对的关系分类准确率来衡量。
医疗问题选择回答
医疗问题选择回答模拟了选择题形式的医疗考试。MedQA [5] 是这个目的的标准数据集。其问题来自于美国和中国的医学考试,通过选择正确答案来评估医生。数据集中包含 61097 个问题。
这里是一个具体的例子:
A 57-year-old man presents to his primary care physician with a 2-month
history of right upper and lower extremity weakness. He noticed the weakness
when he started falling far more frequently while running errands. Since then,
he has had increasing difficulty with walking and lifting objects. His past
medical history is significant only for well-controlled hypertension, but he
says that some members of his family have had musculoskeletal problems. His
right upper extremity shows forearm atrophy and depressed reflexes while his
right lower extremity is hypertonic with a positive Babinski sign. Which of
the following is most likely associated with the cause of this patient’s
symptoms?
A: HLA-B8 haplotype
B: HLA-DR2 haplotype
C: Mutation in SOD1 [correct]
D: Mutation in SMN1
E: Viral infection
从机制上讲,这个任务可以被视为一个评分系统,其中输入是 question+answer_i,输出是一个数值分数。得分最高的 answer_i 将是最终答案。性能可以通过对数据集的 80/10/10 划分的准确性来衡量。这为模型和人工专家的表现提供了一个可比的基准。
医疗问题回答
医学问答是最复杂的医学 NLP 任务。它要求模型为给定的医学问题生成长篇自由文本答案。emrQA [6] 是这个目的的典型数据集。它有 40 万对问答对。仅依靠人工专家的手工工作来获取这样一个数据集会非常昂贵。因此,emrQA 实际上是半自动生成的
-
首先对医学专家进行关于常见问题的调查,
-
然后用占位符替换这些问题中的医学概念,从而创建问题的模板,
-
最终使用标注的实体-关系(如 i2b2)数据集来建立临床背景、填写问题并生成答案。
这是一个具体的例子:
Context: 08/31/96 ascending aortic root replacement with homograft with
omentopexy. The patient continued to be hemodynamically stable making good
progress. Physical examination: BMI: 33.4 Obese, high risk. Pulse: 60\. resp.
rate: 18
Question: Has the patient ever had an abnormal BMI?
Answer: BMI: 33.4 Obese, high risk
Question: When did the patient last receive a homograft replacement?
Answer: 08/31/96 ascending aortic root replacement with homograft with omentopexy.
从机械的角度来看,这项任务可以视为一种语言生成任务,其中输入是 context+question,输出是 answer。最终的表现通常可以通过数据集的 80/20 拆分来衡量,并通过准确匹配和 F1 分数来评估。准确匹配测量与准确真实值匹配的预测百分比。F1 分数测量预测与真实值之间的“重叠”。在这种设置下,预测和真实值都被视为一个令牌包,其中可以计算真正的/假阳性/假阴性。
结论
研究人员和从业者继续在医学领域积极应用自然语言处理(NLP)。虽然看到这种热情令人兴奋,但拥有公开且可重复的基准来衡量这些应用的表现非常重要。这篇博客文章列出了典型任务、相应的公开数据集以及适用的度量标准,这些可以用来量化新医学 NLP 应用的潜在改进。
参考文献
[1] 如何在自己的领域中使用大型语言模型(LLM) towardsdatascience.com/how-to-use-large-language-models-llm-in-your-own-domains-b4dff2d08464
[2] 2010 i2b2/VA 挑战:临床文本中的概念、断言和关系 www.ncbi.nlm.nih.gov/pmc/articles/PMC3168320/
[3] 2019 n2c2/OHNLP 临床语义文本相似性追踪:概述 www.ncbi.nlm.nih.gov/pmc/articles/PMC7732706/
[4] MedNLI — 临床领域的自然语言推断数据集 physionet.org/content/mednli/1.0.0/
[5] 这个病人得了什么病?来自医学考试的大规模开放域问答数据集 arxiv.org/abs/2009.13081
[6] emrQA:用于电子病历问答的大型语料库 arxiv.org/abs/1809.00732
Pump & Dump 萨拉米浓缩咖啡 shot
咖啡数据科学
切分 shot 以了解新的东西
·
关注 发表在 Towards Data Science · 3 分钟阅读 · 2023 年 3 月 17 日
--
咖啡的萃取过程随着时间的推移而发生变化,这一变化贯穿整个浓缩咖啡的萃取过程。为了更好地理解这一点,人们使用了“萨拉米 shot”。制作“萨拉米 shot”时,需要在浓缩咖啡过程中使用多个杯子。我想看看我的新 Pump & Dump 配方与单个颗粒大小的萃取效果相比如何。
萨拉米 shot 可以用来测量萃取效果,并更好地理解何时不同的味道成分被萃取到杯中。
总溶解固体 (TDS) 是使用折射仪测量的,该数字与 shot 的输出重量和咖啡的输入重量结合使用,以确定提取到杯中的咖啡百分比,称为 提取率 (EY)。
数据
首先,我萃取了一次,并将其与我在 之前的数据 中的筛过咖啡进行了比较。筛过的咖啡使用了 4 ml/s 流速控制配置,因此能够减少变量。我还分别处理了内部细度和外部细度,并且发现内部细度提取速度极快。

Pump & Dump 配置提取的细度范围在 <300 微米的内部细度中,这意味着即使是较粗的颗粒也被提取得比之前的数据快得多。

然后我筛选了咖啡粉,进行了与之前类似的操作,使用了 8 克筛过的咖啡粉和 13 克使用后的咖啡粉。然而,我没有分开内部和外部细度。只有粒径大于 500 微米的颗粒提取速度比整个资料要慢。

当我们将 Pump & Dump 的细度与 4 ml/s 流速实验中的内部细度进行比较时,前者的萃取速度要高得多。

其他粒度的情况也是如此。


在观察 EY 发生的百分比时,Pump & Dump 配置在 1:1 比例中提取了大部分咖啡。


这次 salami 咖啡测试进一步展示了该配置在咖啡萃取中的效率。这种配置还表明,如果我们更好地理解咖啡萃取的动态,我们可以显著改善萃取效果。我特别感兴趣的是如何观察热能或热量在咖啡饼中的传递,因为该配置也旨在通过蒸汽加热咖啡粉。
如果你喜欢,可以在 Twitter、YouTube 和 Instagram 上关注我,我会在这些平台上发布不同机器上的意式浓缩咖啡镜头和相关内容。你还可以在 LinkedIn 上找到我,也可以在 Medium 和 Subscribe 上关注我。
我的进一步阅读:
南瓜香料时间序列分析
穿上你最舒适的低保真音乐,拿上宽大的毛衣和你最爱的热饮,让我们开始使用 Python 吧。
·
关注 发表于Towards Data Science ·8 分钟阅读·2023 年 10 月 24 日
--
照片由Nathan Dumlao提供,来源于Unsplash
在北半球,又到了那个时候——苹果、南瓜以及各种肉桂、豆蔻、姜、全香料和丁香的搭配开始出现。随着超市开始为万圣节、感恩节和冬季假期做准备,这是一个重拾我统计建模技能的好时机。抓紧你的调味拿铁,让我们进行一些以函数为导向的季节性建模。完整代码笔记本可以在这里找到。
假设:
“南瓜香料”作为美国的 Google 搜索词,其流行度会有强烈的季节性,因为它与美国秋季节日和季节性食物有关。
零假设:
使用上周或去年数据将更能预测本周“南瓜香料”搜索词的流行程度。
数据:
来自 Google 趋势的过去 5 年数据,提取于 2023 年 10 月 7 日。 [1]
迭代建模方法:
-
制作一个简单模型,其中上周/去年数据作为本周的预测。具体而言,仅仅让我的最终模型在真空中准确或不准确是不够的。我的最终模型必须优于使用历史数据作为直接预测的表现。
-
训练集和测试集的划分将给我两个数据集,一个用于算法学习,另一个用于测试算法的表现。
-
季节性分解将帮助我大致了解数据的可预测性,通过尝试将年度总体趋势与季节性模式和噪声分离来实现。较小规模的噪声意味着更多的数据可以被算法捕捉到。
-
一系列统计测试以确定数据是否是平稳的。如果数据不是平稳的,我需要进行一次差分(运行时间差函数,其中每个时间间隔的数据仅显示与前一个时间间隔数据的差异。这将迫使数据变得平稳。)
-
制作一些 SARIMA 模型,利用自相关图的推断来确定移动平均项,利用部分自相关图的推断来确定自回归项。SARIMA 是时间序列建模的首选,我将在尝试使用 Auto Arima 进行强力搜索之前,尝试 ACF 和 PACF 推断。
-
尝试使用 Auto Arima,它将遍历许多项并选择最佳的项组合。我希望通过实验了解 SARIMA 模型的参数是否会带来更好的模型性能。
-
尝试 ETS 模型,利用季节性分解的推断来判断 x 是随时间的加性还是乘性。ETS 模型比 SARIMA 模型更注重季节性和总体趋势,可能在捕捉南瓜香料与时间的关系时给我带来优势。
性能绘图关键绩效指标(KPIs):
-
尝试使用 MAPE 评分,因为它在许多工作场所中是行业标准,大家可能已经习惯了。它易于理解。
-
尝试使用 RMSE 评分,因为它更有用。
-
将预测结果与测试数据绘制在图表上,并通过视觉检查性能。
目视检查数据:

图片由作者提供。
从上述图表中可以看出,这些数据展示了强大的季节性建模潜力。每年的下半年有明显的峰值,然后是一个渐弱的趋势,接着在降到基准线之前又有一个峰值。
然而,除了 2021 年外,每年的主要峰值都在增加,这也是合情合理的,因为在疫情期间,人们可能没有考虑庆祝季节。
导入:
注意:这些导入在笔记本中显示得有所不同,因为在笔记本中我依赖于seasonal_mod.py,其中包含了许多我的导入。
作者提供的图像。
这些是我用来制作代码笔记本的库。我选择了 statsmodels 而不是 scikit-learn,因为 statsmodels 的时间序列包更符合我的需求,我对大多数线性回归问题更喜欢 statsmodels。
基于函数的方法:
我不知道你是否一样,但我不想每次创建新模型时都写几行代码,然后再写更多代码来验证。因此,我制作了一些函数,以保持我的代码简洁并防止我出错。
作者提供的图像。
这三个小函数协同工作,所以我只需用metrics_graph()运行y_true和y_preds作为输入,它会给我一条真实数据的蓝线和一条预测数据的红线,以及 MAPE 和 RMSE。这将节省我的时间和麻烦。
使用去年的数据作为成功的基准:
我的零售管理经验促使我尝试使用上周的数据和去年的数据作为对今年数据的直接预测。在零售中,我们通常使用上个季节(1 个时间单位前)的数据作为直接预测,以确保例如黑色星期五期间的库存。上周的数据表现不如去年的数据。

作者提供的图像。
上周的数据预测本周的数据显示 MAPE 得分略高于 18,RMSE 约为 11. 相比之下,去年的数据作为对今年数据的直接预测显示 MAPE 得分约为 12,RMSE 约为 7。

作者提供的图像。
因此,我选择将我构建的所有统计模型与使用去年的数据的简单模型进行比较。这个模型在峰值和下降的时间上比我们的简单周模型更准确,然而,我仍然认为我可以做得更好。建模的下一步是进行季节性分解。
以下函数帮助我运行了季节性分解,我将把它作为可重用的代码用于未来所有的建模工作。
作者提供的图像。
下面展示了我如何使用这个季节性分解。

作者提供的图像。
加法模型在残差中表现出每年的重复模式,这表明加法模型无法完全分解所有的重复模式。这是尝试乘法模型来处理年度峰值的一个很好的理由。

作者提供的图像。
现在乘法分解中的残差更有希望。它们变得更随机且规模更小,证明了乘法模型能更好地捕捉数据。残差如此之小——在 1.5 到-1 的范围内,这意味着建模具有很大的潜力。
但现在我想要一个专门运行 SARIMA 模型的函数,只输入顺序。我还想实验运行c、t和ct版本的 SARIMA 模型,因为季节性分解更倾向于乘法模型而非加法模型。通过在trend =参数中使用c、t和ct,我能够为 SARIMA 模型添加乘数。
图片由作者提供。
我将跳过描述我查看 AFC 和 PACF 图的部分以及我尝试 PMD 自动 ARIMA 以找到 SARIMA 模型中最佳项的部分。如果你对这些细节感兴趣,请查看我的完整代码笔记本。
我的最佳 SARIMA 模型:

图片由作者提供。
所以,我最好的 SARIMA 模型的 MAPE 分数比我的基准模型高,接近 29 对比 12,但 RMSE 却低了大约一个单位,从接近 7 降到接近 6。使用这个模型最大的问题是它对 2023 年高峰的预测严重不足,从 2023 年 8 月到 9 月,红线和蓝线之间的误差区域较大。根据你对 RMSE 和 MAPE 的看法,可能会有理由更喜欢它或不如年度基准模型。然而,我还没完。我最终的模型确实比我的年度基准模型好。
最终模型:
我使用了一个 ETS(指数平滑)模型作为最终模型,这让我可以明确使用seasonal参数,使其采用乘法方法。

图片由作者提供。
现在你可能会想,“但这个模型的 MAPE 分数比年度基准模型高。” 你说得对,大约高了 0.3%。不过,我认为这是一种非常公平的交换,因为我的 RMSE 从 7 降低到了大约 4.5。虽然这个模型在 2022 年 12 月的表现比我最好的 SARIMA 模型差一点,但它在那个高峰期的误差比 2023 年秋季的更小,而我更关心的是后者。你可以在这里找到那个模型。
进一步验证:
我将等到 2024 年 10 月 7 日,再进行一次数据提取,看看模型在去年的数据上的表现如何。
结论:
总结一下,我能够证伪零假设,我的最终模型优于朴素的年度模型。我证明了 Google 上的南瓜香料受欢迎程度具有很强的季节性并且可以预测。在朴素、SARMA 模型和 ETS 模型之间,ETS 更好地捕捉了时间与南瓜香料受欢迎程度之间的关系。南瓜香料与时间的乘法关系表明,南瓜香料的受欢迎程度基于除了时间之外的一个或多个独立变量,表达式为 time * unknown_independant_var = pumpkin_spice_popularity。
我学到了什么以及未来的工作:
我的下一步是使用某种版本的 Meta’s graph API 来查找“南瓜香料”在商业文章中的使用情况。我想知道这些数据与我的 Google 趋势数据的相关性如何。我还了解到,当季节性分解指向乘法模型时,我会在过程中更早地选择 ETS。
此外,我对自动化这整个过程很感兴趣。理想情况下,我希望构建一个 Python 模块,其中输入是直接来自 Google Trends 的 CSV 文件,输出可以是一个可用的模型,并且文档足够好,使非技术用户能够创建和测试他们自己的预测模型。对于用户选择难以预测的数据(例如,朴素或随机游走模型可能更适用)的可能性,我希望构建该模块以向用户解释这一点。然后,我可以利用该模块从应用程序中收集数据,以展示未测试数据中的季节性发现。
请关注明年南瓜香料季节的那个应用程序!
[1] Google Trends, N/A (www.google.com/trends)
推动双塔模型的极限
原文:
towardsdatascience.com/pushing-the-limits-of-the-two-tower-model-a577090e5140
双塔模型架构背后的假设在哪些地方失效——以及如何超越这些限制
·发表于 Towards Data Science ·阅读时长 8 分钟·2023 年 12 月 10 日
--

(图像由作者使用生成式 AI 创建)
双塔模型 是现代推荐系统中最常见的架构设计选择之一——关键思想是一个塔学习相关性,第二个浅层塔学习观察性偏差,如位置偏差。
在这篇文章中,我们将仔细审视双塔模型背后的两个假设,特别是:
-
因式分解假设,即假设我们可以简单地将两个塔计算的概率相乘(或将它们的对数相加),以及
-
位置独立性假设,即假设决定位置偏差的唯一变量是项目本身的位置,而不是影响它的上下文。
我们将看到这两个假设在哪些地方失效,以及如何通过新算法如 MixEM 模型、点积模型和 XPA 超越这些限制。
让我们从一个非常简短的提醒开始。
深入探讨用于去偏排名模型的最新技术
towardsdatascience.com
双塔模型:迄今为止的故事
推荐系统中排名模型的主要学习目标是相关性:我们希望模型在给定上下文的情况下预测最佳的内容。这里,上下文简单地指我们从用户的先前互动或搜索历史中学到的一切,具体取决于应用场景。
然而,排名模型通常表现出某些观察偏差,即用户根据展示方式对印象的参与程度的倾向。最突出的观察偏差是位置偏差——用户更倾向于与最先展示的项目进行互动。
双塔模型的关键思想是并行训练两个“塔”,即神经网络,一个主塔用于学习相关性,另一个浅层塔用于学习数据中的各种观察偏差。然后可以将两个塔的对数值相加以计算最终预测,如 YouTube 的“Watch Next”论文中所述:

(赵等 2019)
基础假设是,通过为偏差设置专门的塔,主塔可以专注于主要的学习目标,即相关性。实际上,经验上,双塔模型已经被证明在建模方面带来了显著的改进:
-
华为的 PAL 在华为应用商店的点击率提高了 25%。
-
YouTube 的双塔加性模型将互动率提高了 0.24%,并且
-
AirBnb 的双塔模型将预订率提高了 0.7%。
简而言之,双塔模型通过将学习目标分解为相关性和偏差来工作,并已被证明在整个行业的排名模型中带来了显著的改进。
分解假设
双塔模型依赖于分解假设,即我们可以将点击预测分解为
p(click|x,position) = p(click|seen,x) x p(seen|position, ...),
即,在用户观察到该项目的情况下的点击概率(第一个因素)与在给定位置(以及其他观察特征)的情况下观察到该项目的概率的乘积。YouTube 将其重新表述为对数值的和,而不是概率的乘积,这在大致上是等效的:
logit(click|x,position) = logit(click|seen,x) + logit(seen|position, ...)
然而,很容易看出这个分解假设在哪里破裂。例如,考虑一个训练数据包含两种不同类型用户的场景,类型 1 和类型 2:
-
类型 1 用户总是点击他们被展示的第一个项目。他们急于获得即时奖励。
-
类型 2 用户总是滚动浏览他们被展示的项目,直到找到他们确切需要的内容。他们耐心且挑剔。
现在,我们的分解假设破裂了,因为p(seen|position)在数据中不是常量,而是取决于用户群体!当前模型无法建模由具有不同点击行为和偏见的用户群体混合生成的数据集。
因此,我们应该做些其他的事情,而不是分解。那是什么呢?最近的 Google论文《重新审视双塔模型以进行无偏排序学习》中提出了两个解决方案:
-
点积模型,以及
-
Mix-EM 模型。
让我们更详细地了解它们的工作原理。
点积模型的核心思想是让两个塔模型不输出概率或 logits,而是输出嵌入,然后通过点积将两个嵌入组合起来:
p(click|x,position) = Dot(relevance_embedding, bias_embedding)
之所以有效,是因为嵌入表示比单一的 logit 更具表达能力,它们允许我们在嵌入空间的不同维度中编码不同用户的偏见程度。
在Mix-EM模型中,我们不是使用单一的双塔模型,而是使用多个(作者考虑了 2 个),然后应用期望最大化(EM)算法,其中
-
在 E 步中,我们将训练样本分配给一对双塔模型中的一个,
-
在 M 步中,我们最小化这一对双塔模型的总体损失。
这种方法背后的假设是,不同的双塔模型最终将学会处理不同的用户群体(例如我们上面介绍的类型 1 和类型 2 用户)。
作者在 Google Chrome Web 商店的推荐中测试了这两种算法,并发现
-
使用点积模型,NDGC@5 提升了+0.47%。
-
使用 MixEM,NDGC@5 提升了+1.1%。
相较于标准的双塔加性模型,这个结果证实了分解假设确实不适用于这个问题,并量化了我们由于这种失败而损失的性能。点积模型和 MixEM 模型的效果更好,因为它们是更具表达力的方式来建模两个塔的组合,MixEM 比点积模型更好。
独立假设
标准双塔模型背后的另一个假设是,在位置 p 展示的项目与邻近位置 p-1、p+1 等位置展示的项目是独立的。
然而,这并非现实世界的情况,最近的 Google 论文跨位置注意力以去偏见点击的作者认为。点击概率不仅依赖于显示的项目,还依赖于附近显示的内容,尤其是在项目具有水平和垂直位置的情况下,例如在 Google Chrome 商店中。
为了超越这一限制,作者提出了 XPA,即跨位置注意力。其工作原理如下:
-
偏差塔的输入不仅仅是当前项目的位置,还包括所有邻近项目的位置,这些位置被表示为一个单一的嵌入向量。
-
相关性塔的输入不仅仅是当前项目的特征向量,还包括所有邻近项目的特征向量,这些特征向量被表示为一个单一的嵌入向量。
那么关键问题是如何用单一嵌入向量表示所有邻近项目?这通过交叉注意力层来实现,该层首先使用缩放的 softmax 计算注意力权重:

然后计算邻近项目的嵌入(用波浪号表示)作为加权和,其中权重是注意力权重:

这里有一个架构图显示其工作原理——注意这两个塔不仅仅接收单一项目作为输入,而是一次处理多个项目,因为它查看了每个项目的所有邻居:

最终预测是四个项的组合,其中包括两个相关性塔的输出(分别针对项目本身及其邻居)和两个偏差塔的输出(同样分别针对项目本身及其邻居),经过一个 sigmoid 函数进行缩放。
让我们看看结果。在来自 Chrome Web Store 的生产数据中,作者发现
-
XPA 相较于基础的天真模型(完全不使用位置)提高了 +11.8% 的点击率。
-
XPA 相较于标准双塔模型提高了 +6.2% 的点击率。
6.2% 是一个巨大的提升——通过这一点,作者证明了不仅仅利用项目的位置,还利用显示在其附近的项目的有效性。
观察交叉注意力矩阵 a_jk 作为位置 j 和 k 的函数也是很有趣的。(“位置”在这里指的是 Chrome Web Store UI 中的位置,从左上到右下进行编号。)

矩阵中存在的非对角线元素证明了需要交叉注意力来建模这些数据——一个普通的双塔模型只能建模对角线元素,而不能处理这些非对角线元素。非对角线注意力权重的存在是因为如果用户点击了 Chrome Store 中某一行的一个项目,他们相对更可能点击同一行中的另一个项目。
收获
让我们回顾一下。双塔模型通过允许将学习目标分解为相关性 + 偏差,来改进推荐系统背后的排名模型。
因子分解假设是指我们可以简单地将两个塔的 logits 相加(或将它们的概率相乘)。这个假设在我们考虑到数据中存在不同用户群体时就会破裂,每个群体展示了不同类型和程度的观察偏差。谷歌的 MixEM 和点积模型已被证明能突破这一限制。
独立假设是指观察偏差仅依赖于项目的位置,而不依赖于邻近项目。这个假设在现实应用中会破裂,因为用户参与确实依赖于项目的邻域,而不仅仅是它的位置。XPA 利用来自邻近位置的交叉位置注意力,是突破这一限制的算法。
总结一下,标准的双塔模型假设用户都是相同的,位置也是相同的——MixEM(以及点积模型)突破了第一个限制,而 XPA 突破了第二个限制。
想要以深入了解最新机器学习技术和突破来打动同行或在下一个机器学习面试中表现出色吗? 订阅我的新闻通讯!
对 AI 驱动的数据分析师进行测试
原文:
towardsdatascience.com/putting-an-ai-powered-data-analyst-to-the-test-e8971337264
探索使用 Langchain 和 OpenAI 自动化临时分析
·发表于 Towards Data Science ·阅读时长 8 分钟·2023 年 7 月 12 日
--

作者提供的图片:由 Midjourney 生成
背景 — 对高效分析的需求
在我看来,由于临时请求的巨大数量,分析一直是最难操作的领域之一。通常,这涉及编写 SQL 查询或在电子表格上进行一些分析,这些操作往往花费的时间比预期的要长。这导致分析团队大部分时间都在解决紧急问题、构建战术解决方案,而没有机会进行前瞻性的工作。
我经常考虑一个能够处理临时分析请求的 AI 助手的想法,这有点类似于如今在客户服务中无处不在的聊天机器人。然而,由于某些分析查询的复杂性,这总感觉有些难以实现。现在,随着生成 AI 的进步,我们已经到了自动化枯燥的临时请求的阶段。在这篇文章中,我展示了一个原型分析机器人。我评估了机器人在一些“典型”分析请求上的表现,并简要讨论了其对商业分析的影响。
AI 驱动的数据分析师
分析机器人通过类似聊天的界面作为 AI 数据分析师服务。任何需要临时分析的人都可以像向数据分析师提问一样向机器人提问。机器人将自然语言的查询转换为 SQL,并查询数据以生成自然语言的回答。我在文章末尾提供了机器人的链接;你需要一个 OpenAI API 密钥和数据源 URL 才能尝试它。
一些技术细节 — 如何构建你自己的
构建自己的原型分析机器人可以通过七行 Python 代码轻松实现。机器人的核心功能围绕Langchain和OpenAI(虽然你可以利用任何大型语言模型来完成任务)。然而,如果你使用 OpenAI,你需要一个 API 密钥,你可以在这里注册。该机器人使用SQLDatabaseChain API,该 API 利用 SQLAlchemy 连接到 SQL 数据库。它可以与 SQLAlchemy 支持的任何 SQL 方言一起使用。你可以在 Jupyter 笔记本中使用以下代码构建一个基本的机器人:
测试分析机器人
我在一个来自Kaggle的 CO2 排放数据集上测试了分析机器人,该数据集记录了各种车辆的排放、发动机大小和燃油消耗数据。
请注意,数据在 开放数据公约许可证下,适用于商业和非商业目的。
该测试包含三组难度不同的问题,旨在评估机器人分析能力。
初级水平:这些是直接的单变量统计,需要初级分析推理。由于所有分析师需要理解的内容都在数据集中,因此不需要领域知识。

作者插图:展示分析师接收初级查询时的思维过程
中级水平:这些涉及双变量分析,需要额外的步骤从数据中计算更多变量,然后得出答案。中级查询与初级查询之间的主要区别在于撰写 pandas 查询时所需的额外计算步骤。

作者插图:展示分析师接收中级查询时的思维过程
高级水平:这些问题需要对领域背景有一定的理解才能正确回答。请注意,图示中的实际 Python 查询与初级水平相同,但思维过程更加复杂。

作者插图:展示分析师接收高级查询时的思维过程
我已经像分析师一样手动完成了每个测试,使用的是 pandas。我的测试响应的笔记本链接在这里。你应该确认机器人响应与我的一致。
初级水平查询
问题 1 — 哪辆车的 CO2 排放量平均最高?

作者提供的图像:对“哪辆车的 CO2 排放量平均最高?”的回答
问题 2 — 哪辆车的 CO2 排放量平均最低?

作者提供的图像:对“哪辆车的 CO2 排放量平均最低?”的回答
在每种情况下,机器人都返回了正确的答案,展示了基本的数据处理技能——这是一个初级数据分析师应具备的能力。
中级查询
问题 1 — 哪辆车的排放量与发动机尺寸的比例最高?

作者提供的图像:对“哪辆车的排放量与发动机尺寸的比例最高?”的回答
问题 2 — 哪辆车的排放量与发动机尺寸的比例最低?

作者提供的图像:对“哪辆车的排放量与发动机尺寸的比例最低?”的回答
机器人返回了正确的答案,展示了应对稍微复杂的查询的能力,但仍然相对基础。这一次,机器人需要创建一个比例变量,然后对表格进行排序以找到最高和最低值。
高级查询
问题 1 — 哪辆车最省油?

作者提供的图像:对“哪辆车最省油?”的回答
问题 2 — 哪辆车最省油?

作者提供的图像:对“哪辆车最省油?”的回答
问题 3 — 哪辆车对环境最差?

作者提供的图像:对“哪辆车对环境最差?”的回答
机器人正确回答了所有三个问题。这些问题的不同之处在于它们需要一定程度的领域知识。机器人必须“理解”燃油效率和环境影响才能回答这些问题。一个擅长数据处理但缺乏这些主题知识的分析师可能会感到困难。这些类型的高级查询通常由产品经理甚至 CXO 级别的利益相关者提出。
商业影响
让我们探讨一下实施分析机器人的一些商业影响。
向工程方向转变:如果你是分析师,可能会担心 AI 带来的冗余。然而,我想借此机会向你保证,我认为这种情况不会发生——尽管数据分析师的角色将会演变。我认为这种情况将会发生。为了使这些 AI 机器人有效运作,它们需要干净且经过精心策划的数据源进行查询,就像数据分析师所需的那样。因此,我预见分析角色将更多地转向数据工程或分析工程。这将涉及确保数据生成过程符合标准,集中于策划、扩展和获取高质量的数据源,而不是临时分析。
按需洞察: 产品经理、投资组合主管、CEO,或者任何需要洞察的人,你们的挫败感将得到缓解。这将是一个渐进的过渡,因为需要进行必要的工程工作,但这些机器人将使获取有关你业务的快速洞察变得比以往任何时候都更容易。然而,强大的力量伴随着巨大的责任。洞察的消费者将有责任接受良好的培训,解读这些洞察,提出正确的问题,并做出数据驱动的决策。这将把我们带入决策科学的领域。
机器学习和数据科学的进步:随着分析资源被重定向到数据策划、获取和大规模管理,产生的副产品是数据科学和机器学习的实现。这些精心设计的数据管理结构正是数据科学家和机器学习工程师构建定制化机器学习解决方案所需的,从而加速了 AI 在企业中的应用。
自己尝试
欢迎自己尝试分析机器人。你需要一个 API 密钥,建议你链接到托管在 GitHub 上的数据源。如果愿意,可以使用CO2 排放数据集进行尝试。你需要呈现 GitHub 文件的原始链接,如下所示:
https://raw.githubusercontent.com/john-adeojo/open_datasets/main/CO2%20Emissions_Canada.csv
观看实时演示
作者视频:测试我们基于 AI 的数据分析师
结论
尽管仅在小样本上进行测试,分析机器人已经展示了 AI 在自动化临时分析查询中的潜力,这些查询是许多商业分析团队的痛点。分析的下一步挑战将是有效的数据管理,要求分析师向更具工程导向的角色过渡。决策者也必须进化,学习决策科学的学科,以帮助他们提出正确的问题并做出数据驱动的决策。
如果你有兴趣在商业环境中部署分析机器人解决方案,有几个考虑因素需要注意:
-
数据隐私:该原型利用了 OpenAI API。在使用任何类似解决方案时,当数据传输到第三方时,必须优先考虑数据隐私。
-
扩展:该原型设计为一次在内存中保存一个数据集,不能在关系数据集上执行查询。在商业环境中实施这一点将需要考虑规模上的基础设施要求。
-
数据管理:我之前提到过这一点,但数据管理在这里将是关键。数据集应经过精心策划,并且必须彻底理解数据生成过程,以使任何见解都具有意义。
-
虚假信息:这些机器人有产生不准确回答的倾向,并且很难量化这种情况发生的频率。确实,人类分析师也会犯错。寻求使用这些机器人的决策者需要意识到它们的虚假倾向,学会以健康的怀疑态度看待回答,并进行适当的审查。
关注我的 LinkedIn
订阅 Medium 以获取更多来自我的见解:
[## 通过我的推荐链接加入 Medium — John Adeojo
我分享数据科学项目、经验和专业知识,帮助你在旅程中前进。你可以通过以下方式注册 Medium…
johnadeojo.medium.com](https://johnadeojo.medium.com/membership?source=post_page-----e8971337264--------------------------------)
如果你有兴趣将人工智能或数据科学整合到你的业务运营中,我们邀请你安排一次免费的初步咨询:
通过免费的咨询了解我们在帮助企业实现雄心壮志目标方面的专业知识。我们的数据科学家和…
www.data-centric-solutions.com](https://www.data-centric-solutions.com/book-online?source=post_page-----e8971337264--------------------------------)
对你的预测模型进行测试:回测指南

使用 Midjourney 生成的图像
通过回测学习如何正确评估时间序列模型的性能
·发布于 Towards Data Science ·9 分钟阅读·2023 年 11 月 8 日
--
评估时间序列模型不是一件简单的事。事实上,在评估预测模型时,很容易犯下严重的错误。虽然这些错误可能不会破坏代码或阻止我们获取一些输出数字,但它们可以显著影响这些性能估计的准确性。
在本文中,我们将演示如何正确评估时间序列模型。
为什么标准的机器学习方法不适用于时间序列?
评估机器学习模型性能的最简单方法是将数据集拆分为两个子集:训练集和测试集。为了进一步提高性能估计的稳健性,我们可能需要多次拆分数据集。这一过程称为交叉验证。
以下图示表示了最流行的交叉验证类型之一——k 折方法。在 5 折验证的情况下,我们首先将数据集划分为 5 个部分。然后,我们使用其中的 4 个部分训练模型,并在第 5 个部分上评估其性能。这个过程会重复 4 次,每次用不同的部分进行评估。

5 折交叉验证
基于图示,你可能会发现使用这种方法进行预测的问题。在大多数情况下,我们会使用在评估集之后按时间顺序出现的数据来训练模型。这会导致数据泄漏,我们应当绝对避免。潜在的风险是,模型可能会学习到未来的模式,而这些模式在过去尚未显现出来。这会导致过于乐观的性能估计。
K 折交叉验证以及许多其他方法都假设观察值是独立的。时间序列数据中的时间依赖性显然与这一假设不符,这使得大多数在回归或分类中流行的验证方法变得不可用。因此,我们必须使用专门针对时间序列数据的验证方法。
注意:Bergmeir 等人展示了在纯自回归模型的情况下,只要所考虑的模型具有不相关的误差,使用标准的 K 折交叉验证是可能的。你可以 在这里 阅读更多相关内容。
什么是回测?
回测(也称为后向预测或时间序列交叉验证)是一组旨在满足时间序列特定要求的验证方法。与交叉验证类似,回测的目标是获得模型部署后的可靠性能估计。我们还可以使用这些方法进行超参数调整和特征选择。
回测的想法是复制一个现实的场景。训练数据应对应于在做出预测时可用于训练模型的数据。验证集应反映我们在部署该模型后可能遇到的数据。
下面我们展示了一种称为向前验证(或扩展窗口验证)的方法图示,它遵循了我们刚刚描述的特征。在每个后续时间点,我们有更多的数据来训练我们的模型,相应地,我们的测试集也按相同的时间间隔前移。这种类型的验证保持了时间序列的时间顺序。

向前验证(扩展窗口)
向前验证是回测中最简单的方法。我们可以考虑一些它的修改,以更好地适应我们的使用案例:
-
我们假设了一个扩展窗口。然而,我们可能希望仅使用时间序列的最新子集来训练模型。那么,我们应该使用固定大小的滚动窗口。
-
在重新拟合模型时,我们可以采取各种策略。在最简单的情况下,我们在每次回测迭代中重新拟合模型。或者,我们可以只在第一次迭代中拟合模型,然后使用已拟合的模型(可能具有更新的特征)进行预测。或者我们可以每 X 次迭代重新拟合一次。再次强调,我们应该选择一个与模型的实际使用情况紧密契合的解决方案。
-
我们可以在训练集和验证集之间引入一个间隔,因为验证集的初始部分可能与训练集的最终部分高度相关。通过创建一个间隔(例如,通过移除接近验证集的训练观察值),我们增强了两个数据集之间的独立性。这个过程也称为清洗。
-
处理多个时间序列(如不同产品的销售)可能会带来另一种复杂性。由于数据集中的时间序列可能彼此相关(至少在某种程度上),我们可能希望将每个序列保留在特定的折叠中,以防止信息泄露。有关更多信息,请参阅参考文献部分提到的链接。
在文章的下一部分,我们将演示如何使用最简单的前向验证案例创建自定义回测类。我强烈鼓励你自己尝试其他可能性,或使用流行的专注于时间序列预测的 Python 库的回测功能。

使用 Midjourney 生成的图像
实操示例
首先,我们导入所需的库。由于我们要创建一个自定义回测类,因此将使用标准库。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib as mpl
import seaborn as sns
from sklearn.metrics import mean_absolute_error, mean_squared_error
from sklearn.linear_model import LinearRegression
from sklearn.ensemble import RandomForestRegressor
生成数据
为了简化起见,我们将生成一个跨度为 4 年的每日时间序列。
稍后,我们将测试几个模型,包括两个机器学习模型。为此,我们将创建按月生成的虚拟变量来考虑季节性。有关这种方法的更多细节,请参阅这篇文章。
定义回测器类
自定义回测器类相当长,因此我们将首先查看这里的代码,然后逐部分分析它。
让我们从输入开始:
-
pred_func— 一个根据训练数据和所需特征生成预测的函数。我们决定使用这种方法,因为我们希望保持类的灵活性,并允许用户使用他们选择的任何机器学习库/框架。 -
start_date— 回测的开始日期。 -
end_date— 回测的结束日期。 -
backtest_freq— 我们应该多频繁地训练模型并创建预测。例如,提供“7D”会使我们每周从开始日期起创建一组新的预测。 -
data_freq— 数据的频率。我们使用此参数来为正确的日期创建预测。 -
forecast_horizon— 与data_freq结合使用,我们使用这个参数来确保我们为所需的范围创建预测。
在run_backtest方法中,我们迭代回测中的每一个预测日期。对于每个日期,我们将训练集(包含进行预测时所有可用信息)与验证集(由预测范围决定)分开。随后,我们生成预测并将预测值与实际值一起存储。在最后一步,我们将所有单独的数据帧合并为一个数据帧,其中包含整个回测期间做出的所有预测。
在evaluate_backtest方法中,我们使用先前生成的 DataFrame 来计算各种评估指标。这些指标可以通过提供一个包含指标名称及其计算函数的字典来指定。然后,我们分别计算每个预测期的请求指标,并计算整体结果。
evaluate_backtest方法的第一步是检查是否有回测的 DataFrame(它在我们使用run_backtest方法后才会变得可用)。如果没有,我们需要先实际运行回测。
运行回测
现在是时候运行回测了。我们将比较四个“模型”的表现:
-
朴素预测:在这种方法中,预测等于做出预测时最后已知的值。
-
平均预测:这个预测等于训练集的均值。
-
一个带有月份虚拟变量的线性回归模型。
-
一个带有月份虚拟变量的随机森林模型。
前两个模型将作为简单的基准,而后两个模型旨在真正学习一些东西。然而,这些模型绝不是好的模型。我们仅仅用它们来说明我们类的回测能力。
在以下代码片段中,我们定义了用于获取预测的函数。如前所述,我们选择这种方法以保持灵活性,并能够将任何类型的 ML 模型封装成返回预期预测期的函数。
在以下代码片段中,我们定义了用于运行回测的常量。我们还定义了一个字典,将用来存储评分。
对于指标,我们选择专注于平均绝对误差(MAE)和均方误差(MSE)。我们选择这两个指标是为了展示我们可以在这种方法中使用多个指标。对于实际模型比较,我们将专注于 MAE。
在以下代码片段中,我们回测了朴素模型。因为我们将verbose设置为 True,我们还可以检查回测的每个迭代。我们打印了训练集和验证集的范围以及观察值的数量。
此外,我们将 MAE 评分存储在字典中,指明评分来源的方法。我们这样做是为了稍后将它们结合起来进行评估。
类似地,我们对剩下的三个预测函数进行回测。为了简洁起见,我们没有包含所有代码,因为它们非常相似。我们仅提供一个线性模型的示例,因为我们必须将特征列表作为额外参数添加到run_backtest方法中。
回测结果
通过结合回测结果,我们可以看到 ML 模型在 MAE 方面优于基准模型。
作为回测类的潜在扩展,绘制一些预测与实际值的对比图将有助于进一步评估预测的质量。
总结
-
由于时间序列中的时间依赖性,传统的验证方法如 k 折交叉验证不能使用。
-
回测(或时间序列交叉验证)是为了满足时间序列特定要求而设计的验证方法。
-
我们可以使用回测来获得模型部署后的可靠性能估计。此外,我们还可以利用这些方法进行超参数调整和特征选择。
你可以在这里找到本文中使用的代码。如往常一样,任何建设性的反馈都非常欢迎。你可以通过LinkedIn、Twitter或评论区与我联系。
喜欢这篇文章吗?成为 Medium 会员,继续无限制地阅读学习。如果你使用 这个链接 成为会员,你将无需额外费用地支持我。提前感谢,期待再见!
你可能还对以下内容感兴趣:
探索简单移动平均和指数加权移动平均的细微差别
towardsdatascience.com ## 时间序列预测中的交互项综合指南
了解如何通过使线性模型对趋势变化更具灵活性来改善其拟合效果
towardsdatascience.com ## 时间序列的三种特征工程方法
使用虚拟变量、循环编码和径向基函数
towardsdatascience.com
参考文献
-
Bergmeir, Christoph, Mauro Costantini, and José M. Benítez. “关于交叉验证在方向性预测评估中的有用性。”《计算统计与数据分析》76 (2014): 132–143。
-
Bergmeir, C., Hyndman, R. J., & Koo, B. (2018). 关于交叉验证在评估自回归时间序列预测中的有效性的说明。计算统计与数据分析,120,70–83。
-
Racine, Jeff. “用于依赖数据的一致交叉验证模型选择:hv-block 交叉验证。”《计量经济学杂志》99.1 (2000): 39–61。
-
www.kaggle.com/code/jorijnsmit/found-the-holy-grail-grouptimeseriessplit -
stackoverflow.com/questions/51963713/cross-validation-for-grouped-time-series-panel-data -
datascience.stackexchange.com/questions/77684/time-series-grouped-cross-validation
除非另有说明,否则所有图片均由作者提供。
PyCon 珍品:精选 PyCon DE 2023 中卓越讲座的精选集

图片由作者提供。
单独的 LLM 不是未来。
·发布于Towards Data Science ·8 分钟阅读·2023 年 5 月 2 日
--
在 2023 年 4 月中旬的柏林 PyCon DE 2023 大会上,空气中的兴奋和紧张气氛十分明显,会议室座无虚席,观众排队却被拒之门外。几个月前 ChatGPT 的发布引发了 AI 狂潮,激发了一场创新和合作的浪潮,致力于开发第一个完全开源的最先进指令跟随大型语言模型。在会议的三天里,开源世界宣布了LLaVA、StableLM和RedPajama数据集的发布。
如果我用一句话总结PyCon DE 2023,那就是:“单独的 LLM 不是未来。”
总结Label Studio的 Erin Mikail Staples 和Explosion的 Ines Montani 的讲座,LLM 在与特定任务的数据一起使用时在下游任务中表现更好。此外,与会者中最普遍的讨论话题是 OpenAI 的侵入性数据收集政策,这些政策禁止许多公司甚至整个行业将ChatGPT和GPT-4用于商业目的。
单独的 LLM 不是未来。
本文的目的是概述我在今年 PyCon DE 上最喜欢的演讲。以下是我最喜欢的五个演讲的总结,并包含了程序描述、幻灯片和代码的链接(如果有的话)。大会上的所有演讲都被录制下来,并将在上传后完全公开访问。
从人类反馈中改进机器学习
(Erin Mikail Sharples, Label Studio)
训练于巨大数据集上的模型,例如 ChatGPT,会在下游任务中引入互联网规模的偏见。虽然提示工程,即迭代选择和设计提示以引发生成语言模型的期望响应,是一种流行的方法,但它只是适应了模型的已知局限性。幸运的是,还有更好的替代方案来解决 LLM 中的偏见问题。
在这次演讲中,从人类反馈中进行强化学习(RLHF)是明星话题。RLHF 是一种模型通过迭代从人类提供的反馈中学习,以提高模型性能的过程。RLHF 使你能更精确地控制 LLM,将模型输出与特定需求和使用场景对齐,同时减少 LLM 相关的偏见。Label Studio 是一个开源数据标注平台,具有用户友好的界面和 Python 客户端,允许你将 RLHF 集成到自己的机器学习工作流中。RLHF 不仅提高了下游任务的准确性,还增加了真实性,减少了毒性,成本极低。
我特别喜欢这次演讲,因为它揭示了 RLHF 的概念,这是一种在 ChatGPT 开发中发挥了关键作用的方法。此外,Label Studio 展示了 RLHF 是一个强大而实用的开源工具,可以轻松地添加到你当前的工作流中。
GitHub: heartexlabs/RLHF
Notebook: RLHF_with_Custom_Datasets.ipynb
最灵活的数据注释工具。可以快速安装。构建自定义 UI 或使用预构建的标注模板……
labelstud.io](https://labelstud.io/?source=post_page-----84461368df18--------------------------------)
将 GPT-3 纳入实用的 NLP 工作流
(Ines Montani, Explosion)
当我第一次尝试 ChatGPT 时,我真的怀疑开源 NLP 库如何与 OpenAI 的强大相比。Ines Montani 认为 LLM 是对现有机器学习工作流的补充,而不是替代。
Explosion 发布了一个配方库,使用户能够利用 OpenAI 模型的强大功能,并结合通过其企业注释工具 Prodigy 收集的人工反馈。这个流程是这样的:
-
给 GPT-3.5(ChatGPT 的基础模型)设置任务。
-
检索响应并将其视为零-shot 或少-shot 分类。
-
让人工决策者标记响应是否准确。
-
使用生成的注释来训练或评估你的任务特定模型。
如果我仍然需要说服自己 RLHF 是未来的最终方向,这次讲座做到了。在我上面提到的讲座中,Ines 展示了将人工反馈纳入 NLP 工作流比单独使用 LLMs 可以获得更好的下游任务性能。鉴于我自己使用 ChatGPT 的经验,我对这些说法毫不怀疑。虽然我发现它在执行广泛任务时表现良好,但我绝对不会毫无保留地信任 ChatGPT 或 GPT-4 处理需要专业知识的敏感任务。
GitHub: explosion/prodigy-openai-recipes
## Explosion · spaCy、Prodigy 及其他 AI 和 NLP 开发工具的制造商
Explosion 是一家专注于人工智能和自然语言处理开发工具的软件公司……
文本风格迁移的方法:文本解毒案例
(达琳娜·德门捷娃,慕尼黑工业大学)
GitHub: dardem/text_detoxification
互联网的全球普及为个人提供了一个与不断增长的观众分享信息、想法和观点的平台。2020 年的研究甚至发现,Facebook 的 Feed 推荐算法优先推送煽动性内容,因为这些内容通常会增加用户在平台上的参与度。尽管仇恨言论和有毒文本检测已成为许多研究的主题,但对实际解毒此类文本的工作却较少。
在这个讲座中,Daryna 介绍了 ParaDetox,这是一个新颖的管道和一组平行数据集,这些数据集基于平行的有毒和去毒化数据集进行训练,并使用文本风格转换(TST)来去毒化有毒文本。将其视为一个 seq2seq 文本生成任务,ParaDetox 的第一步是策划有毒文本和去毒化文本的数据集对。这些平行数据集随后用于训练一个语言模型,该模型可以自动去毒化文本输入。目前,ParaDetox 模型可以去毒化俄语和英语文本,及用于训练模型的平行数据集,均托管在 HuggingFace Hub。
在生成文本模型(如 ChatGPT)被公众广泛使用之前,我们只需担心互联网中由人类产生的有害文本。然而,现在我们需要担心人类和机器生成的有毒、有害和仇恨的文本。ParaDetox 还使用了一种创新的方法,通过使用平行语料库来解决这个古老的问题。这种方法是另一个强大的例子,展示了如何利用 LLMs 和 人类输入来创建有效的下游任务解决方案。
[## GitHub - s-nlp/paradetox: 论文“ParaDetox: 使用平行语料库进行去毒化”的数据和信息…
这个仓库包含了有关 Paradetox 数据集的信息——这是第一个用于去毒化任务的平行语料库……
在浏览器中使用 PyScript 实现可操作的机器学习
(Valerio Maggio, Anaconda)
如果你习惯于主要使用 Jupyter 笔记本进行端到端的数据科学项目,你可能会对部署第一个 Web 应用感到害怕。PyScript 旨在改变这种情况,通过提供一个简单的框架,让所有技能水平的编码者都能创建动态的 Python Web 应用。根据 Valerio 的说法,“你可以在浏览器中编写 Python 代码,无需任何安装。”
PyScript 基于 Pyodide,它在浏览器中提供了完整的 PyData Stack(减去一些不支持的模块)。与 PHP 不同,PyScript 是一种客户端技术,这意味着不需要服务器或任何形式的安装。它可以用于共享互动仪表板、可视化数据和创建客户端 Python Web 应用。
尽管 PyScript 应用程序可能不如使用 Streamlit 或 Gradio 开发的应用程序那么先进,但它们为数据科学家提供了一个用户友好的机会,让他们熟悉并增强对 Web 应用部署的信心。作为一个对非 Python 或 R 编程语言感到羞愧过敏的人,PyScript 让我着迷于“部署就像‘部署’一个 HTML 文件一样简单”。
GitHub: pyscript/pyscript
难道不酷吗……在你的浏览器中运行 Python…… | print('现在你可以了!') | | | 点击这里查看示例……
pyscript.net](https://pyscript.net/?source=post_page-----84461368df18--------------------------------)
我们如何管理?现实中的数据团队
(Noa Tamir)
尽管谈论管理显然不如谈论最新的 SOTA 机器学习模型、包或平台那样性感,但这场主题演讲受到了很好的关注,原因也很充分。虽然数据科学家这一角色已经存在了 15 年,但我们的大多数会议讨论的是过程和平台的管理,而忽视了对人员管理的关注。
在这次演讲中,Noa 解释了数据驱动工作的概率性,这意味着它很难管理,并且需要与非数据驱动工作不同的管理技术。新机器学习技术和科技的快速发展给数据团队的管理者带来了独特的挑战。随着这一领域的演变,数据驱动团队的角色也在变化。我们很难理解职位名称和描述中的细微差别,这使得招聘合适的人才更加困难,也可能对员工的工作满意度和职业发展产生负面影响。
优秀的管理者可以通过建立共同的理解和与现有及潜在员工沟通数据角色的具体定义来减轻这些后果。管理者还可以通过帮助员工发展成为专才或通才来支持他们,这两者都为数据团队带来了价值。
Noa 的演讲简洁地描述了数据团队面临的挑战,既诚实又具有验证性。他们为数据团队管理者提供了实际的建议,同时强调了我们工作在一个新的、快速变化的领域中,我们都还在学习。
PyCon DE 2023 既充满了动力又令人不知所措,确实是一场出色的会议。除了挑选引人入胜且相关的演讲和工作坊外,组织者还很好地营造了一个安全包容的氛围。
今年,我很高兴地听到,尽管围绕 LLMs 有很多炒作,但人类在创造有价值的数据解决方案方面仍然发挥着重要作用。很难想象一年来人工智能的状态会是什么样,但我知道的一件事是,我将在 2024 年再次回到柏林参加 PyCon DE。
附言:我会在会议记录发布后尽快附上访问链接。
如果你想跟上最新的数据科学趋势、技术和软件包,可以考虑成为 Medium 会员。你将获得对《Towards Data Science》等文章和博客的无限访问,并且你将支持我的写作。(我从每个会员订阅中赚取少量佣金)。
## 通过我的推荐链接加入 Medium - Mary Newhauser
每月$5 即可访问无限 Medium 文章 🤗 你的会员费直接支持 Mary Newhauser 和…
想要联系一下?
我还写过:
## GPT-4 与 ChatGPT:训练、性能、能力和局限性的探讨
GPT-4 是一次改进,但要调整你的期望。
## 最终的 Pandas 代码清理参考 [## 清洁文本数据的干净方法
清洁文本数据的干净方法。
## 2023 年从数据分析师转型为数据科学家的路径 [## 从数据分析师到数据科学家的转型
从数据分析师转型为数据科学家的技能和资源。
## GPT-4 与 ChatGPT 的比较:训练、性能、能力和局限性探讨
参考资料
(1) H. Liu, C. Li, Q. Wu, Y. J. Lee,视觉指令调优 (2023)。
(2) Stability AI,Stability AI 推出首款 StableLM 语言模型套件 (2023)。
(3) 一起,RedPajama,一个创建领先开源模型的项目,首先通过重现 LLaMA 训练数据集的 1.2 万亿个标记开始 (2023)。
(4) PYSV E.V., PyCon DE & PyData Berlin 2023(2023)。
(5) M. Burgess,ChatGPT 面临严重的隐私问题(2023)。
(6) OpenAI,介绍 ChatGPT(2023)。
(7) OpenAI,GPT-4 是 OpenAI 最先进的系统,能够产生更安全、更有用的回应(2023)。
(8) OpenAI,从人类偏好中学习(2023)。
(9) Heartex Labs,heartexlabs/RLHF(2023)。
(10) Heartex Labs,使用自定义数据集实现 RLHF(2023)。
(11) I. Montani,将 LLMs 融入实际 NLP 工作流程(2023)。
(12) Explosion,explosion/prodigy-openai-recipes(2023)。
(13) D. Dementieva,dardem/text_detoxification(2023)。
(14) V. Logacheva1, D. Dementieva1, S. Ustyantsev, D. Moskovskiy, D. Dale, I. Krotova, N. Semenov, 和 A. Panchenko,ParaDetox:使用平行数据进行去毒化(2022)。
(15) L. Munn,设计中的愤怒:有毒沟通与技术架构(2020)。
(16) V. Maggio,使用 PyScript 在浏览器中进行可操作的机器学习(2023)。
(17) PyScript,pyscript/pyscript(2023)。
(18) N. Tamir,我们如何管理?数据团队管理现实(2023)。
使用 PyMC-Marketing 进行客户生命周期价值预测
探索买到死(BTYD)建模的深度及实际编码技巧
·
关注 发布于 Towards Data Science · 15 min 阅读 · 2023 年 11 月 10 日
--
图片由 Boxed Water Is Better 提供,发布于 Unsplash
TL; DR: 客户生命周期价值(CLV)模型是客户分析中的关键技术,有助于公司识别出哪些是有价值的客户。忽视 CLV 可能导致对短期客户过度投资,这些客户可能只会进行一次购买。‘Buy Till You Die’建模,利用 BG/NBD 和 Gamma-Gamma 模型,可以估算 CLV。尽管最佳实践因数据规模和建模优先级而异,PyMC-Marketing 是推荐的 Python 库,适合那些希望快速实现 CLV 建模的人。
对于那些急于直接查看示例代码的人,请参考我的GitHub 仓库。如果你能留下一个星标,我将非常高兴!
[## GitHub - takechanman1228/Effective-CLV-Modeling
通过在 GitHub 上创建账户来为 takechanman1228/Effective-CLV-Modeling 的发展做贡献。
1. 什么是 CLV?
CLV 的定义是公司在客户整个关系期间可以期望获得的总净收入。 你们中的一些人可能更熟悉‘LTV’(生命周期价值)这个术语。是的,CLV 和 LTV 是可以互换的。

图片由作者提供
-
第一个目标是计算和预测未来的 CLV,这将帮助你了解每个客户能带来多少预期收入。
-
第二个目标是识别盈利客户。模型将通过分析高 CLV 客户的特征来告诉你这些有价值的客户是谁。
-
第三个目标是根据分析采取营销行动,从而优化你的营销预算分配。

图片由作者提供
2. 业务背景
以像 Nike 这样的时尚品牌的电子商务网站为例,该网站可能会利用广告和优惠券来吸引新客户。现在,假设大学生和在职专业人士是两个主要的重要客户群体。对于首次购买,公司在大学生身上花费了$10 的广告费,在在职专业人士身上花费了$20。两者的购买金额都在$100 左右。
如果你负责营销,你会更愿意在哪个细分市场上投入更多?你可能会自然地认为,考虑到大学生的较低成本和较高的投资回报率,将更多资金投入大学生市场是更有逻辑的。

图片由作者提供,照片来自 Pixabay
那么,如果你知道了这些信息会怎样呢?
大学生细分市场往往有较高的流失率,这意味着他们在首次购买后不会再购买,导致平均每位顾客花费 $100。另一方面,工作专业人士细分市场的重复购买率较高,每位顾客平均花费 $400。
在这种情况下,你可能更愿意将更多资源投入到商务专业人士的细分市场,因为它承诺更高的投资回报率。这看起来是一个简单的概念,任何人都可以理解。然而,令人惊讶的是,大多数营销人员专注于实现每次获取成本(CPA),但他们没有考虑到长期内盈利的顾客是谁。

图片由作者提供,照片来自 Pixabay
通过调整“每次获取成本”(CPA),我们可以吸引更多高价值顾客并改善投资回报率。左侧的图表表示没有考虑 CLV 的方法。红线表示 CPA,即我们可以花费的最大成本来获取新顾客。对每位顾客使用相同的营销预算会导致对低价值顾客的过度投资和对高价值顾客的投资不足。
现在,右侧的图表显示了利用 CLV 时的理想支出分配。我们为高价值顾客设置了更高的 CPA,为低价值顾客设置了更低的 CPA。

图片由作者提供,照片来自 Pixabay
这类似于招聘过程。如果你打算招聘前谷歌员工,提供具有竞争力的薪水是必要的,对吧?通过这样做,我们可以在不改变总营销预算的情况下获得更多高价值顾客。
3. 所需数据
我介绍的 CLV 模型仅使用销售交易数据。正如你所见,我们有三列数据:customer_id、交易日期和交易金额。在数据量方面,CLV 通常需要两到三年的交易数据。

图片由作者提供
4. 传统 CLV 公式
4.1 CLV 建模方法
让我们从了解计算 CLV 的两种主要方法开始:历史方法和预测方法。在预测方法下,有两种模型。即概率模型和机器学习模型。

图片由作者提供
4.2 传统 CLV 公式
首先,让我们考虑一个传统的 CLV 公式。在这里,CLV 可以分解为三个组成部分:平均订单价值、购买频率和顾客生命周期。

图片由作者提供
以一家时尚公司为例,平均来看:
-
顾客每次订单消费 $100
-
他们每年购物 4 次
-
他们保持忠诚度达 3 年
在这种情况下,CLV 的计算公式为 100 乘以 4 乘以 3,即每位顾客 $1,200。这个公式非常简单,看起来很直接,对吧?然而,它也有一些局限性。
4.3 传统 CLV 公式的限制

作者提供的图片
限制 #1: 并非所有顾客都一样
这个传统公式假设所有顾客都是同质的,通过分配一个平均数来表示。当一些顾客进行异常大的购买时,平均数并不能代表所有顾客的特点。因此,我们需要在个体层面上进行建模,而不是将所有人聚合在一起。
限制 #2 : 首次购买时间的差异
假设我们使用过去 12 个月作为数据收集周期。

作者提供的图片,图片来源于 Pixabay
这个人大约在一年前进行了第一次购买。在这种情况下,我们可以准确计算他每年的购买频率。是 8 次。
那两个顾客怎么样?一个人六个月前开始购买,另一个人三个月前开始购买。他们的购买节奏相同。然而,当我们查看过去一年的总购买次数时,它们有所不同。关键是我们需要考虑顾客的任期,即自第一次购买以来的时间长度。
限制 #3 : 活着还是死亡?
确定顾客何时被视为“流失”是很棘手的。对于像 Netflix 这样的订阅服务,我们可以认为顾客一旦取消订阅就已经流失。然而,对于零售或电子商务来说,顾客是否“活着”或“死亡”是模糊的。
顾客的“存活概率”取决于他们过去的购买模式。例如,如果一个通常每月购买的人在接下来的三个月内没有购买,他们可能会转向其他品牌。然而,如果一个通常每六个月购买一次的人在接下来的三个月内没有购买,也不必担心。

作者提供的图片,图片来源于 Pixabay
5. 买到死(BTYD)模型
为了应对这些挑战,我们通常转向“买到死”(BTYD)建模。这种方法包括两个子模型:
-
BG-NBD 模型:预测顾客活跃的可能性及其交易频率。
-
Gamma-Gamma 模型:估计平均订单值。
通过结合这些子模型的结果,我们可以有效预测顾客终身价值(CLV)。

作者提供的图片
5.1 BG/NBD 模型
我们认为顾客的状态有两个过程:“购买过程”,即顾客正在积极购买,以及“流失过程”,即顾客停止了购买。
在活跃购买阶段,模型通过“泊松过程”预测顾客的购买频率。
顾客在每次购买后总有可能流失。BG/NBD 模型为这种可能性分配一个概率‘p’。
请参考下图进行说明。数据表明这位客户进行了五次购买。然而,在假设下,模型认为如果客户保持活跃,他们总共会进行八次购买。但是,由于在某些时候“活跃”的概率下降,我们只看到了五次实际购买。

作者提供的图像
在被视为“活跃”时,购买频率遵循泊松过程。泊松分布通常表示随机发生事件的计数。这里,“λ”象征每位客户的购买频率。然而,客户的购买频率可能会波动。泊松分布考虑了购买频率的这种变异性。

作者提供的图像;图表来源于维基百科
下图展示了“p”随时间的变化。随着自上次购买以来的时间增加(T=31),客户“活跃”的概率减少。当再次购买发生(大约在 T=36)时,你会注意到“p”再次上升。

作者提供的图像
这是图形模型。如前所述,它包括 λ 和 p。在这里,λ 和 p 在不同人之间有所变化。为了考虑这种多样性,我们假设 λ 的异质性遵循 Gamma 分布,而 p 的异质性遵循“Beta 分布”。换句话说,该模型使用了一个基于贝叶斯定理的分层方法,也称为贝叶斯层次建模。

作者提供的图像
有关公式的详细推导,请参阅此论文:www.brucehardie.com/papers/bgnbd_2004-04-20.pdf
5.2 Gamma-Gamma 模型
Gamma 分布非常适合建模平均订单值,因为它自然地考虑了消费数据的连续性和严格正值特性,这些数据通常呈现右偏态。Gamma 分布由两个参数决定:形状参数和尺度参数。正如这个图表所示,通过改变这两个参数,Gamma 分布的形态可以发生相当大的变化。

作者提供的图像;图表来源于维基百科
这个图解说明了正在使用的图形模型。该模型在贝叶斯层次方法中使用了两个 Gamma 分布。第一个 Gamma 分布表示每位客户的“平均订单值”。由于这个值在客户之间有所不同,第二个 Gamma 分布捕捉了整个客户群体中平均订单值的变化。先验分布的参数 p、q 和 γ(gamma)通过使用半平坦先验确定。

作者提供的图像
有关公式的详细推导,请参考这篇论文:www.brucehardie.com/notes/025/gamma_gamma.pdf
6. 示例代码
有用的 CLV 库
这里,我想介绍两个用于 CLV 建模的优秀 OSS 库。第一个是 PyMC-Marketing,第二个是 CLVTools。这两个库都包含 Buy-till-you-die 模型。最显著的区别在于,PyMC-Marketing 是一个基于 Python 的库,而 CLVTools 是基于 R 的。PyMC-Marketing 是建立在流行的贝叶斯库 PyMC 上的。之前有一个知名的库叫做 ‘Lifetimes’,但 ‘Lifetimes’ 现在已经进入维护模式,因此它已经转变为 PyMC-Marketing。
完整代码
完整代码可以在我的 Github 上找到。我的示例代码基于 yMC-Marketing 的官方快速入门指南。
## GitHub - takechanman1228/Effective-CLV-Modeling
通过在 GitHub 上创建帐户来为 takechanman1228/Effective-CLV-Modeling 开发做出贡献。
代码讲解
首先,你需要导入 pymc_marketing 和其他库。
import arviz as az
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import pymc as pm
from arviz.labels import MapLabeller
from IPython.display import Image
from pymc_marketing import clv
你需要下载 “在线零售数据集”来自 “UCI 机器学习库”。该数据集包含来自一家总部位于英国的在线零售商的交易数据,并且是根据创作共享署名 4.0 国际(CC BY 4.0)许可授权的。
import requests
import zipfile
import os
# Download the zip file
url = "https://archive.ics.uci.edu/static/public/352/online+retail.zip"
response = requests.get(url)
filename = "online_retail.zip"
with open(filename, 'wb') as file:
file.write(response.content)
# Unzip the file
with zipfile.ZipFile(filename, 'r') as zip_ref:
zip_ref.extractall("online_retail_data")
# Finding the Excel file name
for file in os.listdir("online_retail_data"):
if file.endswith(".xlsx"):
excel_file = os.path.join("online_retail_data", file)
break
# Convert from Excel to CSV
data_raw = pd.read_excel(excel_file)
data_raw.head()

数据清理
需要进行快速数据清理。例如,我们需要处理退货订单,筛选出没有客户 ID 的记录,并通过将数量和单价相乘来创建一个 ‘总销售额’ 列。
# Handling Return Orders
# Extracting rows where InvoiceNo starts with "C"
cancelled_orders = data_raw[data_raw['InvoiceNo'].astype(str).str.startswith("C")]
# Create a temporary DataFrame with the columns we want to match on, and also negate the 'Quantity' column
cancelled_orders['Quantity'] = -cancelled_orders['Quantity']
# Merge the original DataFrame with the temporary DataFrame on the columns we want to match
merged_data = pd.merge(data_raw, cancelled_orders[['CustomerID', 'StockCode', 'Quantity', 'UnitPrice']],
on=['CustomerID', 'StockCode', 'Quantity', 'UnitPrice'],
how='left', indicator=True)
# Filter out rows where the merge found a match, and also filter out the original return orders
data_raw = merged_data[(merged_data['_merge'] == 'left_only') & (~merged_data['InvoiceNo'].astype(str).str.startswith("C"))]
# Drop the indicator column
data_raw = data_raw.drop(columns=['_merge'])
# Selecting relevant features and calculating total sales
features = ['CustomerID', 'InvoiceNo', 'InvoiceDate', 'Quantity', 'UnitPrice', 'Country']
data = data_raw[features]
data['TotalSales'] = data['Quantity'].multiply(data['UnitPrice'])
# Removing transactions with missing customer IDs as they don't contribute to individual customer behavior
data = data[data['CustomerID'].notna()]
data['CustomerID'] = data['CustomerID'].astype(int).astype(str)
data.head()

作者提供的图片
然后,我们需要使用 ‘clv_summary’ 函数创建一个摘要表。该函数返回一个 RFM-T 格式的数据框。RFM-T 代表每个客户的近期性、频率、货币量和在职时间。这些指标在购物者分析中很受欢迎。
data_summary_rfm = clv.utils.clv_summary(data, 'CustomerID', 'InvoiceDate', 'TotalSales')
data_summary_rfm = data_summary_rfm.rename(columns={'CustomerID': 'customer_id'})
data_summary_rfm.index = data_summary_rfm['customer_id']
data_summary_rfm.head()

BG/NBD 模型
BG/NBD 模型在这个库中作为 BetaGeoModel 函数提供。当你执行 bgm.fit() 时,你的模型开始训练。
当你执行 bgm.fit_summary() 时,系统会提供学习过程的统计摘要。例如,这个表格显示了参数的均值、标准差、高密度区间(HDI)等。我们还可以检查 r_hat 值,这有助于评估 Markov Chain Monte Carlo (MCMC) 模拟是否已经收敛。如果 r-hat 小于或等于 1.1,则认为是可接受的。
bgm = clv.BetaGeoModel(
data = data_summary_rfm,
)
bgm.build_model()
bgm.fit()
bgm.fit_summary()

下面的矩阵被称为概率存活矩阵。通过这个矩阵,我们可以推断哪些用户可能会返回,哪些用户可能不会返回。X 轴表示客户的历史购买频率,Y 轴表示客户的最近一次购买时间。颜色表示存活的概率。我们的新客户位于左下角:低频率和高最近性。这些客户的存活概率较高。我们的忠实客户位于右下角:高频率和高最近性客户。如果他们长时间没有购买,忠实客户将成为高风险客户,其存活概率较低。
clv.plot_probability_alive_matrix(bgm);

作者提供的图片
下一步,我们可以预测每个客户的未来交易。你可以使用expected_num_purchases函数。在模型拟合之后,我们可以询问下一期间预期的购买次数。
num_purchases = bgm.expected_num_purchases(
customer_id=data_summary_rfm["customer_id"],
t=365,
frequency=data_summary_rfm["frequency"],
recency=data_summary_rfm["recency"],
T=data_summary_rfm["T"]
)
sdata = data_summary_rfm.copy()
sdata["expected_purchases"] = num_purchases.mean(("chain", "draw")).values
sdata.sort_values(by="expected_purchases").tail(4)

Gamma-Gamma 模型
接下来,我们将转到 Gamma-Gamma 模型来预测平均订单值。我们可以通过Expected_customer_spend函数预测预期的“平均订单值”。
nonzero_data = data_summary_rfm.query("frequency>0")
dataset = pd.DataFrame({
'customer_id': nonzero_data.customer_id,
'mean_transaction_value': nonzero_data["monetary_value"],
'frequency': nonzero_data["frequency"],
})
gg = clv.GammaGammaModel(
data = dataset
)
gg.build_model()
gg.fit();
expected_spend = gg.expected_customer_spend(
customer_id=data_summary_rfm["customer_id"],
mean_transaction_value=data_summary_rfm["monetary_value"],
frequency=data_summary_rfm["frequency"],
)
下面的图表显示了 5 个客户的预期平均订单值。这两位客户的平均订单值超过$500,而这三位客户的平均订单值约为$350。
labeller = MapLabeller(var_name_map={"x": "customer"})
az.plot_forest(expected_spend.isel(customer_id=(range(5))), combined=True, labeller=labeller)
plt.xlabel("Expected average order value");

作者提供的图片
结果
最后,我们可以结合两个子模型来估算每个客户的 CLV。我想提到的一点是参数:Discount_rate。该函数使用折现现金流(DCF)方法。当月折现率为 1%时,一个月后的$100 在今天的价值是$99。
clv_estimate = gg.expected_customer_lifetime_value(
transaction_model=bgm,
customer_id=data_summary_rfm['customer_id'],
mean_transaction_value=data_summary_rfm["monetary_value"],
frequency=data_summary_rfm["frequency"],
recency=data_summary_rfm["recency"],
T=data_summary_rfm["T"],
time=120, # 120 months = 10 years
discount_rate=0.01,
freq="D",
)
clv_df = az.summary(clv_estimate, kind="stats").reset_index()
clv_df['customer_id'] = clv_df['index'].str.extract('(\d+)')[0]
clv_df = clv_df[['customer_id', 'mean', 'hdi_3%', 'hdi_97%']]
clv_df.rename(columns={'mean' : 'clv_estimate', 'hdi_3%': 'clv_estimate_hdi_3%', 'hdi_97%': 'clv_estimate_hdi_97%'}, inplace=True)
# monetary_values = data_summary_rfm.loc[clv_df['customer_id'], 'monetary_value']
monetary_values = data_summary_rfm.set_index('customer_id').loc[clv_df['customer_id'], 'monetary_value']
clv_df['monetary_value'] = monetary_values.values
clv_df.to_csv('clv_estimates_output.csv', index=False)
现在,我将向你展示如何改进我们的营销行动。下面的图表显示了按国家估算的 CLV。
# Calculating total sales per transaction
data['TotalSales'] = data['Quantity'] * data['UnitPrice']
customer_sales = data.groupby('CustomerID').agg({
'TotalSales': sum,
'Country': 'first' # Assuming a customer is associated with only one country
})
customer_countries = customer_sales.reset_index()[['CustomerID', 'Country']]
clv_with_country = pd.merge(clv_df, customer_countries, left_on='customer_id', right_on='CustomerID', how='left')
average_clv_by_country = clv_with_country.groupby('Country')['clv_estimate'].mean()
customer_count_by_country = data.groupby('Country')['CustomerID'].nunique()
country_clv_summary = pd.DataFrame({
'AverageCLV': average_clv_by_country,
'CustomerCount': customer_count_by_country,
})
# Calculate the average lower and upper bounds of the CLV estimates by country
average_clv_lower_by_country = clv_with_country.groupby('Country')['clv_estimate_hdi_3%'].mean()
average_clv_upper_by_country = clv_with_country.groupby('Country')['clv_estimate_hdi_97%'].mean()
# Add these averages to the country_clv_summary dataframe
country_clv_summary['AverageCLVLower'] = average_clv_lower_by_country
country_clv_summary['AverageCLVUpper'] = average_clv_upper_by_country
# Filtering countries with more than 20 customers
filtered_countries = country_clv_summary[country_clv_summary['CustomerCount'] >= 20]
# Sorting in descending order by CustomerCount
sorted_countries = filtered_countries.sort_values(by='AverageCLV', ascending=False)
# Prepare the data for error bars
lower_error = sorted_countries['AverageCLV'] - sorted_countries['AverageCLVLower']
upper_error = sorted_countries['AverageCLVUpper'] - sorted_countries['AverageCLV']
asymmetric_error = [lower_error, upper_error]
# Create a new figure with a specified size
plt.figure(figsize=(12,8))
# Create a plot representing the average CLV with error bars indicating the confidence intervals
# We convert the index to a regular list to avoid issues with matplotlib's handling of pandas Index objects
plt.errorbar(x=sorted_countries['AverageCLV'], y=sorted_countries.index.tolist(),
xerr=asymmetric_error, fmt='o', color='black', ecolor='lightgray', capsize=5, markeredgewidth=2)
# Set labels and title
plt.xlabel('Average CLV') # x-axis label
plt.ylabel('Country') # y-axis label
plt.title('Average Customer Lifetime Value (CLV) by Country with Confidence Intervals') # chart title
# Adjust the y-axis to display countries from top down
plt.gca().invert_yaxis()
# Show the grid lines
plt.grid(True, linestyle='--', alpha=0.7)
# Display the plot
plt.show()

作者提供的图片
法国的客户通常拥有较高的 CLV。另一方面,比利时的客户通常拥有较低的 CLV。根据这个结果,我建议增加在法国获取客户的营销预算,并减少在比利时获取客户的营销预算。当我们用基于美国的数据进行建模时,我们会使用各州而不是国家。
7. 你如何提高模型的准确性?
你可能会想知道:
-
我们能否利用其他类型的数据,例如访问日志?
-
是否可以将更多特征,如人口统计信息或营销活动,纳入模型中?
基本上,BTYD 模型只需要交易数据。如果你想使用其他数据或其他特征,机器学习(ML)方法可能是一个选择。之后,你可以评估贝叶斯和 ML 模型的性能,选择准确性和解释性更好的模型。
下面的流程图展示了更好的 CLV 建模指南。

图片来源:作者
首先,考虑你的数据规模。如果数据量不够大或仅有交易数据,使用 PyMC Marketing 的 BTYD 模型可能是最佳选择。即使数据量足够大,我认为一个好的方法是先从 BTYD 模型开始,如果效果不佳,可以尝试不同的方法。具体来说,如果你的首要任务是准确性而不是解释性,神经网络、XGboost、LightGBM 或集成技术可能会有帮助。如果解释性对你仍然重要,可以考虑随机森林或可解释 AI 方法。
总结来说,我建议无论如何,从 PyMC Marketing 开始是一个好的第一步!
8. 结论
以下是一些关键要点。
-
客户生命周期价值 (CLV) 是公司可以预期从单个客户在其整个关系期间获得的总净利润。
-
我们可以使用 BG/NBD 模型和 Gamma-Gamma 模型建立一个概率模型 (BTYD)。
-
如果你熟悉 Python,可以从 PyMC-Marketing 开始。
感谢阅读!如果你有任何问题/建议,请随时通过 Linkedin 联系我!同时,如果你能关注我在 Towards Data Science 上,我会非常开心。
9. 参考文献
-
BG/NBD 模型 :
www.brucehardie.com/papers/bgnbd_2004-04-20.pdf -
Gamma-Gamma 模型 :
www.brucehardie.com/notes/025/gamma_gamma.pdf -
PyMC Marketing :
github.com/pymc-labs/pymc-marketing -
在线零售数据集 (许可证 CC BY 4.0): https://archive.ics.uci.edu/dataset/352/online+retail
如何修复 TypeError: ObjectId 不是 JSON 可序列化的
原文:
towardsdatascience.com/pymongo-cursor-to-json-9f770740375a
将 Mongo 游标转换为 Python 中的 JSON 对象
·发布在 Towards Data Science ·4 分钟阅读·2023 年 1 月 7 日
--

照片由 Ciprian Boiciuc 提供,出处 Unsplash
欢迎来到我们的教程,介绍如何将 PyMongo 游标序列化为 JSON。在本文中,我们将讨论如何使用自定义 JSONEncoder 正确处理 ObjectId 和 datetime 对象以及其他对象。
在使用 PyMongo 时,一个常见的任务是将数据序列化以便存储或通过网络传输。在本教程中,我们将探讨如何将 PyMongo 游标(这是一个常用于存储 MongoDB 查询结果的数据结构)序列化为 JSON 格式。
常见的错误是 TyperError:
TypeError: ObjectId('') is not JSON serializable
我们还将深入探讨如何正确处理复杂数据类型,如 ObjectId 和 datetime 对象,这些对象不能直接序列化为 JSON。我们将向您展示如何使用自定义 JSONEncoder 正确处理这些对象及您 PyMongo 游标中的其他自定义对象类型。
所以,如果您想了解如何将 PyMongo 游标序列化为 JSON 并处理复杂数据类型,请继续阅读!
创建自定义 JSONEncoder
JSONEncoder —— 这是标准库 json 模块的一个成员 —— 是一个可扩展的 JSON 编码器,用于 Python 数据结构。默认情况下,它支持以下序列化:
+-----------------------------------------+--------+
| Python | JSON |
+-----------------------------------------+--------+
| dict | object |
| list, tuple | array |
| str | string |
| int, float, int & float-derived Enums | number |
| True | true |
| False | false |
| None | null |
+-----------------------------------------+--------+
这意味着每当观察到不同数据类型的对象(未列在上述表格中)时,将引发 TypeError。
在处理 Mongo 中的文档时,每个文档默认都会有一个分配的_id,这对应于集合中每个文档的唯一标识符。现在每当你查询 Mongo 集合时,将返回一个游标,该游标包含(指向)检索到的文档,每个文档还将有一个ObjectId类型的_id字段。
因此,如果你尝试使用默认的JSONEncoder来序列化这些文档,你将会遇到在本教程介绍中提到的错误:
TypeError: ObjectId('') is not JSON serializable
因此,为了能够序列化 PyMongo 游标中包含的这些对象,我们需要扩展默认的JSONEncoder,以便它能够按照我们希望的方式正确处理这些数据类型。为此,我们还需要实现default方法以返回我们希望的映射,如文档中所述。
要扩展以识别其他对象,请子类化并实现一个
[default()](https://docs.python.org/3/library/json.html#json.JSONEncoder.default)方法,该方法返回一个可序列化的对象o(如果可能),否则应调用超类实现(以引发[TypeError](https://docs.python.org/3/library/exceptions.html#TypeError))。
在我们的自定义JSONEncoder中,我将把任何bson.ObjectId和datetime.datetime实例序列化为str。根据你自己 Mongo 游标中的文档,你可能需要指定并处理额外(或更少)的数据类型。
import json
from datetime import datetime
from typing import Any
from bson import ObjectId
class MongoJSONEncoder(json.JSONEncoder):
def default(self, o: Any) -> Any:
if isinstance(o, ObjectId):
return str(o)
if isinstance(o, datetime):
return str(o)
return json.JSONEncoder.default(self, o)
使用 MongoJSONEncoder 编码 Mongo 游标
现在我们已经扩展了默认的JSONEncoder,使其能够编码bson.ObjectId和datetime.datetime类型的对象,我们现在可以编码 Mongo 游标了。
data_json = MongoJSONEncoder().encode(list(cursor))
从 JSON 对象创建 Python 对象
最后,如果你希望将新创建的 JSON 对象转换为 Python 对象(即包含文档值的键值对的字典列表),你只需要调用json.loads()函数:
data_obj = json.loads(data_json)
最终思考
在本教程中,我们学习了如何将 PyMongo 游标序列化为 JSON 并正确处理复杂的数据类型,如ObjectId和datetime对象。我们通过创建一个扩展了默认JSONEncoder并实现了default()方法的自定义JSONEncoder来完成这一任务。
然后我们使用这个自定义编码器来编码 PyMongo 游标,最后,我们使用json.loads()函数将结果 JSON 对象转换为 Python 对象。本教程演示了如何处理ObjectId和datetime对象,但自定义的JSONEncoder也可以扩展以处理 PyMongo 游标中可能存在的其他自定义对象类型。
成为会员 ,在 Medium 上阅读每一篇故事。您的会员费直接支持我和其他您阅读的作者。您还将获得对 Medium 上每一篇故事的完全访问权限。
[## 使用我的推荐链接加入 Medium — Giorgos Myrianthous
作为 Medium 会员,您的会员费的一部分将用于支持您阅读的作者,您也将获得对每一篇故事的完全访问权限…
您可能也感兴趣的相关文章
使用 Python 创建云系统架构图
towardsdatascience.com ## 基础设施即代码
使用代码管理基础设施资源
towardsdatascience.com
Python 中的 pyproject.toml 是什么
管理 Python 项目依赖的 pyproject.toml 文件
·发表在 Towards Data Science ·5 分钟阅读·2023 年 5 月 9 日
--

图片由 Fré Sonneveld 提供,来源于 Unsplash
Python 的依赖管理既复杂又令人沮丧。新手通常倾向于安装他们认为有用的任何依赖(即包),即使是在一个虚拟环境中。因此,这种方法增加了依赖包冲突的可能性,并最终陷入所谓的 依赖地狱。
在我之前的几篇文章中,我们介绍了几种处理 Python 项目依赖的方法,包括 setup.py、setup.cfg 和 requirements.txt 文件。然而,从 Python 3.6 开始,推出了一种新的标准配置文件 pyproject.toml,旨在简化用户管理依赖和元数据定义的方式。
在过去几年中,pyproject.toml 文件已成为管理 Python 项目依赖的标准(也是最受欢迎的)方式。在接下来的几个部分中,我们将深入探讨如何使用该文件实现依赖管理。此外,我们还将演示如何在可编辑模式下安装具有pyproject.toml 规范的项目。
订阅数据管道,这是一个专注于数据工程的新闻通讯
pyproject.toml 之前的依赖管理
当 Python 首次发布时,用于构建发行版的事实标准工具是 distutils。随着时间的推移,setuptools 出现,旨在在 distutils 的基础上构建额外功能。这两个工具都使用了一个 setup.py 文件,用户可以在其中指定依赖项和用于软件包构建分发的元数据。
然而,这造成了一个问题,因为任何选择使用 setuptools 的项目必须在 setup.py 文件中导入该包。因此,setup.py 在不知道其依赖项的情况下不能执行,但同时,该文件的目的就是确定这些依赖项。这就是我们在 Python 依赖管理中遇到所谓的鸡和蛋问题的原因。
我希望这些信息足以让你理解为何需要一种新的方法。如果你有兴趣了解有关 setuptools 和 pip 的鸡和蛋问题的更详细解释,确保阅读 PEP-518。
新提案(PEP-518 的一部分)旨在为 Python 项目指定一种新的方式来提前列出其依赖项,以便像 pip 这样的工具可以确保在项目构建之前安装它们。
pyproject.toml
pyproject.toml 文件作为 Python 增强提案 (PEP) 518 的一部分被引入,规定了 Python 项目必须如何指定构建依赖项。
这些构建依赖项将被存储在位于项目根目录的文件中,该文件遵循 TOML(Tom’s Obvious, Minimal Language)语法。
它包含了元数据,例如项目名称、版本、描述、作者、许可证以及各种其他细节。
pyproject.toml 文件的一个关键特性是能够定义项目依赖项。这允许开发人员指定运行项目所需的包及其版本。这有助于保持项目的一致性,并确保其他开发人员可以轻松地重现该项目。
pyproject.toml 文件还支持 extras 概念,允许开发人员为项目定义可选依赖项。这使得用户可以仅安装运行项目所需的必要依赖项。通常,在 extras 部分可以指定作为测试一部分的额外需求(例如 pytest)。
除了标准的元数据和依赖项外,pyproject.toml 文件还支持自定义字段,第三方工具可以使用这些字段。例如,你可以考虑使用 black 和 mypy 等代码检查工具、格式化工具和校验工具。这允许开发人员扩展文件的功能,并根据需要添加自定义字段。
管理 pyproject.toml 中的依赖项
pyproject.toml 可以与包依赖管理工具一起使用,例如 setuptools 和 poetry。
这是一个使用 poetry 的项目示例文件:
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.poetry]
name = "my-project"
version = "1.0.0"
description = "My Python project"
authors = ["John Doe <john@doe.com>"]
license = "MIT"
[tool.poetry.dependencies]
python = "³.6"
[tool.poetry.dev-dependencies]
pytest = "⁴.6"
[tool.poetry.extras]
docs = ["sphinx"]
这是一个使用 setuptools 的示例:
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
[project]
name = "my_package"
description = "My package description"
readme = "README.rst"
requires-python = ">=3.7"
keywords = ["one", "two"]
license = {text = "BSD 3-Clause License"}
classifiers = [
"Framework :: Django",
"Programming Language :: Python :: 3",
]
dependencies = [
"requests",
'importlib-metadata; python_version<"3.8"',
]
dynamic = ["version"]
[project.optional-dependencies]
pdf = ["ReportLab>=1.2", "RXP"]
rest = ["docutils>=0.3", "pack ==1.1, ==1.3"]
[project.scripts]
my-script = "my_package.module:function"
从 pyproject.toml 安装项目的可编辑模式
如果你在积极开发一个项目,你可能希望将项目本地安装为可编辑模式。当从特定位置以可编辑模式安装包时,对源代码的任何更改会立即在环境中反映出来(而无需重新安装“新”版本)。
假设你正在使用 poetry 来管理你的 Python 依赖,并且为了以可编辑模式安装 Python 项目,你需要在 pyproject.toml 文件中包含以下内容
[build-system]
requires = ["poetry-core>=1.0.8"]
build-backend = "poetry.core.masonry.api"
从项目的根目录,只需运行
$ pip install -e .
另外,poetry install 也会导致可编辑安装。你可以在我最新的文章中了解更多关于如何使用 Poetry 管理 Python 项目依赖的内容:
使用 Poetry 进行依赖管理和打包
towardsdatascience.com
最后的思考
在今天的文章中,我们讨论了在管理依赖和在社区中分发项目时 pyproject.toml 在 Python 中的使用。
总体而言,pyproject.toml 提供了一个标准且易于使用的 Python 项目配置。它简化了定义元数据和依赖的过程,并确保项目可以被其他开发者轻松重现。
订阅数据管道,一个专注于数据工程的新闻通讯
相关的文章你可能也喜欢
## setup.py 与 setup.cfg 在 Python 中的区别
使用 setuptools 管理依赖和分发 Python 包
towardsdatascience.com ## requirements.txt 与 setup.py 在 Python 中的区别
理解在 Python 开发和分发中 requirements.txt、setup.py 和 setup.cfg 的目的……
towardsdatascience.com ## 使用 Poetry 管理 Python 依赖
使用 Poetry 进行依赖管理和打包
towardsdatascience.com
PyrOSM:处理开放街图数据
高效的地理空间操作用于 OSM 地图数据
·
关注 发表在 Towards Data Science · 4 分钟阅读 · 2023 年 10 月 21 日
--
图片由 Tabea Schimpf 提供,来源于 Unsplash
如果你以前处理过 OSM 数据,你知道提取起来并不容易。OSM 数据可能非常庞大,找到有效的解决方案来分析你想要的内容往往是一项挑战。PyrOSM 是一个使读取和处理 OSM 数据的过程更加高效的包。怎么做到的?PyrOSM 基于 Cython(C Python)构建,并使用更快的库来反序列化 OSM 数据,同时还有像 numpy 数组这样的较小优化,使其能够快速处理数据。特别是如果你之前使用过 OSMnx(用于非常类似的用例),你会知道大型数据集加载到内存中需要很长时间,这就是 PyrOSM 可以帮助你处理它们的地方。让我们了解一下这个库能做什么吧!
🌎 PBF 数据
让我们谈谈 OSM 数据的具体文件格式。PBF 代表“Protocolbuffer Binary Format”,它在处理存储 OSM 数据时非常高效。OSM 数据组织成fileblocks,这些是可以独立编码或解码的数据组。Fileblocks 包含PrimitiveGroups,它们又包含成千上万的 OSM 实体,如节点、道路和关系。
数据可以根据用户期望的粒度级别进行缩放。例如,当前 OSM 数据库的分辨率约为 ~1 cm。实际上,如果你愿意,你可以将整个 Open Street Maps 数据下载到一个文件中,这个文件被称为 Planet(约 1000 Gb 的数据)!
👩💻 PyrOSM 基础:读取数据集
PyrOSM 是一个读取 Open Street Map 的 PBF 数据的包,基于两个主要的数据分发商:Geofabrik(全球和国家级数据)和 BBBike(城市级数据)。该包允许用户访问许多类型的功能:
-
建筑物、兴趣点(POIs)、土地使用
-
街道网络
-
自定义过滤器
-
作为网络导出
-
还有更多!
目前 BBBike 支持全球 235 个城市,你可以通过调用“sources.cities.available”方法轻松获取完整列表。入门非常简单,你只需初始化一个 OSM 读取器对象并加载所需的数据:
从这一点开始,你需要使用 OSM 对象来与 Berkeley 数据交互。现在让我们获取 Berkeley 的驾驶街道网络:

Berkeley 的 OSM 街道网络数据框
打印出实际的 street_network 对象显示它存储在一个 GeoPandas GeoDataFrame 中,包含所有 OSM 属性,如长度、高速公路、最高速度等,这对于进一步分析非常有用。
附注: BBBike(数据来源提供商)有许多其他大小的数据格式,包括 Organic Maps OSM、Garmin OSM 或 SVG Mapnik,这取决于你的使用案例。
🔍 更好的过滤
上述数据加载的结果包括了整个 Berkeley 的数据,实际上甚至包括了邻近城市的数据,这并不是理想的。那如果你想要一个更小或更具体的区域呢?这就是使用边界框的用武之地。你可以通过以下方式创建边界框:
-
手动指定一个格式为 [minx, miny, maxx, maxy] 的 4 个坐标列表
-
传入 Shapely 几何对象(例如 LineString 或 Multipolygon)
要查找边界框坐标,我通常使用这个 bbox finder 网站,它允许你绘制矩形然后复制坐标。以下是如何界定 UC Berkeley 校园周围的区域并获取其步行网络:

使用边界框的街道网络
🎯 导出和处理图形
PyrOSM 的另一个优点是它允许网络处理并与其他网络分析库连接。除了将街道网络保存为地理数据框外,PyrOSM 还允许你通过将节点和边存储在两个单独的数据框中来提取它们。这里是节点数据框:

从街道网络中提取的节点数据框
如果你有这些图形表示,保存为各种格式是非常简单的:OSMnx、igraph 和 Pandana 并在这些工具中使用它们。
💭 结束思考
这是 pyrosm 在你的地理空间工作中能为你提供的简短总结!我提到了几种非常有用的方法,比如从某个区域下载特定的数据集,或通过界定感兴趣区域以及它与其他库的关系。我认为 pyrosm 最棒的地方就是它能够弥合庞大的 OSM 数据集与工程或分析问题之间的差距。
感谢阅读!
PyScript.com:云中的 PyScript IDE
原文:
towardsdatascience.com/pyscript-com-a-pyscript-ide-in-the-cloud-2b5bde6f0342
PyScript.com 是 Anaconda 推出的一个新在线 IDE,允许你创建、运行和托管 PyScript 应用。
·发表于 Towards Data Science ·12 分钟阅读·2023 年 4 月 13 日
--

哇!他一定是个非常严肃和重要的程序员,如果他需要这么大的屏幕 —— 我很好奇为什么这些屏幕大多是空白的。照片由 Max Duzij 提供,来源于 Unsplash
注意:2023 年末发布了全新重写的 PyScript 版本,这可能使得此处描述的一些语法过时 —— 请参见 PyScript 正在成长 以获取更新。
好消息!目前还不清楚现有的 IDE 或编辑器哪个适合构建 PyScript 应用,但现在有了 PyScript.com,我们有了一个专用的在线 IDE。
它到底有多好?我们将会揭晓。
我们将来看看 Anaconda 的新 PyScript 在线 IDE:我将介绍这个新平台,我们会看看如何开始使用它编写 PyScript 应用,最后我们将完成一个完全功能且已部署的 PyScript 应用。
PyScript.com
Anaconda Inc. 对其新产品的看法毫不隐晦。
“这个革命性的平台使得 99% 的人能够进行编程,推动了 Anaconda 的使命,旨在使数据科学和 Python 开发民主化。” —— Anaconda Inc.
你无疑知道 Anaconda 是一家基于他们自己 Python 发行版的数据科学平台供应商。你可能还知道他们是 PyScript 的发明者(如果你一直关注我在 Medium 上的文章,你肯定知道 —— 见 你好 PyScript,2023 年 PyScript 有何新变化 和 其他)。
Anaconda 通过PyScript.net网站发布了 PyScript;这是一个开源项目,致力于将 Python 应用程序创建为网页,并托管在 GitHub 上。
PyScript 基于 Pyodide,这是一个被移植到 WebAssembly 的 Python 解释器。WebAssembly 是一种将在浏览器中运行的低级语言,这意味着你现在可以在浏览器中本地运行 Python 程序。
当你考虑到这一点时,这确实是件大事。
使用 PyScript,你可以编写与 Javascript 和 DOM 通信的 Python 应用程序,从而创建无需服务器的以 Python 为中心的 Web 应用程序——上传到 Web 主机,它们就能正常工作!
PyScript.net 和 PyScript.com
PyScript.com 不应与 PyScript.net 混淆。新网站不是开源产品的一部分,而是一个新的在线编程环境。根据 Anaconda 的说法:
“一个自由且灵活的编码平台,世界上任何人都可以使用 Python 驱动的数据交互和计算创建下一代 Web 应用程序”
他们还继续说
“该平台现在作为软件服务免费提供。”
然而,这不会永远完全免费。通过创始人订阅的优惠,你可以用$150 获得一年的即将推出的付费功能的免费访问权(我目前不清楚这些功能是什么,但可以推测 Anaconda 认为它们值得付费。我猜测——希望——当前功能会保持免费)。
那么,究竟怎么回事?我们从 PyScript.com 中能得到什么?
入门
首先,你需要一个账户。没问题,前往网站,注册并登录。
你将看到仪表板,那里会有一个新的项目等待你。屏幕看起来大致是这样的:

PyScript.com 仪表板——作者的截图
“Weathered Moon”是为我创建的默认项目的可爱名称——你的项目会有不同的名字。(如果你觉得这个名字不够可爱——或者太可爱——你可以稍后更改名字。)
正如你所见,有选项可以查看或编辑网站,而点状菜单提供了额外的选项,如删除或复制项目。
点击“编辑”,项目将会打开。界面会有三个面板:左侧是文件管理器和编辑器,右侧是显示正在运行的项目的面板。(如果你使用的是手机或小浏览器窗口,配置可能会有所不同。)
默认项目包含三个文件,
-
index.html:定义了 HTML 中的网页
-
main.py:包含 Python 代码
-
pyscript.toml:这将是空的——稍后我们将看到它如何使用。
这是打开项目的截图——你不会对它的功能感到惊讶!

作者的默认项目截图
这个 Python 程序简单地打印了“Hello World!”,这也是应用程序所做的(嗯,你期待什么呢!)。但这个应用程序还有一点更多的内容。
正如我所说的,pyscript.toml 是空的,但让我们看看 HTML。点击 FILES 下的 index.html,它将(当然)在编辑器中弹出。这是你将看到的内容:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Weathered Moon</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="https://pyscript.net/releases/2023.03.1/pyscript.css" />
<script defer src="https://pyscript.net/releases/2023.03.1/pyscript.js"></script>
</head>
<body>
<py-config src="./pyscript.toml"></py-config>
<py-script src="./main.py"></py-script>
</body>
</html>
这个 HTML 文件可以作为许多 PyScript 应用的基础。它的格式是 HTML,但有一些 PyScript 特有的部分。前两个部分是 <link> 和 <script> 标签,它们加载 pyscript.css 和 pyscript.js。这些是任何应用程序所必需的,它们加载使 Python 在浏览器中运行的组件。
接下来的 PyScript 特有部分在主体中;<py-config src="./pyscript.toml"> 加载配置。这可以直接包含在标签中,但将其隐藏在 pyscript.toml 文件中更为整洁。正如我之前所说,这个文件目前是空的,但我们很快会用到它。
然后,我们在 <py-script src="./main.py"> 标签中包含了来自 main.py 的 Python 代码。
一切都很漂亮、整洁且直接。
让我们回到 main.py。
print("Hello, World!")
然后进行更改。
print("Hello, Moon!")
现在点击“运行”按钮,你将看到右侧窗格中的新输出。

作者截图
这里到底发生了什么?在 Python 中,print 语句将内容写入标准输出设备(通常是屏幕),在 PyScript 中,它将内容写入名为 <py-terminal> 的标签中,除非在 HTML 文件中包含这个标签,否则这个标签会在第一次使用 print 时自动创建。因此,当执行 print("Hello Moon!") 时,网页中会创建 <py-terminal> 标签,并将 print 语句的输出写入其中。
坦白说,当我们在构建应用程序时,这并不是特别有用。它适合用于调试目的,但在构建应用程序时,我们生成的输出应该成为网页的一部分——例如在 <div> 中——要输出文本到任意 HTML 标签中,我们不会使用 print,而是使用 display。不过稍后会详细说明。
不过,首先,我们可以通过从右侧菜单中选择“查看站点”来查看浏览器中的应用程序。

作者截图
我这里不打算提供截图,它与预览窗格完全一样。值得稍微惊讶的是,应用程序会在浏览器的新标签页中出现,并且从网站实时提供。它有一个独特的公共 URL,你可以与任何人分享。为了证明这一点,网址如下:
26efd18d-1c15-4b46-b574-58731b341c76.pyscriptapps.com/5b49c512-f88f-493c-9d62-f0d745a298ed/latest/
现在把它输入到你的浏览器中——开玩笑的,当然这是一个链接。
让我们绘制一个图表
我们将对‘Hello World’应用进行一些扩展,制作一个展示 Python 代码和 HTML 如何良好协作的应用。
首先,作为一个好的起点,使用右侧菜单复制你现有的项目。

作者截图
然后我们会得到一个新项目

作者截图
要更改标题,请点击编辑按钮,如图所示,然后保存。
我们不会做任何特别复杂的事情,只是创建一个包含我们在 Python 中绘制的图表的网页。因此,我们需要调整 HTML 代码以适应这个需求。
<!DOCTYPE html>
<html lang="en">
<head>
<title>Let's Plot a Graph</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="https://pyscript.net/releases/2023.03.1/pyscript.css" />
<script defer src="https://pyscript.net/releases/2023.03.1/pyscript.js"></script>
</head>
<body>
<py-config src="./pyscript.toml"></py-config>
<py-script src="./main.py"></py-script>
<h1>Let's Plot a Graph</h1>
<div id="graph"></div>
</body>
</html>
你可以看到这是与我们开始时的 index.html 一样的,但做了一些更改。我更改了标题,并在正文末尾添加了两个标签,首先是一个 <h1> 标题,然后是一个 id 为“graph”的 <div> — 这就是图表将被绘制的位置。
先不要运行它(它仍然会显示‘Hello Moon’),我们需要将 Python 代码添加到 main.py 中。
import matplotlib.pyplot as plt
import pandas as pd
df = pd.DataFrame()
df['x'] = [1,2,3,4,5,6,7,8,9]
df['y'] = [1,2,3,4,5,6,7,8,9]
fig, ax = plt.subplots()
df.plot("x", "y", ax=ax)
display(fig, target="graph")
在导入模块后,我们创建一个 Pandas 数据框,包含两个列用于 x 和 y 轴。它们各自包含整数 1 到 9。接着,我们创建一个 mathplotlib 图形(当然,它将是一个直线图),然后使用 PyScript 命令 display 将图形显示在 id 为“graph”的 HTML 标签中。
现在我们可以运行它了吗?不,请稍微耐心一点。
我们在 Python 代码中导入了 pandas 和 mathplotlib 库,但这些库并不包含在 PyScript 包中。因此,我们需要在 <py-config> 中指定这些库,并在 <pyscript.toml> 中添加以下内容。
packages = ["pandas", "matplotlib"]
你可以使用一大堆 Python 包,但它们需要在配置中指定,并且需要在 Python 代码中导入。配置部分可以用于将外部文件,如数据文件,加载到应用程序中。
好,现在你可以运行它了。

作者截图
你可能之前已经注意到了,但如果你在其自身网页上查看项目,你会看到底部有一个链接。

作者截图
任何能够看到链接的人也可以访问你的代码。点击‘查看代码’链接将打开一个不可编辑(但可能可复制)的项目版本。
所以这就是一个简单的 PyScript 应用,有几点值得注意:
-
给你的默认文件
pyscript.toml, main.py, & index.html是创建新项目的良好起点。所以,创建新项目的第一步可能是复制默认应用并重命名。 -
如果你想使用某个库,你必须在配置中指定它,并在 Python 代码中导入它。
-
PyScript 命令
display用于将 Python 代码的输出写入具有特定 id 的 HTML 标签中。 -
你的代码从 PyScript.com 服务器提供,可以从其 URL 查看。任何拥有该 URL 的人都可以运行应用程序,并查看你的代码(所以不要在里面放秘密)。
有一件事我没有提到,就是如何返回到你的项目仪表板视图。

作者截图
实际上,点击 PyScript 徽标的任何位置都会带你到这个视图。
兔子标志怎么回事?如果有机会的话,Python 不会吃兔子吗?我不确定这是我们想要的 PyScript 形象。
你还可以通过右上角的菜单进入仪表板:

作者截图
一个完整的应用
我答应你一个应用程序,它相当简单,但这里就是。它基于文章 PyScript, Pandas 和 Plotly:一个互动网页应用,但为了简单起见,它使用 matplotlib 代替 Plotly。它还使用 Bootstrap JavaScript 库来美化 UI。
它看起来是这样的:

作者截图
这个应用下载一些天气数据¹,并允许你从下拉菜单中选择一个图表进行显示。(如果你读过我的其他文章,你可能见过无数个使用不同技术的版本。)
我不会详细讲解应用的工作原理,因为大部分内容在上述文章中已有解释,或者在我们已经覆盖的内容中有说明。Python 代码中也有注释解释正在发生什么。
这是 HTML:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Weathered App</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="https://pyscript.net/releases/2023.03.1/pyscript.css" />
<script defer src="https://pyscript.net/releases/2023.03.1/pyscript.js"></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
</head>
<body>
<py-config src="./pyscript.toml"></py-config>
<py-script src="./main.py"></py-script>
<div class="jumbotron p-2">
<h1>Weather Data</h1>
<p class="lead">
Some graphs about the weather in London in 2020
</p>
</div>
<div class="row">
<div class="col-sm-2 m-2">
<b>Select chart from list:</b>
</div>
<div class="col-sm-4 m-2">
<select class="form-control" id="select" py-change="selectChange()">
<option value="Tmax">Maximum Temperature</option>
<option value="Tmin">Minimum Temperature</option>
<option value="Sun">Sun</option>
<option value="Rain">Rain</option>
</select>
</div>
<div class="row">
<div class="col-sm-6 m-2">
<div id="chart1"></div>
</div>
</div>
</div>
</body>
</html>
这是 Python:
# Import libraries
import pandas as pd
import matplotlib.pyplot as plt
import js
# Get the data
# Note you can't use the 'requests' package or similar
# so we import the date using a built-in pyodide function
from pyodide.http import open_url
url = 'https://raw.githubusercontent.com/alanjones2/uk-historical-weather/main/data/Heathrow.csv'
url_content = open_url(url)
# Create dataframe for the year 2020
df = pd.read_csv(url_content)
df = df[df['Year']==2020]
# Create a matplotlib chart and display it in "chart1"
# Note append is false so that old charts are overwritten
def plot(chart):
fig, ax = plt.subplots()
df.plot("Month", chart, ax=ax)
display(fig, target="chart1", append=False)
# The is the call back form the dropdown menu
# it gets the value selected and calls plot
def selectChange():
choice = js.document.getElementById("select").value
plot(choice)
# Call plot on startup
plot('Tmax')
还有配置:
packages = ["pandas", "matplotlib"]
还有一个链接到最终应用的 这里,你可以在这里看到它的全部,查看代码并复制它,如果你愿意的话。
我们怎么认为?
pyscript.com 当然有优缺点。以下是一些我认为值得注意的:
优点:
-
这是一个简单易用的 IDE。
-
所有需要的东西都在一页上。
-
你创建的内容默认是公开的——太好了,我们可以分享!
-
默认应用是新项目的良好起点。
-
复制功能很好,你可以用它从旧项目中创建新项目,或者创建现有项目的新版本。
-
自动部署和免费托管!(不过 URL 有点麻烦)
缺点:
-
你创建的内容默认是公开的——哦,不,没有专有代码!但我一点也不会感到惊讶,如果付费功能在未来能满足这一点。
-
你不能下载一个项目并将其复制到不同的开发环境中。
-
启动有点慢,但这主要是 PyScript 的问题——也许 IDE 会稍微增加启动时间,我不太确定。
我认为第一个优点是最重要的。它非常易于使用,编辑器与 HTML 和 Python 的兼容性很好,并且你对代码所做的任何更改几乎可以即时看到结果。自动部署和托管也很棒。
总的来说,这是一个受欢迎的包。显然,它并不针对商业项目,但却是分享想法和尝试 PyScript 的绝佳场所。
尝试一下吧!
一如既往,感谢你的阅读,希望你觉得有用。我必须说,我很享受写这篇文章和使用 pyscript.com 的过程。你可以在我的GitHub 网页找到我其他的作品,包括更多关于 PyScript 的内容。
如果你不是 Medium 会员,你可以使用我的推荐链接注册,每月只需 $5 即可阅读任何 Medium 内容。
备注
- 天气数据来自我自己的 GitHub 仓库(见代码中的链接),并且属于公共领域。数据来源于英国气象局历史气象站数据,该数据也可以在英国公共部门信息开放政府许可 v3.0下免费使用。
Python 和超越:扩展你的数据科学编程工具包
·
关注 发表在 Towards Data Science ·发送为 Newsletter ·3 分钟阅读·2023 年 3 月 16 日
--
数据科学家在工作中需要编写的代码量在不同组织和项目中差异很大,其复杂程度也是如此。然而,无论你在编程强度的光谱上处于何种位置,增加你编程技能的深度和广度都是有价值的。
为了支持你在这段旅程中,我们收集了几本优秀的指南,帮助你拓展视野,超越你在数据科学培训初期可能已经掌握的 Python 基础知识。
从学习新语言的基础知识到量子计算的新兴前沿,这里应该有足够的内容供任何数据从业者选择自己的冒险。
-
专注于性能。你的代码总是达成目的的手段;在商业环境中,编写效率低下的代码可能对最终结果产生实际的负面影响。正如内奥米·克里格在最近的延迟改进项目中演示的那样,一些小的变化可以产生重大差异。
-
透视表的艺术。苏珊·梅纳的 Pandas 透视表指南是一个方便的一站式资源。它涵盖了通过此工具操作数据的细节,更重要的是,解释了何时应使用透视表以及它们能帮助生成哪些类型的见解。
-
成为多语言程序员。数据科学家可能决定深入学习第二种(或第三种)编程语言的原因有很多:这可能是获得一个令人兴奋的工作的必要步骤,或者你可能只是有探索新事物的冲动。凯·詹·王关于学习新语言的入门指南,无论你的动机是什么,都是必读之作。

照片由analuisa gamboa拍摄,来自Unsplash
-
全世界的 Julia 爱好者,团结起来****! 艾玛·布德罗已经使用 Julia 语言约六年,亲眼见证了它的强大能力。艾玛的新汇编书汇集了不少于 20 个技巧和窍门,供希望提升 Julia 工作流程的其他人参考。
-
培养一些 SQL 技能。从我们刚刚提到的新兴语言转到一个可靠的老牌语言:SQL。尽管它可能即将迎来 50 周年(!)——对任何近期技术而言都是永恒的——但数据专业人士仍每天使用它,马特·索斯纳的“谜题”汇总将促使你磨练查询技巧。
-
实现质的飞跃。如果最近关于量子计算的对话让你感到困惑,或者至少不完全明白这项新兴技术的具体内容,不要错过Kory Becker的易于理解的介绍。它将带我们了解一些核心基本概念,并鼓励读者使用 Python 和 Qiskit 库编写自己的魔法数字猜测游戏。
-
解决实际问题。你是否想利用你的编程技能来促进可持续发展和充满活力、宜居的社区?无论你是在寻找灵感、具体指导,还是两者兼而有之,我们最近发布了一系列全面的文章,涵盖城市规划、供应链优化和野生动物保护(以及其他主题),其中许多文章包含了强烈的实践编码部分。
我们希望你喜欢本周的推荐!如果你想支持我们发布的工作,请考虑成为 Medium 会员。
直到下一个变量,
TDS 编辑部
Python 中的 args、kwargs 以及传递参数给函数的所有其他方式
在 6 个示例中巧妙设计你的函数参数
·发表于 Towards Data Science ·阅读时长 8 分钟·2023 年 3 月 7 日
--

斜杠和星号的作用是什么?(作者提供的图片)
本文深入探讨了如何设计函数参数。我们将了解*args和**kwargs的作用,/和*的功能,以及如何以最佳方式设计函数参数。设计良好的函数参数更容易被其他开发者理解和使用。在本文中,我们探讨了6 个问题,这些问题展示了你需要知道的一切以成为参数专家。让我们开始编码吧!
准备工作:定义和传递参数
在这一部分,我们将快速了解术语和 Python 提供的所有处理参数传递的方法。
装饰器如何工作,何时使用它们以及 6 个逐渐复杂的示例
towardsdatascience.com
参数和实参之间有什么区别?
许多人将这些术语互换使用,但它们之间是有区别的。参数是用参数提供的值初始化的:
-
参数是定义在函数定义中的名称
-
实参是传递给函数的值

参数是红色的,实参是绿色的(作者提供的图片)
我可以通过哪两种方式传递参数?
你可以通过位置和关键字传递参数。在下面的示例中,我们将值hello作为位置参数传递。值world是通过关键字传递的;我们指定要将world传递给thing参数。
def the_func(greeting, thing):
print(greeting + ' ' + thing)
the_func('hello', thing='world')
位置参数和kwargs(关键字参数)之间的区别在于顺序。你传递位置参数的顺序很重要。如果你调用the_func('world', 'hello'),它会打印world hello。传递kwargs的顺序无关紧要:
the_func('hello', 'world') # -> 'hello world'
the_func('world', 'hello') # -> 'world hello'
the_func(greeting='hello', thing='world') # -> 'hello world'
the_func(thing='world', greeting='hello') # -> 'hello world'
the_func('hello', thing='world') # -> 'hello world'
还要注意(在最后一行),只要kwargs在位置参数之后,你可以混合使用位置参数和关键字参数。
args的性能是否优于kwargs?
查看下面的文章!
//args-vs-kwargs-which-is-the-fastest-way-to-call-a-function-in-python-afb2e817120?source=post_page-----bd2acdce72b5-------------------------------- ## Args vs kwargs:哪种是调用 Python 函数的最快方式?
timeit模块的清晰演示
[towardsdatascience.com
设计函数参数
在这一部分,我们将回答 6 个问题,展示你可以如何设计函数参数。每个答案将附带示例和用例(如有需要)。
1. 如何捕获所有未捕获的位置参数?
使用*args,你可以设计函数使其接受任意数量的参数。例如,查看下面的函数。
def multiply(a, b, *args):
result = a * b
for arg in args:
result = result * arg
return result
在这个函数中,我们正常定义了前两个参数(a和b)。然后我们使用*args将所有剩余的参数打包成一个元组。把*看作是吞噬所有不匹配的参数并将它们推送到名为‘args’的元组变量中。让我们看看实际效果:
multiply(1, 2) # returns 2
multiply(1, 2, 3, 4) # returns 24
最后的调用将值 1 分配给参数a,值 2 分配给b,arg变量填充为(3, 4)。由于这是一个元组,我们可以在函数中循环遍历它并使用这些值进行乘法运算!
//why-and-how-custom-exceptions-lead-to-cleaner-better-code-2382216829fd?source=post_page-----bd2acdce72b5-------------------------------- ## 为什么及如何自定义异常能够使代码更干净、更好
通过创建自己的自定义异常来清理代码
[towardsdatascience.com
2. 如何捕获所有未捕获的关键字参数?
我们在前一部分使用的相同技巧可以用于捕获所有剩余的不匹配的关键字参数:
def introduce(firstname, lastname, **kwargs):
introduction = f"I am {firstname} {lastname}"
for key, value in kwargs.items():
introduction += f" my {key} is {value} "
return introduction
与*args类似,**kwargs关键字会吞噬所有不匹配的关键字参数,并将它们存储在名为kwargs的字典中。然后我们可以像在上面的函数中一样访问这个字典。
print(introduce(firstname='mike', lastname='huls'))
# returns "I am mike huls"
print(introduce(firstname='mike', lastname='huls', age=33, website='mikehuls.com'))
# I am mike huls my age is 33 my website is mikehuls.com
使用kwargs,我们可以向introduce函数添加一些额外的参数。
towardsdatascience.com ## 永远不需要再写 SQL:SQLAlchemy 的 ORM 绝对初学者指南
使用这个 ORM,你可以创建表、插入、读取、删除和更新数据,而无需编写一行 SQL 代码
[towardsdatascience.com
3. 我如何设计函数以只接受关键字参数?
当你真的不想混淆你的参数时,你可以强制你的函数只接受关键字参数。一个完美的使用案例可能是一个将钱从一个账户转到另一个账户的函数。你确实不想以位置方式传递账户号码,因为这样你有可能让开发者不小心交换账户号码:
def transfer_money(*, from_account:str, to_account:str, amount:int):
print(f'Transfering ${amount} FORM {from_account} to {to_account}')
transfer_money(from_account='1234', to_account='6578', amount=9999)
# won't work: TypeError: transfer_money() takes 0 positional arguments but 1 positional argument (and 2 keyword-only arguments) were given
transfer_money('1234', to_account='6578', amount=9999)
# won't work: TypeError: transfer_money() takes 0 positional arguments but 3 were given
transfer_money('1234', '6578', 9999)
在上面的函数中你再次看到*。我将星号视为吞噬所有不匹配的位置参数,但与*args将所有不匹配的位置参数存储在args元组中不同,裸*只是将这些参数作废。
towardsdatascience.com ## 了解 Python 上下文管理器:绝对初学者指南
使用光剑理解 WITH 语句
[towardsdatascience.com
4. 我如何设计函数以只接受位置参数?
以下函数是只允许位置参数的函数示例:
def the_func(arg1:str, arg2:str, /):
print(f'provided {arg1=}, {arg2=}')
# These work:
the_func('num1', 'num2')
the_func('num2', 'num1')
# won't work: TypeError: the_func() got some positional-only arguments passed as keyword arguments: 'arg1, arg2'
the_func(arg1='num1', arg2='num2')
# won't work: TypeError: the_func() got some positional-only arguments passed as keyword arguments: 'arg2'
the_func('num1', arg2='num2')
函数定义中的/强制所有在它之前的参数必须是位置参数。附带说明:这并不意味着所有在/之后的参数必须仅为关键字参数;这些参数可以是位置参数和关键字参数。
我为什么需要这样做?这不会降低代码的可读性吗? 好问题!一个例子可能是当你定义一个函数时,这个函数非常明确,以至于你不需要关键字参数来指定它的作用。例如:
def exceeds_100_bytes(x, /) -> bool:
return x.__sizeof__() > 100
exceeds_100_bytes('a')
exceeds_100_bytes({'a'})
在这个例子中,很明显我们在检查'a'的内存大小是否超过 100 字节。我真的想不出一个更好的名字来给x参数,而且可以在不需要指定x=’a’的情况下调用这个函数。另一个例子是内置的len函数:调用len(target_object=some_list)会显得很尴尬。
作为额外说明,我们可以更改参数名,因为我们知道这样不会破坏对函数的调用:我们不允许使用关键字参数。此外,我们甚至可以在完全向后兼容的情况下扩展这个函数。下面的版本将检查任何提供的参数是否超过 100 字节。
def exceeds_100_bytes(*args) -> bool:
for a in args:
if (a.__sizeof__() > 100):
return True
return False
我们可以用*args替换x,因为在之前的版本中,/确保函数仅以位置参数的形式调用。
## Cython 对绝对初学者的指南:两步实现 30 倍更快的代码
快速应用的简单 Python 代码编译
towardsdatascience.com
5. 混合与匹配 — 如何传递既是位置参数又是kwargs的参数?
作为示例,我们将讨论之前提到的len函数。这个函数只允许位置参数。我们将扩展这个函数,允许开发者选择是否计算重复项。我们希望开发者通过kwargs传递这个关键字:
def len_new(x, /, *, no_duplicates=False):
if (no_duplicates):
return len(list(set([a for a in x])))
return len(x)
如你所见,我们希望计算x变量的len。由于x参数前面有/,我们只能以位置参数的方式传递它。no_duplicates参数必须以关键字的形式传递,因为它跟在*之后。让我们调用这个函数:
print(len_new('aabbcc')) # returns 6
print(len_new('aabbcc', no_duplicates=True)) # returns 3
print(len_new([1, 1, 2, 2, 3, 3], no_duplicates=False)) # returns 6
print(len_new([1, 1, 2, 2, 3, 3], no_duplicates=True)) # returns 3
# Won't work: TypeError: len_() got some positional-only arguments passed as keyword arguments: 'x'
print(len_new(x=[1, 1, 2, 2, 3, 3]))
# Won't work: TypeError: len_new() takes 1 positional argument but 2 were given
print(len_new([1, 1, 2, 2, 3, 3], True))
## 用 OpenCV 破坏《Duck Hunt》 — 初学者的图像分析
编写能够击败所有《Duck Hunt》高分的代码
towardsdatascience.com
6. 混合与匹配 — 综合应用
下面的函数是如何将所有之前讨论的技术结合在一起的极端示例。首先,它强制前两个参数以位置参数的方式传递,接下来的两个参数可以以位置参数和关键字参数的方式传递,然后是两个仅限关键字的参数,最后我们用**kwargs捕捉其余未捕获的参数。
def the_func(pos_only1, pos_only2, /, pos_or_kw1, pos_or_kw2, *, kw1, kw2, **extra_kw):
# cannot be passed kwarg <-- | --> can be passed 2 ways | --> can only be passed by kwarg
print(f"{pos_only1=}, {pos_only2=}, {pos_or_kw1=}, {pos_or_kw2=}, {kw1=}, {kw2=}, {extra_kw=}")
你可以像这样传递这个函数:
# works (pos_or_kw1 & pow_or_k2 can be passed positionally and by kwarg)
pos_only1='pos1', pos_only2='pos2', pos_or_kw1='pk1', pos_or_kw2='pk2', kw1='kw1', kw2='kw2', extra_kw={}
pos_only1='pos1', pos_only2='pos2', pos_or_kw1='pk1', pos_or_kw2='pk2', kw1='kw1', kw2='kw2', extra_kw={}
pos_only1='pos1', pos_only2='pos2', pos_or_kw1='pk1', pos_or_kw2='pk2', kw1='kw1', kw2='kw2', extra_kw={'kw_extra1': 'extra_kw1'}
# doesnt work, (pos1 and pos2 cannot be passed with kwarg)
# the_func(pos_only1='pos1', pos_only2='pos2', pos_or_kw1='pk1', pos_or_kw2='pk2', kw1='kw1', kw2='kw2')
# doesnt work, (kw1 and kw2 cannot be passed positionally)
# the_func('pos1', 'pos2', 'pk1', 'pk2', 'kw1', 'kw2')
## Python 中的多任务处理:通过同时执行操作使程序速度提高 10 倍
将线程和进程应用于加速代码的逐步指南
towardsdatascience.com
结论
在这篇文章中,我们讨论了设计函数参数的所有方法,并展示了如何混合和匹配这些参数,以便开发者能够以最佳方式使用你的函数。
我希望这篇文章如我所希望的那样清晰,如果不是这样,请告诉我我可以做些什么来进一步澄清。与此同时,看看我关于各种编程相关主题的其他文章,例如:
编程愉快!
— 迈克
附言:喜欢我在做的事情吗? 关注我!
[## 使用我的推荐链接加入 Medium — 迈克·胡尔斯
阅读迈克·胡尔斯及其他数千位 Medium 作者的每一个故事。你的会员费直接支持迈克…
mikehuls.medium.com](https://mikehuls.medium.com/membership?source=post_page-----bd2acdce72b5--------------------------------)
Python 断言,或检查猫是否是狗
原文:
towardsdatascience.com/python-assertions-or-checking-if-a-cat-is-a-dog-ce11c55d143
PYTHON 编程
了解在 Python 中使用断言的规则——以及不使用它们的规则
·发表于 Towards Data Science ·13 分钟阅读·2023 年 3 月 2 日
--

错误的断言应该让你停止:有什么问题!图片由 Jose Aragones 提供,来源于 Unsplash
断言是你用来测试对程序的假设的语句。这一简短的定义,一方面很清晰。另一方面,它远未解释你何时应该使用断言。
assert 语句,作为 Python 中主要的断言工具,与内置的 __debug__ 对象密切相关。在我学习 Python 的某个阶段,我对这个对象一无所知,因此我猜许多数据科学家和 Python 开发者也不清楚。在阅读本文后,你将了解如何使用 __debug__ 和断言——以及如何 不 使用它们。
你会在测试中找到断言的主要位置。无论你使用哪个测试框架,它都会使用断言。虽然 unittest 使用特定类型的断言方法(如 .AssertTrue(), .AssertFalse(), .AssertEqual()),pytest 更喜欢裸露的 assert 语句。就个人而言,我喜欢后者的简洁。如果你想断言 x 是 10,可以用这种简单的方法:
assert x == 10
当你想要断言 x 是整数时,可以这样做:
assert isinstance(x, int)
对我来说,这很简单明确,而简单和明确是 Python 代码的重要美德。测试也不例外。
当条件不成立时,assert 语句会引发 AssertionError:
>>> x = 20
>>> assert x == 10
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError
你还可以使用可选消息:
>>> x = 20
>>> assert x == 10, "x is not 10"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError: x is not 10
现在,为什么不能在 if 块中做这个,而不使用 assert 呢?过程如下:
>>> x = 20
>>> if x != 10:
... raise AssertionError("x is not 10")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError: x is not 10
如果你希望你的程序在 x 不为 10 时抛出异常,你可以这样做。但这不应该是 AssertionError,因为 AssertionError 是在特定情况下使用的特定错误类型。然而,还有更多的内容。
正如 Mark Lutz 在他出色的书籍 Learning Python(第 5 版)中所写的那样,assert 语句只是一个快捷方式。在我们上面的例子中,我们可以用两种等效的方式编写断言(让我们使用可选消息);第一种是你已经知道的:
assert x == 10, "x is not 10"
这是一个更长代码段的快捷方式:
if __debug__:
if x != 10:
raise AssertionError("x is not 10")
与之前的 if 块相比,它们确实不同——显而易见的问题是,__debug__ 是什么?对这个问题的答案将帮助我们理解断言是什么以及何时使用它。
什么是 debug?
__debug__ 对象是一个布尔变量,可以直接在你的 Python 会话中使用:

一张来自 Python 3.11 调试模式会话的截图。图片来源:作者
注意,你不能在 Python 会话中更改它:

一张来自 Python 3.11 调试模式会话的截图。图片来源:作者
我将稍后展示如何将 __debug__ 更改为 False。但首先,让我们看看 __debug__ 与断言的关系:

一张来自 Python 3.11 调试模式会话的截图。图片来源:作者
正如你所见,当你在调试模式下运行 Python 时——这是默认模式——__debug__ 为 True,断言按常规方式工作。
让我们在生产模式下打开 Python REPL,以查看断言在那里的工作方式。为此,我们需要提供一个 -O 标志:

一张来自 Python 3.11 生产模式会话的截图。图片来源:作者
正如你所见,__debug__ 现在为 False,这意味着我们在生产模式下工作。这意味着代码将被优化:
-
当
__debug__为True时,所有断言以及跟随if __debug__:检查的其他内容(在这里我将称之为调试模式检查)将被执行。 -
当
__debug__为False时,代码会被优化,以至于调试模式检查中的代码不会被执行。正如我们上面所看到的,这包括所有的断言,它们不会被运行。我们可以在上面的截图中看到这一点。
特别注意,当 __debug__ 为 False 时,无论是 assert True 还是 assert False 都不会做任何事情。所以,特别地,assert False 没有引发 AssertionError,而在调试模式下它会。这完全是因为 __debug__ 为 False,这意味着断言被关闭了。
如何使用 debug 来优化代码执行
如上所述,生产模式下执行的代码是经过优化的。这意味着只有一件事:在调试模式下的检查代码将不会被执行。因此,你可以使用__debug__来添加仅在调试模式下执行的代码;在生产模式下,这段代码将被忽略。这样,你的生产代码将会更快——当然,前提是它包含调试模式检查,包括断言。
为了实现这一点,你可以手动添加代码到调试模式检查中:
if __debug__:
if x < 7:
debug_logger.warning(f"x is below seven: {x = };"
" hence it's set to 7")
x = 7
elif x > 13:
debug_logger.warning(f"x is over thirteen: {x = };"
" hence it's set to 13")
x = 13
else:
debug_logger.info(f"x is fine: {x = }")
如果你在生产模式下运行代码,这个if块的内容将不会被执行,debug_logger也不会记录任何东西。假设你有很多这样的检查(例如,在一个长循环中);忽略它们可以使代码更快。
我可以想象,这可能有点难以思考。我的建议是,下次你编写代码时,考虑是否有一些代码只希望在调试模式下执行,而在生产模式下不执行。有时,你可能找不到任何这样的东西;其他时候,你可能会发现这样的情况。
然而,你应该能够找到在某些地方使用断言会很有效。我们将在下面讨论这个问题。
总结一下,记住两件事:
-
当你使用很多断言和调试检查时,代码可能会显著变慢。
-
如果你想运行一些代码,不管模式如何,为什么还需要检查
__debug__是否为真呢?当然,当使用assert语句时,实际上是会在底层进行检查的——但我们已经知道它是如何进行的。然而,请记住,如果你希望代码在调试模式和生产模式下都能运行,就不要在调试检查中添加代码。
何时使用断言
终于!现在我们知道了__debug__和调试模式,我们可以开始讨论断言。
如上所述,就代码而言,断言是在调试模式下执行的检查:当条件为真时,什么也不会发生,而当条件不满足时,会引发AssertionError。
这并没有完全解释你何时应该使用断言。简单地说,使用断言
-
在测试中(测试总是在调试模式下进行),以检查特定测试是否通过;
-
在开发模式下,以检查绝不应该发生的条件。
至于测试,一切都很清楚。正如我上面所写,pytest使用断言作为检查条件的主要工具。你通常在调试模式下运行单元测试。然而,尝试在生产模式下运行pytest,例如使用python -O -m pytest,你会看到以下警告:
PytestConfigWarning: assertions not in test modules or plugins
will be ignored because assert statements are not executed by
the underlying Python interpreter (are you using python -O?)
self._warn_about_missing_assertion(mode)
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
正如你所见,当你想在生产模式下运行pytest时,它实际上是在我们可以称之为混合模式的模式下运行。测试函数中的所有assert语句都会被执行,但实际代码中的任何断言都不会被执行。这就是PytestConfigWarning告诉我们的。
至于开发模式,情况有所不同。正如你在上面所读到的,assert 语句帮助你检查一个绝不可能发生的条件是否为真。这乍一看可能有些奇怪。我为什么要检查一个不应该发生的条件?这不是像检查猫是否是狗一样吗?
正确!你明白了!在代码中使用断言就像检查猫是否是狗。当然,我们知道它不是——而断言 assert cat is not dog 并不真正旨在检查 cat is not dog,我们确实知道这是真的,而是检查代码是否正确。
换句话说,断言帮助你检查代码是否按预期工作。如果不是,它可能导致一些不可能的情况,例如猫是狗、整数是字符串、自然数是负数、样本大小大于总体大小等。因此,请记住断言是什么:你检查一些显而易见的东西——当它不是时,断言失败,你就知道代码或代码实现的逻辑有问题。
断言
assert cat is not dog并不真正旨在检查cat is not dog,我们确实知道这是真的,而是检查代码是否正确。
如果根据你的代码 cat is dog,这当然是不正确的,断言将失败并引发 AssertionError。这意味着代码不正确。
现在我们知道了何时使用断言。首先,你可以在测试中使用它们。其次,你可以通过添加必须为真的断言来确保代码的正确性。如果这样的断言失败了,代码就是不正确的——因为猫不能是狗。
如果这样的断言失败了,代码就是不正确的——因为猫不能是狗。
还有一点重要的事情要补充。不要过度使用断言。不要仅仅因为可以就把它们放到任何地方。只在重要的地方使用它们,那些地方有重要意义。用它们来捕捉重要的缺陷。
不要过度使用断言。不要仅仅因为可以就把它们放到任何地方。
什么时候不使用断言
既然我们知道了什么时候使用断言,那么何时不使用它们也应该很明确。
首先,你不应该使用断言来处理常规异常。这些异常可能是错误的参数值、数据、错误的密码等。这类错误应该以常规方式处理。
你不应该使用断言来处理常规异常。
让我们来看一下。考虑以下函数:
def preprocess_text(text: str) -> str:
assert isinstance(text, str)
return text.lower().strip()
这个函数旨在以特定方式预处理一个字符串。在我们的例子中,预处理非常简单,text.lower().strip(),但这只是函数可能执行的一种示例。该函数还检查提供的参数 text 的值是否具有正确的类型,即 str;如果不是,它会引发异常,对吧?
错误!为了检查类型,函数使用了assert语句,而我们已经知道这不正确。首先,请注意,如果你提供了不同类型的对象,将引发AssertionError,而且它不会说明应该是什么——即类型不正确。Python 对此有TypeError。
其次,请注意在生产模式下,这个检查不会被执行。这真的是你希望这个函数的行为吗?我更倾向于说,如果你需要检查text的类型,那么你应该在两种模式下都进行检查。在这里,你可能会在调试模式和生产模式中得到不同的行为。我猜很多在这种情况下使用assert的开发者可能不知道这一点。
我们知道哪里出了问题。这个函数不应该使用assert。相反,它可以使用if语句结合raise语句,或者使用专门的工具,如easycheck包:
## GitHub - nyggus/easycheck:一个提供简单且可读断言的 Python 函数的模块
一个提供用于代码内部和测试中的简单、可读断言的 Python 函数的模块。…
我计划写一篇关于easycheck的更长文章,但你已经可以在这里阅读它的特定用例:
easycheck 可以帮助你在类似断言的情况下比较浮点数
towardsdatascience.com
在我们上面的函数中,我们可以添加一个easycheck检查,如下所示:
import easycheck
def preprocess_text(text: str) -> str:
easycheck.check_type(
text,
expected_type=str,
handle_with=TypeError,
message="Argument text must be string, "
f"not {type(text).__name__}"
)
return text.lower().strip()
上述检查可以用以下方式理解——在我看来是自然的:检查text的类型;它应该是str;如果不是str,则引发TypeError,并附上以下消息:f"Argument text must be string, not {type(text).__name__}"。
所以,当你提供一个整数时,你会看到以下内容:
>>> preprocess_text(108)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Argument text must be string, not int
让我们总结一下这个例子:
问题: 我是否应该在preprocess_text()中使用断言来检查text是否是字符串?
答案: 不
问题: 为什么不呢?
答案: 你不应该在这里使用断言,因为不同类型的text不是一种不可能发生的情况。当用户提供了错误类型的text参数时,这种情况确实可能发生。
问题: 那么我应该使用什么呢?
答案: 你可以使用if检查text的类型,并在类型不正确时引发TypeError。或者你可以使用easycheck,这是一个专门针对这种情况的工具。
生产模式、测试和断言
现在,一个重要的问题是你是否应该使用通过-O标志调用的生产模式。
在他的Python 中的清晰代码:开发可维护和高效的代码一书中,Mariano Anaya 说你不应该这么做。你不应该,因为断言有助于捕捉错误,那么为什么仅仅因为你在生产中运行代码就放弃这个机会呢?当断言失败时,代码中出现了严重的问题 —— 无论如何代码都会崩溃,但可能会晚一些。最好尽早引发异常。
我完全同意上述方法,但……
有时使用生产模式更好。这是在执行时间至关重要且代码中的断言显著减慢了速度的情况下。假设代码经过了充分测试,你可能会选择关闭所有断言,以使应用程序运行得更快 —— 尤其是当你使用了很多断言时。
在一些项目中,决定是否使用生产模式很简单。当执行时间不重要时,在调试模式下运行你的生产代码。这就像在生产中测试生产代码 —— 没有比这更好的测试了。在其他项目中,决定也很简单 —— 当执行时间重要时,我的意思是它确实很重要,使用生产模式。在这种情况下,代码应该通过单元测试和集成测试进行良好的覆盖,并且所有测试应在每个新版本部署之前运行。¹ 尽管如此,如果你想使用生产模式,我认为你应该在调试模式和生产模式下运行测试。后者 —— 实际上我上面称之为混合模式 —— 可以帮助你捕捉在调试模式下无法捕捉到的错误。由于我从未找到关于这个主题的任何字样,我计划写一篇专门的文章,详细解释和举例;当它准备好并发布时,我会在这里链接。
在一些项目中,决策将不会那么简单。你必须根据项目的假设、代码和测试的质量以及测试覆盖率来决定是否运行断言或关闭它们。
结论
我写这篇文章是因为我注意到许多 Python 开发者不了解断言是什么。我对此感到同情。我也经历过。在我的 Python 学习旅程的某个时刻,我也不理解断言。
我希望这篇文章对断言和__debug__进行了充分的解释。让我们总结一下我们讨论的内容:
-
断言仅在调试模式下执行,这也是默认的 Python 模式。通过使用
-O标志来在生产模式下运行 Python,即python -O。 -
使用断言来检查必须为真的条件。如果它们失败,说明代码中有问题。
-
不要使用断言来检查其他事情,比如与参数值相关的条件。这些检查应该使用
if检查结合raise,或使用像easycheck这样的专用工具进行。 -
使用
__debug__添加在调试模式下执行的代码,而在生产模式下不执行。 -
当你在生产模式下运行你的生产代码时,你也应该在生产模式下运行你的单元测试和集成测试。
总的来说,总是根据具体情况决定是否使用生产模式。
如果你想了解更多关于 AssertionError 的信息,下面的文章展示了一个小技巧,即如何用不同类型的异常覆盖它。例如,你可能希望在单元测试中使用自定义的项目异常而不是内置的 AssertionError。我认为这不是你在生产代码中实际使用的东西,但这可以帮助你理解异常处理和断言——以及 Python 本身。
[## 如何在 Python 中覆盖 AssertionError 并使用自定义异常
Python 的 assert 语句使用 AssertionError。了解如何使用其他异常代替
betterprogramming.pub](https://betterprogramming.pub/how-to-overwrite-asserterror-in-python-and-use-custom-exceptions-c0b252989977?source=post_page-----ce11c55d143--------------------------------)
如果你对如何格式化长断言感兴趣,你可能会发现以下文章很有趣:
了解为什么在使用消息时不应将 assert 语句括起来。
你会在文中看到,当断言太长无法放在一行时该怎么办——以及为什么永远不要用括号围绕它的条件和消息。
脚注
¹ 嗯,你应该总是在部署新版本之前运行测试。然而,有时你可能决定只运行重新部署的模块的测试,但这取决于应用程序的架构。无论如何,在部署之前运行所有测试总是更安全。我们编写测试是为了运行它们,不是吗?
感谢阅读。如果你喜欢这篇文章,你可能还会喜欢我写的其他文章,你可以在这里查看。如果你想加入 Medium,请使用下面我的推荐链接:
[## 通过我的推荐链接加入 Medium - Marcin Kozak
阅读 Marcin Kozak 的每一个故事(以及 Medium 上成千上万的其他作家的故事)。您的会员费用将直接支持…
medium.com](https://medium.com/@nyggus/membership?source=post_page-----ce11c55d143--------------------------------)
Python 可调用对象:基础和秘密
原文:
towardsdatascience.com/python-callables-the-basics-and-the-secrets-ba88bf0729aa
PYTHON 编程
了解 Python 可调用对象的强大功能。
·发布于 Towards Data Science ·10 分钟阅读·2023 年 10 月 27 日
--

在 Python 中,有许多可调用对象可以选择。照片由 Pavan Trikutam 提供,来源于 Unsplash
在编程语言中,可调用对象通常与函数相关联,这也是有充分理由的。函数可能是可调用对象的最佳示例,但它们并不是唯一的。在 Python 中,还有许多其他可调用类型,它们可以非常有用且强大。你还可以创建自己的可调用对象。本文将讨论这两者。
可调用对象是指可以通过一对括号来调用的对象,例如下面的例子,我们使用了内置函数 sum():
>>> sum([1, 1, 1])
3
对可调用对象的调用,根据其定义,可能是
-
没有任何参数,如
no_args_callable() -
或一系列位置参数和/或关键字参数,如
args_callable(arg1, arg2)、args_callable(arg1, arg2=value2)或args_callable(arg1=value1, arg2=value2)
上述中,我将可调用对象描述为一个名词。然而,可调用对象一词也可以用作形容词,意思是 作为一个可调用对象。因此,可调用对象与可调用对象是相同的。
Python 有一个内置函数 callable(),用于检查一个对象是否是可调用的,或者换句话说,是否是一个可调用对象。请考虑以下实际的可调用对象示例:
>>> callable(lambda x: x + 1)
True
>>> callable(print)
True
>>> def foo(): ...
>>> callable(foo)
True
下面的对象不是可调用的:
>>> callable(None)
False
>>> callable(10)
False
>>> callable("hello")
False
上述正面示例是关于函数的,这也是大多数人对可调用对象的主要关联。然而,实际上,每个 Python 类都是可调用的。如果你了解 Python 中面向对象编程的基础知识,你会知道要创建一个类的实例,你需要执行以下操作¹
>>> class Empty: ...
这看起来完全像是一次调用,实际上确实如此——这就是为什么 Python 类是可调用的原因。
这段代码显示了Empty类是可调用的,但事实上,每一个 Python 类都是可调用的。然而,在 Python 术语中,“可调用类”通常用于表示不同的东西:其实例是可调用的类。
我们的Empty类是可调用的,但其实例不是:
>>> empty_instance = Empty()
>>> empty_instance()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'Empty' object is not callable
这段代码抛出了TypeError,因为Empty类的实例不可调用:
>>> callable(empty_instance)
False
要使类的实例可调用,你需要实现.__call__()方法。我们在下面做到了这一点,尽管该方法是空的——它什么也不做:
>>> class EmptyCallable:
... def __call__(self): ...
>>> empty_call_instance = Empty()
>>> callable(empty_call_instance)
True
>>> empty_call_instance()
如你所见,什么都没发生——这基本上意味着Empty()返回了None。然而,这次没有抛出错误,因为EmptyCallable的实例确实是可调用的。
注意:每个 Python 类都是可调用的,这意味着调用它会创建一个类的新实例。然而,并非所有 Python 类的实例都是可调用的。要使实例可调用,你必须在类体中实现__call__()方法。我们通常只将类称为可调用类,当其实例是可调用的,即使从理论上讲这并不完全正确。
可调用对象的示例
有时候,展示某样东西的最佳方式是……去做它。因此,我将提供一系列可调用对象的示例,然后我们将讨论使对象可调用的情况。
让我们从最明显的示例开始,然后逐渐深入到不太常见的示例。
常规和 lambda 函数
>>> def foo(): ...
>>> callable(foo)
True
>>> callable(lambda: ...)
True
类和类实例
>>> class Empty:
... def __call__(self): ...
... def method(self): ...
... @staticmethod
... def static_method(): ...
... @classmethod
... def class_method(cls): ...
>>> callable(Empty)
True
>>> callable(Empty.class_method)
True
>>> instance = Empty()
>>> callable(instance)
True
>>> callable(instance.method)
True
>>> callable(instance.static_method)
True
来自 operator 模块的函数
>>> from operator import mul, itemgetter, or_
>>> callable(mul), callable(itemgetter), callable(or_)
(True, True, True)
部分对象
>>> from functools import partial
>>> def foo(x, y, z): return f"{x = }, {y = }, {z = }"
>>> foo(1, 2, 3)
'x = 1, y = 2, z = 3'
>>> fooxy5 = partial(foo, z=5)
>>> fooxy5(1, 2)
'x = 1, y = 2, z = 5'
>>> callable(fooxy5)
True
装饰器
>>> def decorator(func):
... def inner():
... print("I'm a callable and I return one!")
... return func
... return inner
>>> callable(decorator)
True
>>> callable(decorator(foo))
True
闭包
>>> callable(lambda x: lambda y:x*y)
True
让我们在这里稍作停顿。这一行代码需要一些解释。
我绝不会推荐编写这段代码。唯一的好处是它展示了你对 Python 复杂性的理解。然而,它绝不应该在生产代码中编写。
让我们看看它是如何工作的:
>>> mult = lambda x: lambda y:x*y
>>> mult_by_5 = mult(5)
>>> mult_by_5(2)
10
你看到发生了什么吗?mult函数是闭包的一个示例:一个返回函数的函数,内层函数可以访问外层函数作用域中的自由变量,即使外层函数mult()已经返回。
如果你对operator模块比较熟悉,你会知道它的一些函数是闭包。示例包括mul、add、itemgetter和methodcaller。了解闭包及其工作原理是很有益的,因为理解它们可以将你的 Python 技能提升到更深层次。
闭包在 Python 中非常有用,并以多种不同方式使用,但这不是本文的主题。我们将另择时间讨论它们。现在对我们来说重要的是闭包是可调用的,并返回可调用的,这与上面的decorator装饰器观察到的情况相同。
让我们重写闭包,使其更具可读性;这样更容易看到发生了什么,以及为什么闭包既是又返回可调用对象:
>>> def defmult(x):
... def inner(y):
... return x * y
... return inner
>>> defmult_by_5 = mult(5)
>>> defmult_by_5(2)
10
绝对更干净。如果你注意到装饰器只是闭包的特定示例,那你是对的。无论如何:
>>> callable(defmult)
True
>>> callable(defmult_by_5)
True
所以,闭包是并且闭包返回可调用对象。
短暂地,我想回到functools.partial。在 Python 中讨论可调用对象时,不能忽视它们——因为部分对象构成了一个极其有用的工具。
这是另一个值得专门讨论的主题,所以我只会展示一些简单的这些强大可调用对象的例子。官方 Python 文档 解释了部分对象如下:
partial()用于部分函数应用,它“冻结”函数的某些参数和/或关键字,结果是一个具有简化签名的新对象。
因此,你可以实现类似于闭包的功能。还记得吗?
>>> mult = lambda x: lambda y: x*y
>>> mult_by_5 = mult(5)
>>> mult_by_5(2)
10
我们可以通过以下方式实现相同的功能:
>>> from functools import partial
>>> def mult(x, y): return x*y
>>> partialmult_by_5 = partial(mult, y=5)
>>> partialmult_by_5(2)
10
你可以创建一个新的可调用对象,使某些(甚至全部,如果这是你需要的)参数被赋予特定的值。你也可以用它来改变函数参数的默认值:
>>> def multiply_str(s: str, n: int = 2) -> str:
... return s*n
>>> multiply_str("abc")
'abcabc'
>>> multiply_str_5 = partial(multiply_str, n=5)
>>> multiply_str_5("abc")
'abcabcabcabcabc'
从技术上讲,部分对象不是函数:
>>> type(multiply_str)
<class 'function'>
>>> type(multiply_str_5)
<class 'functools.partial'>
>>> callable(multiply_str_5)
True
它们是可调用的,而且它们是部分对象。还值得一提的是functools.partialmethod(),它创建的部分对象可用作类方法,与functools.partial()不同,后者的对象用作函数。如果你感兴趣,我希望在不久的将来发布专门的文章;目前,你可以阅读官方文档。
什么时候需要使对象可调用?
如上所示,不仅 Python 充满了可调用对象,我们还可以轻松创建它们。以下文章展示了这一点:
为了学习如何进行创造性编程,我们将在 Python 中实现可调用的浮点数。
[towardsdatascience.com
我在这里展示了如何实现一个可调用类Float,它继承自float。这个类的实例是可调用的,所以我们在讨论可调用的浮点数。
为什么要这样做?你能通过这种方式实现什么?考虑这个从上面文章中提取的例子,如果你对Float类的实现感兴趣,你可以在那里找到它:
>>> i = Float(12.105)
>>> 2*i
24.21
>>> i(round)
12
>>> i(lambda x: 200)
200
>>> i(lambda x: x + 1)
13.105
>>> def square_root_of(x):
... return x**.5
>>> i(square_root_of)
3.479224051422961
>>> i(lambda x: round(square_root_of(x), 5))
3.47922
>>> i = Float(12345.12345)
>>> i(lambda x: Float(str(i)[::-1]))
54321.54321
所以,你可以调用一个Float数字,并将一个函数作为参数提供,函数将应用于实例所保持的数字。相关的文章并没有指出这样一个可调用类是有用的;我也不打算在这里提出这个观点。不试图证明这样的类有意义,文章讨论了 Python 中的创造力,并展示了 Python 编码的乐趣。
那我为什么提到这一点?因为这篇文章确实展示了其他内容。它表明你可以轻松创建可调用对象。关键是要知道不仅仅如何做,而且——如果不是主要的话——何时做。
当你可能需要创建一个可调用对象时,下面的例子提供了一些。我将省略最明显的,比如需要创建一个函数或类。
-
类函数对象。例如,你可能想要创建一个函数类对象,它接受参数并返回一个值,但也有附加的状态或行为。闭包、装饰器和上下文管理器是很好的例子。
-
使用可调用对象的设计模式。策略模式就是一个完美的例子;它允许你定义一系列算法,封装每一个算法,并使它们可互换。
-
动态函数和动态可调用对象。这意味着在运行时创建一个函数或可调用对象。可调用对象允许你轻松做到这一点。
这相当技术性和理论性,所以我们来分析一个实际的例子。假设你有一个ClassifyTextTo类,旨在将文本分类到多个类别中。我们忽略实现细节,专注于类的设计。我们可以写出这个类的以下原型:
class ClassifyTextTo:
def __init__(self, config, path):
self.config = config
self.path = path
def read_text(self):
...
def preprocess_text(self):
...
def classify(self):
...
def diagnose(self):
...
def report(self):
...
def pipeline(self):
self.read_text()
self.preprocess_text()
self.classify()
self.report()
.pipeline()方法解释了整个过程:
-
文本从
self.path中读取 -
文本被预处理,以使其准备好用于分类模型
-
分类模型被运行;它在
self.config中配置 -
模型被诊断
-
创建报告并记录
该类适用于特定的文本——或者更具体地说——适用于位于path中的特定文件。因此,对于每个文件,你创建一个类的实例并运行管道,就像这里:
>>> classify1 = ClassifyTextTo("texts/text1.txt", configuration)
>>> classify1.pipeline()
>>> classify2 = ClassifyTextTo("texts/text2.txt", configuration)
>>> classify2.pipeline()
如果你有更多类似的文本,可以循环处理:
>>> for text in texts:
... classify = ClassifyTextTo(text, configuration)
... classify.pipeline()
或者,更简单地说:
>>> for text in texts:
... ClassifyTextTo(text, configuration).pipeline()
注意,我们正在为每个文本创建ClassifyTextTo的实例。我们需要这样做吗?
当你在一个类中运行管道时,通常创建一个可调用类是一件自然的事情。考虑这个替代原型:
class CallClassifyTextTo:
def __init__(self, config):
self.config = config
def read_text(self, path):
...
def preprocess_text(self):
...
def classify(self):
...
def diagnose(self):
...
def report(self):
...
def __call__(self, path):
self.read_text(path)
self.preprocess_text()
self.classify()
self.report()
尽管实现看起来没什么不同,但差异是显著的,体现在设计上。虽然ClassifyTextTo需要为每个路径创建一个实例,CallClassifyTextTo则不需要。如果所有文本的配置都相同,我们只需使用一个实例。事实上,即使配置需要从路径到路径变化,我们也可以使用一个实例,但那样我们将失去设计的重大优势——为每个路径使用相同的实例:
>>> classify = CallClassifyTextTo(configuration)
>>> classify("texts/text1.txt")
>>> classify("texts/text2.txt")
或者:
>>> classify = ClassifyTextTo(configuration)
>>> for text in texts:
... classify(text)
这是一件自然的事情,因为现在我们有了classify对象,它是可调用的,得益于ClassifyTextTo类中的.__call__()方法。这比第一种方法便宜一点,因为它只创建一个类实例。
对我来说,最重要的是对每条路径调用classify()是一件自然的事,因为这意味着对每条路径运行整个管道。我喜欢这个设计的简单性:
-
CallClassifyTextTo类的实例代表一个特定的模型,而不是路径。 -
运行一个模型是一种操作,而调用实例确实代表了这种操作,就像调用一个函数一样。
我并不是说这是唯一正确的方法。在编程中,通常有几种方法是正确的。在这种情况下,我使用几个标准来决定使用哪一种:
-
代码的可读性和简洁性
-
代码设计如何反映实际对象和操作
-
性能
-
团队和我个人的偏好
结论
我们讨论了 Python 编程中可调用对象的基础和复杂性。我会说,如果你想成为一名高级 Python 开发人员,你需要了解这两方面。幸运的是,它们不像最初看起来那么困难。
另一方面,我只是讨论了如何理解 Python 中的可调用对象。这与理解它们可以使用的每个场景不同。例如,闭包是一个相当复杂的话题,理解它们与理解如何创建具有可调用实例的类完全不同。通常,你使用的唯一闭包是装饰器,但有一天你可能需要在其他场景中使用它们。一个例子是the [rounder](https://github.com/nyggus/rounder) package和_do()函数:
[## rounder/rounder/rounder.py at main · nyggus/rounder
用于在复杂 Python 对象中舍入浮点数和复杂数字的 Python 包。 - rounder/rounder/rounder.py at main…
尽管如此,我希望这篇文章能帮助你理解 Python 可调用对象的基础知识,并了解它们的一些细节。从现在起,在你的 Python 工作中,记住 Python 可调用对象可能非常强大,并考虑是否使用它们可以改善项目的代码设计。
脚注
¹ Empty类什么都不做。然而,这并不意味着它没有任何实际用途。例如,它可以作为一个哨兵,即一个用于指示特定状态或条件的对象。None是最著名的哨兵例子。
感谢阅读。如果你喜欢这篇文章,你可能也会喜欢我写的其他文章,你可以在这里查看。如果你想加入 Medium,请使用下面的推荐链接:
[## 使用我的推荐链接加入 Medium - Marcin Kozak
作为 Medium 会员,您的一部分会员费用会分配给您阅读的作者,并且您可以完全访问每一个故事……
《Python 类简易入门:面向对象编程的权威指南》
通过这本全面的类参考书提升你的 Python 技能
·发布于 Towards Data Science ·阅读时间 18 分钟·2023 年 3 月 13 日
--

图片由 Lukas Bieri 提供,来源于 Pixabay
当涉及到类时,许多 Python 开发者会遇到困难,原因有很多。首先——在我看来——因为面向对象编程的概念并不总是清晰。其次,因为类和面向对象编程(OOP)背后的想法有很多,我们可能在网上找到的解释(主要是在线的)可能是肤浅的。
在这篇文章中,我想讨论 Python 类背后的最重要概念,以及如何使用它们(附带编码示例)。
首先,我们将通过讨论面向对象编程来开始这篇文章。
**Table of Contents**
Object Oriented Programming
Classes in Python
The "self" Parameter
The "__init__" Method
if __name__ == "__main__"
Type Hints
Docstrings (and how to invoke them)
Inheritance
Pro tip on how to use Python classes
面向对象编程
引用和改写参考文献 [1],我们可以说,作为人类,我们完全知道对象是什么:它们是所有可以用我们的感官感受到并且可以被操控的实物。在我们的成长过程中,我们学习了抽象的力量,对象变成了模型。换句话说,我们利用在多年的经验中培养出的想象力,通过简单的概念来简化复杂的问题:这就是使用模型进行抽象的过程。
比如,我们可以将地球建模为一个球体(即使它不是一个球体!)以进行一些计算(例如,确定它的轨道)。
这与我们在软件开发中所做的并没有太大不同。实际上,对我们开发者来说,对象并不是我们可以触摸或感受到的东西,而是执行一些软件任务的模型。
更准确地说,我们可以说“对象是具有相关行为的数据集合”。[1]
让我们举一个例子来澄清这个概念(我们将参考文献[1]进行改述)。
我们想创建一个名为“Apple”的对象。在这种情况下,与之相关的数据可以是颜色(例如,红色)。与之相关的行为可以是我们想把苹果放进篮子里。

软件开发中的一个对象。图片由作者提供。
现在,如果我们遇到一个绿色的苹果怎么办?它仍然是苹果,只是颜色不同:我们可以使用这个概念吗?这就是类发挥作用的地方!
类是定义对象结构的“模板”。这意味着对象有自己特定的一组属性,涉及某个类。
以苹果为例,我们可以说类 Apple 可以包含:
-
数据 → 颜色
-
行为 → 苹果放置的位置
使用这个模板,我们可以创建任意多个对象。例如,我们可以创建对象Apple_green,它使用了带有color=green的数据的类 apple,并把苹果放到桌上的篮子里。
我们还可以有一个名为Apple_red的对象,它使用了带有color=red的数据的类 apple,并把苹果放回到货架上。

对象与类之间的区别。图片由作者提供。
所以,正如我们通过苹果的例子所理解的那样,我们可以说数据代表了某个对象的特定特征。
行为则代表动作;这些动作在 Python 中作为类的方法表达。正如我们稍后会看到的,类的方法的定义方式与定义函数的方式相同。
那么:什么是面向对象编程?
OOP 是“将代码编写为建模对象的实践” [1]。
换句话说,这意味着通过数据和行为创建一系列交互的对象。
这并不意味着在 Python 编程时我们总是需要创建类。这里我们需要理解一个简单的概念:Python 的强大之处在于一切在这个编程语言中都是对象。实际上,即使我们看不到,我们在 Python 中实际上是在处理对象和类。
例如,假设我们定义一个变量a = 3。如果我们想查看它的类型,可以写以下代码:
a = 3
type(a)
>>>
int
我们使用了内置函数type()来确定变量a的属性,它告诉我们这是一个int(也就是整数)。但int是 Python 内置类;这意味着在 Python 中,我们可以定义一个整数变量,如上所示,而无需创建实际的类,因为 Python 为我们做了这件事(在其他编程语言中,我们实际上需要为如此简单的示例创建一个类)。
所以,让我们记住:无论何时编程 Python,我们总是必须处理对象和类,即使我们没有显式声明它们,因为 Python 在后台会完成调用内置类的工作。
Python 中的类
在 Python 中,类必须以大写字母定义。下面是如何定义一个简单类的示例:
class MyFirstClass:
a = 15 # this is the data of the class
pass
我们创建了一个简单的类,它设置了a=15。
现在,让我们通过添加一个方法来创建一个实际做些有用工作的类(注:本文中使用的Point类取自参考文献[1],并根据我的口味和需求进行了修改):
class Point:
def origin(self): # this is a method
self.x = 0
self.y = 0
所以,我们创建了一个名为Point的类,并有一个名为origin的方法,它做了一件简单的事情:将二维坐标点设为x=0和y=0(原点)。
现在,正如我们所见,该方法传递了一个名为self的参数。我们可以按需调用它,但作为标准,Python 开发者社区称之为self:我们稍后会更详细地解释它是如何工作的。
现在,如果我们想调用类,我们可以定义一个点p并执行以下操作:
# Invoke the class
p = Point()
现在,我们可以通过编写以下内容来访问类中定义的方法:
# Access the method
p.origin()
所以,现在我们的点已经到了原点。为了确保,我们可以做以下操作:
# Print the coordinates
print(p.x, p.y)
>>>
0 0
“self”参数
现在,让我们解释一下之前介绍的self参数是什么,以及如何使用它。
self参数用于调用方法。这就是为什么我们将其称为self作为标准的原因:因为我们在调用方法。这就像我们在告诉 Python:“嘿!这个方法是自我调用的”。
当我们需要将多个参数传递给方法时,self必须是第一个参数:否则,另一个参数将调用该方法(正如我们所说,不需要将其命名为self:这个名字只是开发者之间的惯例)。
让我们看看一个使用self的示例,并与另一个不使用它的示例进行比较。
class Test:
def printing(self):
print("this is a test")
所以,我们创建了一个简单的类,当我们调用printing方法时,它会打印“this is a test”:
# Invoke the class
test = Test()
# Call the method
test.printing()
>>>
this is a test
现在,让我们看看没有self的示例:
class Test2:
def printing2():
print("this is a test")
# Invoke the class
test2 = Test2()
# Call the method
test2.printing2()
>>>
Type Error: printing2() takes 0 positional arguments but 1 was given
所以,在这种情况下,Python 返回一个错误,告诉我们一个简单的事情:方法不能被调用,因为它没有参数。
这就是我们需要self的原因。
现在,让我们创建一个具有多个参数的函数:
class Point:
def move(self, x, y):
self.x = x
self.y = y
def origin(self):
self.move(0,0)
return(self.x, self.y)
我们创建了一个类,该类:
-
移动两个坐标(
x和y)。正如我们所见,self调用了该方法。 -
将坐标移到原点。当我们调用
origin方法时,它实际上会将两个坐标(它唯一的参数是self)移动到原点(self.move(0, 0)),并返回坐标。
所以,我们可以调用类并将坐标移动到原点:
#Invoke the class
p = Point
# Call the method
p.origin()
>>>
0, 0
而且坐标已经移到了原点。
现在,让我们回顾一下之前的几行:我们说move方法移动了坐标,但这并不准确。实际的移动是由origin方法完成的。
实际上,move 方法只做了一件简单的事情:它调用了所需的值。这就是为什么一个类的第一个方法有特殊命名规则的原因。
“init”方法
到目前为止,我们创建了一个将点的坐标(x和y)移动到原点的类。但如果我们想将点移动到任何特定位置怎么办?
为此,我们需要创建另一个类。第一次尝试时,让我们用之前使用的相同逻辑创建它。可以是这样的:
class Point:
def move(self, x, y):
self.x = x
self.y = y
def position(self):
print(self.x, self.y)
现在,让我们调用类和方法:
# Invoke the class
p = Point()
# Get to position
p.position(5,6)
>>>
TypeError: Point() takes no arguments
然后……我们遇到一个错误!它告诉我们Point类不接受任何参数。这怎么可能?我们已经指定了传递self、x和y;所以,除了self调用方法之外,类应该接受两个值!
错误在于第一个方法。正如我们在上一段中所说,第一个方法只做一件事:它调用所需的值。它不会将点移动到特定位置。
这就是为什么一个类的第一个方法必须是一个叫做__init__的特殊方法的原因。因此,为了正确调用所需的值,我们必须按如下方式创建Point类:
class Point2:
def __init__(self, x, y):
self.x = x
self.y = y
def position2(self):
print(self.x, self.y)
现在,让我们调用它:
# Invoke the class
p = Point2(5,6)
# Get to position
p.position2()
>>>
5 6
就这样!
**NOTE:**
there is even the possibility to create classes that do not uses the
__init__ method. In these cases, the variables can be invoked in
other ways.
Anyway: we won't talk about that case here because we believe this is
not a Pythonic way to develop Python software (also, the code becomes
a little bit longer and less redeable).
如果 name == ”main”
现在,在某些类的末尾,我们可能会看到if __name__ == "__main__"。如果你不知道这是什么,这里有一个详细的解释。
引用并改述自[2]:
__name__是一个特殊的内置变量,我们在创建可交互模块时必须使用它,因为它允许我们区分我们导入的模块和我们没有导入的模块。在 Python 中,模块指的是包和库,甚至是任何可以与其他代码分离并独立运行的代码片段。鉴于类(甚至函数,或任何 Python 文件)可以独立运行,它们可以被认为是模块。
__main__另一方面是一个特殊的模块,表示脚本或程序中的顶级代码的名称。基本上,如果我们执行
if __name__ = "__main__",我们是在检查我们执行的代码是否是顶级代码。
那么,什么是顶级代码?
我们从这个线程中取一个非常说明性的例子:
import sys # top-level
3 + 4 # top-level
x = 0 # top-level
def f(): # top-level
import os # not top-level!
return 3 # not top-level
if x: # top-level
print 3 # not top-level
else:
print 4 # not top-level, but executes as part of an if statement
# that is top-level
class TopLevel(object): # top-level
x = 3 # not top-level, but executes as part of the class statement
def foo(self): # not top-level, but executes as part of the class statement
print 5 # not top-level
好的,正如我们所看到的,问题与 Python 解释器有关。
让我解释一下。
如果一个模块是独立执行的,那么没有问题。当一个模块被导入到另一个 Python 文件中时,问题就会出现。假设上述脚本被导入到另一个文件中:当我们导入它时,它会立即执行所有顶级代码以及属于if或类声明的代码,正如上述代码片段中的注释所述。
让我们更详细地查看它,举个例子。
让我们创建一个名为package_1.py的 Python 文件,如下所示:
# Define a function to print "Hello"
def print_hello():
print("hello")
#Invoke the function
print_hello()
如果我们通过终端执行它,我们得到:

执行package_1.py文件。图片由作者提供。
所以,它完全有效。
现在,让我们创建一个名为main.py的 Python 文件,并将package_1.py文件导入其中,然后运行main.py:

main.py 文件的执行。图像来源:作者。
如我们所见,当 main.py 运行时,模块 package_1.py 会立即执行!这是我们不希望发生的情况,原因很简单:我们希望在实际调用代码时使用从模块中导入的代码。换句话说,如果整个模块在导入后立即执行代码,那就完全没有用。
为了避免这种情况,我们在 package_1.py 中使用了 if __name__ = "__main__",并在其后调用 print_hello() 函数:

使用‘if name == “main”’执行 main.py 文件。图像来源:作者。
如 [2] 所述:
if __name__ = "__main__"防止了在模块执行时顶层代码的自动执行。
此外,之前我们在 package_1.py 文件中调用了 print_hello() 函数,因为我们希望函数在程序独立运行时能够实际工作。现在,在 if __name__ = "__main__" 后调用 print_hello() 将在 package_1.py 独立运行时调用该函数。
所以,为了回顾和澄清这个例子,如果我们在 package_1.py 中的 if __name__ = "__main__" 后调用 print_hello():
-
当
package_1.py独立运行(即,通过终端)时,print_hello()将被执行。 -
当我们运行
main.py时,print_hello()将不会被执行。
**NOTE**
We understand that we've used simple examples, but a general
case is that we create modules based on classes:
this is why we have covered this topic in this article.
类型提示
从 Python 3 开始,“PEP 484 — 类型提示” 引入了 Python 中的类型提示。
类型提示是建议传递给函数(或在类的情况下传递给方法)的类型。我们都知道注释和文档字符串必须发挥作用,但类型提示确实有助于我们理解函数的预期。
**NOTE:**
here we'll talk about functions and not classes, just for the sake
of simplicity.
As we have seen, in fact, a method of a class is defined exactly as
a function: so what we'll see in this paragraph is generalizable
to classes.
让我们做一个简单的例子。创建一个返回值的函数:
def any_call(variable: any) -> any:
return variable
因此,我们创建了一个接受一个参数(称为 variable)的函数,类型提示告诉我们:
-
variable的类型可以是任何类型,例如字符串、整数等…… -
函数返回任何类型(实际上,它返回
variable)
你觉得这有用吗?好吧,我几周前发现了这个功能,觉得非常惊艳!特别是,当示例更复杂时,这就变得非常有趣。
例如,假设我们需要一个函数,该函数接收一个列表作为参数,并返回列表中元素的数量。该函数可以如下所示:
def count_values(element: list[any]) -> int:
return sum(1 for elements in element if elements)
-----------------------
count_values([1,2,3,4])
>>>
4
----------------------
count_values(["hello", "stranger", "who", "are", "you", "?"])
>>>
6
因此,我们的函数仅接受 element 作为参数,并且由于类型提示,我们知道它必须是任何类型的列表。然后,函数返回一个整数类型;实际上,它计算列表中元素的数量。
所以,我们认为这个观点很清楚。这是一个非常好的实现,能够提高我们代码的可读性。
文档字符串(以及如何调用它们)
文档是每个软件项目中最重要的部分,原因很简单:在接下来的两个月里,我们几乎不会记得我们做了什么以及为什么做。
因此,编写代码的注释和解释是非常重要的。不幸的是,注释是不够的,因为它们必须非常简短(但要有解释性)。
所以,我们可以做的是使用 docstrings。 PEP 257 规定:
docstring 是在模块、函数、类或方法定义中的第一个语句出现的字符串文字。这样的 docstring 成为该对象的
__doc__特殊属性。所有模块通常都应该有 docstrings,而模块导出的所有函数和类也应有 docstrings。公共方法(包括
__init__构造函数)也应有 docstrings。一个包的文档可以在包目录中的__init__.py文件的模块 docstring 中进行记录。
换句话说,在软件开发中,docstring 是一种在代码中用于记录代码本身的字符串类型。正如参考文献 [1] 所述,“与注释不同,docstrings 在运行时可用,简化了代码检查,并在执行过程中提供帮助或元数据”。
让我们看看如何在 Python 类中使用 docstrings:
class Point:
"""
this class moves a point in 2D, passing its coordinates
as argument of the method 'position'
"""
def __init__(self, x: int, y: int) -> None:
self.x = x
self.y = y
def position(self) -> int:
print(self.x, self.y)
要访问文档(即 docstrings),我们输入以下内容:
Point.__doc__
>>>
" this class moves a point in 2D, passing its coordinates as
argument of the function 'position' "
所以,这就是我们如何访问与类相关的 docstrings。如果我们想访问类中特定方法的 docstring 呢?
让我们看看:
class Point:
""" this class moves a point in 2D, passing its coordinates
as argument of the function 'position'
"""
def __init__(self, x: int, y: int) -> None:
self.x = x
self.y = y
def position(self) -> int:
""" this actually moves the point"""
print(self.x, self.y)
要访问 position 方法的文档:
Point.position.__doc__
>>>
'this actually moves the point'
我们到了。
但我们实际上可以(并且应该!)做得更好。例如:
class Point:
""" this class moves a point in 2D, passing its coordinates
as argument of the function 'position'
Args:
param 1 (int): the coordinate x of the point in 2D
param 2 (int): the coordinate y of the point in 2D
"""
def __init__(self, x: int, y: int) -> None:
self.x = x
self.y = y
def position(self) -> int:
""" This method actually moves the point.
Returns:
int: prints the integers that represent the coordinates x and y
of the point in 2D
"""
print(self.x, self.y)
这可能看起来有些冗余,因为我们已经使用了类型提示,但实际上并不是因为:
-
我们可能需要在第一次尝试中,仅调用我们类的文档(也许因为我们在另一个文件中将其作为模块导入)。
-
我们可以使用工具(例如 Sphinx)来创建 HTML 格式的文档;这些工具直接使用 docstrings。
这就是为什么使用非常解释性的 docstrings 是非常重要的。
继承
在这里,我们想讨论 Python 的一个魔法:继承。
正如我们之前所说,面向对象编程(OOP)是用对象之间相互作用的方法来开发代码。这意味着当我们在 Python 中创建类时,我们经常需要让它们相互作用。
在交互时,类会继承其他类的属性和功能。
是的:就像你的老姑姑决定把她的 210 亿美元留给你一样(谁没有这样的姑姑?!)。
首先,我们必须说明,我们创建的每个类都使用了继承。这是因为,正如我们之前所说,Python 使用内置类。所以,当我们调用内置类时,我们的代码会继承调用的内置类或对象的属性。
现在,让我们看看继承在实际中的应用(注意:我从参考文献 [1] 中取出了以下类,并根据我的口味和需求进行了修改)。
我们想创建一个存储一些联系人的名字和姓氏的类,填充一个空列表。我们可以创建类似这样的东西:
class Contact:
"""
This class saves the name and the surnames
of some contacts in a list
"""
# Create empty list
all_contacts = []
def __init__(self, name: str, surname: str) -> None:
""" This method initializes the arguments and appends
the arguments (name and surname)into the empty list.
Returns:
nothing.
"""
self.name = name
self.surname = surname
Contact.all_contacts.append(self) # Append to list
def __repr__(self) -> str:
"""
The built-in __repr__ method provides a string representation
of an object.
Returns:
the compiled list with name and surname of the contacts
"""
return (
f"{self.__class__.__name__}("
f"{self.name!r}, {self.surname!r}"
f")"
)
现在,让我们尝试一下:
# Define a contact
contact_1 = Contact("Federico", "Trotta")
# Show the contacts in the list
Contact.all_contacts
>>>
[Contact('Federico', 'Trotta')]
现在,假设我们想收集联系人的电子邮件,但由于某种原因,我们想创建一个单独的类。我们可以创建一个如下的类:
class Email(Contact): # Email is inerithing from Contact
def get_mail(self, mail:"mail") -> None:
return mail
现在,让我们添加一个如下的电子邮件:
Email("federico trotta", "federico@example.com")
现在,如果我们调用 Contact.all_contacts,我们会得到:
[Email('federico trotta', 'federico@example.com'),
Contact('Federico', 'Trotta')]
所以,由于我们的 Email 类将 Contact 类作为参数,它继承了其属性。例如,传递给 Email 类的参数会附加到 Contact 类中的列表。
同时,这个类继承了 __init()__ 方法需要两个参数的事实。实际上,这就是我们只用一个参数所得到的:
mail_2 = Email("Jhon@example.com")
>>>
TypeError: __init__() missing 1 required positional argument: 'surname'
但是,等一下!!!我们在 Email 类中没有使用 __init()__ 方法,那发生了什么?
结果是 Email 类继承了 __init()__ 方法,所以不需要再次使用它!
如果我们想在子类中使用另一个 __init()__ 方法(Email 是一个子类),我们必须做出一些调整,例如以下几点:
class General(Contact):
def __init__(self, name: str, email: str) -> None:
super().__init__(name, email)
self.email = email
它的工作方式与之前的完全相同:
# Create a contact
general_contact = General("Federico Trotta", "federico@example.com")
# Write the contact into the list
Contact.all_contacts
>>>
General('Federico Trotta', 'federico@example.com')
因此,Email 类和 General 类的工作方式完全相同,结果也完全相同,但继承的强大之处在于,没有像我们在 Email 中使用 __init()__ 方法一样,代码会更简洁。
同时,正如我们所见,在 General 类的情况下,我们使用了 super.__init()__ 方法:在子类中我们必须使用它,因为它初始化了父类(Contact)的继承属性。
关于如何使用 Python 类的专业提示
如果你发现这篇文章是因为你在理解类时遇到困难,那么你可能不清楚为什么应该使用它们。如果是这样,欢迎加入俱乐部:我曾经也有同样的困惑。
我的意思是:我明白类(当然,还有函数)帮助我们自动化代码,但必须调用它们给我带来了一些困难。
这发生在我身上,因为我开始学习 Python 用于数据科学(这是我如何正确做这件事的提示),说实话,在许多情况下,当我们用 Python 做数据科学时,使用类并没有理由。
所以,我的建议是,正确理解使用类的必要性是将它们视为模块。这意味着,当你想开始一个新的 Python 项目时,一个很好的建议是创建一个 main.py 文件,在其中调用所有你需要的类;无论如何,这些类不应该在 main.py 中创建:它们应该在单独的 Python 文件中创建(通常,我们为每个类创建一个 Python 文件)并在 main.py 中导入使用。
另外,到目前为止我们谈论了模块。这是我写的一篇关于这个话题的文章:
[ ## 掌握模块化编程:如何将你的 Python 技能提升到下一个层次
编写模块化 Python 代码的最佳实践
towardsdatascience.com
结论
在这篇文章中,我们看到了一个关于类的综合指南,希望它能阐明相关的主要主题。
你现在唯一需要做的就是多加练习,希望这个指南在你需要时能帮助到你。
免费 Python 电子书:
开始学习 Python 数据科学但遇到困难? 订阅我的通讯,获取我的免费电子书:这将为你提供正确的学习路径,以便通过动手实践学习 Python 数据科学。
喜欢这个故事吗?通过我的推荐链接成为 Medium 会员,每月 5 美元 点击这里:我将获得一小笔佣金,对你没有额外费用:
[## 使用我的推荐链接加入 Medium — Federico Trotta
阅读 Federico Trotta 的每一个故事(以及 Medium 上成千上万的其他作家的故事)。您的会员费直接支持…
medium.com](https://medium.com/@federicotrotta/membership?source=post_page-----881ed609fb6--------------------------------)
参考文献和视频资料:
[1] Python 面向对象编程 — S.F. Lott, D. Phillips
[2] 如果 name == “main” 对于 Python 开发者 (视频)
我的 Python 脚本如何更像自然对话
原文:
towardsdatascience.com/python-code-like-natural-english-ff5cb09e97b9
管道是一种非常出色的技术,可以使代码更加人性化。
·发布在 Towards Data Science ·5 min 阅读·2023 年 3 月 22 日
--

图片由 Pavel Danilyuk 提供,来自 Pexels。
你的代码也是你的文档。
人们说伟大的程序员不会给代码添加注释。他们相信,如果代码难以编写,其他人也应该很难理解和修改。因此,他们编写简单明了的代码。
虽然我并不主张完全不添加注释,但这句话中确实有一部分真理,我无法否认。代码应该能够被任何人阅读!
这就是为什么 SQL 代码很棒的原因。声明式语法比任何通用编程语言都要更具可读性。我们清楚地知道我们选择了什么,过滤条件,如何汇总等。
加快你的 SQL 学习曲线。
towardsdatascience.com
我们能否组织我们的 Python 脚本以提高可读性?如果我们能使 Python 代码看起来更声明式,这会改善代码质量吗?而且会更有趣吗?
我一直在为我的一些项目尝试不同的代码风格。使用管道操作符提高了可读性到一个极致的水平。慢慢地,我将大部分代码库转换为利用这种技术。
但在深入了解管道之前,
什么让你的代码库难以阅读?
进化使我们在日常任务的各个方面创造了可接受的规范。我们可能没有意识到这些规范,但以不同方式进行会让我们和周围的人生活变得困难。
在编程中,我们也制定了可接受的规范。软件设计原则、模式和代码风格指南就是这些规范。
当我们在代码中遵循这些规范时,读者可以轻松理解我们代码的结构。如果不遵循这些规范,会给你和其他人带来困难。
如果这些是新的,可以查看 SOLID 设计原则和 PEP 8 风格指南。
在使用管道操作符重构代码时,单一职责原则(SRP)具有特别重要的意义。它指出我们代码的每个块(一个函数、一个类等)应该处理代码中的一个且仅有一个方面。
这是一个违反 SRP 的代码示例:
import requests
def calculate_transaction_value(amount):
response = requests.get('https://api.exchangeratesapi.io/latest?base=USD&symbols=EUR')
exchange_rate = response.json()['rates']['EUR']
transaction_value = amount * exchange_rate
return transaction_value
上述代码查询 API 以获取最新的汇率,并计算以美元为单位的交易值。虽然这个函数很简单,但它处理了两个方面——从外部 API 获取汇率和计算值。
这种方法存在几个实际问题。由于这不是文章的范围,我将为另一篇文章保存这些问题。但最重要的是,我们可以通过引入模块化来提高代码的可读性。
这是应用 SRP 后的代码(仍未应用其他原则)。
import os
import requests
def get_exchange_rate():
url = "https://api.apilayer.com/exchangerates_data/latest?symbols=EUR&base=USD"
payload = {}
headers = {"apikey": os.environ["API_KEY"]}
response = requests.request("GET", url, headers=headers, data=payload)
result = response.json()["rates"]["EUR"]
return result
def calculate_transaction_value(exchange_rate, amount):
transaction_value = amount * exchange_rate
return transaction_value
这是你在代码中使用它的方式:
exchange_rate = get_exchange_rate()
transaction_value = calculate_transaction_value(100, exchange_rate)
print(f'Transaction value in EUR: {transaction_value}')
>> Transaction value in EUR: 93.71199999999999
现在,任何人都可以轻松阅读这些步骤并理解它们。主要的代码块非常简单。而且每当他们需要更多细节时,可以查看函数定义。
然而,当代码库变得庞大时,这在大多数实际项目中是如此,跟踪所有创建的变量并跟随进展是困难的。
这时管道就派上用场了。
管道提升我们代码库的可读性。
如果你使用 shell 命令,其中一个惊人的地方是我们可以将一个操作的输出传递给下一个操作。这为指令提供了逻辑流程。
我们还可以重构我们的 Python 代码,使其具有逻辑流程。我们可以使用 pipe 包来做到这一点。我们可以从 PyPI 安装它:
pip install pipe
我们现在可以将函数转换为管道操作符。我们只需用管道装饰器标注函数即可。
## 我在几乎所有数据科学项目中使用的 5 个 Python 装饰器
装饰器提供了一种新的便利方式,从缓存到发送通知都可以使用它们。
[towardsdatascience.com
import os
import requests
from pipe import Pipe
def get_exchange_rate():
url = "https://api.apilayer.com/exchangerates_data/latest?symbols=EUR&base=USD"
payload = {}
headers = {"apikey": os.environ["API_KEY"]}
response = requests.request("GET", url, headers=headers, data=payload)
result = response.json()["rates"]["EUR"]
return result
@Pipe
def calculate_transaction_value(exchange_rate, amount):
transaction_value = amount * exchange_rate
return transaction_value
这是我们如何按顺序组织和调用函数的示例。
transaction_value = (
get_exchange_rate()
| calculate_transaction_value(100)
)
在上述示例中,请注意管道操作会自动从前一个操作中提取函数的第一个参数。我们并没有将exchange_rate参数传递给calculate_transaction_value函数。相反,我们只传递了金额。
我有意将这个示例保持简单。但在实际项目中,你可能会遇到更长的构造。
这是我最近项目中修改的摘录(仍然是简化版)。
sales_regional_lead_data = (
get_sales_leads(region="EMEA")
| create_placeholder_dataframe(
years=3
) # Create a dataset replicating each sales lead for 12 months x years
| merge_budgets_to_sales_leads(
get_project_budgets() | aggregate_project_budgets_to_sales_leads()
)
| merge_crm_data_to_sales_leads(
get_crm_data() | aggregate_crm_data_to_sales_leads()
)
| merge_invoice_data_to_sales_leads(
get_project_invoices() | aggregate_invoices_to_sales_leads()
)
| merge_work_in_progress_to_sales_leads(
get_project_work_in_progress() | aggregate_work_in_progress_to_sales_leads()
)
| recognize_partial_invoices(finished_pct_cutoff=.8)
| compute_sales_for_each_sales_lead()
| compute_delivery_for_each_sales_lead()
| compute_margin_for_each_sales_lead()
| compute_average_margin_for_each_sales_lead()
| compute_average_delivery_for_each_sales_lead()
| compute_average_sales_for_each_sales_lead()
| load_sales_leads_to_database(
db_config, table_name="sales_leads", if_exists="append"
)
)
这种代码版本更容易理解,因为它更接近自然对话。任何人都可以理解每个步骤及其改变行为的参数。即使是非技术人员的利益相关者也能愉快地将其作为小说阅读。
组织代码的最佳方法是将其拆分为模块并保存在单独的文件中。然后,这些文件在文件系统中以逻辑文件夹结构进行组织。
7 种方法使你的 Python 项目结构更优雅
以下是可管理、可扩展且易于理解的 Python 项目结构的最佳实践。
towardsdatascience.com
因此,只有这段代码存在于模块的__init__.py文件中。我从子模块中导入函数。因此,文件夹结构大致如下:
sales/
├── __init__.py
├── crm_data.py
├── invoices.py
├── budgets.py
├── work_in_progress.py
├── sales_leads.py
├── matrices.py
├── db.py
结论
作为程序员,我们的首要任务往往是让事情运作起来。但如果这是唯一的目标,我们就会错过重点。
我们的代码应该足够容易阅读和理解,让其他人无需我们的帮助或不断查看文档页面。
尝试了几种技术后,给我留下深刻印象的是管道操作。本文主要讲述为什么我认为这种操作很棒。但这里并没有涵盖管道操作的全部范围。
我已经写了一篇关于管道操作的全面文章。请查看以获取更多使用方法。
在 Python 中使用管道操作以获得更可读和更快的编码
一个方便的 Python 包,可以节省大量编码时间,并通过类 Unix 风格的管道操作提高可读性。
towardsdatascience.com
感谢阅读,朋友!在 LinkedIn、Twitter 和 Medium 上和我打个招呼吧。
还不是 Medium 会员?请使用这个链接来 成为会员,因为这样你不会增加额外费用,我将因推荐你而获得少量佣金。
Python:正确计算积分
原文:
towardsdatascience.com/python-computing-integrals-the-right-way-22e9257a5836
用 Python 提升你的积分能力
·发表于 Towards Data Science ·7 分钟阅读·2023 年 9 月 3 日
--
在处理科学计算或物理问题时,计算一些函数的积分是非常常见的。

图片由 Jeswin Thomas 提供,来自 Unsplash
在这篇简短的文章中,我想展示三种在 Python 中计算一维函数积分的方法。我们将把这些方法分为两种情况:
-
第一种情况:计算采样函数的积分
-
第二种情况:计算通用函数的积分
在第一种情况下,我们想要积分的函数已经被采样到一些样本点上,我们无法访问底层的“真实”函数。例如,我们不知道该函数的“公式”,也不能再对该函数采样其他点。换句话说,我们只有 x 数组和相应的 y 值。
在第二种情况下,我们考虑有一个函数对象,我们可以传递一个样本点,它会返回该点的函数值。这是理想的方法,因为我们可以访问函数的所有信息。
请注意,我们始终可以使用采样方法:如果我们仍然可以访问函数,我们可以选择样本点并使用它们对函数进行采样,并使用第一种方法。但正如我们将看到的,选择这些样本点正是计算干净积分的重要部分。
积分定义
在这篇文章中,我们将重点讨论一个返回单一值的单变量函数。我们想要计算的积分是标准定义:

现在我们的目标是尽可能准确地计算 I —— 在我们所知道的“f”的范围内。作为例子,我们将尝试计算以下积分:

使用基本数学,可以显示该积分的值为

在这篇文章中,我们将探讨不同的积分计算方法,并看看我们离真实值有多近。
第一种方法:对采样函数进行积分
假设我们从一个采样函数中提取数据,形式为 X 数组及相应的 Y 数组。你将如何计算 (X,Y) 曲线下的积分?
由于函数已经被采样,我们处理的不是连续信号,而是离散信号。换句话说,我们不知道“f”,只知道 f 的一系列值:

因此,将积分公式转化为离散符号形式似乎是合理的,其中积分符号变成离散符号,而 ‘dx’ 变成每个 x 样本之间的距离:

看起来很合理,对吗?让我们看看在我们的例子中效果如何。为了计算 xs 的差值,我们将使用 numpy 的 diff 函数。为简化起见,我们将使用 numpy 的 linspace 函数生成等间隔的 xs:
import numpy as np
def f(x):
return 2*x + np.cos(2*np.pi/4*x)
expected_value = 1 + 2/np.pi
N = 101
xs = np.linspace(0, 1, N)
ys = f(xs)
I = np.sum(np.diff(xs) * ys[1:])
print(f"Integral estimated to: {I}")
print(f"Error: {I/expected_value-1:.4%}")
Integral estimated to: 1.641606682344361
Error: 0.3047%
所以仅有 0.3% 的误差已经相当不错,我们应该能通过更多点来改进积分,对吗?
N = 1000
xs = np.linspace(0, 1, N)
ys = f(xs)
I = np.sum(np.diff(xs) * ys[1:])
print(f"Integral estimated to: {I}")
print(f"Error: {I/expected_value-1:.4%}")
Integral estimated to: 1.6371201417061898
Error: 0.0306%
更好,但仍不完美。
误差源于边界效应:注意我们没有使用 y 的第一个值(y_0),每个 x 之间的距离(x_i+1 — x_i)与 x_i 的 f 值相乘。这种方法被称为“矩形”法则或“黎曼和”,对应于以下几何形状:

(右侧)矩形法则,来自公共领域的图片托管在维基百科
这基本上是从采样值估计积分的最简单——也是最差——的方法。
为了改进我们的方法,一个好步骤是使用 numpy 的 trapz,它专门针对这个问题设计:它使用 梯形法则 从 X 向量和 Y 向量计算积分。基本上,它使用以下几何形状计算积分:

让我们看看它的表现:
I = np.trapz(ys, x=xs)
print(f"Integral estimated to: {I}")
print(f"Error: {I/expected_value-1:.8%}")
Integral estimated to: 1.6366196412056895
Error: -0.00079982% # compared to 0.3047% for the rectangle rul
因此误差约为 1e-3%!与矩形法则的 0.3% 相比,已经相当不错了。
使用更多点(假设我们可以有更多点,这并不总是可能的),我们得到:
# for N = 1000
Integral estimated to: 1.6366196412056895
Error: -0.00000801%
误差约为 1e-5%,开始变得相当不错。
要进一步改进,可以采取以下措施:
-
增加采样点数量(如果可能):作为一般规则,增加点的数量通常会减少误差,但减少的程度各不相同。换句话说,增加样本数量可以将误差减少到一定程度——它变得越来越低效。请注意,这需要仍然“访问”函数——如果可以的话,你最好使用第二种方法。此外,计算 f 的值有时会耗时。
-
做出假设:在使用矩形或梯形规则时,我们做出一个基本假设,即函数在每个 x 采样点之间是“常数”的。如果我们“允许”这样做,我们可以做出其他假设,例如函数表现得像某个阶数的多项式,并利用这些信息来计算积分。鉴于我们使用的是“采样方法”,通常我们无法获得有关真实函数的更多信息,因此做出不同的假设(常数、多项式)并没有比其他方法更好,只是不同而已。
最终,这两种方法都需要对真实函数有访问和/或更多的了解。这就是为什么我们要转向第二种方法!
第二种方法:对连续函数进行积分
在这种情况下,我们仍然可以访问函数 f。
对于这种情况,有很多计算积分的可能性。尝试理解这些方法,它们的工作原理、优缺点,是掌握这些问题复杂性的良好起点。
这就是scipy.integrate发挥作用的地方:这个模块提供了很多积分方案,默认设置大多数情况下已经足够。
在我们的例子中,我们将使用quad函数,它是用于一维函数的通用积分函数。
我们来看看它对我们的问题的表现:
from scipy.integrate import quad
I = quad(f, 0, 1)[0] # integrate f between 0 and 1
print("Best integral value", I)
print(f"Error {I/expected_value-1:.16%}")
Best integral value 1.6366197723675815
Error 0.0000000000000000%
啦啦!数值精度积分,直接开箱即用。所以记住,与其自己对函数 f 进行采样,并使用梯形法计算积分,不如让 scipy 做繁重的工作!
Scipy 确实充满了许多非常有用的工具,适用于许多应用。偶尔查看一下其文档。
总结
所以记住:
-
如果你只有从未知函数 f 中采样的 X 和 Y 向量,请使用 numpy 的 trapz 以梯形法快速且安全地计算其积分。这可能是复杂性和准确性之间最好的折中选择。
-
如果你可以访问 f,请使用 scipy 的 quad 函数来计算积分: 它可能会直接给你惊人的结果,如果需要,你还可以进一步自定义积分方案的参数。
在下一篇文章中,我会展示如何计算复值函数的积分,敬请期待!
如果你考虑加入 Medium,请使用这个链接快速订阅并成为我的推荐会员:
[## 使用我的推荐链接加入 Medium - Yoann Mocquin
作为 Medium 会员,你的部分会员费将用于你阅读的作者,同时你可以全面访问每个故事…
并订阅以便在我发布新文章时获得通知:
每次发布时都通过邮件通知我!新发布的内容将包括数据变换、先进的绘图和模拟…
最后,你可以查看我的其他一些文章,关于傅里叶变换或数据科学的线性代数技术:
对你的时间序列进行去趋势处理可能会改变游戏规则。
[towardsdatascience.com ## PCA/LDA/ICA:成分分析算法比较
复习这些著名算法的概念和差异。
[towardsdatascience.com ## PCA 白化与 ZCA 白化:numpy 2D 可视化
数据白化过程包括一种变换,使得变换后的数据具有单位矩阵作为…
[towardsdatascience.com ## 使用 numpy 的有限差分法解决方案提高了 300 倍
有限差分法是一种强大的技术来解决复杂问题,而 numpy 使其变得更快!
[towardsdatascience.com
Python 装饰器:全面指南
原文:
towardsdatascience.com/python-decorators-a-comprehensive-guide-5bde06d2fb27
PYTHON 编程
文章介绍了 Python 的强大语法糖:装饰器。
·发布在 Towards Data Science ·阅读时间 11 分钟·2023 年 10 月 19 日
--

做一个优秀的装饰器设计师——以及 Python 代码设计师。照片由 Spacejoy 提供,来源于 Unsplash
Python 装饰器是那些看似简单但如果不了解就非常困难的概念之一。许多 Python 初学者将它们视为一种神奇的工具,必须学习并在自己的代码中使用,以便实现真正的魔法。但使用内置的装饰器或来自第三方库的装饰器是不够的;这就像用儿童商店买来的魔法盒子做魔法一样。真正的魔法来自于编写自己的装饰器。
我记得当时我迫切想学习如何在真实项目中编写和使用自己的装饰器,而不仅仅是为了好玩。当那个时刻终于到来时,我感受到的愉悦是巨大的。这段经历让我渴望寻找更多机会来实现自己的装饰器。
我希望在阅读本文后,你对 Python 装饰器不会再有任何困惑。因此,本文旨在向那些尚未理解 Python 装饰器概念的人介绍这个话题。我希望以一种易于理解的方式揭示其背后的魔力。
装饰器的内容远不止这些。我们将讨论基础知识,但好消息是,这些基础知识应该足以让你实现即使是复杂而实用的装饰器。在未来的文章中,我们将深入探讨装饰器的复杂细节及其各种应用场景。
各个水平的 Python 程序员都能从这篇文章中受益。初学者将学习装饰器的基础知识,而中级程序员将获得更深入的理解;高级程序员可以利用这篇文章来刷新记忆。此外,有时从不同的角度看待某个特定概念是好的,不仅仅是我们多年来使用的角度——在这里,我提供了对装饰器及其有用性的观点,希望能对各个水平的读者有所帮助。
装饰器简介
Python 装饰器是一个强大而多用途的工具,但应谨慎使用以避免过度使用和滥用。装饰器是一个函数,允许你修改另一个函数的行为。装饰器也可以作为类来编写,但这种情况较少见,本文将不予讨论。
当你有一个装饰器,比如my_decorator()时,你可以用它来装饰另一个函数,比如foo(),如下所示:
@my_decorator
def foo(x, y):
# do something; in result,
# you obtain changed_x and changed_y
return changed_x, changed_y
在装饰了foo()函数后,我们不能再知道它的行为,而不了解my_decorator装饰器的作用。装饰器可能会添加新的行为,比如日志记录,或者完全改变函数的行为和返回值。例如,装饰过的foo()函数可能返回一个字典而不是一个元组,或者可能返回None。要了解装饰后的foo()函数的行为,我们必须检查my_decorator装饰器的定义。
我不知道“装饰器”一词在 Python 上下文中的词源。在 Python 中,这个词源自装饰器模式,但这并没有解释其原始词源。如果你知道,请在评论中与我们分享。
我个人认为 Python 装饰器是一种美妙的语法糖。它们被称为装饰器也不奇怪,因为它们装饰了被装饰的函数。我欣赏装饰器的外观和功能。
不过,我了解装饰器可能带来的困难。如上所述,我们不能知道一个装饰过的函数的行为,而不了解其装饰器的定义。此外,多个装饰器可以作用于一个函数,这使得事情变得更加复杂。
让我将装饰器背后的思想整理成三步:
-
需求。你有一个函数,但你需要改变它的行为。这可能由于各种原因。例如,你可能需要为应用程序中的所有函数添加日志记录,或改变外部模块中函数的行为。
-
定义。你编写一个装饰器函数,负责这种更新的行为。它可以接受一个或多个参数,除了原始函数之外。装饰器函数通常会调用原始函数,但这并不是必须的。
-
使用。你用新函数覆盖原函数。这可以通过装饰或赋值的方式完成,但装饰更为常见。使用原始名称调用装饰函数意味着调用新函数,因为原函数不再存在,除非它被复制。
这三步中的每一步都同样重要,所以我们逐步讨论这三步。
第 1 步:需求
好的,所以你需要改变函数的行为。
为什么使用装饰器来改变函数的行为而不是简单地重写它?有几个原因:
-
你可能无法重写函数。例如,它可能是来自外部模块的函数。
-
你可能不想重写函数。例如,它可能是一个大型或复杂的函数,或者是一个在许多不同地方使用的函数。
-
重写函数可能很麻烦。例如,它可能被许多不同的函数调用。
-
你可能只需要在开发中改变函数的行为,但希望在生产中使用原函数。
-
你可能需要改变许多函数的行为。在这种情况下,编写装饰器比逐个重写每个函数要高效得多。
最常见的场景是你需要改变许多函数的行为。在这种情况下,你可以编写一个装饰器,并将其应用到所有需要改变的函数上。这可以节省大量时间和代码。
以下是一些装饰器可以使用的例子:
-
向项目中所有现有函数添加日志记录。
-
测量并记录应用中每个函数的执行时间。
-
向应用中调用的函数添加身份验证和授权。
-
缓存函数返回的输出。
-
将函数从写入本地文件的数据更改为写入远程数据库的数据。
-
在测试中使某个特定函数静默。例如,你可以使用装饰器防止函数在测试期间写入远程数据库。
装饰器在 Python 中也被广泛用于模拟。模拟允许你创建假对象来模拟真实对象的行为。这对于测试依赖于外部资源的代码(如数据库和网络服务)非常有用。
第 2 步:定义
你是否注意到,在用@语法装饰一个函数后,装饰过的函数是用原函数的名称来调用的?这就是使函数成为装饰器的原因。如果原函数,比如foo(),仍然可以作为foo()使用,并且有一个新的函数,比如foo_changed(),其行为是foo()的改变后的行为,这里并没有涉及装饰。因此,装饰一个函数涉及到覆盖原函数。你可以用一个新名称保留原函数的副本,但原函数本身已被装饰函数替代。
装饰涉及到覆盖原函数。
是时候离开抽象的世界,转向实际操作了。让我们创建一个简单的装饰器,称为 scream(),使函数尖叫:
from typing import Callable
(1) def scream(func: Callable) -> Callable:
(2) def inner(*args, **kwargs):
(3) print("SCREAM!!!")
(4) return func(*args, **kwargs)
(5) return inner
这个装饰器接受一个可调用对象(func)作为输入,并返回一个可调用对象(inner)。inner 函数在调用原始函数(func)之前,会向控制台打印“SCREAM!!!”。为了简化代码,我放弃了文档字符串,但在实际工作中,你应该为装饰器添加文档字符串。
在实际工作中,你应该为装饰器添加文档字符串。
这里是 scream() 装饰器逐行的解析:
-
def scream(func: Callable) -> Callable:→ 这是函数签名。scream()装饰器可以用于装饰任何可调用对象(func),并且它也返回一个可调用对象。¹ -
def inner(*args, **kwargs):→ 这是内层函数。它打印“SCREAM!!!”;你可以使用任何你想要的名字,但我通常使用inner,像这里一样。装饰函数时,你可以使用被装饰函数接受的任何参数。例如,scream()装饰器可以用于任何函数,接受任何数量和类型的参数(因此使用了*args, **kwargs)。 -
print("SCREAM!!!")→ 这是添加到被装饰函数原始行为中的新行为。不管函数做什么,它将首先尖叫(通过打印"SCREAM!!!"),然后执行它最初应该做的事情。注意,在这个装饰器中,新行为是之前添加的,但它也可以之后添加,之前和之后都添加,甚至代替原始行为。 -
return func(*args, **kwargs)→ 这是函数的原始行为。 -
return inner→ 任何装饰器的标准行:返回inner函数。这意味着当scream()装饰器被使用时,它会用内层函数替换原始函数。
在附录 1 中,你会找到 scream() 装饰器的两个其他版本:
-
尖叫两次:在原始行为之前和之后
-
嚎叫代替了原始函数应该做的事情。
第三步:使用
要装饰一个函数,你可以用两种方式使用装饰器。本小节描述了这两种方式。
方法 1:将装饰器用作 *@decorator*
首先,我们需要一个要装饰的函数。让我们使用两个函数来说明你可以对任意多的函数使用相同的装饰器,并且这些函数可以非常不同。
我们想要装饰的第一个函数是 foo():
def foo():
return "foo tells you that life is great"
当我们调用这个函数时,我们将看到以下输出:
>>> foo()
'foo tells you that life is great'
这是几乎最简单的 Python 函数:它不接受任何参数并返回一个字符串。这是我们想要装饰的第二个函数,bar():
from typing import List
def bar(
x: int,
string: str,
func: Callable = lambda a, b: a * b,
**kwargs
) -> List[str]:
"""Applies a callable to each character in the given string.
It does so, passing in the given integer and any additional
keyword arguments. Returns a list of the results.
"""
return [func(x, s_i, **kwargs) for s_i in string]
这个函数比foo()复杂。它有三个参数:一个整数x、一个字符串string和一个可调用的func。它还接受任何额外的关键字参数。该函数将func()应用于string中的每个字符和x,如果有额外的关键字参数,也会传递进去。最后,它返回一个结果列表。
一个简单的调用可能是这样的:
>>> bar(3, "abc")
['aaa', 'bbb', 'ccc']
让我们使用不同的可调用对象作为func:
>>> def concatenate(i: int, s: str, sep: Optional[str] = "-") -> str:
... return f"{str(i)}{sep}{s}"
>>> bar(5, "abc", func=concatenate)
['3-a', '3-b', '3-c']
>>> bar(3, "abc", func=concatenate, sep=":")
['3:a', '3:b', '3:c']
对于我们的目的来说,foo()和bar()的功能并不重要。重要的是foo()是一个非常简单的函数,而bar()虽然简洁但更复杂。
我们可以使用装饰器语法装饰这两个函数,这在以@字符开头的两行中显示。为了完整性和清晰性,我将展示完整代码,因此我会重复函数的代码:
from typing import Callable, List
@scream
def foo():
return "foo tells you that life is great"
@scream
def bar(
x: int,
string: str,
func: Callable = lambda a, b: a * b,
**kwargs
) -> List[str]:
return [func(x, s_i, **kwargs) for s_i in string]
让我们运行这两个函数:
>>> foo()
SCREAM!!!
'foo tells you that life is great'
>>> bar(5, "abc", func=concatenate)
SCREAM!!!
['3-a', '3-b', '3-c']
>>> bar(3, "abc", func=concatenate, sep=":")
SCREAM!!!
['3:a', '3:b', '3:c']
方法 2:将装饰器用作 *function()*
使用装饰器函数最常见的方式是作为装饰器。然而,还有一种不太常见的方式:
def foo():
return "foo tells you that life is great"
foo = scream(foo)
这样,你只需将要装饰的函数作为参数调用装饰器函数。
无论你使用哪种方法来应用scream()装饰器,当你运行foo()时,装饰后的函数将会尖叫,然后做它最初要做的事情。
结论
这篇文章解释了 Python 装饰器的基础知识。我尽力做到全面,但仍有许多装饰器的细节我们没有讨论。我们将在未来的文章中深入探讨这些问题。
学习 Python 装饰器的重要性有几个原因,不仅仅是如何使用它们,还包括如何编写新的装饰器。首先,装饰器是一个强大的工具,可以帮助你编写简洁且易读的代码。如果你知道如何编写自定义装饰器,你会发现它们往往能为你节省大量时间和精力。
其次,装饰器可以用来快速更新遗留代码。例如,如果你需要更改一个或多个函数的行为,但修订这些函数本身不可行,你可以使用装饰器。虽然总是可以重写这些函数,但如果所有函数的行为都是相同的,一个装饰器可能就足够了。
第三,装饰器是 Python 中最重要的语法糖之一。如果你不理解它们,你可能会被认为是 Python 初学者。我无法想象一个不知道如何使用装饰器的 Python 开发者,更不用说理解它们了。
最后,装饰器在 Python 代码库中非常常见。如果你不熟悉装饰器,你将无法理解许多现有的代码。
因此,所有中级和高级 Python 开发者应该了解装饰器的概念,理解如何使用它们,并能够编写它们。
尽管装饰器乍看起来可能很复杂,但我相信如果你读到这篇文章的这个部分,你会同意一旦理解了基本原理,它们其实并不是那么复杂。事实上,它们可以是一个非常简单且实用的编码工具。
脚注
¹ 为了简便起见,我将使用“装饰器函数”这一术语,而不是“装饰器可调用对象”。但请注意,这只是对以函数和类两种方式定义的装饰器的简写。我只是想避免过度使用“可调用对象”这个词,即使它在 Python 文本中经常出现。
附录
附录 1:scream()装饰器的两个其他版本
版本 2:在运行函数之前和之后尖叫
from typing import Callable
def scream(func: Callable) -> Callable:
def inner(*args, **kwargs):
print("SCREAM!!!")
output = func(*args, **kwargs)
print("SCREAM AGAIN!!!")
return output
return inner
你将看到用上述装饰器装饰后的foo()的输出:
>>> foo()
SCREAM!!!
'foo tells you that life is great'
SCREAM AGAIN!!!
版本 3:尖叫代替运行函数
from typing import Callable
def scream(func: Callable) -> Callable:
def inner(*args, **kwargs):
print("SCREAM, JUST SCREAM!!!")
return inner
以及:
>>> foo()
SCREAM, JUST SCREAM!!!
这个版本的scream()装饰器完全覆盖了被装饰函数的原始行为。被装饰的函数现在只会尖叫,原始行为完全被移除。这种结构在许多不同情况下都非常有用。例如,你可以用它来完全静音一个函数:
from typing import Callable
def silence(func: Callable) -> Callable:
def inner(*args, **kwargs):
pass
return inner
你可以在[easycheck](https://pypi.org/project/easycheck/) Python 包的代码中看到这种静音器的例子:
[## easycheck/easycheck/easycheck.py 在主分支 · nyggus/easycheck
一个模块提供了 Python 函数用于简单且可读的断言式检查,可在代码内部以及其他地方使用…
在这段代码中查找switch函数。
你还会看到,你可以堆叠装饰器;以下是来自上面easycheck库的一个例子:
@switch
@make_it_true_assertion
def assert_paths(*args: Any, handle_with: type = AssertionError, **kwargs: Any) -> None:
return check_if_paths_exist(*args, handle_with=handle_with, **kwargs)
我们将在未来的文章中讨论装饰器的这些细节。
感谢阅读。如果你喜欢这篇文章,你可能也会喜欢我写的其他文章;你可以在这里查看它们。如果你想加入 Medium,请使用我下面的推荐链接:
[## 使用我的推荐链接加入 Medium - Marcin Kozak
阅读 Marcin Kozak 的每一个故事(以及 Medium 上的其他成千上万的作者)。你的会员费用直接支持…
medium.com](https://medium.com/@nyggus/membership?source=post_page-----5bde06d2fb27--------------------------------)
我在几乎所有数据科学项目中使用的 5 个 Python 装饰器
原文:
towardsdatascience.com/python-decorators-for-data-science-6913f717669a
装饰器提供了一种新颖且便捷的方法,从缓存到发送通知应有尽有。
·发表于 Towards Data Science ·6 分钟阅读·2023 年 3 月 13 日
--

图片由 Elena Mozhvilo 提供,来源于 Unsplash
一开始,每个开发者的目标都是让事情运作起来。逐渐地,我们开始关注可读性和可扩展性。这时我们开始考虑装饰器。
装饰器是为函数提供额外行为的绝佳方式。作为数据科学家,我们经常需要将一些小功能注入到函数定义中。
使用装饰器,你会惊讶于你能减少多少代码重复并提高可读性。我确实感到惊讶。
[## 使用 Streamlit 在几分钟内创建 GPT3 驱动的应用程序
学会构建智能应用程序,而无需过多担心软件开发。
levelup.gitconnected.com](https://levelup.gitconnected.com/streamlit-openai-gpt3-example-app-b333da955ceb?source=post_page-----6913f717669a--------------------------------) ## 如何使用 GitHub Actions 构建简单的 ETL 流水线
ETL 不必复杂。如果是这样的话,可以使用 GitHub Actions。
towardsdatascience.com
这是我在几乎每个数据密集型项目中使用的五种最常见的装饰器。
1. 重试装饰器
在数据科学项目和软件开发项目中,我们经常依赖外部系统。事情并不总是在我们的控制之下。
在完全转到不同的数据模型之前,可以尝试一些简单的技巧。
[towardsdatascience.com [## Python 网络抓取的宁静交响曲——三重奏
在 Python 中进行网络抓取的最简单、最灵活和最全面的方法
当发生意外事件时,我们可能希望我们的代码等待一段时间,以便外部系统自行修正并重新运行。
我倾向于在 Python 装饰器中实现这个重试逻辑,以便可以注解任何函数来应用重试行为。
这是一个重试装饰器的代码。
import time
from functools import wraps
def retry(max_tries=3, delay_seconds=1):
def decorator_retry(func):
@wraps(func)
def wrapper_retry(*args, **kwargs):
tries = 0
while tries < max_tries:
try:
return func(*args, **kwargs)
except Exception as e:
tries += 1
if tries == max_tries:
raise e
time.sleep(delay_seconds)
return wrapper_retry
return decorator_retry@retry(max_tries=5, delay_seconds=2)
def call_dummy_api():
response = requests.get("https://jsonplaceholder.typicode.com/todos/1")
return response
在上述代码中,我们尝试获取 API 响应。如果失败,我们重试相同的任务 5 次。每次重试之间,我们等待 2 秒。
2. 缓存函数结果
我们的代码库中有些部分很少改变其行为。然而,它们可能会占用大量计算资源。在这种情况下,我们可以使用装饰器来缓存函数调用。
有一种更好的方法来管理依赖关系、打包和发布 Python 项目。
[towardsdatascience.com
如果输入相同,函数只会运行一次。在每次后续运行中,结果将从缓存中获取。因此,我们不必一直进行昂贵的计算。
def memoize(func):
cache = {}
def wrapper(*args):
if args in cache:
return cache[args]
else:
result = func(*args)
cache[args] = result
return result
return wrapper
装饰器使用字典来存储函数参数和返回值。当我们执行这个函数时,装饰器会检查字典中的先前结果。实际函数只有在没有存储值时才会被调用。
以下是一个计算斐波那契数的函数。由于这是一个递归函数,相同的函数调用会被执行多次。但有了缓存,我们可以加速这个过程。
@memoize
def fibonacci(n):
if n <= 1:
return n
else:
return fibonacci(n-1) + fibonacci(n-2)
以下是这个函数在有缓存和无缓存情况下的执行时间。请注意,缓存版本的运行时间只有毫秒的一小部分,而未缓存版本几乎花了一分钟。
Function slow_fibonacci took 53.05560088157654 seconds to run.
Function fast_fibonacci took 7.772445678710938e-05 seconds to run.
使用字典来保存先前的执行数据是一种直接的方法。然而,还有一种更复杂的方式来存储缓存数据。你可以使用内存数据库,比如 Redis。
3. 计时函数
这并不令人惊讶。当处理数据密集型函数时,我们渴望了解运行所需的时间。
通常的做法是收集两个时间戳,一个在函数开始时,一个在函数结束时。然后我们可以计算持续时间,并将其与返回值一起打印出来。
但对多个函数重复进行这项工作是很麻烦的。
相反,我们可以让装饰器来完成这件事。我们可以对任何需要打印持续时间的函数进行注解。
这是一个示例 Python 装饰器,当函数被调用时,它会打印运行时间:
import time
def timing_decorator(func):
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"Function {func.__name__} took {end_time - start_time} seconds to run.")
return result
return wrapper
你可以使用这个装饰器来计时函数的执行时间:
@timing_decorator
def my_function():
# some code here
time.sleep(1) # simulate some time-consuming operation
return
调用函数将打印运行所需的时间。
my_function()
>>> Function my_function took 1.0019128322601318 seconds to run.
4. 记录函数调用
这个装饰器很大程度上是对前一个装饰器的扩展。但它有一些特定的用途。
如果你遵循软件设计原则,你会欣赏单一职责原则。这本质上意味着每个函数将有其唯一的责任。
Plotly dash 应用程序是用 Python 构建生产级仪表盘的最快方式。
[
当你以这种方式设计你的代码时,你还会希望记录函数的执行信息。这就是日志记录装饰器派上用场的地方。
以下示例说明了这一点。
import logging
import functools
logging.basicConfig(level=logging.INFO)
def log_execution(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
logging.info(f"Executing {func.__name__}")
result = func(*args, **kwargs)
logging.info(f"Finished executing {func.__name__}")
return result
return wrapper
@log_execution
def extract_data(source):
# extract data from source
data = ...
return data
@log_execution
def transform_data(data):
# transform data
transformed_data = ...
return transformed_data
@log_execution
def load_data(data, target):
# load data into target
...
def main():
# extract data
data = extract_data(source)
# transform data
transformed_data = transform_data(data)
# load data
load_data(transformed_data, target)
上面的代码是一个简化版的 ETL 流水线。我们有三个独立的函数来处理提取、转换和加载。我们用我们的log_execution装饰器包装了它们。
现在,每当代码被执行时,你会看到类似这样的输出:
INFO:root:Executing extract_data
INFO:root:Finished executing extract_data
INFO:root:Executing transform_data
INFO:root:Finished executing transform_data
INFO:root:Executing load_data
INFO:root:Finished executing load_data
我们也可以让执行时间在这个装饰器中打印出来。但我更喜欢将它们分开到不同的装饰器中。这样,我可以选择在函数中使用哪一个(或两个)。
以下是如何在一个函数上使用多个装饰器的方法。
@log_execution
@timing_decorator
def my_function(x, y):
time.sleep(1)
return x + y
5. 通知装饰器
最后,一个在生产系统中非常有用的装饰器是通知装饰器。
再次,即使经过多次重试,即使是经过良好测试的代码库也会失败。当这种情况发生时,我们需要通知某人以便快速采取行动。
如果你曾经构建过数据管道并希望它能永久稳定工作,这并不新鲜。
以下装饰器会在内部函数执行失败时发送一封电子邮件。在你的情况下,它不一定是电子邮件通知。你可以配置它发送 Teams/slack 通知。
import smtplib
import traceback
from email.mime.text import MIMEText
def email_on_failure(sender_email, password, recipient_email):
def decorator(func):
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
# format the error message and traceback
err_msg = f"Error: {str(e)}\n\nTraceback:\n{traceback.format_exc()}"
# create the email message
message = MIMEText(err_msg)
message['Subject'] = f"{func.__name__} failed"
message['From'] = sender_email
message['To'] = recipient_email
# send the email
with smtplib.SMTP_SSL('smtp.gmail.com', 465) as smtp:
smtp.login(sender_email, password)
smtp.sendmail(sender_email, recipient_email, message.as_string())
# re-raise the exception
raise
return wrapper
return decorator
@email_on_failure(sender_email='your_email@gmail.com', password='your_password', recipient_email='recipient_email@gmail.com')
def my_function():
# code that might fail
结论
装饰器是一种非常方便的方式来为我们的函数应用新行为。如果没有它们,将会有很多代码重复。
在这篇文章中,我讨论了我最常用的装饰器。你可以根据你的特定需求扩展这些装饰器。例如,你可以使用 Redis 服务器来存储缓存响应,而不是使用字典。这将使你对数据有更多的控制,比如持久性。或者,你可以调整代码,逐步增加重试装饰器中的等待时间。
在我所有的项目中,我都会使用这些装饰器的某个版本。尽管它们的行为略有不同,但这些是我经常使用装饰器的共同目标。
希望这篇文章对你有所帮助。
感谢阅读,朋友!如果你喜欢我的文章,请在 LinkedIn、Twitter 和 Medium 上保持联系。
还不是 Medium 的会员?请使用这个链接 成为会员,因为在不增加额外费用的情况下,我可以获得一小部分推荐佣金。
Python dictcomp 管道示例
原文:
towardsdatascience.com/python-dictcomp-pipelines-in-examples-4c4ec4e66f3
PYTHON 编程
见识 dictcomp 管道的威力
·发表于Towards Data Science ·11 分钟阅读·2023 年 4 月 20 日
--

管道一个接一个地处理任务。图片来源于Daniel Schludi在Unsplash
这篇文章的灵感来源于几年前我在一个实际项目中参与的任务。在提出理解管道的概念后,我注意到这个解决方案可以通过使用OptionalBool数据结构,并结合 dictcomp 管道来很好地实现,OptionalBool是我在另一篇文章中提出的。
这篇文章旨在展示我们如何实现这样的管道。我将详细讲解一些细节,以便代码变得清晰且令人信服。你可以将其视为一个案例研究,展示 dictcomp 管道的实现。
我们已经讨论了 Python 中生成器管道的威力:
这篇文章提出了一种优雅的方式来构建生成器管道
towardsdatascience.com
后来,我提出了一个 Python 特有的理解管道概念:
理解管道是一个 Python 特有的构建管道的概念
towardsdatascience.com
理解式管道构成了生成器管道的泛化。它们类似于生成器管道,但前者创建一个带有结果的生成器,而后者则以任何类型的理解式形式输出结果:
-
一个生成器,来自一个生成器管道
-
一个列表,来自一个列表推导式管道
-
一个字典,来自一个字典推导式管道
-
一个集合,来自一个集合推导式管道
在上述文章中,我专注于展示这种管道的工作原理以及如何构造它们。在此过程中,我使用了一些比较简单的示例。在这里,我们将使用一个更复杂的示例,其中我们将使用一个字典推导式管道。
任务:文档过滤
想象一下你的公司有大量的标准操作程序(SOP)。它们混乱不堪,使用了一个过时的系统进行组织。你需要使用特定的键来过滤它们。例如,你需要找出哪些包含“Python”一词。
当然,这种过滤可以复杂得多,因此你需要编写一个程序,以便将来能够更改过滤逻辑。此外,目前标准以本地文件的形式存储在你从本地计算机可以访问的共享驱动器中。然而,这种情况应该很快会改变,因此你还需要使读取逻辑易于更改。
如上所述,我们的过滤任务也很简单。然而,下一任务可能需要更高级的解析逻辑,例如特定字段或表格等。再次强调,整体机制将类似,我们只需重写解析器。在实现时,你应该考虑到这一点。
实现
我们可以将该任务的算法描述如下:
-
我们有一个
documents的列表(或其他序列);documents中的每个document可以被读取为一个字符串(这取决于文档的类型)。在我们的第一个例子中,这将是文件路径的列表。但文档也可以通过其他方式访问,例如从数据库中。 -
在读取一个
document后,解析文本并应用过滤逻辑。在我们的例子中,这归结为检查文本是否包含“Python”一词。如果包含,返回True;否则,返回False。 -
对
documents中的所有文档执行上述步骤。 -
作为输出,返回一个字典,其中文档的字符串表示作为键(在我们的示例中为字符串形式的路径),这些布尔值作为对应的值。
下面的代码块展示了dictcomp_pipeline模块的内容。它以通用方式实现了上述逻辑。
# dictcomp_pipeline.py
from pathlib import Path
from collections.abc import Sequence
from typing import Any, Optional
# Type aliases
Paths = Sequence[Path]
KeywordArgs = Optional[dict[str, Any]]
def read_text(path: Path) -> str:
"""Read text from path and return a string."""
return path.read_text()
def parse_text(text: str, word: str) -> bool:
"""Parse text from string and return a bool value."""
return word.lower() in text.lower()
def run_dictcomp_pipeline(
documents: Any,
read_text_kwargs: KeywordArgs = None,
parse_text_kwargs: KeywordArgs = None) -> dict[str, bool]:
read_text_kwargs = read_text_kwargs or {}
parse_text_kwargs = parse_text_kwargs or {}
texts = {
doc: read_text(doc, **read_text_kwargs)
for doc in documents
}
return {
str(doc): parse_text(text, **parse_text_kwargs)
for doc, text in texts.items()
}
在附录 1 中,你会发现这个代码包含了扩展版的文档字符串,它们解释了一些关键细节,我们也将在下文中涵盖这些细节。
泛化体现在两个方面:数据读取器的实现方式和数据解析器的实现方式。我们将在下一节中讨论这些方面,并逐一讨论这三个函数。
函数
读取数据
这个特定的实现接受一个参数path,它是一个pathlib.Path实例。尽管我们以特定方式实现了read_text()函数,但管道函数(get_dictcomp_pipeline())并未固定在这个实现上。你可以重新实现read_text()以满足你的需求;例如,它可以从 PDF 文件、网页或数据库中读取文档。
你可以自由更改函数,但必须保持以下几点不变:
-
函数必须将
get_dictcomp_pipeline()中的一个documents元素作为第一个参数。它作为位置参数传递,因此你可以根据函数要完成的任务为其命名。在这个特定实现中,它是path。 -
如果函数接受更多参数,它们必须作为关键字参数,并且在所有
read_text()的调用中都应相同。 -
函数应返回一个字符串形式的文本。
解析数据
尽管在我们的任务中,我们在文档中查找“Python”这个词,管道使得用户能够查找任何单词,这要归功于parse_data()函数的签名。它接受两个参数,text和word,都是字符串。如果在text中找到word,函数返回True;否则返回False。
这个解析逻辑忽略了单词的大小写,通过对文本和搜索的单词都使用.lower()字符串方法来简单实现。
与read_data()的情况类似,你可以更改函数的逻辑以及其签名,但需记住以下条件:
-
第一个参数,作为位置参数使用,是
read_text()返回的文本,类型为str。 -
如果函数接受更多参数,它们在所有
parse_text()的调用中都应相同。这些额外的参数必须作为关键字参数。 -
函数返回一个布尔值。
管道
run_dictcomp_pipeline()函数的实现方式使得上述两个函数可以有不同的实现,取决于数据源和解析逻辑。这是通过将这些函数作为关键字参数传递,从而解包相应的字典来完成的。
正如上述两个小节提到的一个限制,任何额外的参数必须对所有文档具有相同的值。如果你觉得这过于限制,你需要重新实现run_dictcomp_pipeline()函数,代价是增加额外的复杂性。
单元测试:管道在行动中
为了检查管道是否有效,让我们使用pytest实现一些单元测试。我将在这里跳过read_text()和parse_text()函数的单元测试,但可以随意将它们作为练习添加。
在这里,我们有一个测试来检查应用程序是否按预期工作。
# test_dictcomp_pipeline.py
import pathlib
import pytest
from dictcomp_pipeline import get_dictcomp_pipeline
@pytest.fixture
def files():
n_files = 11
paths = [pathlib.Path(".") / f"txt_file_{i}.txt"
for i in range(n_files)]
for i, path in enumerate(paths):
text = "Shout Bamalama!\nI'm an elephant, and so what?\n\n"
if i % 2 == 0:
text = f"{text}Python"
path.write_text(text)
yield paths
for path in paths:
path.unlink()
def test_get_dictcomp_pipeline(files):
isPython = get_dictcomp_pipeline(
files,
parse_text_kwargs={"word": "Python"}
)
assert isPython == {
'txt_file_0.txt': True,
'txt_file_1.txt': False,
'txt_file_2.txt': True,
'txt_file_3.txt': False,
'txt_file_4.txt': True,
'txt_file_5.txt': False,
'txt_file_6.txt': True,
'txt_file_7.txt': False,
'txt_file_8.txt': True,
'txt_file_9.txt': False,
'txt_file_10.txt': True
}
让我们看看测试是如何工作的。files固定装置创建了 11 个文本文件,其中六个文件的内容包含了“Python”这个词。这些文件在调用test_get_dictcomp_pipeline()时创建。然后测试运行管道函数并断言输出是否符合预期。最后,测试文本文件被删除,你将在固定装置代码的最后两行看到这些操作。
运行测试后,你应该会看到来自pytest的点赞:

一个管道将多个功能整合为一个,因此上面对我们管道函数的测试可以被视为,至少在某种程度上,是一个集成测试。
即使有非常多的文档,字典也应能很好地保存输出,因为它的类型是dict[str, bool],这样的字典不会占用太多内存。当然,你可能还是希望将其处理为生成器;在这种情况下,你应该将管道函数修订为生成器管道。一个解决方案是使生成器生成tuple[str, bool]类型的值。这样,它将是一个包含表示文档的字符串和布尔值的元组,布尔值表示解析函数是否找到了所搜索的词。
扩展示例
在上面的示例中,我们使用了一个bool值,但在某些任务中,这可能过于限制。例如,你可能需要考虑并非所有文档都是 SOP。换句话说,你有一些文档,其中有些是 SOP,有些不是;任务是检查文档是否为 SOP,如果是,则检查是否包含特定的词、短语或多个词/短语。在这种情况下,你可以使用更复杂的数据结构,如本文中提出的OptionalBool数据结构和类型:
## An OptionalBool Type for Python: None, False or True
使用OptionalBool而不是Optional[bool]。
在我们的任务中,OptionalBool值为None意味着相应的文档不是 SOP;False,表示它是 SOP 但不包含所搜索的短语;而True,表示它是 SOP 并且包含了所搜索的短语。
本文的动机来自一个真实的例子,当时我遇到了类似的情况。那时,我完全不同地实现了解决方案,但今天我肯定会考虑使用OptionalBool和字典推导管道。为了使用OptionalBool,run_dictcomp_pipeline()的签名需要稍作修改,因为我们将用OptionalBool替代bool。
你可以在附录 2 中找到修订后的代码版本,包括测试文件。
结论
我们讨论了使用 dictcomp 流水线的实际例子。虽然我们实现的任务相当简单,但解决方案更为通用,使得用户可以重新实现创建流水线的两个函数——而不需要更改流水线函数本身。
你可能已经注意到,我们实现的 dictcomp 流水线看起来只是一个字典推导式。因为它确实就是一个字典推导式。就像生成器流水线是一个生成器一样。流水线隐藏在正在进行的操作中:输入 → 函数 → 函数 → … → 函数 → 输出。在我们的例子中,流水线非常简短,但在许多其他场景中,它可以包含更多步骤。
我想向你展示在什么样的场景下这样的推导式流水线可以发挥作用。然而,这个特定的任务可以以多种方式实现。选择哪种方式应主要取决于你希望生成什么样的代码。如果你只是需要实现一个特定的任务,那么没有必要像我们这样对函数进行泛化。如果你打算编写一个供他人使用的框架,你可能会更进一步地泛化它。
附录 1
第一个示例中 run_dictcomp_pipeline() 的代码,带有完整的文档字符串:
# dictcomp_pipeline.py
from pathlib import Path
from collections.abc import Sequence
from typing import Any, Optional
# Type aliases
Paths = Sequence[Path]
KeywordArgs = Optional[dict[str, Any]]
def read_text(path: Path) -> str:
"""Read text from path and return a string.
You can rewrite this function to read from another source.
The function must return a string, but it can take any
number of keyword arguments. The first argument must
work as positional, and it must represent a document
from the `documents` sequences from `run_dictcomp_pipeline()`.
"""
return path.read_text()
def parse_text(text: str, word: str) -> bool:
"""Parse text from string and return a bool value.
You can rewrite this function to use different
parsing logic. The function must return a bool,
but it can take any number of keyword arguments.
The first argument must remain unchanged, and must
work as a positional argument.
"""
return word.lower() in text.lower()
def run_dictcomp_pipeline(
documents: Any,
read_text_kwargs: KeywordArgs = None,
parse_text_kwargs: KeywordArgs = None) -> dict[str, bool]:
"""Run dictcomp pipeline.
The function does not handle exceptions: if anything goes
wrong, the pipeline breaks and the corresponding exception
is raised.
Args:
paths (Paths): sequence with paths to files with
documents to read
word (str): word to look for in documents
read_text_kwargs (KeywordArgs, optional): dictionary with
keyword arguments to be used in a call to `read_text()`,
if needed. Defaults to None, meaning that no arguments
are passed.
parse_text_kwargs (KeywordArgs, optional): dictionary with
keyword arguments to be used in a call to `parse_text()`,
if needed. Defaults to None, meaning that no arguments
are passed.
Returns:
dict[Path, bool]: dictionary with the output of
the pipeline; its result represent the parsing logic
used in the documents
"""
read_text_kwargs = read_text_kwargs or {}
parse_text_kwargs = parse_text_kwargs or {}
texts = {
doc: read_text(doc, **read_text_kwargs)
for doc in documents
}
return {
str(doc): parse_text(text, **parse_text_kwargs)
for doc, text in texts.items()
}
附录 2
在本附录中,你将找到处理OptionalBool值的解决方案的扩展代码。你还需要 optionalbool 模块中的 OptionalBool 类的代码,你可以从以下文章中复制粘贴:
[## 一个可选布尔类型(OptionalBool):None、False 或 True]
使用 OptionalBool 替代 Optional[bool]。
这里是代码:
# optionalbool_dictcomp_pipeline.py
from pathlib import Path
from collections.abc import Sequence
from typing import Any, Optional
from optionalbool import OptionalBool
# Type aliases
Paths = Sequence[Path]
KeywordArgs = Optional[dict[str, Any]]
def read_text(path: Path) -> str:
"""Read text from path and return a string."""
return path.read_text()
def parse_text(text: str,
word: str,
standards_phrases: Sequence[str]
) -> OptionalBool:
"""Parse text from string and return a bool value."""
if not any(phrase.lower() in text.lower() for phrase in standards_phrases):
return OptionalBool(None)
return OptionalBool(word.lower() in text.lower())
def run_dictcomp_pipeline(
documents: Any,
read_text_kwargs: KeywordArgs = None,
parse_text_kwargs: KeywordArgs = None
) -> dict[str, OptionalBool]:
read_text_kwargs = read_text_kwargs or {}
parse_text_kwargs = parse_text_kwargs or {}
texts = {
doc: read_text(doc, **read_text_kwargs)
for doc in documents
}
return {
str(doc): parse_text(text, **parse_text_kwargs)
for doc, text in texts.items()
}
以及测试文件:
# test_optionalbool_dictcomp_pipeline.py
import pathlib
import pytest
from optionalbool_dictcomp_pipeline import run_dictcomp_pipeline
from optionalbool import OptionalBool
@pytest.fixture
def files():
n_files = 11
paths = [pathlib.Path(".") / f"doc_file_{i}.txt"
for i in range(n_files)]
for i, path in enumerate(paths):
text = "Shout Bamalama!\nI'm an elephant, and so what?\n\n"
if i % 2 == 0:
text = f"{text}Python"
if i % 3 != 0:
text = (
"This is a Standard Operating Procedure\n"
f"{text}"
)
path.write_text(text)
yield paths
for path in paths:
path.unlink()
def test_get_dictcomp_pipeline(files):
standards_phrases = ["Standard Operating Procedure", "SOP",]
isPython = run_dictcomp_pipeline(
files,
parse_text_kwargs={"word": "Python",
"standards_phrases": standards_phrases}
)
for v in isPython.values():
assert isinstance(v, OptionalBool)
assert isPython == {
'doc_file_0.txt': None,
'doc_file_1.txt': False,
'doc_file_2.txt': True,
'doc_file_3.txt': None,
'doc_file_4.txt': True,
'doc_file_5.txt': False,
'doc_file_6.txt': None,
'doc_file_7.txt': False,
'doc_file_8.txt': True,
'doc_file_9.txt': None,
'doc_file_10.txt': True
}
感谢阅读。如果你喜欢这篇文章,你可能也会喜欢我写的其他文章;你可以在这里查看。如果你想加入 Medium,请使用下面的推荐链接:
[## 使用我的推荐链接加入 Medium - Marcin Kozak]
阅读 Marcin Kozak 的每一个故事(以及 Medium 上其他成千上万的作者的故事)。你的会员费用将直接支持…
medium.com](https://medium.com/@nyggus/membership?source=post_page-----4c4ec4e66f3--------------------------------)
Python enumerate() 函数解释
原文:
towardsdatascience.com/python-enumerate-function-explained-7907837e171c
在这篇文章中,我们将探索如何使用 Python enumerate()函数
·发布于Towards Data Science ·3 分钟阅读·2023 年 2 月 20 日
--

照片由Nick Fewings提供,来源于Unsplash
目录
-
介绍
-
使用 enumerate()与列表
-
使用 enumerate()与字符串
-
使用 enumerate()与字典
-
使用 enumerate()与 zip()
-
结论
介绍
Python enumerate()函数是一个内置函数,可以在遍历 Python 可迭代对象时跟踪其索引。
Python enumerate()函数的语法是:
enumerate(iterable, start=0)
在哪里:
-
iterable — 可以是任何可迭代的 Python 对象,如字符串、元组、列表、集合、字典等。
-
start — 指定起始索引的参数(可选)。默认为 0。
使用 enumerate()与列表
作为一个简单的例子,我们将创建一个Python 列表,然后使用带有enumerate()函数的 for 循环打印列表的每个元素及其索引:
my_list = ['Apple', 'Banana', 'Orange', 'Pineapple']
for i, elem in enumerate(my_list):
print(i, elem)
你应该得到:
0 Apple
1 Banana
2 Orange
3 Pineapple
使用 enumerate()与字符串
enumerate()函数也适用于 Python 字符串,因为它们是可迭代对象。
它的工作方式类似于与 Python 列表的示例。不同之处在于,对于字符串,我们将遍历字符串中的每个字符,并将其与在字符串中的索引(位置)一起打印出来:
my_string = 'Apple'
for i, char in enumerate(my_string):
print(i, char)
你应该得到:
0 A
1 p
2 p
3 l
4 e
使用 enumerate()与字典
另一个有趣的例子是使用enumerate()函数与Python 字典。
虽然字典中的键值对是无序的且未索引,但使用enumerate()对你的代码来说是一个非常有用的选项。
输出的主要区别在于,当遍历字典时,你可以选择遍历字典的键、字典的值或字典的键值对。
遍历字典键
my_dict = {
'Apple': 3,
'Banana': 1,
'Orange': 2,
'Pineapple': 5
}
for i, key in enumerate(my_dict.keys()):
print(i, key)
你应该得到:
0 Apple
1 Banana
2 Orange
3 Pineapple
遍历字典值
my_dict = {
'Apple': 3,
'Banana': 1,
'Orange': 2,
'Pineapple': 5
}
for i, value in enumerate(my_dict.values()):
print(i, value)
你应该得到:
0 3
1 1
2 2
3 5
遍历字典键值对
当将键值对遍历与enumerate()功能结合使用时,输出将是每个索引和一个 元组 ,其中包含字典中每个条目的键值对:
my_dict = {
'Apple': 3,
'Banana': 1,
'Orange': 2,
'Pineapple': 5
}
for i, (key, value) in enumerate(my_dict.items()):
print(i, (key, value))
你应该得到:
0 ('Apple', 3)
1 ('Banana', 1)
2 ('Orange', 2)
3 ('Pineapple', 5)
使用 enumerate() 和 zip()
一个稍微高级的示例是将enumerate()与其他 Python 函数一起使用,例如。
两个函数组合的功能允许同时遍历多个列表,同时跟踪元素对的索引:
fruits = ['Apple', 'Banana', 'Orange', 'Pineapple']
prices = [3, 1, 2, 5]
for i, (fruit, price) in enumerate(zip(fruits, prices)):
print(i, fruit, price)
你应该得到:
0 Apple 3
1 Banana 1
2 Orange 2
3 Pineapple 5
结论
在这篇文章中,我们探讨了 Python enumerate() 函数。
现在你知道了基本功能,你可以尝试将其与其他可迭代的 数据结构 一起使用,以处理更复杂的用例。
如果你有任何问题或有编辑建议,请随时在下方留言,并查看更多我的 Python Functions 教程。
原文发布于 https://pyshark.com 于 2023 年 2 月 20 日。
Python 异常测试:清晰而有效的方法
超越基础:针对 Pytest 和 Unittest 的高级 Python 异常测试
·
关注 发布于 Towards Data Science ·4 min read·Jul 24, 2023
--
测试异常不仅仅是一种形式 - 它是编写可靠代码的关键部分。在本教程中,我们将探讨测试 Python 代码的方法,这些方法包括处理和不处理异常,验证异常消息的准确性,涵盖pytest和unittest,并为每个框架提供带有和不带参数化的测试。
在本教程结束时,你将对如何为代码编写干净、高效和有用的异常测试有一个扎实的理解。
让我们查看以下示例:
def divide(num_1: float, num_2: float) -> float:
if not isinstance(num_1, (int, float)) \
or not isinstance(num_2, (int, float)):
raise TypeError("at least one of the inputs "
f"is not a number: {num_1}, {num_2}")
return num_1 / num_2
我们可以为上述函数测试几种情况——正常流、零分母和非数字输入。
现在,让我们看看这样的测试在使用pytest时会是什么样子:
pytest
from contextlib import nullcontext as does_not_raise
import pytest
from operations import divide
def test_happy_flow():
with does_not_raise():
assert divide(30, 2.5) is not None
assert divide(30, 2.5) == 12.0
def test_division_by_zero():
with pytest.raises(ZeroDivisionError) as exc_info:
divide(10.5, 0)
assert exc_info.value.args[0] == "float division by zero"
def test_not_a_digit():
with pytest.raises(TypeError) as exc_info:
divide("a", 10.5)
assert exc_info.value.args[0] == \
"at least one of the inputs is not a number: a, 10.5"
我们还可以进行一个合理性检查,看看当我们测试无效流时,错误的异常类型或尝试检查在正常流中抛出的异常时会发生什么。在这些情况下,测试将失败:
# Both tests below should fail
def test_wrong_exception():
with pytest.raises(TypeError) as exc_info:
divide(10.5, 0)
assert exc_info.value.args[0] == "float division by zero"
def test_unexpected_exception_in_happy_flow():
with pytest.raises(Exception):
assert divide(30, 2.5) is not None
那么,上面的测试为什么会失败呢?with上下文捕获请求的特定类型异常,并验证异常类型是否确实是我们要求的。
在test_wrong_exception_check中,抛出了一个异常(ZeroDivisionError),但未被TypeError捕获。因此,在堆栈跟踪中,我们会看到ZeroDivisionError被抛出但未被TypeError上下文捕获。
在test_redundant_exception_context中,我们的with pytest.raises上下文尝试验证请求的异常类型(在这种情况下我们提供了Exception),但由于没有异常被抛出——测试失败,并显示Failed: DID NOT RAISE <class Exception>。
现在,进入下一个阶段,让我们探索如何通过使用parametrize使我们的测试更加简洁和清晰。
参数化
from contextlib import nullcontext as does_not_raise
import pytest
from operations import divide
@pytest.mark.parametrize(
"num_1, num_2, expected_result, exception, message",
[
(30, 2.5, 12.0, does_not_raise(), None),
(10.5, 0, None, pytest.raises(ZeroDivisionError),
"float division by zero"),
("a", 10.5, None, pytest.raises(TypeError),
"at least one of the inputs is not a number: a, 10.5")
],
ids=["valid inputs",
"divide by zero",
"not a number input"]
)
def test_division(num_1, num_2, expected_result, exception, message):
with exception as e:
result = divide(num_1, num_2)
assert message is None or message in str(e)
if expected_result is not None:
assert result == expected_result
ids参数更改显示在 IDE 测试栏视图中的测试用例名称。在下面的截图中,我们可以看到它的实际效果:左侧有ids,右侧没有ids。

作者截图
既然我们已经覆盖了pytest框架,让我们看看如何使用unittest编写相同的测试。
unittest
from unittest import TestCase
from operations import divide
class TestDivide(TestCase):
def test_happy_flow(self):
result = divide(0, 10.5)
self.assertEqual(result, 0)
def test_division_by_zero(self):
with self.assertRaises(ZeroDivisionError) as context:
divide(10, 0)
self.assertEqual(context.exception.args[0], "division by zero")
def test_not_a_digit(self):
with self.assertRaises(TypeError) as context:
divide(10, "c")
self.assertEqual(context.exception.args[0],
"at least one of the inputs "
"is not a number: 10, c")
如果我们想在unittest中使用parameterized,我们需要安装该包。让我们看看在unittest中参数化的测试会是什么样的:
参数化
import unittest
from parameterized import parameterized # requires installation
from operations import divide
def get_test_case_name(testcase_func, _, param):
test_name = param.args[-1]
return f"{testcase_func.__name__}_{test_name}"
class TestDivision(unittest.TestCase):
@parameterized.expand([
(30, 2.5, 12.0, None, None, "valid inputs"),
(10.5, 0, None, ZeroDivisionError,
"float division by zero", "divide by zero"),
("a", 10.5, None, TypeError,
"at least one of the inputs is not a number: a, 10.5",
"not a number input")
], name_func=get_test_case_name)
def test_division(self, num_1, num_2, expected_result, exception_type,
exception_message, test_name):
with self.subTest(num_1=num_1, num_2=num_2):
if exception_type is not None:
with self.assertRaises(exception_type) as e:
divide(num_1, num_2)
self.assertEqual(str(e.exception), exception_message)
else:
result = divide(num_1, num_2)
self.assertIsNotNone(result)
self.assertEqual(result, expected_result)
在unittest中,我们也修改了测试用例名称,类似于上面的pytest示例。然而,为了实现这一点,我们使用了name_func参数以及一个自定义函数。
总结一下,今天我们探讨了测试 Python 异常的有效方法。我们学会了如何识别预期的异常是否被抛出,并验证异常消息是否符合我们的期望。我们检查了多种测试divide函数的方法,包括使用pytest的传统方法和使用parametrize的更清晰方法。我们还探索了unittest等效的parameterized,它需要安装该库,以及不使用它的情况。使用ids和自定义测试名称在 IDE 的测试栏中提供了更清晰和更有信息量的视图,使我们更容易理解和导航测试用例。通过使用这些技术,我们可以改进单元测试,确保代码适当地处理异常。
祝测试愉快!

Python 数据工程师
原文:
towardsdatascience.com/python-for-data-engineers-f3d5db59b6dd
初学者的高级 ETL 技巧
·发布于 Towards Data Science ·阅读时间 17 分钟·2023 年 10 月 21 日
--

在这个故事中,我将讲述 Python 中的高级数据工程技术。毫无疑问,Python 是最受欢迎的数据编程语言。在我近十二年的数据工程职业生涯中,我遇到过各种代码问题。这个故事简要总结了我如何解决这些问题并学会写出更好的代码。我将展示一些使我们的 ETL 更快并有助于提高代码性能的技术。
列表推导式
想象一下你正在遍历一个表的列表。通常,我们会这样做:
data_pipelines = ['p1','p2','p3']
processed_tables = []
for table in data_pipelines:
processed_tables.append(table)
但我们可以使用列表推导式。它们不仅更快,还减少了代码,使其更简洁:
processed_tables = [table for table in data_pipelines]
例如,循环处理一个超大的文件以转换(ETL)每一行,从未如此简单:
def etl(item):
# Do some data transformation here
return json.dumps(item)
data = u"\n".join(etl(item) for item in json_data)
列表推导式对于 ETL 处理大数据文件非常有用。假设我们有一个需要转换为换行符分隔格式的数据文件。在你的 Python 环境中尝试运行这个示例:
import io
import json
def etl(item):
return json.dumps(item)
# Text file loaded as a blob
blob = """
[
{"id":"1","first_name":"John"},
{"id":"2","first_name":"Mary"}
]
"""
json_data = json.loads(blob)
data_str = u"\n".join(etl(item) for item in json_data)
print(data_str)
data_file = io.BytesIO(data_str.encode())
# This data file is ready for BigQuery as Newline delimited JSON
print(data_file)
输出将是换行符分隔的 JSON。这是 BigQuery 数据仓库中的一种标准格式,准备好加载到表中了:
{"id": "1", "first_name": "John"}
{"id": "2", "first_name": "Mary"}
<_io.BytesIO object at 0x10c732430>
生成器
如果我们处理的是逐行存储的 CSV 和 DAT 文件,那么我们的文件对象已经是一个生成器,我们可以使用列表推导式来处理数据,不会消耗太多内存:
for line in open('very_big_file.csv'):
validate_schema(line)
# or the same using list comprehension:
data_errors = [validate_schema(line) for line in open('very_big_file.csv')]
在我们实际将记录插入数据仓库表之前验证记录,对于批量数据处理管道可能是有用的。
我们经常需要在将数据文件加载到数据仓库之前验证它们。如果一个记录失败,那么整个批次都会失败。
我们可以用它来创建接近实时的分析管道。这也是一种非常经济高效的方式来处理数据,相比于流数据管道设计模式。我之前在这里写过:
选择合适的架构和示例
[towardsdatascience.com
或者,在处理大数据时,如果我们的文件不是换行符分隔的文本,我们可以使用yield。这始终是一个好习惯,因为我们希望以内存高效的方式处理数据文件。例如:
# Create a file first: ./very_big_file.csv as:
# transaction_id,user_id,total_cost,dt
# 1,John,10.99,2023-04-15
# 2,Mary, 4.99,2023-04-12
# Example.py
def etl(item):
# Do some etl here
return item.replace("John", '****')
# Create a generator
def batch_read_file(file_object, batch_size=19):
"""Lazy function (generator) can read a file in chunks.
Default chunk: 1024 bytes."""
while True:
data = file_object.read(batch_size)
if not data:
break
yield data
# and read in chunks
with open('very_big_file.csv') as f:
for batch in batch_read_file(f):
print(etl(batch))
# In command line run
# Python example.py
这将读取一个本地文件并以 19 字节为块进行处理。输出将是:
transaction_id,user
_id,total_cost,dt
1
,****,10.99,2023-04
-15
2,Mary, 4.99,20
23-04-12
这只是处理二进制数据的一个示例。在现实生活中,将文件内容分割成段使用分隔符(即换行符'\n’或'}{')可能会更容易,这取决于我们的数据结构。
假设文本数据来自某个外部位置,即云存储。我们也可以将其处理为流。我们不希望加载整个数据文件并运行split('\n')逐行处理,这会消耗大量内存。我们可以使用re.finditer,它像生成器一样,以块的方式读取数据文件,这样我们就可以运行所需的 ETL 而不会消耗太多内存。
import io
import re
def etl(item):
print(f'Transforming item: {item}')
return item.replace("John", '****')
# Helper function to split our text file into chunks
# using separator
def splitStr(string, sep="\s+"):
if sep=='':
return (c for c in string)
else:
return (_.group(1) for _ in re.finditer(f'(?:^|{sep})((?:(?!{sep}).)*)', string))
# Text file loaded as a blob
blob = """transaction_id,user_id,total_cost,dt
1,John,10.99,2023-04-15
2,Mary, 4.99,2023-04-12
"""
# data = blob.split("\n") # We wouldn't want to do this on large datasets
# as it would require to load big data file as a whole in the first place
# consuming lots of memory
# We would want to use our generator helper function
# and process data in chunks
data = splitStr(blob, sep='\n')
data_str = u"\n".join(etl(item) for item in data)
print('New file contents:')
print(data_str)
data_file = io.BytesIO(data_str.encode())
print('This data file is ready for BigQuery:')
print(data_file)
输出:
python example.py ✔ 48 19:52:06 dataform_env
Transforming item: transaction_id,user_id,total_cost,dt
Transforming item: 1,John,10.99,2023-04-15
Transforming item: 2,Mary, 4.99,2023-04-12
Transforming item:
New file contents:
transaction_id,user_id,total_cost,dt
1,****,10.99,2023-04-15
2,Mary, 4.99,2023-04-12
This data file is ready for BigQuery:
<_io.BytesIO object at 0x103672980>
Python 数据验证属性
我们可以使用Python 属性 [2]来验证数据记录。如果记录不是我们定义的类的实例,则必须抛出异常。
我们可以将数据存储为数据类的对象。
就是这么简单。假设我们有一个流数据管道,我们想要验证记录中的一些字段。
简单来说——它们必须匹配现有的表格模式。
我们可以使用 Python 属性来实现。请看下面的示例。
class ConnectionDataRecord(object):
def __init__(self, user, ts):
self.user = user
self.ts = ts
@property
def user(self):
return self._user
@description.setter
def user(self, d):
if not d: raise Exception("user cannot be empty")
self._user = d
@property
def ts(self):
return self._ts
@value.setter
def ts(self, v):
if not (v > 0): raise Exception("value must be greater than zero")
self._ts = v
如果我们选择打破规则并分配一些不符合我们标准的值,则会抛出异常。例如,如果我们尝试调用ConnectionDataRecord('', 1),将会抛出异常。
或者,我们可以使用一个名为Pydantic的库。请看下面的代码。如果我们用不符合要求的对象调用该函数,它将抛出一个错误。
from pydantic import BaseModel
class ConnectionDataRecord(BaseModel):
user: str
ts: int
record = ConnectionDataRecord(user="user1", ts=123456789)
装饰器
装饰器的设计目的是使我们的代码看起来更简洁,并为其添加额外的功能。我们可以简单地将一个函数作为参数传递给另一个函数(装饰器),并在这个包装器内部进行一些数据转换。想象一下,我们有很多不同的 ETL 函数来处理数据,但我们只需要一个将结果上传到数据湖的函数。这就是我们如何做到的:
如果一些代码逻辑重复,使用装饰器是一个好习惯。
这有助于更容易维护代码库,并节省了我们在需要更改重复逻辑时的很多时间。
def etl_decorator(func):
def wrapper():
result = func()
return f'Processing {result}'
return wrapper
@etl_decorator
def unzip_data():
return "unzipped data"
print(unzip_data()) # Output: Processing unzipped data
装饰器因其有效性被广泛使用。考虑这个 Airflow DAG 示例:
@dag(default_args=default_args, tags=['etl'])
def etl_pipeline():
@task()
def extract():
return json.loads(data_string)
@task(multiple_outputs=True)
def transform(order_data_dict: dict):
return {"total_count": len(order_data_dict)}
@task()
def load(total_order_value: float):
print(f"Total order value is: {total_count}")
extracted = extract()
transformed = transform(extracted)
load(transformed["total_count"])
与 API 的工作
作为数据工程师,你会经常执行 HTTP 请求,调用各种 API 端点。下面是一个 GET 请求的示例。
response = requests.get('https://api.nasa.gov/neo/rest/v1/feed?start_date=2015-09-07&end_date=2015-09-08&api_key=your_api_key'))
print(response.json())
它从免费的NASA 小行星 API中提取一些数据,并返回所有在该日期接近地球的小行星。只需在上面的 URL 路径中替换你的 API 密钥或使用我创建的密钥。requests库处理所有事情,但还有更好的方法。
我们可以使用会话并以流的形式处理来自我们 API 端点的数据。
这将确保我们不会遇到任何内存问题,并以流式方式处理我们的 GET 请求[3]:
import requests
session = requests.Session()
url="https://api.nasa.gov/neo/rest/v1/feed"
apiKey="your_api_key"
requestParams = {
'api_key': apiKey,
'start_date': '2023-04-20',
'end_date': '2023-04-21'
}
response = session.get(url, params = requestParams, stream=True)
print(response.status_code)
理解 HTTP 请求的工作原理在数据工程中至关重要。
我每天处理各种 API 请求,不必依赖其他框架或库。
例如,就在几周前,我在Dataform 迁移项目上工作,意识到现有的 Google 库(from google.cloud import dataform_v1beta1)无法创建调度。解决方法是使用 Dataform API [4],这就像向特定端点发出 POST 请求一样简单:
from google.cloud import dataform_v1beta1
import requests
import google.auth.transport.requests
from google.oauth2 import service_account
...
# Get Dataform and BigQuery credentials from encrypted file:
print(f'Getting BigQuery credentials from encrypted file...')
credentials = service_account.Credentials.from_service_account_file(
'./../credentials.json'
, scopes=['https://www.googleapis.com/auth/cloud-platform'])
def create_dataform_workflow_config(credentials, id, workflow_config, repository_id):
'''
The function will create a schedule (workflow) in Dataform configs by making a direct API call
and using request_params with requests module
, i.e.
https://cloud.google.com/dataform/reference/rest/v1beta1/projects.locations.repositories.workflowConfigs/create
https://cloud.google.com/dataform/reference/rest/v1beta1/projects.locations.repositories.workflowConfigs#WorkflowConfig
If successful will create a workflow:
{'name': 'projects/my-project-data-staging/locations/us-central1/repositories/dataform-poc/workflowConfigs/test_live_20230831', 'releaseConfig': 'projects/my-project-data-staging/locations/us-central1/repositories/dataform-poc/releaseConfigs/staging', 'invocationConfig': {'includedTags': ['test']}, 'cronSchedule': '40 13 * * *', 'timeZone': 'Africa/Abidjan'}
If the workflow exists an error will be sent:
{'error': {'code': 409, 'message': "Resource 'projects/123456789/locations/us-central1/repositories/dataform-poc/workflowConfigs/test_live_20230831' already exists", 'status': 'ALREADY_EXISTS', 'details': [{'@type': 'type.googleapis.com/google.rpc.ResourceInfo', 'resourceName': 'projects/123456789/locations/us-central1/repositories/dataform-poc/workflowConfigs/test_live_20230831'}]}}
Accepts workflow_config as request_body, i.e.
request_body = {
# "name": "projects/123456789/locations/us-central1/repositories/dataform-poc/workflowConfigs/test_live_20230830",
"releaseConfig": "projects/my-project-data-staging/locations/us-central1/repositories/dataform-poc/releaseConfigs/staging",
"invocationConfig": {
"includedTags": [
"test"
]
},
"cronSchedule": "40 13 * * *",
"timeZone": "Africa/Abidjan"
}
'''
request = google.auth.transport.requests.Request()
credentials.refresh(request)
print('Creating a workflow...')
# Make the request
try:
session = requests
url=f'https://dataform.googleapis.com/v1beta1/projects/123456789/locations/us-central1/repositories/{repository_id}/workflowConfigs/'
headers = {
"Authorization": "Bearer " + credentials.token,
"Content-Type" : "application/json; charset=utf-8"
}
query_params = {
"workflowConfigId": id
}
request_body = workflow_config
page_result = session.post(url, params=query_params, json=request_body, headers=headers)
print(page_result.json())
except Exception as e:
print(e)
这个请求的核心在于我们将workflow_config作为json发送,并使用来自 Google 文档[4]的知识在路径参数中添加workflowConfigId。
这将创建一个必要的调度,以在 BigQuery 的 Dataform 中运行我们的数据转换脚本。
类似地,正如我们在 GET 请求中所做的那样,我们可以使用 Python 生成器将数据流入我们的 POST API 端点:
import time
import requests
def etl_data_generator():
yield b"Foo"
time.sleep(3)
yield b"Bar"
requests.post("http://some.api.endpoint", data=etl_data_generator())
思路很清楚。我们可以以节省内存的方式处理和发送数据。
处理 API 速率限制
所有 API 都有速率限制,我们在提取数据时要记住这一点。我们可以使用装饰器来处理它。简单的装饰可以像这样实现:
from ratelimit import limits
import requests
CALLS = 10
TIME_PERIOD = 900 # time period in seconds
@limits(calls=CALLS, period=TIME_PERIOD)
def call_api():
response = requests.get('https://api.example.com/data')
if response.status_code != 200:
raise Exception('API response: {}'.format(response.status_code))
return response.json()
使用这个装饰器,我们的函数在 15 分钟内不会发起超过 10 次 API 调用。
处理这种情况的最简单方法是使用time.sleep(),但 Python 速率限制允许我们以这种优雅的方式做到这一点。
Python 中的 Async 和await
以异步方式执行 ETL 是另一个极其有用的功能。我们可以使用asyncio库来同时运行任务。让我们考虑这个简单的同步示例,其中我们在for循环中处理表:
import requests
def pull_data(url, requestParams):
return requests.get(url, params = requestParams, stream=True)
for table in api_endpoints_list:
data = pull_data(table.api_url, table.requestParams)
etl(data)
运行这段代码时,我们必须等待每个表完成pull_data()任务,但使用Async,我们可以并行处理它们。
考虑使用以下代码:
import asyncio
import aiohttp
async def pull_data(session, url, requestParams):
async with session.get(url, params = requestParams, stream=True) as response:
return await response
async def main():
async with aiohttp.ClientSession() as session:
tasks = [pull_data(session, url, requestParams) for table in api_endpoints_list:]
tasks_data = await asyncio.gather(*tasks)
for data in tasks_data:
etl(task_data)
asyncio.run(main())
它将同时从报告 API 中提取数据,并显著提高我们的 ETL 性能。
它帮助管理 ETL 任务,同时系统资源以最佳方式分配。
例如,我们可以同时运行两个 ETL 作业,但我们可以定义执行顺序:
async def etl():
job1 = asyncio.create_task(perform_etl_script1())
job2 = asyncio.create_task(read_s3_data())
job3 = asyncio.create_task(upload_s3_data())
await job2
await job1
await job3
使用 Map 和 Filter
映射和过滤比列表推导式的速度更快。
我们可以逐行转换数据,将map函数应用于数据集中的项目,将其处理为iterable:
import math
numbers = [10,20]
factorials = list(map(lambda i: math.factorial(int(math.sqrt(i**3))), numbers))
print(factorials)
# Output:
# [8222838654177922817725562880000000, 16507955160908461081216919262453619309839666236496541854913520707833171034378509739399912570787600662729080382999756800000000000000000000]
我们希望使用filter来提取符合特定条件的对象,即
numbers = [10,21]
even_numbers = list(filter(lambda i: i% 2 == 0, numbers))
print(even_numbers)
# Output:
# [10]
使用 Pandas 处理大型数据集
后来的 Pandas 库版本提供了一个方便的上下文管理器,可以像这样使用:
batchsize = 10 ** 5
with pd.read_csv(filename, chunksize=batchsize) as reader:
for batch in reader:
etl(batch)
它将以批处理模式处理数据,假设我们不需要一次性将整个数据集加载到数据框中。
它有广泛的应用,从 OLAP 报告到机器学习(ML)管道。例如,我们可能想要创建一个推荐模型训练任务,并需要像这样准备数据集:
batch_data=pd.read_table('recommendation_data.csv',chunksize=100000,sep=';',\
names=['group','user','rating','date','id'],index_col='id',\
header=None,parse_dates=['date'])
df=pd.DataFrame()
%time df=pd.concat(batch.groupby(['group','user',batch['date'].map(lambda x: x.year)])['rating'].agg(['sum']) for batch in batch_data)
这样,Pandas 将确保我们的应用程序始终有足够的内存来处理数据。
使用 joblib 进行管道处理和并行计算
joblib.dump()和joblib.load()方法允许我们高效地管道大型数据集转换。joblib将存储和序列化大数据,处理任意 Python 对象,如numpy数组。
你认为
scikit-learn用什么来保存和加载机器学习模型?正确的答案是 -joblib。
首先,为什么要保存模型?——简单来说,因为我们可能在管道后面需要它,即使用新数据进行预测等。
我们不希望重新训练我们的机器学习模型,因为这是一个非常耗时的任务。
另一个原因是我们可能希望保存相同模型的不同版本,以便查看哪个版本表现更好。joblib有助于完成所有这些工作[5]:
from joblib import dump, load
import os
import numpy as np
import joblib
filename = os.path.join(savedir, 'test.joblib')
to_persist = [('foo', [1, 2, 3]), ('bar', np.arange(5))]
# Save a model
joblib.dump(to_persist, filename)
# ['...test.joblib']
# Load a model
joblib.load(filename)
# [('foo', [1, 2, 3]), ('bar', array([0, 1, 2, 3, 4]))]
这些函数明确连接了我们在磁盘上保存的文件和原始 Python 对象的执行上下文。因此,除了文件名,joblib还接受文件对象:
# WRITE
with open(filename, 'wb') as fo:
joblib.dump(model, fo)
# READ
with open(filename, 'rb') as fo:
model = joblib.load(fo)
AWS S3 模型转储/加载示例:
import tempfile
import boto3
import joblib
s3_client = boto3.client('s3')
bucket_name = "my-bucket"
key = "model.pkl"
# WRITE
with tempfile.TemporaryFile() as fp:
joblib.dump(model, fp)
fp.seek(0)
s3_client.put_object(Body=fp.read(), Bucket=bucket_name, Key=key)
# READ
with tempfile.TemporaryFile() as fp:
s3_client.download_fileobj(Fileobj=fp, Bucket=bucket_name, Key=key)
fp.seek(0)
model = joblib.load(fp)
# DELETE
s3_client.delete_object(Bucket=bucket_name, Key=key)
使用 joblib 进行并行计算
这非常高效,因为它依赖于多进程,并且会使用多个 Python 工作者在所有 CPU 核心上或跨多台机器并发执行任务。考虑这个例子:
import time
from joblib import Parallel,delayed
import math
t1 = time.time()
# Normal
results = [math.factorial(int(math.sqrt(i**3))) for i in range(1000,2000)]
t2 = time.time()
print('\nComputing time {:.2f} s'
.format(t2 - t1))
# Using all CPU cores
t1 = time.time()
results = Parallel(n_jobs=-1)(delayed(math.factorial) (int(math.sqrt(i**3))) for i in range(1000,2000))
t2 = time.time()
print('\nComputing time {:.2f} s'
.format(t2 - t1))
我们可以利用所有 CPU 核心来释放硬件的全部潜力。
在这里,我们告诉Parallel使用所有核心(-1),计算速度提高了 5 倍:
# The output:
Computing time 59.67 s
Computing time 12.18 s
单元测试 ETL 管道
在我整个数据工程师职业生涯中,我学到的最重要的一点是所有东西都必须进行单元测试。这不仅包括SQL,还包括ETL 作业和与我们数据管道中使用的其他服务的集成。
我们可以使用unittest Python 库来测试我们的代码。假设我们有一个助手模块,用于检查一个数字是否是素数:
# ./prime.py
import math
def is_prime(num):
'''Check if num is prime or not.
'''
for i in range(2,int(math.sqrt(num))+1):
if num%i==0:
return False
return True
我们如何测试这个函数内部的逻辑?
unittest使这一切变得非常简单:
# ./test.py
import unittest
from prime import is_prime
class TestPrime(unittest.TestCase):
def test_thirteen(self):
self.assertTrue(is_prime(13))
现在如果我们在命令行中运行这个,我们将测试逻辑:
python -m unittest test.py
# Output:
# .
# ----------------------------------------------------------------------
# Ran 1 test in 0.000s
# OK
这是正确的,因为 13 是一个素数。让我们进一步测试一下。我们知道 4 不是一个素数,因此我们希望针对这个特定函数的单元测试在断言为 False 时返回通过:
# ./test.py
import unittest
from prime import is_prime
class TestPrime(unittest.TestCase):
def test_thirteen(self):
self.assertTrue(is_prime(13))
def test_four(self):
self.assertFalse(is_prime(4))
python -m unittest test.py
# Output:
# ..
# ----------------------------------------------------------------------
# Ran 2 tests in 0.000s
# OK
很简单。让我们看一个更高级的示例。
让我们假设我们有一个 ETL 服务,从某个 API 中提取数据,这需要很多时间。然后我们的服务将转换这个数据集,我们希望测试这个 ETL 转换逻辑是否持续存在。
我们该怎么做呢?
我们可以使用unittest库中的 mock 和 patch 方法。考虑这个应用程序文件asteroids.py
# ./asteroids.py
import requests
API_KEY="fsMlsu69Y7KdMNB4P2m9sqIpw5TGuF9IuYkhURzW"
ASTEROIDS_API_URL="https://api.nasa.gov/neo/rest/v1/feed"
def get_data():
print('Fetching data from NASA Asteroids API...')
session = requests.Session()
url=ASTEROIDS_API_URL
apiKey=API_KEY
requestParams = {
'api_key': apiKey,
'start_date': '2023-04-20',
'end_date': '2023-04-21'
}
response = requests.get(url, params = requestParams)
print(response.status_code)
near_earth_objects = (response.json())['near_earth_objects']
return near_earth_objects
def save_data():
# Do some ETL here
data = get_data()
return data
print(save_data())
如果我们运行 app.py,输出将会是列出在特定日期接近地球的小行星:
# python ./asteroids.py
Fetching data from NASA Asteroids API...
200
{'2023-04-20': [{'links': {'self': 'http://api.nasa.gov/neo/rest/v1/neo/2326291?api_key=fsMlsu69Y7KdMNB4P2m9sqIpw5TGuF9IuYkhURzW'}, 'id': '2326291', 'neo_reference_id': '2326291', 'name': '326291 (1998 HM3)', 'nasa_jpl_url': 'http://ssd.jpl.nasa.gov/sbdb.cgi?sstr=2326291', 'absolute_magnitude_h': 19.0, 'estimated_diameter': {'kilometers': {'estimated_diameter_min': 0.4212646106, 'estimated_diameter_max': 0.9419763057}, 'meters': {'estimated_diameter_min': 421.2646105562, 'estimated_diameter_max': 941.9763057186}, 'miles': {'estimated_diameter_min': 0.2617616123, 'estimated_diameter_max': 0.5853167591}, 'feet': {'estimated_diameter_min': 1382.1017848971, 'estimated_diameter_max': 3090.4735428537}}, 'is_potentially_hazardous_asteroid': False, 'close_approach_data':
....
从 API 中提取数据可能需要很多时间,但我们希望我们的单元测试运行得更快。我们可以模拟一些假的 API 响应到我们的get_data()函数中,然后使用它来测试 save_data()函数中的 ETL 逻辑:
# ./test_etl.py
import unittest
from asteroids import *
import unittest.mock as mock
class TestEtl(unittest.TestCase):
def test_asteroids_etl(self):
with mock.patch('asteroids.get_data') as GetDataMock:
GetDataMock.return_value = ['asteroid_1', 'asteroid_2']
self.assertEqual(['1', '2'], save_data())
输出将是:
AssertionError: Lists differ: ['1', '2'] != ['asteroid_1', 'asteroid_2']
First differing element 0:
'1'
'asteroid_1'
- ['1', '2']
+ ['asteroid_1', 'asteroid_2']
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (failures=1)
在我们的单元测试中,我们替换了(使用mock)asteroids.get_data函数返回的值,并期望它们被转换为(ETL)['asteroid_1', 'asteroid_2'],而我们的 ETL 函数未能做到这一点。单元测试失败了。
单元测试是非常强大的。
它帮助我们处理在 ETL 管道中部署新功能时的人为错误。更多高级示例可以在我之前的故事中找到。我在 CI/CD 管道中非常频繁地使用它 [6]:
初学者指南:为什么单元测试和集成测试对你的数据平台如此重要
[towardsdatascience.com
监控内存使用情况
我经常使用无服务器部署 ETL 微服务。这是一种非常整洁且具有成本效益的工具。我部署 Lambdas 和 Cloud Functions,不希望它们因内存过多而被过度配置。
我之前在这里写过:
使用这些模板像专业人士一样部署数据管道
确实,我们为什么要给我们的 Lambda 配置 3Gb 的内存并支付更多费用,而数据可以在 256Mb 的内存中处理?
有多种方法可以监控我们的 ETL 应用程序内存使用情况。其中一种最受欢迎的方法是tracemalloc [7]库。
它可以跟踪 Python 内存块,并以(
# asteroids.py
import requests
import json
import tracemalloc
tracemalloc.start()
API_KEY="fsMlsu69Y7KdMNB4P2m9sqIpw5TGuF9IuYkhURzW"
ASTEROIDS_API_URL="https://api.nasa.gov/neo/rest/v1/feed"
@profile
def get_data():
print('Fetching data from NASA Asteroids API...')
session = requests.Session()
url=ASTEROIDS_API_URL
apiKey=API_KEY
requestParams = {
'api_key': apiKey,
'start_date': '2023-04-20',
'end_date': '2023-04-27'
}
response = requests.get(url, params = requestParams).text
with open('out.csv', 'w') as fd:
fd.write(response)
get_data()
print(tracemalloc.get_traced_memory())
tracemalloc.stop()
输出将是:
Fetching data from NASA Asteroids API...
(85629, 477039)
我们可以看到峰值使用量约为 540Kb。
让我们看看如何通过使用stream来进行简单的优化:
# asteroids_stream.py
import requests
import json
import tracemalloc
tracemalloc.start()
API_KEY="fsMlsu69Y7KdMNB4P2m9sqIpw5TGuF9IuYkhURzW"
ASTEROIDS_API_URL="https://api.nasa.gov/neo/rest/v1/feed"
def get_data():
print('Fetching data from NASA Asteroids API...')
session = requests.Session()
url=ASTEROIDS_API_URL
apiKey=API_KEY
requestParams = {
'api_key': apiKey,
'start_date': '2023-04-20',
'end_date': '2023-04-27'
}
response = session.get(url, params = requestParams, stream = True)
print('Saving to disk...')
with open('out.csv', 'wb') as fd:
for chunk in response.iter_content(chunk_size=1024):
fd.write(chunk)
get_data()
print(tracemalloc.get_traced_memory())
tracemalloc.stop()
# asteroids_stream.py
Fetching data from NASA Asteroids API...
Saving to disk...
(85456, 215260)
我们可以看到峰值内存使用量减少了一半。
使用 SDK
作为数据工程师,我们通常需要与云服务提供商频繁合作。简而言之,SDK 是一组服务库,允许以编程方式访问云服务。我们希望学习并掌握市场领导者如 Amazon、Azure 或 Google 的一两个 SDK。
我经常以编程方式访问的服务之一是 Cloud Storage。实际上,在数据工程中,几乎每个数据管道都依赖于云中的数据存储,即 Google Cloud Storage 或 AWS S3。
最常见的数据管道设计是围绕数据存储桶创建的。我在之前的故事中描述了这一模式 [9]。
选择合适的架构及示例
[towardsdatascience.com
在云存储中创建的对象可以触发其他 ETL 服务。这在使用这些事件编排数据管道时变得非常有用。
在这种情况下,我们希望能够在用作数据平台的数据湖的云存储中读取和写入数据。

典型的数据管道。作者提供的图像
在这个图示中,我们可以看到我们首先将数据提取并保存到数据湖存储桶中。然后,它将触发数据仓库的数据摄取,并将数据加载到我们的表中,以便使用商业智能(BI)工具进行 OLAP 分析。
下面的代码片段解释了如何使用 AWS SDK 以流的形式保存数据。
# nasa.py
import boto3
import requests
import os
S3_DATA = os.environ['S3_DATA_BUCKET'] #"your.datalake.bucket"
API_KEY="fsMlsu69Y7KdMNB4P2m9sqIpw5TGuF9IuYkhURzW"
ASTEROIDS_API_URL="https://api.nasa.gov/neo/rest/v1/feed"
print('Fetching data from NASA Asteroids API...')
session = requests.Session()
url=ASTEROIDS_API_URL
apiKey=API_KEY
requestParams = {
'api_key': apiKey,
'start_date': '2023-04-20',
'end_date': '2023-04-21'
}
response = session.get(url, params = requestParams, stream=True)
print(response.status_code)
# Perform Multi-part upload to AWS S3 datalake:
s3_bucket = S3_DATA # i.e. 'data.staging.aws'
s3_file_path = 'nasa/test_nasa_.csv' # i.e. "path_in_s3"
s3 = boto3.client('s3')
print('Saving to S3\. Run to download: aws s3 cp s3://{}/{} ./'.format(s3_bucket,s3_file_path))
with response as part:
part.raw.decode_content = True
conf = boto3.s3.transfer.TransferConfig(multipart_threshold=10000, max_concurrency=4)
s3.upload_fileobj(part.raw, s3_bucket, s3_file_path, Config=conf)
在你的命令行中运行以下命令以从 NASA API 提取小行星数据:
S3_DATA_BUCKET="your.staging.databucket" python nasa.py
# Output:
# Fetching data from NASA Asteroids API...
# 200
# Saving to S3\. Run to download: aws s3 cp s3://your.staging.databucket/nasa/test_nasa_.csv ./
结论
这个故事总结了我在 ETL 服务中几乎每天使用的 Python 代码技术。我希望你也能发现它有用。它有助于保持代码的整洁,并高效地执行数据管道转换。无服务器应用模型是一个非常具有成本效益的框架,我们可以在其中部署几乎不花费任何费用的 ETL 微服务。我们只需要优化内存使用,并以原子方式部署它们,以便它们运行得更快。它几乎可以处理我们数据平台中的任何类型的数据管道。在我之前的故事中可以找到这些架构类型和设计模式的良好总结。
它在多大程度上满足你的业务需求?选择的困境。
[towardsdatascience.com
理解基本的 HTTP 方法在数据工程中至关重要,它有助于为我们的数据管道创建稳健的 API 交互。使用joblib对我们的函数和模型进行管道化可以编写快速高效的代码。通过流的方式从 API 拉取数据,并以内存高效的方式运行 ETL 任务,可以防止资源过度分配,并确保我们的数据服务不会耗尽内存。可以使用 CI/CD 工具持续运行单元测试,这有助于在我们的代码更改达到生产环境之前,及早发现错误和人为失误。希望你喜欢阅读这篇文章。
推荐阅读:
[1] stackoverflow.com/questions/519633/lazy-method-for-reading-big-file-in-python
[2] docs.python.org/3/library/functions.html#property
[5] joblib.readthedocs.io/en/stable/persistence.html#persistence
[6] medium.com/towards-data-science/test-data-pipelines-the-fun-and-easy-way-d0f974a93a59
[7] docs.python.org/3/library/tracemalloc.html
[8] levelup.gitconnected.com/infrastructure-as-code-for-beginners-a4e36c805316
[9] medium.com/towards-data-science/data-pipeline-design-patterns-100afa4b93e3
Python getattr() 函数解释
原文:
towardsdatascience.com/python-getattr-function-explained-pyshark-cc7f49c59b2e
在本文中,我们将探讨如何使用 Python getattr() 函数。
·发表于 Towards Data Science ·4 分钟阅读·2023 年 3 月 20 日
--

目录
-
介绍
-
使用 getattr() 动态访问对象的属性
-
使用 getattr() 构建动态 API
-
使用 getattr() 动态加载模块
-
结论
介绍
Python getattr() 函数是一个内置函数,允许动态访问对象的属性。具体来说,它用于检索 Python 对象的名称属性。
Python getattr() 函数的语法是:
getattr(object, name[, default])
其中:
-
object — 我们希望从中检索属性的 Python 对象
-
name — Python 对象中命名属性的名称
-
default — 可选参数,用于指定如果未找到指定属性时的返回值。如果未指定,代码将返回AttributeError。
getattr() 函数在调用时,会搜索指定的 Python 对象中的名称属性并返回其值。
在接下来的章节中,我们将探讨一些 getattr() 函数的常见使用案例。
使用 getattr() 动态访问对象的属性
Python getattr() 函数最流行的使用案例之一是动态访问对象的属性。
让我们开始创建一个新的 Python 对象 Car,它有三个属性(make、model、price):
class Car:
def __init__(self, make, model, price):
self.make = make
self.model = model
self.price = price
接下来,我们将创建一个带有一些示例值的该类实例:
car = Car('Audi', 'Q7', 100000)
现在我们可以使用 getattr() 函数动态访问这个类的属性。
例如,假设我们想要检索刚刚创建的 car 对象的 price 属性:
attr_name = 'price'
attr_value = getattr(car, attr_name)
print(attr_value)
你应该得到:
100000
如果你尝试检索对象没有的属性,你会看到 AttributeError。
例如,这个对象没有属性colour,所以让我们看看当我们尝试检索它时会发生什么:
attr_name = 'colour'
attr_value = getattr(car, attr_name)
print(attr_value)
你应该得到:
AttributeError: 'Car' object has no attribute 'colour'
如果你正在处理多个类,而不知道它们是否具有你正在寻找的属性,这种方法非常有用,它可以节省大量时间和代码量,快速运行这些测试以检索属性值。
使用 getattr() 构建动态 API
Python getattr() 函数的另一个用例是构建 Python 中的动态 API。
让我们开始创建一个简单的 Calculator 类,包含几个执行数学计算的方法:
class Calculator:
def add(self, x, y):
return x + y
def subtract(self, x, y):
return x - y
现在我们可以围绕这个 Calculator 类构建一个 API,它将允许动态调用任何方法(使用 Python getattr() 函数):
class CalculatorAPI:
def __init__(self, calculator):
self.calculator = calculator
def call_method(self, method_name, *args):
method = getattr(self.calculator, method_name, None)
if method:
return method(*args)
else:
return f"Method '{method_name}' not found"
一旦 API 构建完成,我们可以用不同的计算,如加法和减法来测试它,并检查结果:
calculator = Calculator()
api = CalculatorAPI(calculator)
print(api.call_method("add", 7, 8))
print(api.call_method("subtract", 9, 1))
你应该得到:
15
8
在这个例子中,我们使用 Python getattr() 函数动态访问 Python 类的所需方法。
使用 getattr() 动态加载模块
Python getattr() 函数的另一个用例是在运行时动态加载模块。
在这个例子中,我们将使用一个内置的 Python 模块,这实际上是 import 语句的实现。具体来说,我们将使用 import_module() 函数进行编程导入。
我们将使用 getattr() 函数来访问加载模块中的特定函数。
假设我们想构建一个小程序,询问用户要导入哪个模块、要访问该模块的哪个函数以及要执行什么操作。
例如,我们想导入数学模块,访问 sqrt() 函数并找到 25 的平方根。
我们将以编程方式加载模块和函数,并执行计算:
#Import the required dependency
import importlib
#Define module name
module_name = 'math'
#Programmatically load module
module = importlib.import_module(module_name)
#Define function name
function_name = 'sqrt'
#Programmatically load function
function = getattr(module, function_name)
#Define input for the function
num = 25
#Calculate the result
result = function(num)
#Print the result
print(f"Result: {result}")
你应该得到:
5.0
虽然这是一个非常简单的例子,看起来不像是 sqrt() 函数的有用应用,但它说明了动态加载模块和函数的一般思路。
结论
在这篇文章中,我们探讨了 Python getattr() 函数。
现在你已经了解了基本功能,你可以在项目中练习使用它,以向代码中添加更多功能。
如果你有任何问题或建议,请随时在下面留言,查看更多我的 Python Functions 教程。
最初发布于 https://pyshark.com 2023 年 3 月 20 日。
Python help() 函数解释
原文:
towardsdatascience.com/python-help-function-explained-fba9c15f42b1
在这篇文章中,我们将探讨如何使用 Python help() 函数
·发布于 Towards Data Science ·4 分钟阅读·2023 年 1 月 13 日
--

图片由 Toa Heftiba 提供,来源于 Unsplash
目录
-
介绍
-
使用交互式帮助工具访问文档
-
使用 help() 访问对象文档
-
使用 help() 访问用户定义的函数文档
-
结论
介绍
在 Python 中,我们经常使用新的模块、函数、类或对象,这些模块、函数、类或对象我们以前没有使用过,且这些文档我们还没有阅读过。
我们可以使用 Python help() 函数来更快地获取这些信息,而不是浏览文档网站寻找特定的函数或类。
Python help() 函数用于显示指定模块、函数、类或对象的文档。
help() 函数的处理定义如下:
help([object]) -> display documentation
使用交互式帮助工具访问文档
你可以在不带任何参数的情况下调用 Python help() 函数,它会启动一个交互提示符,你可以利用它来查找任何 Python 对象的文档。
让我们启动交互式帮助工具:
#Start help utility
help()
你应该会看到一个帮助工具在终端中启动:
Welcome to Python 3.7's help utility!
If this is your first time using Python, you should definitely check out
the tutorial on the Internet at https://docs.python.org/3.7/tutorial/.
Enter the name of any module, keyword, or topic to get help on writing
Python programs and using Python modules. To quit this help utility and
return to the interpreter, just type "quit".
To get a list of available modules, keywords, symbols, or topics, type
"modules", "keywords", "symbols", or "topics". Each module also comes
with a one-line summary of what it does; to list the modules whose name
or summary contain a given string such as "spam", type "modules spam".
help>
一旦帮助工具启动,我们可以利用它查找 Python 对象的文档。
例如,让我们尝试在帮助工具中运行 map 查找 Python map() 函数 的文档:
help> map
你应该会得到函数文档:
Help on class map in module builtins:
class map(object)
| map(func, *iterables) --> map object
|
| Make an iterator that computes the function using arguments from
| each of the iterables. Stops when the shortest iterable is exhausted.
|
| Methods defined here:
|
| __getattribute__(self, name, /)
| Return getattr(self, name).
|
| __iter__(self, /)
| Implement iter(self).
|
| __next__(self, /)
| Implement next(self).
|
| __reduce__(...)
| Return state information for pickling.
|
| ----------------------------------------------------------------------
| Static methods defined here:
|
| __new__(*args, **kwargs) from builtins.type
| Create and return a new object. See help(type) for accurate signature.
如你所见,文档包含函数描述、方法和文档字符串。
使用 help() 访问对象文档
你可以在不使用交互式帮助工具的情况下,一步访问 Python 对象文档。
只需以以下格式运行 Python help() 函数,并将 Python 对象作为参数传递进去:
help([object])
让我们尝试使用这种方法访问Python map() 函数的文档:
#Find documentation of Python map() function
help(map)
并且你应该会得到:
Help on class map in module builtins:
class map(object)
| map(func, *iterables) --> map object
|
| Make an iterator that computes the function using arguments from
| each of the iterables. Stops when the shortest iterable is exhausted.
|
| Methods defined here:
|
| __getattribute__(self, name, /)
| Return getattr(self, name).
|
| __iter__(self, /)
| Implement iter(self).
|
| __next__(self, /)
| Implement next(self).
|
| __reduce__(...)
| Return state information for pickling.
|
| ----------------------------------------------------------------------
| Static methods defined here:
|
| __new__(*args, **kwargs) from builtins.type
| Create and return a new object. See help(type) for accurate signature.
如你所见,显示的文档与我们使用交互式帮助工具找到的文档是一样的。
使用 help() 访问用户定义函数的文档
Python help() 函数也可以显示用户定义函数的信息。
在之前的示例中,我们访问了 Python 内置函数的文档,现在让我们创建一个自己的函数,并写一个简短的描述,然后尝试访问它的文档。
首先,创建一个空的 main.py 文件,然后创建一个简单的函数,该函数将两个数字相加并返回它们的和:
#Define a function
def add(x, y):
'''
This function adds two given integer arguments
Parameters:
x : integer
y : integer
Output:
val: integer
'''
val = x + y
return val
现在我们已经定义了函数,在同一个 Python 文件中,我们可以调用 help() 函数,并将函数名称(add)作为参数传递:
#Define a function
def add(x, y):
'''
This function adds two given integer arguments
Parameters:
x : integer
y : integer
Output:
val: integer
'''
val = x + y
return val
#Find documentation of user defined function add()
help(add)
并且你应该会得到:
Help on function add in module __main__:
add(x, y)
This function adds two given integer arguments
Parameters:
x : integer
y : integer
Output:
val: integer
它显示了存储在 docstring 中的函数文档,包括其描述、输入参数和返回值。
结论
在本文中,我们探讨了如何使用 Python help() 函数,包括交互式帮助工具,访问内置函数以及用户定义函数的文档。
如果你有任何问题或有修改建议,请随时在下面留言,并查看更多我的Python 函数教程。
最初发布于 https://pyshark.com 2023 年 1 月 13 日。
Python 继承:你应该继承自 dict 还是 UserDict?
原文:
towardsdatascience.com/python-inheritance-should-you-inherit-from-dict-or-userdict-9b4450830cbb
PYTHON PROGRAMMING
他们说你不应该继承 dict 而应该继承 UserDict。这是真的吗?
·发布于 Towards Data Science ·15 分钟阅读·2023 年 5 月 10 日
--

字典是 Python 基本数据类型之一。照片由 Waldemar 提供,Unsplash
继承自 dict 通常不是最佳选择——不仅因为他们这么说,还因为重载的方法不会工作。相反,你应该继承 collections.UserDict。但如果你不想重载 dict 方法,只是想添加新的方法呢?在本文中,我们将讨论何时以及如何继承 dict 和 collections.UserDict 类。
在他那本精彩的书籍 Fluent Python. 2nd ed. 中,Luciano Ramalho 解释了为什么你不应该创建继承自 dict 的自定义类。这条规则的理由,一开始看起来很奇怪,但其实简单而关键:dict 是一个高度优化的类型,由 C 实现,它不会调用你在 dict 子类中重载的方法。
这将是一个令人讨厌的惊喜,不是吗?让我们来看一个例子。假设你想创建一个类似字典的类,其中提供的值将被转换为它们的字符串表示。让我们尝试通过继承 dict 内置类型来做到这一点:
class StringDict(dict):
def __setitem__(self, key, value):
super().__setitem__(key, str(value))
这看起来像是完全有效的 Python 代码。让我们看看这怎么运作:
>>> class StringDict(dict):
... def __setitem__(self, key, value):
... super().__setitem__(key, str(value))
...
>>> mydict = StringDict(first=1, second=2, third=3)
>>> mydict
{'first': 1, 'second': 2, 'third': 3}
嗯,这根本不起作用——或者说,这个__setitem__方法根本不起作用。我们想将值转换为字符串,但它们没有被转换。不过,我们没有看到任何错误;这个类本身以某种方式工作——实际上,它的工作方式就像一个普通的字典一样。(或者说,它提供了相同的结果但更慢;我们稍后会讨论这个问题。)
为了达到你想要的效果,你应该继承 UserDict:
>>> from collections import UserDict
>>> class StringUserDict(UserDict):
... def __setitem__(self, key, value):
... super().__setitem__(key, str(value))
...
>>> mydict = StringUserDict(first=1, second=2, third=3)
>>> mydict
{'first': '1', 'second': '2', 'third': '3'}
正如你所见,我们在定义中唯一改变的就是继承 UserDict 而不是 dict。
所以现在你知道了。使用 UserDict 就足够了。太好了。
会吗?
结果
等等。我们来考虑一下。在决定使用 UserDict 而不是 dict 是否如此出色之前,我们应该考虑一些事情。
首先,我们知道 Python 的内建类型是高度优化的,因为它们是用 C 实现的,而这种实现本身也经过了高度优化。
其次,我们知道我们不应该继承 dict,因为它的 C 实现方法不会调用用 Python 实现的重写方法。
第三,简单的一点需要检查,collections.UserDict 是用 Python 实现的。在 Linux 中,你可以在这里找到它的定义:

UserDict 定义的本地化在 Linux 中。来自 VS Code 的截图。图片由作者提供
在这种情况下,以下问题自然会出现:如果是这样,我自定义的继承 UserDict 的类会有良好的性能吗?
我立即猜测不会。dict 的优化来自 C 实现,而 UserDict 是用 Python 实现的。它为什么应该有所优化呢?我们将在接下来的部分中检查这一点。
UserDict 与 dict 的基准测试
对于基准测试,我们将使用标准库中的 timeit 模块。你可以在这里了解更多信息:
最流行的 Python 代码时间基准测试工具,内建的 timeit 模块提供了比大多数工具更多的功能…
[towardsdatascience.com
为了使基准测试稍微简单和结构化一点,让我们定义一个简单的函数来对两个或更多代码片段进行时间基准测试:
import rounder
import timeit
import pprint
def compare(
__snippet1,
__snippet2,
*args,
number=10_000_000,
repeat=7,
setup="from collections import UserDict"):
snippets = [__snippet1, __snippet2, *args]
results = {}
for i, snippet in enumerate(snippets):
name = snippet if len(snippet) < 30 else f"snippet {i + 1}"
results[name] = min(timeit.repeat(
snippet, number=number, repeat=repeat, setup=setup
)) / number,
results = rounder.signif_object(results, digits=4)
pprint.pprint(results)
几件事:
- 这个函数使用了
[rounder](https://pypi.org/project/rounder/)包,将字典中的所有数字四舍五入到四位有效数字;你可以在这里了解更多信息:
## rounder:在复杂的 Python 对象中四舍五入数字
rounder 包允许你通过一个命令将任何对象中的所有数字进行四舍五入。
[towardsdatascience.com
-
__snippet1和__snippet2是仅限位置的参数,因此你不能通过名称调用它们。这要归功于双下划线前缀。 -
多亏了两个片段参数后的
*args,你可以提供更多的片段,也可以作为位置关键字;你可以根据需要使用任意多个。 -
所有剩余的参数都是仅限关键字的。在这里,这是通过将它们放在
*args之后实现的。¹ -
这个函数报告的结果是七次运行中最快的一次的平均值。因此,所有结果都是直接可比的,即使
timeit.repeat()函数使用了不同的number值。 -
这个函数隐式返回
None并打印基准测试的简短报告,使用标准库pprint模块中的pprint()函数。通常,避免将返回语句替换为打印²,除非你的函数/方法是一个打印的函数。
好的,我们马上会看到这个函数的实际效果。首先,让我们比较一下dict()和UserDict()创建实例的速度。然而,我们可以通过两种方法实例化一个常规字典,即dict()和(显著更快的){},所以我们会同时考虑这两者:
>>> compare("UserDict()", "dict()", "{}")
{'UserDict()': (1.278e-07,), 'dict()': (3.826e-08,), '{}': (1.518e-08,)}
在本文的所有基准测试中,我使用了 Python 3.11,在 Windows 10 机器上,WSL 1 环境中,32 GB 内存和四个物理(八个逻辑)核心。基准测试显示,创建一个新实例时,UserDict的速度是dict的两倍慢。
如上所述,我们在结果字典中看到的值代表了创建一个UserDict或常规dict(通过两种方法创建)的时间。显然,创建一个UserDict实例需要更多时间,大约1.3e-07秒——而{}需要大约1.5e-08秒。差异不大?注意当你需要创建一个单一实例时,但想象一下创建数百万个字典。因此,创建一个常规字典所需的时间大约是创建UserDict的3–8 倍,具体取决于实例化方法。
让我们看看较大字典的情况。我们将通过字典推导式创建一个简单的数值字典。由于UserDict不允许使用 dictcomp 语法(另一个缺点),我们唯一能做的就是先使用 dictcomp 语法创建一个常规字典,然后将其转换为UserDict实例:
>>> compare(
"UserDict({i: i**2 for i in range(1000)})",
"{i: i**2 for i in range(1000)}",
number=100_000)
{'snippet 1': (0.0001316,), 'snippet 2': (5.027e-05,)}
一个常规字典快了大约 2.5 倍。考虑到创建一个空字典的速度甚至更快,这似乎相当惊人。我们必须记住,这些基准测试的结果可能会有所不同。但我们也必须记住,当我们使用许多重复测试(这里是十万次——我们可以使用更多)时,结果的差异应该相对较小。
当我们比较查找时间时,大小会影响结果吗?基本上,键查找与字典大小无关,因此,字典大小的不同应该不会影响结果。
首先,一个小字典:
>>> setup = """from collections import UserDict
... d = {'x': 1, 'y': 2, 'z': 3}
... ud = UserDict(d)
... """
>>> compare("ud['x']", "d['x']", setup=setup)
{"ud['x']": (4.754e-08,), "d['x']": (1.381e-08,)}
好的,所以慢了大约 3.5 倍。现在,对于一个更大的10_000键值对的字典:
>>> setup = """from collections import UserDict
... d = {str(i): i for i in range(10_000)}
... ud = UserDict(d)
... """
>>> compare("ud['9999']", "d['9999']", setup=setup, number=1_000_000)
{"ud['9999']": (7.785e-08,), "d['9999']": (2.787e-08,)}
对于 1000 万个元素的情况:
>>> compare("ud['9999']", "d['9999']", setup=setup, number=100_000)
{"ud['9999']": (6.662e-08,), "d['9999']": (2.499e-08,)}
因此,大小确实不重要,每次dict都快了大约 3–3.5 倍。让我们看看,这次仅针对中等大小的字典,如何处理不存在的键:
>>> compare(
... "ud.get('a', None)",
... "d.get('a', None)",
... setup=setup,
... number=1_000_000)
{"d.get('a', None)": (4.318e-08,), "ud.get('a', None)": (4.525e-07,)}
这次差距更大,dict的速度超过了10倍。
检查一个键是否在字典中呢?
>>> compare("'a' in ud", "'a' in d", setup=setup, number=1_000_000)
{"'a' in d": (1.465e-08,), "'a' in ud": (4.562e-08,)}
所以,再次是 3–3.5 倍快。
现在,让我们基准测试一个频繁操作,即遍历字典;再次检查不同大小的字典:
>>> setup = """from collections import UserDict
... d = {str(i): i for i in range(10)}
... ud = UserDict(d)
... """
>>> compare(
... "for i, v in ud.items(): pass",
... "for i, v in d.items(): pass",
... setup=setup,
... number=1_000_000
... )
{'for i, v in d.items(): pass': (1.726e-07,),
'for i, v in ud.items(): pass': (1.235e-06,)}
>>> setup = """from collections import UserDict
... d = {str(i): i for i in range(10_000)}
... ud = UserDict(d)
... """
>>> compare(
... "for i, v in ud.items(): pass",
... "for i, v in d.items(): pass",
... setup=setup,
... number=10_000
... )
{'for i, v in d.items(): pass': (0.0001255,),
'for i, v in ud.items(): pass': (0.00112,)}
>>> setup = """from collections import UserDict
... d = {str(i): i for i in range(100_000)}
... ud = UserDict(d)
... """
>>> compare(
... "for i, v in ud.items(): pass",
... "for i, v in d.items(): pass",
... setup=setup,
... number=10_000
... )
{'for i, v in d.items(): pass': (0.001772,),
'for i, v in ud.items(): pass': (0.01718,)}
好的,对于小型字典来说,dict在遍历其键值对(通过.items()方法提供)时大约快 7 倍。对于中型字典(在我们的实验中有 1 万元素),快约 9 倍。对于更大的字典(有100_000个元素),结果类似,因此一旦开始循环,循环本身似乎并不依赖于字典的类型。
由于这只是一个相当小的基准测试,我们可以得出结论:常规字典在遍历其项目时应该比UserDict快约 5–10 倍。
结论基准测试
也许我们在这里停下来吧。我们可以进行更多基准测试,但这不是重点。我不想进行dict和UserDict在执行时间上的全面比较;如果你感兴趣,可以尝试代码进行一系列可靠的基准测试。相反,我想阐明这个问题,并检查是否像我基于UserDict和dict的实现知识所预期的那样,前者明显比后者慢。
而且——除非你认为 5–10 倍更慢是一个微不足道的数字。所以,如果你能的话,考虑使用常规字典,而不是那些继承UserDict的字典,除非你必须改变dict的行为。
啊……为什么我们不能直接继承dict?!为什么?
或者……我们可以吗?
不要继承dict?那为什么不呢?!
也许你已经注意到,不继承dict的规则与用 C 实现的dict方法有关,这些方法不会调用在 Python 中重载的内置dict方法。但如果你只是想给dict添加一些功能,而不触动已经用 C 实现的方法呢?
这是一个非常好的问题。答案简短而简单:是的,你可以这样做!你可以从dict继承;只需不要重载dict的方法,仅此而已。
问题是,基于dict的类会像dict一样高效吗?或者说像collections.UserDict一样吗?为了回答这个问题,我们需要运行更多的基准测试。
让我们想象我们在一个字典中保存一些数据,我们想要添加一个.summarize()方法来计算数据的一些摘要统计信息。它可能像这样(仅作为示例):
from collections.abc import Sequence
from typing import Callable
def try_calculate(func: Callable, *args, **kwargs):
"""Try calculations; when data are incorrect, return nan."""
try:
return func(*args, **kwargs)
except TypeError:
return float("nan")
class RichDict(dict):
measures = {
"sum": sum,
"n": len,
"mean": lambda x: sum(x) / len(x),
}
def summarize(self):
statistics = {}
for k, v in self.items():
if isinstance(v, str):
statistics[k] = {"n": len(v)}
elif isinstance(v, Sequence):
statistics[k] = {
name: try_calculate(func, v)
for name, func
in self.measures.items()
}
return statistics
RichDict是一个dict,多了一个方法:.summarize()。这个方法执行以下操作:
-
它遍历数据的键值对(通过
.items方法获取)。 -
当值是字符串时,
statistics仅包含长度,并作为一个包含一个键n的字典返回。 -
当值是
Sequence时,计算主要的摘要统计信息。度量以可调用的形式保存在类属性RichDict.measures中,它是一个字典。 -
该方法保护计算:如果无法计算一个度量,则会捕获异常并返回
float("nan")(表示不是数字)作为计算结果。这样,例如,Python 在尝试计算空列表的均值时不会抛出错误。
如果你想添加一个度量,可以轻松做到:
RichDict.measures["min"] = min
RichDict.measures["max"] = max
如果函数更复杂,你可以使用lambda函数:
RichDict.measures["max-to-mean"] = lambda x: max(x) / min(x)
或者,更好地,首先定义一个函数,然后在这里分配它:
def max_to_min(x: float) -> float:
return max(x) / min(x)
RichDict.measures["max-to-mean"] = max_to_min
请注意,由于.measures是一个类属性,所有的RichDict实例(包括即将创建的和已经存在的)都将具有扩展的度量,包括min和max统计信息。
这是RichDict在实际应用中的一个例子:
>>> d = RichDict(x=[1,4,5,7],
... y=[1,"1",2],
... z="Shout Bamalama!",
... f=10)
>>>
>>> stats = d.summarize()
>>> stats # doctest: NORMALIZE_WHITESPACE
{'x': {'sum': 17, 'n': 4, 'mean': 4.25, 'min': 1, 'max': 7, 'max-to-min': 7},
'y': {'sum': nan, 'n': 3, 'mean': nan, 'min': nan, 'max': nan, 'max-to-min': nan},
'z': {'n': 15}}

上面,RichDict类有一个类属性,包含用于序列数据的度量;对于字符串,.summarize() 方法只计算一个度量。更新类,使其具有两个类属性measures_seq和measures_str,其设计方式与上面的measures相同。对于字符串,.summarize() 方法应按序列的方式计算度量,即使用measures_str。
你可以在附录 1 中找到解决方案。

在代码中,我使用了标准库中的doctests模块进行文档测试。如果你有兴趣了解更多关于这个有趣模块的内容,可以从这篇文章中了解:
## 使用 doctest 进行 Python 文档测试:简单方法
doctest 允许进行文档、单元和集成测试以及测试驱动开发。
towardsdatascience.com
好了,现在我们知道RichDict有效,我们知道我们可以子类化dict。我们现在想要了解的是RichDict增加的功能(用 Python 定义,而不是 C 语言,就像dict的基础代码一样)是否会为dict的常规行为增加一些开销。为此,我们将基准测试此类行为,例如创建一个新的RichDict与创建一个新的dict、键查找等。
让我们进行类似于上面为UserDict进行的基准测试。你可以在这个 GitHib gist中找到相关代码。你会在那里找到下面使用的setup值。
>>> compare("UserDict()", "RichDict()", "dict()", setup=setup)
{'UserDict()': (2.236e-07,), 'RichDict()': (1.073e-07,), 'dict()': (5.892e-08,)}
如上所示,当创建一个空实例时,RichDict的速度明显比UserDict快(约快 2 倍),但比dict慢(约慢 2 倍)。
>>> compare(
... "UserDict({i: i**2 for i in range(1000)})",
... "RichDict({i: i**2 for i in range(1000)})",
... "{i: i**2 for i in range(1000)}",
... number=100_000,
... setup=setup)
{'snippet 1': (0.0001765,), # UserDict
'snippet 2': (6.845e-05,), # RichDict
'snippet 3': (5.388e-05,)} # dict
这一次,RichDict比UserDict快约 2.5 倍,但比dict稍慢(约慢 1.3 倍)。
下面,你将找到更多基准测试的示例,为方便起见,示例之间以空行分隔:
>>> setup += """d = {'x': 1, 'y': 2, 'z': 3}
... ud = UserDict(d)
... rd = RichDict(d)
... """
>>> compare("ud['x']", "rd['x']", "d['x']", setup=setup)
{"ud['x']": (5.111e-08,), rd['x']": (3.024e-08,), "d['x']": (1.475e-08,)}
>>> compare(
... "'a' in ud",
... "'a' in rd",
... "'a' in d",
... setup=setup,
... number=1_000_000)
{"'a' in d": (1.366e-08,), # dict
"'a' in rd": (2.228e-08,), # RichDict
"'a' in ud": (4.436e-08,)} # UserDict
>>> compare(
... "ud.get('a', None)",
... "rd.get('a', None)",
... "d.get('a', None)",
... setup=setup,
... number=1_000_000)
{"d.get('a', None)": (1.935e-08,), # dict
"rd.get('a', None)": (3.016e-08,), # RichDict
"ud.get('a', None)": (5.125e-07,)} # UserDict
>>> compare(
... "for i, v in ud.items(): pass",
... "for i, v in rd.items(): pass",
... "for i, v in d.items(): pass",
... setup=setup,
... number=1_000
... )
{'for i, v in d.items(): pass': (0.001783,),
'for i, v in rd.items(): pass': (0.001743,),
'for i, v in ud.items(): pass': (0.01627,)}
数字本身说明了我们需要的内容,所以我暂时将它们留给你。
总结基准测试结果
RichDict通常比dict慢(尽管有时只是很少),但比UserDict快。
因此,如果你只是想给dict添加一些功能,而不覆盖其内置方法,你绝对 可以子类化 dict。我会说这应该是你首选的方法,而不是子类化collections.UserDict,因为后者明显更慢。请记住,我们讨论的是当你不需要改变字典的常规行为,只是添加一些新行为的情况。
还要记住,使用内置类型的方式会有一个代价:你的类(在我们的例子中是RichDict)会比dict更慢。不过,它仍然比UserDict快,而UserDict的创建目的就是为了让你继承……嗯,不是从dict继承,而是让你创建一个具有dict相同行为的新类型(类)。不幸的是,使用UserDict是相当昂贵的,因为它的性能比dict差得多。
结论
让我们总结一下关于子类化dict和UserDict的讨论。我们了解到,我们有三种选择:
-
从
UserDict继承,当你想要覆盖dict的内置行为时。这将是最慢的选项。 -
从
dict继承,当你不想覆盖dict的内置行为,而是添加新功能(方法)时。这将比选项 1 快。 -
使用内置的
dict类型,而不创建自定义类。如果你需要自定义功能,你可以在接受dict实例作为参数的函数中实现它们。这是最快的选项(见下文)。
我们还没有讨论第三种选项,因为它不涉及子类化。不过,不需要讨论太多,因为这是最简单的方法,它使用的是一种更程序化的方法,而不是面向对象的方法。一方面,使用这种方法的summarize()函数比使用选项 2 中的RichDict.summarize()方法快一点(如果有的话)。这个要点包含了相应的基准测试代码;在我的机器上,它提供了一个小而稳定(从运行到运行)性能提升。另一方面,我们知道,常规dict的所有其他行为明显比RichDict要快。因此,一般来说,选项 3 提供了处理具有附加功能的字典的最快方法。
因此,如果性能很重要,最明智的选择似乎是第三种选项——即使用常规字典,并在外部函数中实现所需的附加行为。根据情况,这也可能是代码最清晰的选项,因为它不需要自定义数据结构,而是结合了字典(Python 中最常见的数据结构之一)和函数。通常,这意味着代码更清晰。
第二种选择意味着更差的性能,因为向dict添加方法会导致其行为的额外开销。如我们所知,选项 3 通过将方法移到字典之外来消除这种开销。
第一种选择在性能方面绝对是最差的。我认为只有在满足以下三个条件中的每一个时,这个选项才有意义:
-
性能不重要
-
你需要重写一个或多个内置的
dict方法。 -
由于创建一个将所有所需功能组合在一起的类,代码将更清晰、更易于使用。
脚注
¹ 我计划写一篇关于仅位置参数和仅关键字参数的专门文章。一旦发表,我会在这里链接。
² 顺便提一下,在交互式会话中,函数的返回会有相同的效果(当然,当结果未被赋值时)。不过,我是在脚本中运行基准测试的,而不是交互式会话中。
附录 1
练习的解决方案
你可以用各种方式来做。下面的解决方案避免了重复,但也使得向计算中添加另一种类型(到Sequence和str)变得容易。
from collections.abc import Sequence
from typing import Callable
class RichDict(dict):
measures_seq = {
"sum": sum,
"n": len,
"mean": lambda x: sum(x) / len(x),
}
measures_str = {
"n": len,
}
def summarize(self):
statistics = {}
for k, v in self.items():
if isinstance(v, str):
measures = self.measures_str
elif isinstance(v, Sequence):
measures = self.measures_seq
else:
continue
statistics[k] = {
name: try_calculate(func, v)
for name, func
in measures.items()
}
return statistics
NaN = float("nan")
def try_calculate(func: Callable, *args, **kwargs):
"""Try calculations and when the data are incorrect, return nan."""
try:
return func(*args, **kwargs)
except TypeError:
return NaN
感谢阅读。如果你喜欢这篇文章,你可能还会喜欢我写的其他文章;你可以在这里查看。如果你想加入 Medium,请使用我下面的推荐链接:
## 使用我的推荐链接加入 Medium - Marcin Kozak
阅读 Marcin Kozak 的每一个故事(以及 Medium 上的其他成千上万位作家的故事)。你的会员费直接支持…
Python:init 不是构造函数:深入探讨 Python 对象创建
使用 Python 的构造函数创建快速且内存高效的类
·发布于 Towards Data Science ·阅读时间 9 分钟·2023 年 11 月 27 日
--

Python 如何构建对象(图像由 ChatGPT 提供)
你知道__init__方法不是构造函数吗?但如果__init__不创建对象,那究竟是什么呢?对象在 Python 中是如何创建的?Python 甚至有构造函数吗?
本文的目标是更好地理解 Python 如何创建对象并操控这一过程以构建更好的应用程序。
首先,我们将深入了解 Python 如何创建对象。接下来,我们将应用这些知识,讨论一些有趣的用例,并提供一些实际示例。让我们开始编码吧!
1. 理论:在 Python 中创建对象
在这一部分,我们将弄清楚在你创建对象时 Python 背后发生了什么。在下一部分,我们将运用这些新知识进行第二部分的实践。
如何在 Python 中创建对象?
这应该很简单;你只需创建一个类的实例。或者,你可以创建一个新的内置类型,比如str或int。在下面的代码中,创建了一个基本类的实例。它只包含一个__init__函数和一个say_hello方法:
class SimpleObject:
greet_name:str
def __init__(self, name:str):
self.greet_name = name
def say_hello(self) -> None:
print(f"Hello {self.greet_name}!")
my_instance = SimpleObject(name="bob")
my_instance.say_hello()
注意__init__方法。它接收一个name参数,并将其值存储在SimpleObject实例的greet_name属性上。这允许我们的实例保持状态。
现在问题出现了:为了保存状态,我们需要有东西来保存状态。__init__ 从哪里得到对象?
那么,init 是构造函数吗?
答案是:从技术上讲,没有。构造函数实际上创建新对象;__init__方法仅负责设置对象的状态。它只是通过参数接收值,并将这些值分配给像greet_name这样的类属性。
在 Python 中,对象的实际创建发生在初始化之前。对于对象创建,Python 使用一个名为**__new__****的方法,该方法存在于每个对象上。
在 5 分钟内创建、构建和发布一个 Python 包
towardsdatascience.com
__new__ 做了什么?
__new__ 是一个类方法,意味着它是直接在类上调用的,而不是在类的实例上。它存在于每个对象上,并负责实际创建和返回对象。__new__ 的最重要的方面是它必须返回一个类的实例。我们将在本文后面进一步研究这个方法。
__new__ 方法来自哪里?
简短的回答是:Python 中的一切都是对象,object 类有一个 **__new__** 方法。你可以把这看作是“每个类都继承自 *object* 类”。
请注意,即使我们的 SimpleObject 类没有继承任何东西,我们仍然可以证明它是 object 的一个实例:
# SimpleObject is of type 'object'
my_instance = SimpleObject(name="bob")
print(isinstance(my_instance, object)) # <-- True
# but all other types as well:
print(isinstance(42, object)) # <-- True
print(isinstance('hello world', object)) # <-- True
print(isinstance({"my": "dict"}, object)) # <-- True
总结来说,一切都是对象,object 定义了 __new__ 方法,因此 Python 中的一切都有一个 __new__ 方法。
__new__ 和 __init__ 有何不同?
__new__ 方法用于实际创建对象:分配内存并返回新对象。一旦对象创建完成,我们可以用 __init__ 来初始化它;设置初始的状态。
## Python 的 args、kwargs 和传递参数的所有其他方式
精巧地设计你的函数参数的 6 个示例
towardsdatascience.com
Python 对象创建的过程是什么样的?
内部,下面的函数在你创建新对象时会被执行:
-
__new__:分配内存并返回新对象 -
__init__:初始化新创建的对象;设置状态
在下面的代码中,我们通过重写**__new__**来展示这一点。在下一部分我们将利用这一原则做一些有趣的事情:
class SimpleObject:
greet_name:str
def __new__(cls, *args, **kwargs): # <-- newly added function
print("__new__ method")
return super().__new__(cls)
def __init__(self, name:str):
print("__init__ method")
self.greet_name = name
def say_hello(self) -> None:
print(f"Hello {self.greet_name}!")
my_instance = SimpleObject(name="bob")
my_instance.say_hello()
(我们将在接下来的部分解释为什么和如何工作。)这将打印以下内容:
__new__ method
__init__ method
Hello bob!
这意味着我们可以访问初始化我们类的实例的函数!我们还看到 __new__ 先执行。在下一部分我们将了解 __new__ 的行为:super().__new__(cls) 是什么意思?
__new__ 是如何工作的?
__new__的默认行为如下所示。在这一部分,我们将尝试理解发生了什么,以便在下一部分的实际示例中对其进行调整。
class SimpleObject:
def __new__(cls, *args, **kwargs):
return super().__new__(cls)
请注意,__new__是在super()方法上调用的,它返回一个“引用”(实际上是一个代理对象)到SimpleObject的父类。请记住,SimpleObject继承自object,其中定义了__new__方法。
分解:
-
我们获得了我们所在类的基类的“引用”。以
SimpleObject为例,我们获得了object的“引用” -
我们在“引用”上调用
__new__,因此object.__new__ -
我们将
cls作为参数传递。这就是像
*__new__*这样的类方法的工作方式;它是对类本身的引用
综合起来:我们请求SimpleObject的父类创建一个SimpleObject的新实例。
这与my = object.__new__(SimpleObject)是一样的
那么我可以使用__new__创建一个新实例吗?
是的,请记住,默认的__new__实现实际上直接调用它:return super().**__new__**(cls)。因此,下面代码中的方法做了同样的事情:
# 1\. __new__ and __init__ are called internally
my_instance = SimpleObject(name='bob')
# 2\. __new__ and __init__ are called directly:
my_instance = SimpleObject.__new__(SimpleObject)
my_instance.__init__(name='bob')
my_instance.say_hello()
在直接方法中发生的事情:
-
我们在
SimpleObject上调用__new__函数,传递SimpleObject类型。 -
SimpleObject.__new__在其父类(object)上调用__new__ -
object.__new__创建并返回一个SimpleObject的实例 -
SimpleObject.__new__返回新实例 -
我们调用
__init__来初始化它。
这些事情在非直接方法中也会发生,但它们是在幕后处理的,所以我们没有注意到。
轻松在运行时计算文件路径
towardsdatascience.com
实际应用 1:子类化不可变类型
现在我们知道__new__是如何工作的,我们可以利用它做一些有趣的事情。我们将理论付诸实践,子类化一个不可变类型。这样,我们可以拥有自己的特殊类型,其方法定义在一个非常快速的内置类型上。
目标
我们有一个处理许多坐标的应用程序。因此,我们希望将坐标存储在元组中,因为它们很小且内存高效。
我们将创建自己的Point类,继承自tuple。这样,Point是一个tuple,因此它非常快速且小巧,并且我们可以添加如下功能:
-
对对象创建的控制(例如,只在所有坐标都是正数时创建新对象)
-
额外的方法,例如计算两个坐标之间的距离。
cython-for-absolute-beginners-30x-faster-code-in-two-simple-steps-bbb6c10d06ad?source=post_page-----9134d971e334-------------------------------- ## Cython 的绝对初学者指南:两步实现代码 30 倍加速
为闪电般快速的应用程序提供简单的 Python 代码编译
[towardsdatascience.com
带有 new 重写的 Point 类
在第一次尝试中,我们仅创建一个继承自元组的Point类,并尝试使用x, y坐标初始化元组。这不会成功:
class Point(tuple):
x: float
y: float
def __init__(self, x:float, y:float):
self.x = x
self.y = y
p = Point(1,2) # <-- tuple expects 1 argument, got 2
失败的原因是因为我们的类是tuple的子类,而tuple是不可变的。记住,tuple是通过__new__创建的,然后__init__运行。在初始化时,元组已经被创建,不能再被修改,因为它们是不可变的。
我们可以通过重写__new__来解决这个问题:
class Point(tuple):
x: float
y: float
def __new__(cls, x:float, y:float): # <-- newly added method
return super().__new__(cls, (x, y))
def __init__(self, x:float, y:float):
self.x = x
self.y = y
这之所以有效,是因为在__new__中,我们使用super()来获取Point的父类引用,即tuple。接下来,我们使用tuple.__new__并传递一个可迭代对象((x, y))来创建一个新元组。这与tuple((1, 2))是一样的。
控制实例创建和附加方法
结果是一个Point类,底层是一个tuple,但我们可以添加各种额外功能:
class Point(tuple):
x: int
y: int
def __new__(cls, x:float, y:float):
if x < 0 or y < 0: # <-- filter inputs
raise ValueError("x and y must be positive")
return super().__new__(cls, (x, y))
def __init__(self, x:float, y:float):
self.x = x
self.y = y
def distance_from(self, other_point: Point): # <-- new method
return math.sqrt(
(other_point.x - self.x) ** 2 + (other_point.y - self.y) ** 2
)
p = Point(1, 2)
p2 = Point(3, 1)
print(p.distance_from(other_point=p2)) # <-- 2.23606797749979
注意我们添加了一个计算Point之间距离的方法,以及一些输入验证。我们现在在__new__中检查提供的X和y值是否为正,并在不符合条件时完全阻止对象创建。
## 使用 Docker 和 Compose 的环境变量和文件的完整指南
通过这个简单的教程保持你的容器安全和灵活
[towardsdatascience.com
实际应用 2:添加元数据
在这个示例中,我们从不可变的float创建了一个子类,并添加了一些元数据。下面的类将生成一个真正的float,但我们添加了一些关于符号的额外信息。
class Currency(float):
def __new__(cls, value: float, symbol: str):
obj = super(Currency, cls).__new__(cls, value)
obj.symbol = symbol
return obj
def __str__(self) -> str:
return f"{self.symbol} {self:.2f}" # <-- returns symbol & float formatted to 2 decimals
price = Currency(12.768544, symbol='€')
print(price) # <-- prints: "€ 12.74"
正如你所见,我们继承自float,这使得Currency的实例实际上是一个float。如你所见,我们还可以访问诸如用于美观打印的符号等元数据。
还要注意这是一个实际的浮点数;我们可以毫无问题地执行float操作:
print(isinstance(price, float)) # True
print(f"{price.symbol} {price * 2}") # prints: "€ 25.48"
## 参数与关键字参数:哪种方式在 Python 中调用函数最快?
timeit模块的清晰演示
towardsdatascience.com
实际应用 3:单例模式
有些情况下你不想每次实例化类时都返回一个新的对象。例如,一个数据库连接。单例模式将类的实例化限制为唯一实例。该模式用于确保一个类只有一个实例,并提供一个全局访问点来访问该实例:
class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super(Singleton, cls).__new__(cls)
return cls._instance
singleton1 = Singleton()
singleton2 = Singleton()
print(id(singleton1))
print(id(singleton2))
print(singleton1 is singleton2) # True
这段代码创建一个Singleton类的实例(如果它尚不存在),并将其作为属性保存在cls上。当Singleton再次被调用时,它返回之前存储的实例。
## 使用 Python 的 AtExit 在程序退出后运行代码
注册在脚本结束或出错后运行的清理函数
towardsdatascience.com
其他实际应用
其他一些应用包括:
-
控制实例创建
我们在
Point示例中已经看到过:在创建实例之前添加额外的逻辑。这可以包括输入验证、修改或日志记录。 -
工厂方法
根据输入在
__new__中确定将返回哪个类。 -
缓存
对于资源密集型对象创建。像单例模式一样,我们可以在类本身上存储之前创建的对象。我们可以在
__new__中检查是否已经存在等效的对象,并返回它,而不是创建一个新的。
## 从你的 Git 仓库创建可以用 PIP 安装的自定义私有 Python 包
使用你的 git 仓库分享你自己构建的 Python 包。
towardsdatascience.com
结论
在这篇文章中,我们深入探讨了 Python 对象创建,了解了它是如何工作的以及为什么这样工作。然后我们看了一些实际示例,演示了我们可以用新获得的知识做很多有趣的事情。控制对象创建可以使你创建高效的类,并显著提高你的代码的专业性。
为了进一步改进你的代码,我认为最重要的是真正理解你的代码,了解 Python 的工作原理并应用合适的数据结构。为此,请查看我的其他文章或这个演示。
我希望这篇文章能像我期望的那样清晰,但如果不清楚,请告诉我可以进一步澄清的内容。同时,请查看我在 其他文章 上关于各种编程相关主题的文章:
祝编码愉快!
— Mike
附言:喜欢我做的事吗? 关注我!
阅读 Mike Huls 在 Medium 上的文章。我是一名全栈开发者,对编程、技术充满热情,…
mikehuls.medium.com](https://mikehuls.medium.com/?source=post_page-----9134d971e334--------------------------------)
Python 列表:处理有序数据集合的终极指南
Python 列表的全面指南
·发表于 Towards Data Science ·10 分钟阅读·2023 年 7 月 19 日
--

图片由 Jill Wellington 提供,来源于 Pixabay
在编程时,我们总是需要处理数据结构。我的意思是,我们需要将信息存储在某个地方,以便以后可以重新使用。
Python 是一种非常灵活的编程语言,给我们提供了使用不同类型数据结构的可能性。
在这篇文章中,我们将分析 Python 列表。因此,如果你是 Python 初学者,并且正在寻找关于列表的全面指南,那么这篇文章绝对适合你。
在这里你将学到:
Table of Contents:
What is a list in Python?
The top 9 features in Python lists, with examples
How to create a list in Python
Accessing list elements
Modifying the elements of a list
Adding elements to a list
Removing elements from a list
Concatenating lists
Calculating the lenght of a list
Sorting the elements of a list
List comprehension
什么是 Python 列表?
在 Python 中,列表是一种内置的数据结构,允许我们以文本或数字的形式存储和操作数据。
列表以有序的方式存储数据,这意味着可以通过位置访问列表中的元素。
列表也是一种可修改的数据结构,与 元组 相对。
最后,列表还可以存储重复的值而不会引发错误。
Python 列表的 9 大特性及示例
学习 Python 的最佳方式是亲自上手敲代码,并且尽可能地解决实际问题。
所以,现在我们将通过代码示例展示 Python 列表的 9 个主要特性,因为正如我们将看到的,理论在编程中意义不大:我们只需要编写代码并解决问题。
如何在 Python 中创建列表
要创建列表,我们需要使用方括号:
# Create a simple list
numbers = [1,2,3,"dog","cat"]
# Show list
print(numbers)
>>>
[1, 2, 3, 'dog', 'cat']
创建列表的另一种方法是使用内置方法 list。例如,假设我们想创建一个包含从 0 到 9 的数字的列表。我们可以使用内置方法 range 来创建这个范围,然后将其作为参数传递给 list 方法来创建列表,如下所示:
# Create a list in the range
list_range = list(range(10))
# Show list
print(range_list)
>>>
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
我们还可以创建所谓的列表中的列表,即嵌套列表。例如,假设我们想存储与跑步的人测量时间(以秒为单位)相关的数据。如果我们需要这些数据作为列表,我们可以像这样创建一个列表的列表:
# Create a list of lists
times = [["Jhon"], [23, 15, 18], ["Karen"], [17, 19, 15],
["Tom"], [21, 19, 25]]
# Print list
print(times)
>>>
[['Jhon'], [23, 15, 18], ['Karen'], [17, 19, 15], ['Tom'], [21, 19, 25]]
访问列表元素
列表中的元素可以通过其位置访问。我们需要记住的是,在 Python 中,我们从 0 开始计数。这意味着第一个元素通过 0 访问:
# Create a list of elements
values = [1,2,3,"dog","cat"]
# Print elements by accessing them
print(f"The first element is: {values[0]}")
print(f"The fourth element is: {values[3]}")
>>>
The first element is: 1
The fourth element is: dog
因此,我们只需要注意正确计数。
访问列表中的列表稍微复杂一些,但并不难。我们首先需要访问与外部列表相关的位置,然后在内部列表中计数。
正如我们所说的,实践胜于理论。在 Python 中,我们用一个例子来展示这个概念:
# Create a list of lists
times = [["Jhon"], [23, 15, 18], ["Karen"], [17, 19, 15],
["Tom"], [21, 19, 25]]
# Print
print(f"The first runner is:{times[0]}.\nHis first registered time is:{times[1][0]}\nThe min registered time is:{min(times[1])}")
>>>
The first runner is:['Jhon'].
His first registered time is:23
The min registered time is:15
Jhon 是第一个登记的跑步者,因此我们用 times[0] 访问它。
然后,我们想计算他第一次登记的时间。为此,我们需要输入 times[1][0],因为:[1] 表示第二个位置,相对于外部列表。意思是我们访问了内部列表 [23, 15, 18]。最后,[0] 访问了内部列表中的第一个数字,确实是 23。
修改列表中的元素
正如我们所说,列表是可修改的,要修改列表中的元素,我们需要访问它。
那么,让我们做一个例子:
# Create a list of numbers
numbers = [1, 2, 3, 4, 5]
# Modify the third element
numbers[2] = 10
# Print new list
print(numbers)
>>>
[1, 2, 10, 4, 5]
所以,在这种情况下,我们修改了第三个元素,将其从 3 改为 10。
我们还可以修改文本,特别是句子。让我们看一个例子:
# Create a sentence in a list
sentence = list("Hello, World!")
# Substitute "world" with "Python"
sentence[7:] = list("Python!")
# Print sentence
print(''.join(sentence))
>>>
"Hello, Python!"
所以,在这里,我们用 sentence[7:] 替换了列表 "sentence" 中从第七个(从 0 开始计数,如前所述)元素到最后一个元素的所有字母。
然后,我们使用了 ''.join(sentence) 方法来将句子作为一个整体打印。事实上,如果我们只是使用 print(),它会将字母逐个打印,如下所示:
print(sentence)
>>>
['H', 'e', 'l', 'l', 'o', ',', ' ', 'P', 'y', 't', 'h', 'o', 'n', '!']
向列表中添加元素
由于列表是可变的,我们可以向其中添加新元素,如果需要,并且我们有几种方法可以做到这一点。
第一种方法是使用 append() 方法,这在我们只需要向列表中添加一个元素时特别适用。例如:
# Create a list with fruits
fruits = ['apple', 'banana']
# Append the element "orange" to the list
fruits.append('orange')
# Print list
print(fruits)
>>>
['apple', 'banana', 'orange']
向现有列表中添加元素的另一种方法是使用 extend() 方法,这在需要一次添加多个元素时特别适用。例如,如下所示:
# Create a list of numbers
numbers = [1, 2, 3, 4, 5]
# Extend the list with new numbers
numbers.extend([6, 7, 8])
# Print list
print(numbers)
>>>
[1, 2, 3, 4, 5, 6, 7, 8]
从列表中移除元素
由于可变性,我们可以向列表中添加元素,也可以删除元素。
在这里,我们有两种方法:我们可以使用切片功能,或者可以直接指定要删除的元素。
让我们通过 Python 示例来看看这些:
# Create a list with fruits
fruits = ['apple', 'banana', 'orange']
# Remove the element banana
fruits.remove('banana')
# Print list
print(fruits)
>>>
['apple', 'orange']
所以,remove() 方法允许我们通过输入其值直接从列表中删除特定元素。
我们可以使用的另一种方法是通过以下方式访问我们想要删除的元素的位置:
# Create a list of numbers
numbers = [1, 2, 3, 4, 5]
# Delete on element: slicing method
popped_element = numbers.pop(2)
# Print
print(numbers)
print(f"The deleted element is:{popped_element}")
>>>
[1, 2, 4, 5]
The deleted element is:3
因此,pop() 方法通过访问索引从列表中删除一个元素。
选择使用哪一个?这取决于情况。如果我们有一个非常长的列表,通常使用 remove() 方法是个好主意,这样我们可以直接写出我们实际上想要删除的元素,而不会在计算索引时出错。
合并列表
列表的可变性使我们能够执行许多任务,例如将多个列表合并成一个列表。
这个操作很简单,使用 + 来进行,如下所示:
# Create a list
list1 = [1, 2, 3]
# Create a second list
list2 = [4, 5, 6]
# Concatenate list
combined_list = list1 + list2
# Print cncatenated lists
print(combined_list)
>>>
[1, 2, 3, 4, 5, 6]
当然,这个功能也可以在字符串上执行:
# Create a list
hello = ["Hello"]
# Create another list
world = ["world"]
# Concatenate
single_list = hello + world
# Print concatenated
print(single_list)
>>>
['Hello', 'world']
另一种合并列表的方法是将嵌套列表展平。换句话说,我们可以从嵌套列表中创建一个单一的“直线”列表,如下所示:
# Create a nested list
lists = [[1, 2], [3, 4], [5, 6]]
# Create a unique list
flattened_list = sum(lists, [])
# Print unique list
print(flattened_list)
>>>
[1, 2, 3, 4, 5, 6]
基本上,我们使用 sum() 方法来获取列表 lists 中的所有元素,并将它们附加到一个空列表 [] 中。
计算列表的长度
在前面的示例中,我们自己创建了列表,以演示如何操作列表的 Python 示例。
然而,当使用 Python 时,常常会从不同的来源检索数据,这意味着有人创建了一个我们实际上不知情的列表。
当我们面对一个未知列表时,我们最好先计算它的长度。我们可以这样做:
# Create a list
fruits = ['apple', 'banana', 'orange']
# Print list lenght
print(f"In this list there are {len(fruits)} elements")
>>>
In this list there are 3 elements
所以,len() 方法计算列表中有多少个元素,而不必担心它们的类型。这意味着元素可以是所有数字、所有字符串,或两者兼有:len() 方法会统计它们全部。
对列表元素进行排序
当我们不知道列表的内容时,另一个可能执行的操作是对其元素进行排序。
我们有不同的方法来实现这一点。
我们从 sort() 方法开始:
# Creaye a list of numbers
numbers = [5, 2, 1, 4, 3]
# Sort the numbers
numbers.sort()
# Print sorted list
print(numbers)
>>>
[1, 2, 3, 4, 5]
因此,我们可以直接将列表作为参数传递给 sort() 方法,它将对元素进行排序。
但如果我们想要排序一个包含字符串的列表呢?例如,假设我们想要按字母顺序对列表中的元素进行排序。我们可以这样做:
# Create a list of strings
words = ['cat', 'apple', 'dog', 'banana']
# Sort in alphabeticla order
sorted_words = sorted(words, key=lambda x: x[0])
# Print sorted list
print(sorted_words)
>>>
['apple', 'banana', 'cat', 'dog']
因此,在这种情况下,我们使用 sorted() 方法,需要指定:
-
关于我们想要排序的列表的参数。在这种情况下,是
words。 -
key。这意味着我们需要指定一种方法。在这种情况下,我们使用了一个 lambda 函数,通过x[0]获取每个元素的第一个字母,遍历所有元素:这是我们选择每个单词第一个字母的方式。
对字符串进行排序的另一种方式是按每个元素的字符数进行排序。换句话说,假设我们想要将较短的单词放在列表的开头,而将最长的单词放在末尾。我们可以这样做:
# Create a list of words
words = ['cat', 'apple', 'dog', 'banana']
# Sort words by lenght
words.sort(key=len)
# Print sorted list
print(words)
>>>
['cat', 'dog', 'apple', 'banana']
因此,即使使用sort()方法,我们也可以传递一个参数key。在这种情况下,我们选择了len,它计算每个单词的长度。因此,列表现在是按照从最短的单词到最长的单词的顺序排列的。
列表推导式
列表推导式是一种快速且简洁的方式,通过一行代码使用循环和语句的力量创建一个新列表。
让我们看一个例子。假设我们想取 1 到 6 的数字,并创建一个包含它们平方值的列表。我们可以这样做:
# Create a list of squared numbers
squares = [x ** 2 for x in range(1, 6)]
# Print list
print(squares)
>>>
[1, 4, 9, 16, 25]
现在,我们可以不使用列表推导式而达到相同的结果,但需要大量代码,如下所示:
# Create empty list
squares = []
# Iterate over the numbers in the range
for squared in range(1, 6):
# Calculare squares and append to empty list
squares.append(squared ** 2)
# Print list
print(squares)
>>>
[1, 4, 9, 16, 25]
因此,我们得到相同的结果,但列表推导式使我们只需一行代码即可实现。
我们还可以在列表推导式中使用if语句,这使得它比“标准方法”更加快捷和优雅,对于标准方法,我们需要使用for循环进行迭代,然后用if语句选择所需的值。
例如,假设我们想创建一个新的平方数列表,但只想要偶数。我们可以这样做:
# Create a list with numbers in a range
numbers = list(range(1, 11))
# Get the even squared numbers and create a new list
squared_evens = [x ** 2 for x in numbers if x % 2 == 0]
# Print list with squared & even numbers
print(squared_evens)
>>>
[4, 16, 36, 64, 100]
因此,我们需要记住,为了取得偶数,我们可以利用它们能被 2 整除的事实。所以,x % 2 == 0 获取那些被 2 除时余数为 0 的数字。也就是说:它们是偶数。
结论
在本文中,我们展示了关于 Python 列表的全面指南。
列表是一种非常重要且有用的数据结构。它们不难学习,但对于每个 Python 程序员来说都是一个基本资产。

Federico Trotta
我是 Federico Trotta,我是一名自由技术写作员。
想与我合作吗?联系我。
Python 列表与 NumPy 数组:深入探讨内存布局和性能优势
快速计算
探索分配差异和效率提升
·发表于 Towards Data Science ·9 分钟阅读·2023 年 7 月 14 日
--

NumPy 数组中的数据像书架上的书一样紧凑地排列。照片由 Eliabe Costa 拍摄,来源于 Unsplash
在本文中,我们将深入探讨原生 Python 列表和 NumPy 数组之间的内存设计差异,揭示为什么在许多情况下 NumPy 可以提供更好的性能。
我们将比较数据结构、内存分配和访问方法,展示 NumPy 数组的强大功能。
介绍
想象一下,你正在准备去图书馆找一本书。现在,你发现图书馆有两个货架:
第一个货架上装满了各种精美的盒子,有些盒子里装着光盘,有些装着图片,还有些装着书籍。只有物品的名称附在盒子上。
这代表了原生 Python 列表,其中每个元素都有自己的内存空间和类型信息。
然而,这种方法存在一个问题:盒子里有许多空余空间,浪费了货架空间。而且,当你想找一本特定的书时,你必须查看每一个盒子,这会花费额外的时间。
现在让我们来看第二个货架。这次没有盒子;书籍、光盘和图片都根据它们的类别紧凑地放在一起。
这是 NumPy 数组,它们在内存中以连续的方式存储数据,从而提高了空间利用率。
由于物品都是按类别分组的,你可以快速找到一本书,而不必在许多盒子中搜索。这就是为什么在许多操作中,NumPy 数组比原生 Python 列表更快的原因。
Python 列表:一种灵活但效率较低的解决方案
Python 中的一切都是对象
让我们从 Python 解释器开始:虽然 CPython 是用 C 编写的,但 Python 变量不是 C 中的基本数据类型,而是包含值和附加信息的 C 结构。
以 Python 整数x = 10_000为例,x不是栈上的基本类型。相反,它是指向内存堆对象的指针。
如果你深入研究Python 3.10的源代码,你会发现x所指向的 C 结构如下图所示:

Python 整数与 C 原生整数。图像来源:作者。
PyObject_HEAD包含了如引用计数、类型信息和对象大小等信息。
Python 列表是包含一系列对象的对象
从中我们可以推断出,Python 列表也是一个对象,只不过它包含指向其他对象的指针。
我们可以创建一个只包含整数的列表:
integer_list = [1, 2, 3, 4, 5]
我们还可以创建一个包含多种对象类型的列表:
mixed_list = [1, "hello", 3.14, [1, 2, 3]]
Python 列表的优缺点
正如我们所看到的,Python 列表包含一系列指针对象。这些指针反过来指向内存中的其他对象。
这种方法的优点是灵活性。你可以将任何对象放入 Python 列表中,而无需担心类型错误。
然而,缺点也很明显:

Python 列表包含一系列指针对象。图像来源:作者
每个指针所指向的对象在内存中是分散的。当你遍历一个 Python 列表时,你需要根据指针查找每个对象的内存位置,这会导致性能下降。
NumPy 数组:一种用于增强性能的连续内存布局
接下来,让我们探索 NumPy 数组的组成部分和排列方式,以及它如何有利于缓存局部性和向量化。
NumPy 数组:结构和内存布局
根据 NumPy 的内部描述,NumPy 数组由两部分组成:
-
一部分存储了 NumPy 数组的元数据,描述了数据类型、数组形状等。
-
另一部分是数据缓冲区,它以紧凑的方式在内存中存储数组元素的值。

NumPy 数组:结构和内存布局。图像来源:作者
NumPy 数组的内存布局
当我们观察 ndarray 的.flags属性时,我们发现它包括:
In 1: np_array = np.arange(6).reshape(2, 3, order='C')
np_array.flags
Out 1: C_CONTIGUOUS : True
F_CONTIGUOUS : False
OWNDATA : False
WRITEABLE : True
ALIGNED : True
WRITEBACKIFCOPY : False
-
C_CONTIGUOUS,表示数据是否可以使用行优先顺序读取。 -
F_CONTIGUOUS,表示数据是否可以使用列优先顺序读取。
行优先顺序是 C 语言使用的数据排列方式,标记为order=’C’。这意味着数据按行存储。
另一方面,列优先顺序由 FORTRAN 使用,标记为order=’F’,按列存储数据。

NumPy 数组的内存布局。图片由作者提供
NumPy 内存布局的优势
由于ndarray设计用于矩阵操作,它的所有数据类型都是相同的,具有相同的字节大小和解释。
这使得数据紧密打包在一起,带来了缓存局部性和向量化计算的优势。
缓存局部性:NumPy 的内存布局如何提高缓存利用率
什么是 CPU 缓存
NumPy 的连续内存布局有助于提高缓存命中率,因为它与 CPU 缓存的工作方式相匹配。为了更好地解释这一点,我们首先了解一下CPU 缓存的基本概念。
CPU 缓存是 CPU 和主内存(RAM)之间的小型高速存储区域。CPU 缓存的目的是加快内存中的数据访问速度。

CPU 缓存是 CPU 和主内存(RAM)之间的小型高速存储区域。图片由作者提供
当 CPU 需要读写数据时,它首先检查数据是否已经在缓存中。
如果所需数据在缓存中(缓存命中),CPU 可以直接从缓存中读取。如果数据不在缓存中(缓存未命中),CPU 会从 RAM 中加载数据并将其存储在缓存中以供将来使用。
CPU 缓存通常以缓存行的形式组织,这些缓存行是连续的内存地址。当 CPU 访问 RAM 时,缓存会将整个缓存行加载到高速缓存中。
这意味着,如果 CPU 访问相邻的内存地址,在加载缓存行之后,后续访问更有可能命中缓存,从而提高性能。

当 CPU 访问 RAM 时,缓存会将整个缓存行加载到高速缓存中。图片由作者提供
NumPy 如何利用缓存
NumPy 的连续内存布局利用了这一事实。
NumPy 数组将数据存储在连续的内存地址中,这有助于提高缓存局部性。
当访问数组中的一个元素时,整个缓存行(包含相邻的数组元素)会被加载到缓存中。
当你遍历数组时,你会依次访问每个元素。由于数组元素在内存中是连续存储的,因此在遍历过程中缓存命中更有可能发生,从而提高性能。
这类似于去图书馆读书。你不仅取出所需的书,还会拿出相关的书并将它们放在桌子上。
这样,当你需要查阅相关材料时,它们会更容易获取,比起在书架上寻找更加高效。
向量化:释放 NumPy 内存布局的威力
什么是向量化
向量化是一种利用单指令多数据(SIMD)功能的技术,这些功能可在 CPU 或 GPU 上同时执行多个数据操作。
向量化操作可以通过同时处理多个数据项显著提高代码执行效率。
NumPy 的连续内存布局促进了向量化操作。
为什么向量化适用
假设你是一个每天必须向不同家庭送货的送货员。
假设包裹在车辆中按顺序排列,而房屋沿街编号。在这种情况下,送货员可以有效地沿街按顺序送达包裹。
这种高效的方法类似于 NumPy 的内存布局,在向量化中带来了以下好处:
-
数据对齐:NumPy 数组的连续内存布局确保数据在内存中以向量化友好的方式对齐。这使得 CPU 能够高效地加载和处理 NumPy 中的数据。
-
顺序访问模式:NumPy 在内存中紧凑的数据有助于提高向量化性能。顺序访问模式还充分利用了 CPU 缓存和预取功能,减少了内存访问延迟。
-
简化代码:NumPy 提供了一系列函数(例如,
np.add、np.multiply)和操作(例如,数组切片),这些函数和操作自动处理向量化操作。你可以编写简洁高效的代码,而无需担心底层实现。
副本和视图:NumPy 在性能优化方面的出色设计
在之前的讨论中,我们讨论了 NumPy 如何利用其连续内存布局来实现性能优势。
现在,让我们讨论 NumPy 如何通过副本和视图获得性能优势。
副本和视图是什么
副本和视图是定义现有数据与原始数组之间关系的两种选项。根据这两种选项的特性,可以总结如下:
-
副本:使用与原始数组不同的内存空间,但数据内容相同。
-
视图:引用与原始数组相同的内存地址。

一个副本可以有多个视图。图片来源:作者
如果我们将其与书籍进行比较,视图就像书中的书签,而不需要创建书籍的副本。
另一方面,副本是原书的复制品,包含文本和图像的完整副本。当你在这个副本上做笔记时,原书不会受到影响。
充分利用这两种特性
利用视图和副本的特性可以帮助我们编写简洁高效的代码。
以算术操作为例。如果你使用 a = a + 1,将会创建一个新的副本。然而,如果你使用 a += 1 或 np.add,则会应用广播,并且直接在原始数组上进行加法操作。
请看以下代码,该代码测量了执行时间:
执行上述代码将产生类似于以下结果:

使用视图进行计算所需时间更少。截图由作者提供
从结果来看,使用视图进行计算所需时间更少。
区分副本和视图
确认每次计算结果是视图还是副本将需要付出很大努力。
不过,还有一种更直接的验证方法:
-
使用
may_share_memory来判断参数中的两个数组是否引用相同的内存空间。这个判断可能不够严格。True 并不一定意味着数组是共享的,但 False 确认数组绝对不共享。 -
如果你需要更准确的答案,可以使用
share_memory函数。然而,这个函数的执行时间比may_share_memory要长。
结论
总结来说,我们了解了 NumPy 数组与原生 Python 列表之间在内存安排上的差异。
由于 NumPy 数组中相同数据类型的连续排列,显著提高了缓存局部性和向量化的性能优势。
在 NumPy 的设计中分离视图和副本,为代码执行性能和内存管理提供了更大的灵活性。
在接下来的系列文章中,我将从基础开始,重申工作中数据科学的最佳实践。如果你有任何建议或问题,请随时评论,我会逐一解答。
除了提高代码执行速度和性能外,使用各种工具提高工作效率也是一种性能提升:

Python 工具箱
查看列表6 个故事

[## 加入 Medium 使用我的推荐链接 - Peng Qian
作为 Medium 的会员,你的部分会员费用将用于你阅读的作者,同时你可以全面访问每个故事……
medium.com](https://medium.com/@qtalen/membership?source=post_page-----a74ce774bc1e--------------------------------)
Python Meets Pawn 2:基于开局的国际象棋大师聚类
在这篇博客中,我将引导你通过使用 Python 分析国际象棋大师开局的过程。
·发表于 Towards Data Science ·7 分钟阅读·2023 年 12 月 22 日
--

由 Midjourney 创建的照片
-
我在回答哪些问题
-
第一部分:获取数据
-
第二部分:特征工程
-
第三部分:聚类
-
结果与有趣的事实
我在回答哪些问题
我对国际象棋的热情不是什么秘密,这里我分享了自己棋局开局的分析。但今天,我将踏入一个新领域:国际象棋大师的世界。他们通常使用什么开局?他们的选择有多么多样?我对这些开局在不同国际象棋大师中的分布很感兴趣。顶级棋手是否偏爱相似的开局?是否可以根据他们的偏好进行分组?我不知道——让我们来探讨一下!
第一部分:获取数据
国际象棋的一个伟大方面是其数据的可获取性。有许多来源,包括pgnmentor,你可以在这里查看和下载关于开局和棋手的数据(免费)。这些数据每年更新多次,包括 Portable Game Notation (PGN)格式的棋局,这是国际象棋游戏最流行的格式。由于下载是逐个进行的,我选择了 11 位著名的国际象棋大师来下载和分析他们的开局。请注意,这个列表是主观的,包含了一些我最喜欢的国际象棋大师:
-
Shakhriyar Mamedyarov
-
Teimour Radjabov
-
Hikaru Nakamura
-
Magnus Carlsen
-
Fabiano Caruana
-
丁立人
-
Ian Nepomniachtchi
-
Viswanathan Anand
-
Anish Giri
-
Vugar Gashimov
-
Vladimir Kramnik
完整的代码将在博客末尾提供。为了解析 PGN 文件,我使用了名为‘Chess’的 Python 库中的 PGN 模块。
我用于解析数据的函数如下所示:
def parse_pgn_file(file_path):
"""
Parses a PGN (Portable Game Notation) file containing chess games.
Args:
file_path (str): Path to the PGN file.
Returns:
pd.DataFrame: A DataFrame containing game information.
"""
games = [] # Initialize an empty list to store parsed games.
with open(file_path, "r") as pgn_file:
while True:
game = chess.pgn.read_game(pgn_file) # Read a game from the PGN file.
if game is None:
break # Exit the loop when no more games are found.
games.append(game) # Append the parsed game to the list.
data = [] # Initialize an empty list to store game data.
for game in games:
data.append({
"Event": game.headers.get("Event", ""),
"Date": game.headers.get("Date", ""),
"Result": game.headers.get("Result", ""),
"White": game.headers.get("White", ""),
"Black": game.headers.get("Black", ""),
"Moves": " ".join(str(move) for move in game.mainline_moves()),
"ECO": game.headers.get("ECO", "")
}) # Extract relevant information from game headers and moves.
df = pd.DataFrame(data) # Create a DataFrame from the extracted data.
return df # Return the DataFrame containing game information.
以下是我解析和组合数据的表格显示。我将利用现有的“ECO”列,指示每盘棋中使用的开局。棋类中的 ECO 代码指的是“国际象棋开局百科全书”,这是一种用于分类各种开局的系统。每个代码由一个字母和两个数字组成,如 B12 或 E97,独特地标识某一特定开局或变体。

解析的数据集(图片来源:作者)
特级大师们拥有数千盘棋局,涵盖 484 个独特的组合 ECO 代码。鉴于有 500 个独特的 ECO 代码,这 11 位特级大师几乎使用了职业生涯中的所有范围。然而,每位特级大师玩了多少个独特的开局?让我们查看以下图表:

独特开局图表(图片来源:作者)
这些数字与他们在数据集中的棋局数量高度相关,但总体而言,图表显示特级大师们在棋局中使用了各种各样的开局。
第二部分:特征工程
让我们开始查看每位特级大师最受欢迎的开局:
-
B90 — 西西里防御,Najdorf 变体 : Anand, Giri, Nepomniachtchi
-
D37 — 皇后弃兵 : Carlsen, Mamedyarov, Radjabov
-
C42 — 俄国棋局 : Gashimov, Kramnik
-
A05 — 印度王攻 : Nakamura
-
C65 — 西班牙棋局,柏林防御 : Caruana
-
E60 — 格鲁恩费尔德和印度棋局 : Ding
我猜看到一位俄国特级大师偏好俄国棋局并不奇怪。Gashimov 也偏好俄国棋局,表明苏联棋校在阿塞拜疆的强大影响。基于他们喜欢的开局发现一些模式是很有趣的。然而,为了实现更详细和分隔的分组,我将应用聚类技术,同时考虑其他开局。
让我们检查每位特级大师的开局分布。我将数据集以特级大师为索引,使用独特的 ECO 代码作为列,以棋局数量为值进行了透视。以下图表是马格纳斯·卡尔森的示例:

马格纳斯的开局分布(图片来源:作者)
尽管特级大师们使用了各种开局,但明显有些开局比其他开局更具优势。大多数特级大师似乎偏好大约五种特定的开局,这影响了我决定集中于一个包含前 5 名开局的数据框。
对于聚类,我选择测试两个数据框:透视比例和前 5 个开局。使用后者取得了最佳结果,我将在下面详细解释。有关更多选项和详细见解,请参阅末尾提供的完整代码。在前 5 个开局数据框中,我使用了独热编码。在 11 位国际象棋大师中,前 5 个选择中有 24 个独特的 ECO 代码。这个数据框中的二进制值指示每位国际象棋大师的前 5 个开局中是否包含特定的 ECO 代码:

Top5 数据框(图片由作者提供)
下表显示了每位国际象棋大师的前 5 个 ECO。我们已经可以看到一些模式,但聚类将帮助我们更有效地区分它们。

每位国际象棋大师的前 5 个开局结果(图片由作者提供)
第三部分:聚类
前 5 个最受欢迎的开局数据集包含 24 列。为了简化,我应用了 PCA(主成分分析)。这种方法有助于减少数据维度,同时保留重要信息。虽然第一个主成分提供了不错的结果,但我选择了两个成分。为什么?它们提供了几乎相同的洞察,并且使得可视化更容易。
对于分组国际象棋大师,我使用了 K-means 聚类。这就像把书籍分类到不同的类型中。首先,我选择了聚类的数量或“类型”。然后,将每位国际象棋大师的开局风格匹配到最接近的聚类中,就像将书籍分配到最合适的类型一样。这个过程会不断调整:代表每组共同风格的聚类中心会重新计算,国际象棋大师会相应地重新分配。这个过程会重复,直到聚类准确地表示出不同的游戏风格。通过 K-means,国际象棋开局中的不同模式浮现出来,突显了国际象棋大师们之间的不同策略。
选择正确的聚类数在任何聚类项目中都是关键。为此,我使用了肘部法则。这是一种确定数据分组理想聚类数的简单方法。你绘制一个图表,其中每个点代表一个不同的聚类数,并计算每个聚类的“组内平方和”(WCSS)。WCSS 衡量数据点到聚类中心的距离。在图表上,有一个点,在该点之后增加聚类数不会显著减少 WCSS。这个点类似于一个肘部,指示最佳的聚类数。它确保了聚类数和数据点之间的紧密分组之间的平衡。下面的图表演示了在我们的案例中,最佳聚类数是 4。

确定最佳聚类数的肘部法则(图片由作者提供)
确定了聚类数量后,我对特级大师进行了聚类。为了评估我的聚类效果,我使用了轮廓系数。这个分数衡量了一个对象与其自身聚类的相似性与其他聚类的相似性。高轮廓系数表明数据聚类效果良好。该分数范围在-1 到 1 之间,我获得了 0.69 的分数,表明聚类效果有效。
最后,我在二维空间中可视化了聚类数据和质心(每个聚类的“中心”)。这一步将复杂的数据转化为易于理解和视觉上吸引人的格式,非常适合一目了然地看到模式和差异:

分析结果(图片由作者提供)
结果和有趣的事实
我的分析揭示了国际象棋特级大师在开局方面展现了广泛的 repertoire,但他们之间有些偏好有所不同。基于这些开局对他们进行聚类不仅是可行的,而且得出了有趣的见解。例如,阿塞拜疆象棋传奇人物马梅杰罗夫和拉杰博夫被归为一组。有趣的是,安اند、吉里和卡鲁阿纳也紧密聚集在一起。仔细观察他们的前 5 个最爱开局,确认了这些结果。值得注意的是,安 Anand 和吉里分享了完全相同的前 5 个开局。这是否意味着吉里对安 Anand 的钦佩?确实,在互联网研究后,我发现吉里非常欣赏安 Anand 并从他的棋局中学习。以下是这些开局:
-
B90 — 西西里防御,奈杰多夫变体
-
C50 — 意大利开局
-
C42 — 俄国开局
-
C65 — 西班牙开局,柏林防御
-
C67 — 西班牙开局,柏林防御,其他变体
完整代码及 Jupyter notebook 文件可以在这里找到。
Python 遇见棋子:通过数据分析解码我的棋局开局
原文:
towardsdatascience.com/python-meets-pawn-decoding-my-chess-openings-with-data-analysis-097a34cef20a
在这篇博客中,我将引导你通过使用 Python 分析你在 Chess.com 平台上进行的棋局。
·发表于 Towards Data Science ·8 分钟阅读·2023 年 11 月 17 日
--

照片由 DALL·E 创建
国际象棋一直是我的热情所在,这是一款由我的父亲介绍给我的美丽游戏。我早年间常与家人下棋,后来转向了 Chess.com 的数字棋盘。最近,国际象棋的受欢迎程度有所回升,这得益于知名主播和国际象棋大师的教育努力。这股新的兴趣浪潮激发了我在一系列关于棋局开局的思考:‘我经常使用的开局是什么,它们对我有多成功?’意识到我对自己的偏好或成功率一无所知,我决定将我最热爱的两个事物结合起来:国际象棋和 Python。
让我们开始理解这些步骤,学习如何使用 Chess.com API,并了解如何查看你在国际象棋中的开局动作吧!
附注:这篇博客假设你的笔记本电脑上已经安装了 Python,最好还安装了 Jupyter Notebook(或其他 IDE)。
Chess.com API
首先,你需要安装 Chess.com 库来使用其 API。你可以使用终端(或命令提示符)中的 “pip” 命令来安装,也可以在 Jupyter Notebook 中使用“!”符号来执行该语法。
pip install chess.com
你可以在 chesscom.readthedocs.io/en/latest/ 找到所有的说明和详细信息。这里包含了可以使用的每一种方法和参数。
你还需要传统的 pandas 和 numpy 库,你可以像上面一样安装它们。
获取数据
首先,让我们设置好所需的所有库,然后向 API 发出第一次请求。我们将使用一个叫做‘get_player_games_by_month’的方法来查看特定年份和月份玩的所有游戏。为了了解我们获得的数据类型,我们将查看一个示例游戏。通过使用 Python 内置的‘pprint’库,我们可以使 JSON 响应更易于阅读。
# Import necessary libraries
from chessdotcom import get_player_game_archives, get_player_games_by_month, Client
import pandas as pd
import numpy as np
from pprint import pprint
# Configure the user agent for the API requests to Chess.com
# this part is mandatory as per new version of API
Client.request_config["headers"]["User-Agent"] = (
"My Python Application. "
"Contact me at xxxx@gmail.com"
)
# get games for the month of November 2023
response_sample = get_player_games_by_month("mikayil94", year=2023, month=11)
# print the JSON
pprint(response_sample.json)
真正酷的部分在于 PGN(可移植棋局记录)部分——它包含了我们所需的一切,如开局名称和更多细节的链接(ECOUrl)

有一个叫做‘get_player_game_archives’的方法,它帮助我们获取平台上旧游戏的列表,这些游戏按我们玩它们的年份和月份排序。日期以链接格式呈现,所以我们只需要从每个链接中提取日期部分。
# Retrieve a list of months during which the player 'mikayil94' has played games
response1 = get_player_game_archives("mikayil94")
list_of_played_months = []
for i in response1.json['archives']:
list_of_played_months.append(i[-7:])
现在是重点部分!我们可以使用之前找到的年份和月份,将值传递给‘get_player_games_by_month’方法,以获取关于我们游戏的更多信息。每场游戏将衍生出以下列:‘time_class’,‘date’,‘white’,‘black’,‘game_link’,‘opening_code’,‘opening_name’,‘opening_link’,‘result’。‘time_class’部分来源于不同于其他内容的地方,这些内容都在 PGN 部分内。我们真正需要的分析数据是玩家的名称(白方和黑方)和开局名称。每个开局的链接也是非常有用的。这样,我们可以了解更多信息,并提高使用这个开局的能力。此外,链接到游戏本身也很棒,因为它让我们可以回顾并理解我们是如何赢得或输掉每场游戏的。
# Create a DataFrame to store game information
my_games_df = pd.DataFrame(columns = ['time_class', 'date', 'white', 'black', 'game_link', 'opening_code', 'opening_name', 'opening_link', 'result'])
# Loop through each month and retrieve games played in that month
for months in list_of_played_months:
response2 = get_player_games_by_month("mikayil94", year=months.split("/")[0], month=months.split("/")[1])
# Extract relevant information from each game and add it to the DataFrame
for i in response2.json['games']:
time_class = i['time_class']
pgn = i['pgn']
if "ECOUrl" not in pgn : continue # Skip the game if it doesn't have an ECO URL
# Extract various details from the PGN (Portable Game Notation) of the chess game
date = pgn[pgn.find("Date"):].split(" ")[1].split("]")[0].strip('\"')
white = pgn[pgn.find("White"):].split(" ")[1].split("]")[0].strip('\"')
black = pgn[pgn.find("Black"):].split(" ")[1].split("]")[0].strip('\"')
game_link = pgn[pgn.find("Link"):].split(" ")[1].split("]")[0].strip('\"')
opening_code = pgn[pgn.find("ECO"):].split(" ")[1].split("]")[0].strip('\"')
opening_name = pgn[pgn.find("ECOUrl"):].split(" ")[1].split("]")[0].split("/")[-1].strip('\"')
opening_link = pgn[pgn.find("ECOUrl"):].split(" ")[1].split("]")[0].strip('\"')
result = np.where(pgn[pgn.find("Termination"):].split(" ")[1].split("]")[0].strip('\"') == 'mikayil94', 'Win', 'Loss') # if my username is in this field, it means I was the Winner.
# Create a new DataFrame for the current game and append it to the main DataFrame
my_games_df_new = pd.DataFrame({'time_class' : [time_class], 'date' : [date], 'white' : [white], 'black' : [black], \
'game_link' : game_link, 'opening_code' : opening_code, 'opening_name' : [opening_name], 'opening_link' : [opening_link], 'result' : [result]})
my_games_df = pd.concat([my_games_df, my_games_df_new], ignore_index=True)
创建最终结果的变量
现在我们有了数据,我们需要添加一些内容以使其更清晰、更易于了解发生了什么。了解每场游戏中的开局是谁下的很重要。我是黑方时对这个开局进行了应对,还是白方时使用了它?为了解这个问题,我会检查每场游戏中我所处的一方。然后,通过查看我赢了还是输了每场游戏,我可以计算出每种开局的胜率。
# Add a new column 'opening_side' to the DataFrame. If the player 'mikayil94' is white, set the value to 'white', otherwise 'black'
my_games_df['opening_side'] = np.where(my_games_df.white == 'mikayil94', 'white', 'black')
# Add a new column 'result_binary'. If the result of the game is 'Win', set the value to 1, otherwise 0
my_games_df['result_binary'] = np.where(my_games_df.result == 'Win', 1, 0)
# Group the DataFrame by opening name, link, code, and the side 'mikayil94' played.
# Aggregate the data to count the total number of wins and total games played for each group
my_openings = my_games_df.groupby(["opening_name", "opening_link", "opening_code", "opening_side"], as_index=False).agg(
games_win = ('result_binary', 'sum'), # Sum of 'result_binary' to get total wins
games_count = ('result_binary', 'count') # Count of 'result_binary' to get total games played
)
# Calculate the win percentage for each opening and add it as a new column 'win_percentage'
# The win percentage is rounded to two decimal places
my_openings['win_percentage'] = round(my_openings.games_win / my_openings.games_count, 2)
结果就在这里!
现在我们可以看到结果了!我使用了 matplotlib 和 seaborn 库(如果没有,使用 pip 安装)来可视化数据。我创建了一个名为“opening_and_side”的新变量,用于可视化,指示哪一方(白方或黑方)使用了这个开局。我只查看了至少玩过 10 次的开局,以确保我的分析是准确的。
import matplotlib.pyplot as plt
import seaborn as sns
# Prepare the data for visualization
# Add new column, to concatenate opening name and opening side, which will be used in visualization
my_openings['opening_and_side'] = my_openings.opening_name + '[as ' + my_openings.opening_side + ']'
# filter data to show only games with at least 10 count
viz_data = my_openings[my_openings.games_count > 10].sort_values("win_percentage", ascending=False)[['opening_and_side', 'win_percentage']]
# Create a bar plot
plt.figure(figsize=(15, 10))
sns.barplot(x='win_percentage', y='opening_and_side', data=viz_data, palette="viridis", ci=None)
plt.title('Win Percentage by Chess Opening')
plt.xlabel('Win Percentage')
plt.ylabel('Opening Name')
plt.xticks(rotation=45)
plt.tight_layout()
# Display the plot
plt.show()

经过分析后的关键收获:
- 奥文防御! 这是我在 2018 年和 2019 年的常用开局,但直到现在我才意识到我其实用得很好。这并不是一个常见的开局,因此对于让对手措手不及非常有效!事实证明,如果查看国际象棋大师的对局,这个开局相当稳固。黑方获胜的概率是 46.3%,而白方的胜率为 34.6%。你可以在国际象棋开局数据库中查看更多信息,地址是:
old.chesstempo.com/chess-openings.html。

- 对Barnes 开局-1…d5–2.e4表现不错。我之前不知道这个开局叫做 Barnes 开局,也不知道我在这个开局中的胜率很高。即使国际象棋开局数据库说这对白方并不是最佳开局,因为在下 f3 后,白方的评估值为-0.4,这会削弱王翼。但由于这并不是一个常见的开局,它似乎让我的对手感到意外。在这种情况下,黑方不应该吃掉那个兵,但我的对手大多数情况下都吃了,这让局面变得更均衡。

- 范特-克鲁伊斯开局——在用白棋下这个开局时,我总是陷入更糟的局面,而当对手使用这个开局时,我则会获得优势,所以,这开局不太好!国际象棋开局数据库支持这一点:它显示,白方玩家使用这个开局时获胜的概率只有 36.5%,而对手的获胜概率是 45.3%!

-
在对抗国王兵开局-离经叛道的皇后攻击时表现不佳。在 2019 年之后对这种开局没有近期的记录,我通常容易犯错误,陷入陷阱,不过,生活就是不断学习!😃
-
对国王兵开局-拿破仑攻击表现不好。幸运的是,我已经很久没用这个开局了!游戏开始时过早地调动皇后通常不是一个好棋步 😃
-
当我用黑棋对抗皇后兵开局-加速伦敦系统时,通常不会立即陷入困境。但回头看,我用这个开局的胜率并不如我所希望的那么高。看来我需要花些时间更多地学习和练习这个开局。
结论
很高兴 Chess.com 提供了这个公开 API,让我们可以进行这种有趣的分析并发现一些有趣的事情。通过查看我的对局,我发现实际上在开始学习所有著名开局之前,我的表现更好。有时候,使用不寻常的开局可能是个好事。那么,为什么不试试让你的对手惊讶的 Barnes 开局或奥文防御呢?只要小心对手使用离经叛道的皇后攻击时不要犯错误。
感谢你陪伴我读到最后!希望你读得愉快,并且可能对国际象棋、Python,或使用 Python 分析你自己的国际象棋对局感兴趣 😃
Jupyter notebook 文件可以在这里找到。
Python 在网络上
在没有任何服务器的情况下展示 Python 应用
·发表于 Towards Data Science ·阅读时间 9 分钟·2023 年 10 月 11 日
--

图片由 Ales Nesetril 提供,来自 Unsplash
介绍
使用 流行的 Python 可视化库 可以相对简单地在本地创建各种形式的图表和仪表板。然而,将你的结果分享给其他人则可能复杂得多。
实现这一目标的一种可能方法是使用诸如 Streamlit、Flask、Plotly Dash 等库,并支付网络托管服务费用以覆盖服务器端,并运行你的 Python 脚本在网页上显示。另一种选择是,一些提供商如 Plotly Chart 或 Datapane 也提供免费的云支持,你可以将 Python 可视化图上传并嵌入到网页上。在这两种情况下,如果你的项目预算较小,你都能实现你所需要的功能,但是否有可能免费实现类似的结果呢?
作为本文的一部分,我们将探索三种可能的方法:
为了展示这三种方法,我们将创建一个简单的应用程序,以探索来自全球的历史通货膨胀数据。为此,我们将使用 世界银行全球通货膨胀数据库,有关数据许可的所有信息可以在 此链接 [1] 中找到。
数据下载后,我们可以使用以下预处理函数,以更好地调整数据集以进行可视化,并仅导入我们将用于分析的 3 个 Excel 表格(总体通胀数据、食品和能源价格的通胀数据)。
import pandas as pd
def import_data(name):
df = pd.read_excel("Inflation-data.xlsx", sheet_name=name)
df = df.drop(["Country Code", "IMF Country Code", "Indicator Type", "Series Name", "Unnamed: 58"], axis=1)
df = (df.melt(id_vars = ['Country', 'Note'],
var_name = 'Date', value_name = 'Inflation'))
df = df.pivot_table(index='Date', columns='Country',
values='Inflation', aggfunc='sum')
return df
inf_df = import_data("hcpi_a")
food_df = import_data("fcpi_a")
energy_df = import_data("ecpi_a")
每个数据集将会有一个日期索引,每年一行,每个国家的通胀百分比值一列(图 1)。

图 1:总体通胀数据集(图像由作者提供)。
本项目中使用的所有代码可以在我的 GitHub 个人资料上自由访问,本项目生成的在线仪表板可以通过 这个链接访问。
面板
Panel 是 HoloViz 生态系统中的一个开源 Python 库。可以使用以下命令简单安装:
pip install panel
数据导入后,我们可以继续开发我们的应用程序:
-
我们首先导入必要的库。
-
指定一个模板来样式化应用程序及其标题。
-
创建一个下拉小部件,用户可以选择一个国家进行检查。在此情况下,瑞士被提供为应用程序加载时的默认选择。
-
3 个辅助函数旨在将所选国家作为输入,然后返回系列的不同时间部分,以便向用户清晰地显示原始通胀数据。
-
最终,3 个辅助函数与下拉小部件绑定,并一起添加到界面上的一列中。
import pandas as pd
import matplotlib.pyplot as plt
import panel as pn
from holoviews import opts
import hvplot.pandas
pn.config.template = 'fast'
pn.config.template.title="Panel Inflation Monitoring Application"
country_widget = pn.widgets.Select(name="Country", value="Switzerland", options=list(inf_df.columns))
def pivot_series(inf_df, country):
df = pd.DataFrame({'Date':inf_df[country].index, 'Inflation':[round(i, 3) for i in inf_df[country].values]})
df = df.pivot_table(values='Inflation', columns='Date')
return df
def make_df_plot(country):
df = pivot_series(inf_df, country)
return pn.pane.DataFrame(df.iloc[:, : 17])
def make_df_plot2(country):
df = pivot_series(inf_df, country)
return pn.pane.DataFrame(df.iloc[:, 17:34])
def make_df_plot3(country):
df = pivot_series(inf_df, country)
return pn.pane.DataFrame(df.iloc[:, 34:])
bound_plot = pn.bind(make_df_plot, country=country_widget)
bound_plot2 = pn.bind(make_df_plot2, country=country_widget)
bound_plot3 = pn.bind(make_df_plot2, country=country_widget)
panel_app = pn.Column(country_widget, bound_plot, bound_plot2, bound_plot3)
panel_app.servable()
结果是,我们应得到如下输出(图 2):

图 2:显示表格数据(图像由作者提供)。
按照类似的结构,我们可以继续制作一个滑块,用户可以选择要检查的年份范围,并创建一个图表以可视化国家历史趋势(图 3)。
years_widget = pn.widgets.RangeSlider(name='Years Range', start=1970, end=2022, value=(1970, 2022), step=1)
def make_inf_plot(country, years):
df = inf_df[country].loc[inf_df[country].index.isin(range(years[0], years[1]))]
return df.hvplot(height=300, width=400, label=country + ' Overall Inflation')
bound_plot = pn.bind(make_inf_plot, country=country_widget, years=years_widget)
panel_app = pn.Column(years_widget, bound_plot)
panel_app.servable()

图 3:总体通胀趋势(图像由作者提供)。
现在我们已经能够可视化总体通胀数据,我们可以添加第二个图表,用户可以选择检查食品或能源价格的通胀趋势(图 4)。
type_plot_widget = pn.widgets.Select(name="Inflation Type", value="Food", options=["Food", "Energy"])
def make_type_plot(plt_type, country, years):
if plt_type == "Food":
df = food_df[country].loc[inf_df[country].index.isin(range(years[0], years[1]))]
return df.hvplot(height=300, width=400, label=country + ' Food Inflation')
else:
df = energy_df[country].loc[inf_df[country].index.isin(range(years[0], years[1]))]
return df.hvplot(height=300, width=400, label=country + ' Energy Inflation')
bound_plot = pn.bind(make_type_plot, plt_type=type_plot_widget, country=country_widget, years=years_widget)
panel_app = pn.Column(type_plot_widget, bound_plot)
panel_app.servable()

图 4:食品/能源通胀趋势(图像由作者提供)。
最后,我们还可以在仪表板上添加一个探索器小部件,以便用户能够创建自己的图表(图 5)。
hvexplorer = hvplot.explorer(inf_df)
pn.Column(
'## Feel free to explore the entire dataset!', hvexplorer
).servable()

图 5:探索器小部件(图像由作者提供)。
一旦创建了完整的应用程序并将其存储在 pane_example.py 文件中,我们可以运行以下命令以可视化结果。
panel serve panel_example.py --autoreload --show
然后,可以使用以下命令将应用程序转换为 HTML 格式:
panel convert panel_example.py --to pyodide-worker --out docs
转换后,应该可以使用 HTTP 服务器启动它。网页应该可以通过以下链接访问:http://localhost:8000/docs/panel_example.html
python3 -m http.server
Shiny for Python
Shiny 是一个最初为 R 开发的开源库,现在也可供 Python 用户使用。可以使用以下命令轻松安装:
pip install shiny
导入数据后,我们可以继续工作,首先导入必要的依赖项,然后构建应用程序的布局。具体采用以下步骤:
-
首先为应用程序创建一个标题。
-
设计一个包含下拉菜单和滑块的侧边栏(用于作为输入填充以下图表)。
-
在侧边栏旁输出 2 个图表(展示一个国家的整体通胀趋势及其年度通胀变化)。
-
在应用程序末尾添加一个最终下拉菜单和图表(用户可以检查食品/能源价格的通胀趋势)。
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from shiny import ui, render, reactive, App
app_ui = ui.page_fluid(
ui.h2("Python Shiny Inflation Monitoring Application"),
ui.layout_sidebar(
ui.panel_sidebar(
ui.input_selectize("country", "Country",
list(inf_df.columns)
),
ui.input_slider("range", "Years", 1970, 2022, value=(1970, 2022), step=1),
),
ui.panel_main(
ui.output_plot("overall_inflation"),
ui.output_plot("annual_change")
)
),
ui.input_selectize("type", "Inflation Type",
["Food", "Energy"]
),
ui.output_plot("inflation_type")
)
定义布局后,我们可以继续创建不同的图表:
def server(input, output, session):
@output
@render.plot
def overall_inflation():
df = inf_df[input.country()].loc[inf_df[input.country()].index.isin(range(input.range()[0], input.range()[1]))]
plt.title("Overall Inflation")
return df.plot()
@output
@render.plot
def annual_change():
annual_change = inf_df[input.country()].diff().loc[inf_df[input.country()].index.isin(range(input.range()[0], input.range()[1]))]
plt.title("Annual Change in Inflation")
return plt.bar(annual_change.index, annual_change.values, color=np.where(annual_change>0,"Green", "Red"))
@output
@render.plot
def inflation_type():
if input.type() == "Food":
df = food_df[input.country()].loc[inf_df[input.country()].index.isin(range(input.range()[0], input.range()[1]))]
plt.title(input.country() + ' Food Inflation')
return df.plot()
else:
df = energy_df[input.country()].loc[inf_df[input.country()].index.isin(range(input.range()[0], input.range()[1]))]
plt.title(input.country() + ' Energy Inflation')
return df.plot()
app = App(app_ui, server)
然后可以使用以下命令在本地启动应用程序(见图 6):
shiny run --reload app.py

图 6:Shiny 应用程序示例(图片来源于作者)。
如果有兴趣将代码转换为 HTML 以便在网页上共享,我们需要首先安装 shinylive,然后使用以下命令(确保将应用程序命名为 app.py!)。
pip install shinylive
shinylive export . docs
转换后,应该可以使用 HTTP 服务器启动应用程序。网页应该可以通过以下链接访问:http://[::1]:8008/
python3 -m http.server --directory docs --bind localhost 8008
PyScript
PyScript 是 Anaconda 开发的一个框架,用于直接在 HTML 文件中编写 Python 代码。导入 pyscript.js 脚本后,Python 代码将会自动执行并处理,从而在应用程序中呈现结果。
为运行我们的应用程序所需的所有 HTML 代码如下所示。然后,Python 代码可以直接粘贴在
<html>
<head>
<title>Inflation Monitoring</title>
<meta charset="utf-8">
<link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/latest/pyscript.js"></script>
</head>
<body>
<py-config>
packages = ["pandas", "matplotlib", "numpy"]
</py-config>
<py-script>
# TODO: Your Python Code Here
</py-script>
<div id="input" style="margin: 20px;">
<h1> Pyscript Inflation Monitoring Application</h1>
Choose the paramters to use: <br/>
<input type="number" id="s_year" name="params" value=1970 min="1970" max="2022"> <br>
<label for="s_year">Starting Year</label>
<input type="number" id="e_year" name="params" value=2022 min="1970" max="2022"> <br>
<label for="e_year">Ending Year</label>
<select class="form-control" name="params" id="country">
<option value="Switzerland">Switzerland</option>
<option value="Italy">Italy</option>
<option value="France">France</option>
<option value="United Kingdom">United Kingdom</option>
</select>
<label for="country">Country</label>
</div>
<div id="graph-area"></div>
</body>
</html>
在这种情况下,我们首先导入库并定义一个绘图函数,用于创建整体通胀趋势图和年度变化图。使用 js 库,我们可以获取 HTML 文件中指定的输入参数,并调用我们的绘图函数。
最后,创建一个代理,以便检查最终用户是否随时间更改了任何参数,如果是的话,自动更新其在 Python 中存储的值和相应的图表。
import js
import pandas as pd
import numpy as np
from io import StringIO
import matplotlib.pyplot as plt
from pyodide.ffi import create_proxy
def plot(country, s_year, e_year):
df = inf_df[country].loc[inf_df[country].index.isin(range(s_year, e_year))]
annual_change = inf_df[country].diff().loc[inf_df[country].index.isin(range(s_year, e_year))]
fig, (ax1, ax2) = plt.subplots(2)
fig.suptitle('Overall inflation and annual change in ' + country)
ax1.set_ylabel("Inflation Rate")
ax2.set_ylabel("Annual Change")
ax1.plot(df.index, df.values)
ax2.bar(annual_change.index, annual_change.values, color=np.where(annual_change>0,"Green", "Red"))
display(plt, target="graph-area", append=False)
s_year, e_year = js.document.getElementById("s_year").value, js.document.getElementById("e_year").value
country = js.document.getElementById("country").value
plot(str(country), int(s_year), int(e_year))
def get_params(event):
s_year, e_year = js.document.getElementById("s_year").value, js.document.getElementById("e_year").value
country = js.document.getElementById("country").value
plot(str(country), int(s_year), int(e_year))
ele_proxy = create_proxy(get_params)
params = js.document.getElementsByName("params")
for ele in params:
ele.addEventListener("change", ele_proxy)
一旦开发完成应用程序并将其存储在.html文件中,我们可以通过使用网页浏览器打开文件来立即启动它(图 7)。

图 7:PyScript 示例应用程序(图片由作者提供)。
部署
为了将我们的应用程序部署到网上,可能需要将我们的输入数据与应用程序一起存储在一个文件中(例如,Python 转换为 HTML 后,可能不再能够从 XLSX 中加载数据)。一种可能的方法是:
-
将最初导入的 3 个数据帧导出为 CSV 文件。
-
一次打开一个 CSV 文件,并将全部内容粘贴到一个变量中(如下所示)。
-
在与应用程序其余部分相同的文件中使用此设置(而不是import_data函数)。
from io import StringIO
inf_df = """TODO: PASTE YOUR CSV FILE HERE"""
csvStringIO = StringIO(inf_df)
inf_df = pd.read_csv(csvStringIO, sep=",").set_index('Date')
使用上述设置并将 Panel 和 Python Shiny 应用程序转换为 HTML 代码后,便可以在不需要支付任何服务器费用的情况下将应用程序托管到网上。
一种简单的方法是使用 GitHub Pages 并将我们的项目文件添加到在线仓库中。有关 GitHub Pages 的更多信息,请参见此处。
结论
在本文中,我们探讨了三种不同的选项,这些选项可以用来在不支付任何服务管理费用的情况下共享你的 Python 应用程序。虽然我们也看到这种方法存在一些固有的局限性,因此在设计更复杂的应用程序或处理大量数据时,可能不是最佳选择。
如果你对在线展示你的机器学习项目感兴趣(无需服务器架构),Tensorflow.js 和 ONNX 可能是你需求的两个优秀解决方案。
联系方式
如果你想跟进我最新的文章和项目,请在 Medium 上关注我(follow me on Medium)并订阅我的邮件列表。以下是我的一些联系方式:
参考文献
[1] 世界银行,全球通货膨胀数据库。访问网址:www.worldbank.org/en/research/brief/inflation-database。许可:知识共享署名 4.0 国际许可协议 (CC-BY 4.0)。
Python OOP 教程:如何创建类和对象
原文:
towardsdatascience.com/python-oop-tutorial-how-to-create-classes-and-objects-c36a92b01552
关于在面向对象编程(OOP)中使用类和对象的简单指南
·发表于 Towards Data Science ·阅读时间 6 分钟·2023 年 1 月 4 日
--

由 Taylor Heery 提供的照片,来源于 Unsplash
介绍
在 Python 编程中,一切都是对象。变量甚至函数都是对象。类是一个模具,用于创建对象。
想象一个冰棒托盘。 首先,你制造冰棒托盘以创建你所需的大小、形状和深度;这就是类。然后,你可以决定向冰棒托盘中倒入什么来冻结——也许你加入水并简单制作冰块,或者你加入不同种类的水果和果汁制作冰棒。你创建的每个冰棒都是一个对象,对象可以有不同的“数据”或口味。
本文将通过代码演示如何创建自己的类并在 Python 代码中使用它。类的不同组件可以分解为以下内容:构造函数、获取器和设置器、属性、装饰器、私有命名、类方法、属性和继承。
何时使用类/对象与模块:
-
当你需要多个具有类似行为但数据不同的独立实例时使用类
-
当你需要支持继承时使用类;模块不支持继承
-
如果你只需要一个东西,就使用模块
-
使用最简单的解决方案;模块通常比类更简单
类的介绍示例
以下是一个简单类的示例。在这个类中,我们看到三个组件:__init__ 方法,它是初始化方法或构造函数,一个称为 toss 的设置方法,以及一个称为 get_sideup 的获取方法。
class Coin():
def __init__(self): # Constructor
self.sideup = "Heads"
def toss(self): # Method
if(random.randint(0, 1) == 0):
self.sideup = "Heads"
else:
self.sideup = "Tails"
def get_sideup(self): # Method
return self.sideup
如何在你的主 Python 脚本中使用它?在你的脚本中,你只需调用对象并将其设置为一个新变量。然后你可以开始使用它的组件。
my_coin = Coin() # Creates the object
my_coin.toss()
print("This side is up: ", my_coin.get_sideup())
让我们来分解一下。
类组件
对象初始化方法
当你看到一个方法具有特殊名称__init__时,你会知道这是对象初始化方法。这被称为构造函数,因为它在内存中构造了对象。当你创建类的对象时,这个方法会自动运行。
class Person():
def __init__(self, name):
self.name = name
我们上面的 __init__ 方法需要一个名为name的参数。当我们使用 Person 类创建对象时,我们应该像这样传入一个名字:Person("Bob")。
self 参数指定它指的是对象本身。记住,类是一个模板,我们可以使用这个模板来初始化(然后稍后修改)多个对象。例如,我们可以用 Person 类创建两个对象:
me = Person("Author")
me.name # --> "Author"
you = Person("Reader")
you.name # --> "Reader"
Getter 和 Setter
一些面向对象语言支持私有对象属性,这些属性不能从外部直接访问。因此,你需要getter和setter方法来读取和写入私有属性的值。
在 Python 中,所有属性和方法都是公开的。我们不需要 getter 和 setter。为了做到“Pythonic”,使用属性。
class Duck():
def __init__(self, input_name):
self.hidden_name = input_name # The user won't know to try duck.hidden_name
def get_name(self): # Getter
print("inside the getter")
return self.hidden_name
def set_name(self, input_name): # Setter
print("inside the setter")
self.hidden_name = input_name
name = property(get_name, set_name)
最后一行将 getter 和 setter 方法定义为name属性的属性。现在它会在以下情况下调用 getter 和 setter 方法:
pet = Duck("Donald")
pet.name
# --> inside the getter
# --> "Harold"
pet.name = "Daffy"
# --> inside the setter
装饰器
装饰器是另一种定义属性的方法(即我们上面做的事情)。
class Duck():
def __init__(self, input_name):
self.hidden_name = input_name # The user won't know to try duck.hidden_name
@property
def name(self): # Getter
print("inside the getter")
return self.hidden_name
@name.setter
def name(self, input_name): # Setter
print("inside the setter")
self.hidden_name = input_name
name = property(get_name, set_name)
隐私命名
首先在名称中使用两个下划线。这使得一旦你创建了对象,属性就无法在类定义外部访问。这也有助于防止子类意外覆盖属性。
在我们的 Duck 类中,代替使用 hidden_name,使用 __name。
self.hidden_name = input_name → self.__name = input_name
类方法
到目前为止,我们演示的都是实例方法。我们怎么知道?实例方法的第一个参数是self。当你调用实例方法时,调用只会影响你正在使用的对象的副本。
类方法影响整个类(因此影响所有对象副本)。类方法使用cls参数,而不是self参数。类方法可以通过使用类装饰器@classmethod来定义。
@classmethod
def count_objects(cls):
print("The class has", cls.count, "objects")
静态方法是第三种类型的方法,它既不影响类也不影响其对象。它不使用 self 或 cls 参数。它只是为了方便而存在。
@staticmethod
def commercial():
print("This product is brought to you by Medium.")
属性
实例属性是我们希望对象实例共享的外部行为。一个学生类可能具有以下属性:
-
方法:student.get_gpa(),student.add_class(),student.get_schedule()
-
数据:student.first_name,student.last_name,student.class_list
dir(object_instance) 给你提供该对象的属性列表。
object_instance.__dict__ 为你提供特定于该实例的所有实例属性(及其值)
类属性是类的属性,而不是类的 实例 的属性。这是类的所有对象共享的属性。假设我们想跟踪每个学生都是人类:
class Student():
isHuman = True # --> class attribute
def __init__(self, ...):
...
如果你想了解更多,可以查看这个关于 Python 类属性的详尽指南。
继承
继承允许你创建一个类的层次结构,其中一个类继承了父类的所有属性和行为。然后,你可以在子类上进行自己的规格定义,这些定义不同于父类。
例如,我们有一个父类 Animal,它具有吃和睡的能力。然后我们创建一个子类 Cat,它继承了 Animal 的属性,并增加了自己特有的属性。
class Animal():
def eat(self):
print("Munch munch")
def sleep(self):
print("Zzz...")
class Cat(Animal):
def meow(self):
print("Meow!")
你需要做的就是将 Animal 类传递给 Cat。现在 Cat 类有了 eat() 和 sleep() 方法。你可以通过在 Cat 中定义方法来覆盖 eat 或 sleep 方法。你也可以通过 __init__() 方法覆盖任何方法。
子类可以添加父类中没有的方法(例如 meow())。父类将不包含此方法。
当子类自己做某些事情但仍需要从父类中获取某些内容时,使用 super():
class Person():
def __init__(self, name):
self.name = name
class EmailPerson(Person):
def __init__(self, name, email):
super().__init__(name)
self.email = email
继承的好处:
-
允许子类重用父类的代码
-
不必从头开始创建类,你可以专门化或扩展一个类
-
父类可以定义一个接口,以允许子类与程序进行交互
-
允许程序员组织相关对象
总结
-
类是一个模具(冰棒托),对象是从该类中创建的(冰棒)
-
对象可以调用其类的实例方法(使用 self)来接收和更改数据
-
隐私命名有助于防止子类意外覆盖属性
-
类本身具有方法(使用 cls),你可以跟踪和操作该类的所有对象实例
-
继承允许我们扩展相似的类
Python OPP 以及为何 repr() 和 str() 重要
原文:
towardsdatascience.com/python-opp-and-why-repr-and-str-matter-1cff584328f4
PYTHON 编程
这篇文章探讨了使用 repr() 和 str() 为 Python 类提供的各种面貌
·发表于 Towards Data Science ·14 分钟阅读·2023 年 11 月 3 日
--

Python 类需要字符串表示,以便向用户和开发者提供比一堆字母更多的信息。图片由 Surendran MP 在 Unsplash 提供
Python 类有许多面貌。例如,你可以创建一个空类:
class MyClass:
pass
它仍然可以有所用处,例如作为 哨兵值。你可以添加一个 __init__() 方法:
class MyClass:
def __init__(self, value):
self.value = value
这仍然会是一个非常简单的类,但这次它将保持一个特定的值。
Python 类的一个极佳功能是它们可以被用作类型,如下所示:
def foo(x: MyClass, n: int) -> list[MyClass]:
return [x] * n
记住,不实现 __init__() 方法并不意味着它不存在。实际上,我们上面重载了 __init__() 方法,而不仅仅是实现了它。这是 Python 类的另一个重要方面,你应该知道:你可以重载许多其他方法,如 __new__()、__eq__() 和 __setattr__()。如果你不重载这些方法,有些方法会有默认实现(如 __init__()、__new__()、__setattr__() 和 __eq__()),而其他方法则没有(如 __lt__() 和所有其他比较方法,除了 __eq__()、__getitem__()、__setitem__() 和 __len__())。
一个类可以继承另一个类,如下所示:
class MyClass(dict):
@staticmethod
def say_hello(self):
print("Hello!")
同样如上所述,它可以使用静态方法,也可以使用类方法。你可以创建混合类和抽象基类、单例模式,还可以做许多其他事情,有时非常有用。
Python 类有很多面向不同的特性,详细讨论每一种特性需要几年时间,我们将在未来的文章中进行探讨。在这篇文章中,我们将重点关注一个特定方面:__repr__() 和 __str__() 方法的区别和作用。
初看起来,你可能认为这是一个小话题,但实际上它非常重要。实现一个 Python 类很容易,但实现一个 好的 Python 类则需要更多的努力。正是这些小细节使得一个熟练的 Python 开发者与普通开发者有所区别。
注意:为了运行 doctests,我使用了 Python 3.11。不要惊讶于较旧版本的 Python 可能会提供稍微不同的结果。如果你想了解更多关于 Python doctest 的内容,请阅读以下文章:
Python 文档测试与 doctest:简单方法
doctest 允许进行文档测试、单元测试、集成测试以及测试驱动开发。
towardsdatascience.com
repr 与 str
理论上,repr() 应该返回一个明确的对象字符串表示,从中你应该能够重建对象。另一方面,str() 应该返回一个人类可读的对象字符串表示。
因此,理论上,repr() 应该提供有关其所用对象的详细信息,而 str() 应该提供一个可读的字符串来解释对象是什么以及可能包含什么。例如,我们使用 str() 来查看交互式会话中的对象或用于日志记录。但当我们调试并需要更多细节时,repr() 是更好的选择。正如我们将在下一部分看到的,我们通常会间接调用这些函数,甚至可能不知道这一点——或者至少没有想到这一点。
我们在上面比较了 repr() 和 str() 函数。要在类中实现或重载它们,我们需要使用相应的方法,分别是 __repr__() 和 __str__()。如果一个类定义了 __repr__() 方法,当你调用 repr() 时,它会用于生成该类对象的字符串表示。str() 和 __str__() 也是如此。
我们很快就会看到这一点——首先让我们了解一下我提到的间接调用 repr() 和 str() 的含义。
间接调用 repr() 和 str()
有一个与这两个函数相关的秘密,了解它是很有帮助的。请考虑以下代码:
>>> class StrAndRepr:
... def __repr__(self): return f"I am __repr__"
... def __str__(self): return "I am __str__"
>>> str_and_repr = StrAndRepr()
>>> str_and_repr
I am __repr__
>>> print(str_and_repr)
I am __str__
注意最后两个调用。正如你所看到的,在 Python 会话中使用 print() 打印对象与仅使用对象名称之间可能会有所不同。

在 Python 会话中调用 print(obj) 和仅调用对象名之间的区别。图片由作者提供
下图总结了这一点:print(obj)调用str(obj),而obj调用repr(obj)。
repr与str
上面,我解释了repr()和__repr__()以及str()和__str__()的概念。前一对应该提供比后一对更多的信息。
然而,实践中往往显示出不同的情况:
>>> class MyClass: ...
>>> inst = MyClass()
>>> inst.__repr__()
'<__main__.MyClass object at 0x7f...>'
>>> inst.__str__()
'<__main__.MyClass object at 0x7f...>'
>>> inst.__repr__() == repr(inst)
True
>>> inst.__str__() == str(inst)
True
如你所见,这两个方法的默认实现是相同的:
>>> str(inst) == repr(inst)
True
因此,即使是默认实现的__str__()和__repr__(),当你在 Python 类中没有重载这两个方法时,也违反了上述规则。此外,开发者可以重载这两个方法中的一个或两个,而在实际应用中,这也可能意味着违反这一规则。
当仅实现其中一个方法时会发生什么?为了展示这一点,我将实现以下四个类:
>>> class StrAndRepr:
... def __repr__(self): return "I am repr of StrAndRepr"
... def __str__(self): return "I am str of StrAndRepr"
>>> class OnlyStr:
... def __str__(self): return "I am str of OnlyStr"
>>> class OnlyRepr:
... def __repr__(self): return "I am repr of OnlyRepr"
>>> class NeietherStrNorRepr: ...
因此,我们定义了四个类:一个既没有__str__()也没有__repr__(),两个有其中一个,和一个两个都有。让我们看看如果我们对它们的实例调用str()和repr()会发生什么:
>>> str_and_repr = StrAndRepr()
>>> str(str_and_repr)
'I am str of StrAndRepr'
>>> repr(str_and_repr)
'I am repr of StrAndRepr'
>>> only_str = OnlyStr()
>>> str(only_str)
'I am str of OnlyStr'
>>> repr(only_str)
'<__main__.OnlyStr object at 0x7f...>'
>>> only_repr = OnlyRepr()
>>> str(only_repr)
'I am repr of OnlyRepr'
>>> repr(only_repr)
'I am repr of OnlyRepr'
>>> neither_str_nor_repr = NeietherStrNorRepr()
>>> str(neither_str_nor_repr)
'<__main__.NeietherStrNorRepr object at 0x7...>'
>>> repr(neither_str_nor_repr)
'<__main__.NeietherStrNorRepr object at 0x7f...>'
以下是上述doctest的结论:
-
实现既没有
__str__()也没有__repr__():对于两者,都会使用默认实现;它们是一样的,都提供类的名称和实例的地址。 -
实现
__str__()和__repr__():通常,这是一种推荐的方法。它使你的代码更具可读性和可维护性——尽管同时也更长。 -
仅实现
__str__():Python 会在str()中使用它,但对于repr()将使用默认实现。 -
仅实现
__repr__():Python 会将其用于str()和repr()。
那么,我应该实现什么呢?
这要看情况。最明显的结论是,如果你实现了一个复杂的类,你应该定义两个这些方法。这将给你更多的调试代码和更好的日志记录的机会。
然而,当你没有太多时间编程而截止日期临近时,你至少应该实现其中一个方法。不实现任何方法意味着类的字符串表示将包含很少的有用信息,因为它将包含类的名称和实例的地址。因此,只有在你确定类的名称是你需要的全部信息时才这样做。例如,在原型设计中,这通常是你需要的全部信息。
对于小类,实现其中一个方法可能就足够了,但一定要确保这确实足够。此外,你有多久会没有时间来实现像__str__()或__repr__()这样简单的方法?我知道这种情况可能会发生——但我认为这种情况不会比偶尔发生的多。说实话,在我超过五年的 Python 开发中,这种情况甚至没有发生过一次。
因此,我认为时间很少是一个问题。而空间,另一方面,可能是。当你的模块包含多个小类,每个类占用几行时,为所有这些类实现__repr__()和__str__()可能会使模块的长度增加一倍。这可能会带来很大差异,因此值得考虑是否需要这两个方法,如果不需要,应该实现哪个方法。
许多内置类使用相同的实现来处理__repr__()和__str__(),包括dict和list。许多来自知名附加包的类也是如此,一个来自数据科学领域的完美例子是pandas.DataFrame。
让我们总结一下我们的讨论,形成一套规则。说实话,尽管我已经使用它们很多年了,这还是我第一次想到将它们写下来。我希望你能在编码实践中找到它们的用处,以决定是否实现__repr__()和__str__()这两个方法中的一个或两个,或者都不实现。
-
当你编写一个原型类且不打算使用它的字符串表示时,可以忽略
__repr__()和__str__()。然而,对于生产代码,最好再三考虑。在开发过程中,除非需要通过类的实例调试代码,否则我通常会跳过这些方法。 -
当你的类生成具有多个属性的复杂实例时,我通常会考虑同时实现
__repr__()和__str__()。然后:(i)__str__()方法应提供一个简单的人类可读的字符串表示,这可以通过使用print()和str()函数打印实例来获得。(ii)__repr__()方法应提供尽可能多的信息,包括重建类实例所需的所有信息;这可以通过repr()函数或在交互式会话中输入实例名称来获得。 -
如果你的类需要用于调试,无论是否实现
__str__(),都要使其__repr__()方法尽可能详细。这并不意味着__repr__()的输出必须异常长;而是,在这种情况下,包含调试所需的任何信息。 -
当一个类需要一个人类可读的字符串表示,并且同时你需要实现详细的
__repr__()方法时,实现__str__()。 -
如果一个类需要一个人类可读的字符串表示,但你不需要详细的
__repr__(),则仅实现__repr__()。这将使用户从两个方法中获得一个不错的人类可读字符串表示,并避免看到默认的__repr__()表示,通常这没有太大价值。在仅实现__repr__()时,保持返回字符串格式的一致性很重要,这将使用户更容易阅读和理解str()和repr()的输出。
实现__repr__()和__str__()
现在我们知道了何时实现这两种方法,值得考虑如何实现它们。只有两个规则是你必须遵守的,而且幸运的是,这两个规则都很简单。
第一个处理方法的参数,另一个处理其返回值的类型。因此,我们可以使用这两种方法的预期签名来展示它们,即:
def __repr__(self) -> str:
...
def __str__(self) -> str:
...
这就全部了吗?
基本上,是的——但是……我写这些是预期的签名,但实际上,你应该将它们视为必需的签名。下面你会明白为什么。
为了了解原因,你应该知道一个有趣的事情,我猜很多 Python 用户可能不知道。就我而言,我在很长一段时间内也不知情。
这个规则适用于当你希望类的__str__()与str()和print()配合使用,而__repr__()与repr()以及在会话中使用实例名称时。为此,让我们实现一个具有非可选参数的__str__()的类:
>>> class StrWithParams:
... def __str__(self, value):
... return f"StrWithParams with value of {value}"
这个方法会工作吗?
>>> inst = StrWithParams()
>>> inst.__str__(10)
'StrWithParams with value of 10'
嘿,它确实适用!那我怎么刚才写了__str__()应该不接受参数呢?
理论上不会——尽管它是可能的。这在一个不切实际的条件下才会发生,即唯一调用该方法的方式是inst.__str__(10)(值本身并不重要)。如上所述,我们看到过这样的调用,它确实有效。但接下来我们将看到三个痛苦的失败:
>>> str(inst, value=10)
Traceback (most recent call last):
...
TypeError: 'value' is an invalid keyword argument for str()
>>> print(inst)
Traceback (most recent call last):
...
TypeError: StrWithParams.__str__() missing 1 required positional argument: 'value'
>>> print(inst, value=10)
Traceback (most recent call last):
...
TypeError: 'value' is an invalid keyword argument for print()
因此,使用参数的__str__()不是语法错误,但它绝对是一个静态错误。

来自 Visual Studio Code 的截图。Sonarlint 显示__str__()不应该接受参数。图片由作者提供
这绝对是一个静态错误,但如上所示,一个更大的问题是,使用参数的__str__()很可能会在运行时引发TypeError异常,如上所示。
在会话中直接键入inst会调用repr(),由于我们没有实现它,使用了默认实现:
>>> inst
<__main__.StrWithParams object at 0x7f...>
但如前所示,调用print(inst)失败了,原因很简单,因为没有直接提供非可选参数value的方式。
现在,让我们转到另一个问题,即返回一个非字符串类型的对象。这似乎是一个静态错误。我们考虑两种版本:未类型化和类型化的类定义:

来自 Visual Studio Code 的截图。基于未类型化的类定义,Sonarlint 显示__str__()应该返回一个字符串。图片由作者提供

来自 Visual Studio Code 的截图。基于类型化的类定义,Mypy 显示__str__()应该返回一个字符串。图片由作者提供
因此,从__str__()方法返回非字符串对象绝对是一个静态错误——但这是否也会导致在运行时引发TypeError异常呢?
是的,会的:
>>> class StrNotStr:
... def __str__(self):
... return 10
>>> inst = StrNotStr()
>>> inst.__str__()
10
>>> str(inst)
Traceback (most recent call last):
...
TypeError: __str__ returned non-string (type int)
__repr__() 的规则是一样的:
>>> class ReprWithParams:
... def __repr__(self, value):
... return f"ReprWithParams with value of {value}"
>>> inst = ReprWithParams()
>>> inst.__repr__(10)
'ReprWithParams with value of 10'
>>> repr(inst, value=10)
Traceback (most recent call last):
...
TypeError: repr() takes no keyword arguments
>>> inst
Traceback (most recent call last):
...
TypeError: ReprWithParams.__repr__() missing 1 required positional argument: 'value'
>>> class ReprNotStr:
... def __repr__(self):
... return 10
>>> inst = ReprNotStr()
>>> inst.__repr__()
10
>>> repr(inst)
Traceback (most recent call last):
...
TypeError: __repr__ returned non-string (type int)
因此,请记住不要为 __repr__() 和 __str__() 使用参数,并记住它们都应返回字符串。但也值得记住,当你违反这两个规则中的任何一个时会发生什么。
自定义类示例
如上所述,当你实现一个复杂的自定义类时,通常应实现 __str__() 和 __repr__(),并且它们应有所不同。
在这个上下文中,“复杂”是什么意思?它可能意味着不同的东西,但在下面的例子中,它意味着类包含一些不需要在常规字符串表示中包含的属性,但我们可能希望在调试或日志记录时包含它们。
我们将实现一个流行的 Point 类,但我们会使它更复杂一些:
-
它的主要属性是
x和y,定义点的坐标。 -
它还有一个可选的
group属性,用于定义实例的组成员身份;它可以是像著名的 Iris 数据集中的物种这样的组。 -
你还可以给类的实例添加评论。它可以是任何评论,例如“纠正组”,“双重检查坐标”或“可能的错误”。评论不会用于比较—只是作为关于特定点的信息来源;我们将在下面的代码中看到这一点。
这是 Point 类的实现:
from typing import Optional
class Point:
def __init__(
self,
x: float,
y: float,
group: Optional[str] = None,
comment: Optional[str] = None) -> None:
self.x = x
self.y = y
self.group = group
self.comment = comment
def distance(self, other: "Point") -> float:
"""Calculates the Euclidean distance between two Point instances.
Args:
other: Another Point instance.
Returns:
The distance between two Point instances, as a float.
>>> p1 = Point(1, 2)
>>> p2 = Point(3, 4)
>>> p1.distance(p2)
2.8284271247461903
>>> p1.distance(Point(0, 0))
2.23606797749979
"""
dx = self.x - other.x
dy = self.y - other.y
return (dx**2 + dy**2)**.5
def __str__(self) -> str:
"""String representation of self.
>>> p1 = Point(1, 2, "c", "Needs checking")
>>> p1
Point(x=1, y=2, group=c)
Comment: Needs checking
>>> print(p1)
Point(1, 2, c)
When group is None, __str__() and __repr__() will
provide different representations:
>>> p2 = Point(1, 2, None)
>>> p2
Point(x=1, y=2, group=None)
>>> print(p2)
Point(1, 2)
"""
if self.group is not None:
return f"Point({self.x}, {self.y}, {self.group})"
return f"Point({self.x}, {self.y})"
def __repr__(self) -> str:
msg = (
f"Point(x={self.x}, y={self.y}, "
f"group={self.group})"
)
if self.comment is not None:
msg += (
"\n"
f"Comment: {self.comment}"
)
return msg
def __eq__(self, other) -> bool:
"""Compare self with another object.
Group must be provided for comparisons.
Comment is not used.
>>> Point(1, 2, "g") == 1
False
>>> Point(1, 2, "c") == Point(1, 2, "c")
True
>>> Point(1, 2) == Point(1, 2)
False
>>> Point(1, 2) == Point(1, 3, "s")
False
"""
if not isinstance(other, Point):
return False
if self.group is None:
return False
return (
self.group == other.group
and self.x == other.x
and self.y == other.y
)
if __name__ == "__main__":
import doctest
doctest.testmod()
让我们分析一下 __repr__() 和 __str__() 之间的区别:
细节的层次
如上所述,这种评论通常在类实例的常规字符串表示中不是必需的。因此,我们不需要在 __str__() 中包含它们。然而,当我们进行调试时,评论可以非常有用,尤其是当它们提供有关特定类实例的重要信息时。
这就是为什么我们应该在 __repr__() 中包含评论,但在 __str__() 中不包含评论的原因。请考虑这个例子:
>>> p1 = Point(1, 2, "c", "Needs checking")
>>> p1
Point(x=1, y=2, group=c)
Comment: Needs checking
>>> print(p1)
Point(1, 2, c)
更详细的图片
在我们的实现中,这两个方法提供了类实例的不同视图。比较
Point(x=1, y=2, group=c)
Comment: Needs checking
通过
'Point(1, 2, c)'
除了提供评论外,__repr__() 通过提供属性名称,提供比 __str__() 更详细的图片。在这个特定的类中,这可能差别不大,但当一个类有更多的属性需要包括在字符串表示中,并且它们的名称比这里更长时,差异可能会更加明显。即使在这里,__str__() 提供的信息也比 __repr__() 更简洁。
从 __repr__() 重建实例
我们也提到过这一点。如果可能的话,提供在 __repr__() 中所需的所有信息以重建实例是一个好的实践。在这里,__str__() 对我们来说还不够:
>>> str(p1)
'Point(1, 2, c)'
>>> p1_recreated_from_str = Point(1, 2, "c")
>>> p1
Point(x=1, y=2, group=c)
Comment: Needs checking
>>> p1_recreated_from_str
Point(x=1, y=2, group=c)
在这里,评论没有用于比较实例,因此 p1 == p1_recreated_from_str 返回 True 这并不重要:
>>> p1 == p1_recreated_from_str
True
这只是说明从用户的角度来看这两个实例是相等的。然而,从开发者的角度来看,它们并不相同:p1 不 等于 p1_recreated_from_str。如果我们想要完全重建p1,我们需要使用其__repr__()表示形式:
>>> p1
Point(x=1, y=2, group=c)
Comment: Needs checking
>>> p1_recreated_from_repr = Point(
... 1, 2, "c", comment="Needs checking")
>>> p1_recreated_from_repr
Point(x=1, y=2, group=c)
Comment: Needs checking
结论
我希望阅读这篇文章能帮助你理解repr()和str()之间,以及__repr__()和__str__()之间的微妙差异。这样的细微差别可能对中级 Python 用户不是必需的,但如果你想成为高级 Python 用户或开发者,这正是你需要在日常编码中了解并使用的。
这只是冰山一角,但我不会仅仅停留在这里。我们之前讨论过 Python 的这些细微之处,未来的文章中我们会进一步探讨。
感谢阅读。如果你喜欢这篇文章,你可能也会喜欢我写的其他文章;你可以在这里看到它们。如果你想加入 Medium,请使用下面的推荐链接:
## 使用我的推荐链接加入 Medium - Marcin Kozak
作为一个 Medium 会员,你的一部分会费会分配给你阅读的作者,并且你可以完全访问每一个故事……
解释 Python ord() 和 chr() 函数
原文:
towardsdatascience.com/python-ord-and-chr-functions-explained-dcb39944c480
在这篇文章中,我们将探讨如何使用 Python ord() 和 chr() 函数。
·发布于 Towards Data Science ·阅读时长 3 分钟·2023 年 1 月 12 日
--

由 Brett Jordan 提供的照片,刊登在 Unsplash
目录
-
介绍
-
使用 ord() 将字符转换为 Unicode 代码点
-
使用 ord() 将字符串转换为 Unicode 代码点
-
使用 chr() 将整数转换为 Unicode 字符
-
结论
介绍
Python ord() 函数是一个内置函数,它返回指定字符的 Unicode 代码点。
Unicode 代码点是一个整数,用于表示 Unicode 标准 中的字符。
ord() 函数的处理定义如下:
ord(character) -> Unicode code
其中 character 是一个 Unicode 字符。
Python chr() 函数是一个内置函数,它返回指定字符的 Unicode 代码点。
chr() 函数的处理定义如下:
chr(integer) -> Unicode character
使用 ord() 将字符转换为 Unicode 代码点
让我们尝试使用 ord() 函数来查找字母 A、B 和 C 的 Unicode 代码点:
#UCP of letter A
a = ord('A')
#UCP of letter B
b = ord('B')
#UCP of letter C
c = ord('C')
#Print values
print(a)
print(b)
print(c)
你应该得到:
65
66
67
每个整数代表一个 Unicode 字符。
你可以使用 ord() 函数查找其他字符的 Unicode 代码点,包括特殊字符。
使用 ord() 将字符串转换为 Unicode 代码点
注意 ord() 函数只能接受一个字符作为参数,如 介绍 中提到的:
ord(character) -> Unicode code
如果你尝试将其用于一个包含多个字符的字符串,你会得到一个 TypeError:
#UCP of string
x = ord('Python')
你应该得到:
TypeError: ord() expected a character, but string of length 6 found
那么我们如何将整个字符串转换为 Unicode 代码点呢?
我们需要逐个字符地处理它,有几种方法可以解决这个任务:
-
使用 Python 的map()函数
-
使用列表推导式
使用 ord() 和 map() 将字符串转换为 Unicode 代码点
使用Python map() 函数我们可以对字符串的每个元素应用 Python ord() 函数:
#Define a string
py_str = 'Python'
#UCP of string
ucp_vals = list(map(ord, py_str)
#Print UCP values
print(ucp_vals)
你应该得到:
[80, 121, 116, 104, 111, 110]
使用 ord() 和列表推导式将字符串转换为 Unicode 代码点
解决这个任务的另一种方法是使用 Python 中带有列表推导式的ord()函数:
#Define a string
py_str = 'Python'
#UCP of string
ucp_vals = [ord(char) for char in py_str]
#Print UCP values
print(ucp_vals)
你应该得到:
[80, 121, 116, 104, 111, 110]
使用 chr() 将整数转换为 Unicode 字符
你也可以通过使用chr()函数来逆转ord()函数的操作,它将一个 Unicode 代码点(以整数格式)转换为一个 Unicode 字符。
例如,让我们看看 97、98 和 99 的 Unicode 代码点代表了哪些字符:
#UCP of letter A
c1 = chr(97)
#UCP of letter B
c2 = chr(98)
#UCP of letter C
c3 = chr(99)
#Print values
print(c1)
print(c2)
print(c3)
你应该得到:
a
b
c
结论
在这篇文章中,我们探讨了如何使用 Python 的ord()和chr()函数。
现在你知道了基本功能,你可以通过与其他可迭代的数据结构一起练习,以应对更复杂的用例。
如果你有任何问题或对某些编辑有建议,请随时在下方留言,并查看更多我的Python 函数教程。
原文发布于 https://pyshark.com 于 2023 年 1 月 12 日。
Python Pandas 到 Polars:数据过滤
原文:
towardsdatascience.com/python-pandas-to-polars-data-filtering-a67ccb70a8b3
你可能需要尽快做出转变
·发布在 Towards Data Science ·5 分钟阅读·2023 年 4 月 18 日
--

照片由 Daphné Be Frenchie 拍摄,发布在 Unsplash
我非常欣赏 Pandas。我从开始学习数据科学的第一天起就一直在使用它。Pandas 在数据清洗、预处理和分析的大多数任务中已经绰绰有余。
我对 pandas 唯一的不满是在处理大数据集时。Pandas 进行内存分析,所以当数据量变得非常大时,它的性能开始下降。
另一个与数据大小相关的缺点是某些操作会产生中间副本。因此,为了能够高效地工作,数据集应相对较小于内存。
对于如此大的数据集,存在不同的替代方案。最近获得显著人气的替代方案之一是 Polars。
有大量文章关注 Polars 与 pandas 的速度比较,但很少有从实际角度解释如何使用 Polars 执行常见的数据清洗和处理操作的文章。
在这一系列文章中,我将向你展示一些常用 Pandas 函数的 Polars 版本。第一个主题是数据过滤操作。在开始示例之前,让我们简要提及一下 Polars 的优势。
Polars 提供了什么?
Polars 是一个用于 Rust 和 Python 的 DataFrame 库。
-
Polars 利用你计算机上的所有可用核心,而 pandas 仅使用单个 CPU 核心来执行操作。
-
Polars 相较于 pandas 更加轻量,并且没有依赖项,这使得导入 polars 的速度非常快。导入 polars 只需 70 毫秒,而导入 pandas 需要 520 毫秒。
-
Polars 进行查询优化,以减少不必要的内存分配。它还能够以流式方式部分或完全处理查询。因此,polars 可以处理比机器上可用 RAM 更大的数据集。
使用 pandas 和 polars 进行数据过滤
我们将通过几个示例来学习如何过滤 polars DataFrames。我们还将看到相同操作的 pandas 版本,以便于从 pandas 过渡到 polars。
首先,我们将创建一个 DataFrame 来进行操作。我们将使用我准备的示例数据集。你可以从我的 数据集 仓库下载。
# pandas
import pandas as pd
# read csv
df_pd = pd.read_csv("datasets/sales_data_with_stores.csv")
# display the first 5 rows
df_pd.head()

pandas DataFrame 的前 5 行 (图片由作者提供)
# polars
import polars as pl
# read_csv
df_pl = pl.read_csv("datasets/sales_data_with_stores.csv")
# display the first 5 rows
df_pl.head()

polars DataFrame 的前 5 行 (图片由作者提供)
pandas 和 polars 都有相同的函数来读取 csv 文件并显示 DataFrame 的前 5 行。Polars 还显示了列的数据类型和输出的形状,我认为这是一个很有用的附加功能。
示例 1:按数值过滤
让我们过滤价格高于 750 的行。
# pandas
df_pd[df_pd["cost"] > 750]
# polars
df_pl.filter(pl.col("cost") > 750)
我将仅展示 pandas 或 polars 版本的输出,因为它们是相同的。

(图片由作者提供)
示例 2:多个条件
pandas 和 polars 都支持按多个条件过滤。我们可以使用“and”和“or”逻辑来组合这些条件。
让我们过滤价格大于 750 且商店值为 Violet 的行。
# pandas
df_pd[(df_pd["cost"] > 750) & (df_pd["store"] == "Violet")]
# polars
df_pl.filter((pl.col("cost") > 750) & (pl.col("store") == "Violet"))

(图片由作者提供)
示例 3:isin 方法
pandas 的 isin 方法可以用来将行值与一组值进行比较。当条件由多个值组成时,它非常有用。polars 版本的方法是“is_in”。
我们可以按照如下方式选择 PG1、PG2 和 PG3 的行:
# pandas
df_pd[df_pd["product_group"].isin(["PG1", "PG2", "PG5"])]
# polars
df_pl.filter(pl.col("product_group").is_in(["PG1", "PG2", "PG5"]))
输出的前 5 行:

(图片由作者提供)
示例 4:选择部分列
要选择一部分列,我们可以将列名列表传递给 pandas 和 polars DataFrames,如下所示:
cols = ["product_code", "cost", "price"]
# pandas (both of the following do the job)
df_pd[cols]
df_pd.loc[:, cols]
# polars
df_pl.select(pl.col(cols))
输出的前 5 行:

(图片由作者提供)
示例 5:选择部分行
我们可以使用 loc 或 iloc 方法来选择 pandas 的部分行。在 polars 中,我们使用非常类似的方法。
这是一个简单的示例,选择第 10 行到第 20 行之间的行:
# pandas
df_pd.iloc[10:20]
# polars
df_pl[10:20]
要选择相同的行但仅选择前三列:
# pandas
df_pd.iloc[10:20, :3]
# polars
df_pl[10:20, :3]
如果我们想通过名称选择列,可以使用 pandas 中的 loc 方法。
# pandas
df_pd.loc[10:20, ["store", "product_group", "price"]]
# polars
df_pl[10:20, ["store", "product_group", "price"]]
示例 6:按数据类型选择列
我们还可以选择特定数据类型的列。让我们做一个选择具有 64 位整数(即 int64)数据类型的列的示例。
# pandas
df_pd.select_dtypes(include="int64")
# polars
df_pl.select(pl.col(pl.Int64))
输出的前 5 行:

(图片由作者提供)
我们做了几个示例来比较Pandas和Polars之间的过滤操作。总体而言,Polars与Pandas非常相似,但在某些情况下采用了类似于Spark SQL的方法。如果你对使用Spark SQL进行数据清洗和操作很熟悉,你会发现这些相似之处。
话虽如此,考虑到在处理大型数据集时Polar的效率,它可能很快成为取代Pandas进行数据清洗和操作任务的有力候选者。
你可以成为 Medium 会员 以解锁我所有的写作内容,以及 Medium 的其他内容。如果你已经是会员了,请不要忘记 订阅 ,以便在我发布新文章时收到电子邮件。
感谢阅读。如果你有任何反馈,请告诉我。
Python 怪癖:了解如何通过一个不返回任何东西的函数来修改变量
深入了解 Python 如何传递参数和可变性,以防止意外错误
·发布于 Towards Data Science ·8 分钟阅读·2023 年 4 月 13 日
--

跟踪意外的错误(图片来自 cottonbro studio on Pexels)
在这篇文章中,我们将戴上侦探帽,解开一个“Python 神秘”。在这一集里,我们将了解一个不返回值的函数如何改变一个变量。(下面有示例)。不仅如此:它只对某些类型的变量‘有效’。此外,这种行为很容易让人陷入陷阱,因此了解其原因非常重要。
我们将重点理解神秘背后的机制。更好地理解 Python 不仅会让你成为更优秀的开发者,还会节省你解决难以理解的错误的沮丧。让我们开始编程吧!
神秘——一个例子
首先让我们更深入地分析一下我们的“Python 神秘”:假设我们有两个函数:
-
接受 一个变量
-
修改 该变量
-
不要返回 该变量
def change_string(input_string:str) -> None:
""" Notice that this functions doesn't return anything! """
input_string += 'a'
def change_list(input_list:list) -> None:
""" Notice that this functions doesn't return anything! """
input_list.append('a')
对于这两个函数,我们定义一个变量,打印出来,调用函数并传递变量,然后再次打印出来
my_str = 'hello'
print(my_str) # 'hello'
change_string(input_string=my_str)
print(my_str) # 'hello'
my_list = ['hello']
print(my_list) # ['hello']
change_list(input_list=my_list)
print(my_list) # ['hello', 'a'] !?
发生了什么?为什么my_list变量改变了,而my_str变量没有?尽管这些函数没有返回任何东西!我们有三个问题,将在三个相应的章节中解答:
-
函数如何“访问”变量?
-
为什么列表被修改而字符串没有改变?
-
我们如何防止这种行为?
通过同时做多件事来加快你的程序
towardsdatascience.com
1. 函数如何访问变量
为了弄清楚这一点,我们需要理解变量是如何进入函数的:我们需要了解 Python 是如何将变量传递给函数的。有很多种方法可以做到这一点。为了理解 Python 如何将变量传递给函数,我们首先需要了解 Python 如何在内存中存储值。
1.1 Python 如何存储变量
你可能会认为当我们定义一个变量时,比如:person = 'mike',内存中有一个名为 ‘person’ 的对象,其值为 ‘mike’(参见下面的图片)。这只是部分正确。

变量在 Python 和其他语言(例如 C)的内存存储方式(由作者专业绘制)
Python 使用 引用。它在内存中创建一个对象,然后创建一个名为 ‘person’ 的引用,指向内存中的对象,具体的内存地址和值是 ‘mike’。可以把它看作是在对象上挂一个标签,这个标签上写着变量的名字。
如果我们做类似这样的操作:person2 = person,我们不会在内存中创建一个新对象,只是创建了一个名为‘person2’的新引用,指向已经存在的内存中的对象:

创建一个新的引用,指向相同的对象(图片由作者提供)
重新定义 person2 = ‘bert' 将导致 Python 在内存中创建一个新对象,并将名为“person2”的引用指向那里:

1.2 Python 是传递对象还是引用给函数?
理解一个关键点是,当我们调用 somefunction(person) 时 我们并没有给函数一个内存中的对象,而只是该对象的引用。
Python 变量是 “按引用” 传递的,而不是 “按值” 传递的。
这是解决谜团的第一个答案:我们给函数提供了一个内存中值的引用,而不是给函数提供一个 对象的副本。这就是为什么我们可以修改值 而不需要函数返回 任何东西。
现在让我们来看解决方案的另一部分:为什么有些变量可以被修改而有些不能。
## 参数与关键字参数:哪种方式在 Python 中调用函数最快?
timeit 模块的清晰演示
[towardsdatascience.com
2. 为什么有些值可以被改变而有些不能? — 可变性
可变性是对象在创建后改变其值的能力。让我们首先了解一下可变变量:
IMMUTABLE MUTABLE
int, float, decimal, complex (numbers) list
bool set
str dict
tuple
frozenset
正如你所见,str是不可变的;这意味着它在初始化后不能改变。那么我们之前如何“修改”了我们的字符串(例如:input_string += ‘a')。接下来的部分解释了当我们尝试更改和覆盖可变和不可变值时会发生什么。
看一看背后的机制,了解 Python 的瓶颈所在
[towardsdatascience.com
2.1 当我们尝试更改不可变值时会发生什么?
我们创建了一个名为my_str的变量,值为'a'。接下来,我们使用id函数打印变量的内存地址。这是引用指向的内存位置。
重申一下:在下面的例子中,我们创建了一个引用,名为my_str,它指向一个内存中的对象,该对象的值为'a',并位于内存地址 1988650365763。
my_str = 'a'
print(id(my_str)) # 1988650365763
my_str += 'b'
print(id(my_str)) # 1988650363313
接下来,在第 3 行,我们将'b'添加到my_str中,并再次打印内存位置。如你所见,通过内存位置的变化,my_str在添加了'b'后变得不同。这意味着在内存中创建了一个新对象。
看起来 Python 似乎在更改字符串,但实际上它只是创建了一个新的内存对象,并将名为my_str的引用指向那个新对象。值为'a'的旧对象将被移除。查看这篇文章了解更多关于为什么 Python 不直接覆盖内存中的对象以及旧值如何被移除的内容。
2.2 当我们尝试更改可变值时会发生什么?
让我们用一个可变变量做同样的实验:
my_list= ['a']
print(id(my_list)) # 1988503659344
my_list.append('b')
print(id(my_list)) # 1988503659344
所以名为my_list的引用仍然指向内存中对象所在的同一位置。这证明了内存中的对象已经改变!还要注意,列表中的元素可以包含不可变类型。如果我们尝试更改这些变量,情况与之前所述相同。
2.3 当我们尝试覆盖变量时会发生什么?
正如我们在前面的部分所看到的,Python 不会覆盖内存中的对象。让我们看看实际效果:
# Immutable var: string
my_str = 'a'
print(id(my_str)) # 1988650365936
my_str = 'b'
print(id(my_str)) # 1988650350704
# Mutable var: list
my_lst = ['a', 'list']
print(id(my_lst)) # 1988659494080
my_lst = ['other', 'list']
print(id(my_lst)) # 1988659420608
如你所见,所有内存位置都发生了变化,包括可变和不可变的变量。这是 Python 处理变量的默认方式。注意我们并没有尝试改变可变列表的内容:我们定义了一个新的列表;我们并不是在改变它,而是将完全新的数据分配给my_lst。
2.4 为什么有些值是可变的而有些不是?
可变性通常是设计选择;一些变量保证内容保持不变并且有序。
## 入门 Cython:如何在 Python 中每秒进行超过 1.7 亿次计算
将 Python 的简便性与 C 的速度结合
towardsdatascience.com
解决方案:按引用传递和可变性的实际操作
在这一部分,我们将运用新学到的知识来解决谜题。在下面的代码中,我们声明了一个(可变的)列表,并将其(通过引用)传递给一个函数。然后函数能够更改列表的内容。我们可以通过以下事实看到这一点:内存地址在第 3 行和最后一行是相同的,而内容已经改变:
# 1\. Define list and check out the memory-address and content
my_list = ['a', 'list']
print(id(my_list), my_list) # 2309673102336 ['a', 'list']
def change_list(input_list:list):
""" Adds value to the list but don't return the list """
print(id(input_list), input_list) # 2309673102336 ['a', 'list']
input_list.append('b')
print(id(input_list)) # 2309673102336 ['a', 'list', 'b']
# 2\. Pass the list into our function (function doesn't return anything)
change_list(input_list=my_list)
# 3\. Notice that the memory location is the same and the list has changed
print(id(my_list), my_list) # 2309673102336 ['a', 'list', 'b']
这如何与不可变值一起工作?
好问题。让我们用一个不可变的元组来检查一下:
# 1\. Define a tuple, check out memory address and content
my_tup = {'a', 'tup'}
print(id(immutable_string), my_tup) # 2560317441984, {'a', 'tup'}
def change_tuple(input_tuple:tuple):
""" 'overwrites' the tuple we received, don't return anything """
print(id(input_tuple)) # 2560317441984, {'a', 'tup'}
input_tuple = ('other', 'tuple')
print(id(input_tuple)) # 2560317400064, {'other', 'tup'}
# 2\. Pass the list into our function (nothing is returned from function)
change_tuple(input_tuple=immutable_tuple)
# 3\. Print out memory location and content again
print(id(my_tup), my_tup) # 2560317441984, {'a', 'tup'}
由于我们不能改变值,我们必须在change_tuple函数中“覆盖”input_tuple。这并不意味着内存中的对象被覆盖,而是创建了一个新的对象。
然后我们修改在change_tuple函数作用域内存在的引用input_tuple,使其现在指向这个新对象。当我们退出函数时,这个引用会被清理,在外部作用域中,my_tup引用仍然指向旧对象的内存地址。
简而言之:“新”元组仅存在于函数的作用域中。
## 使用 OpenCV 毁灭《鸭子猎人》——初学者的图像分析
编写能打破所有《鸭子猎人》高分的代码
towardsdatascience.com
3. 如何防止不希望出现的行为
你可以通过给函数一个my_list.copy()来防止这种行为。这会先创建列表的副本,并将该副本的引用提供给函数,从而使所有更改都作用于副本而不是my_list:
# 2\. Pass the list into our function (nothing is returned from function)
change_list(input_list=my_list.copy())
## 完整指南:使用 Docker 和 Compose 的环境变量和文件
通过这个简单的教程,让你的容器既安全又灵活。
towardsdatascience.com
结论
我们讨论了可变性以及 Python 如何将变量传递给函数;这两个概念在设计 Python 代码时非常重要。通过这篇文章,我希望你避免难以理解的错误和大量的调试时间。
我希望这篇文章能像我期望的那样清晰,如果不是这样,请告诉我我可以做些什么来进一步澄清。同时,查看我在其他文章中讨论的各种编程相关主题:
编程愉快!
— Mike
附注:喜欢我在做的事吗? 关注我!
[## 通过我的推荐链接加入 Medium - Mike Huls
阅读 Mike Huls 的每个故事(以及 Medium 上的其他成千上万的作者)。你的会员费直接支持 Mike…
mikehuls.medium.com](https://mikehuls.medium.com/membership?source=post_page-----343a40cc6923--------------------------------)
Python sorted() 函数解析
原文:
towardsdatascience.com/python-sorted-function-explained-8e46bc002147
本文将探讨如何使用 Python 的 sorted() 函数
·发表于 Towards Data Science ·4 分钟阅读·2023 年 1 月 16 日
--

Andre Taissin 摄影,来自 Unsplash
目录
-
介绍
-
基本排序使用 sorted()
-
使用 key 函数与 sorted()
-
使用
sorted()对自定义对象进行排序 -
结论
介绍
Python sorted() 函数是用于排序可迭代对象的内置函数。
它使用 timsort 作为排序算法,该算法源自归并排序和插入排序。
Python sorted() 函数的语法是:
sorted(iterable, key=None, reverse=False)
其中:
-
iterable — 可以是任何可迭代的 Python 对象,如字符串、元组、列表、集合、字典 等。
-
key — 可选参数,允许添加一个函数(例如 lambda 函数)作为排序的关键字。默认为 None。
-
reverse — 可选参数,允许反转可迭代对象(按降序排序),如果设置为 True。默认为 False。
sorted() 函数的过程定义为:
sorted(iterable) -> sorted list
基本排序使用 sorted()
sorted()函数有很多应用,下面我们来看几个基本的示例。
将数字列表按升序排序
最简单的例子是将一个 列表 的数字按升序排序:
#Create a list of numbers
nums = [3, 1, 9, 7, 5]
#Sort the list of numbers
s_nums = sorted(nums)
#Print sorted list
print(s_nums)
你应该得到:
[1, 3, 5, 7, 9]
将数字列表按降序排序
类似于之前的示例,我们将排序一个数字列表,但现在按降序排序:
#Create a list of numbers
nums = [3, 1, 9, 7, 5]
#Sort the list of numbers
s_nums = sorted(nums, reverse=True)
#Print sorted list
print(s_nums)
你应该得到:
[9, 7, 5, 3, 1]
排序一个字符串列表
Python 的 sorted() 函数也可以排序包含字符串元素的列表。
排序数字的过程非常简单直观,也可以扩展到排序字符串。
Python sorted() 函数根据每个字符串的第一个字符对字符串进行排序(例如,‘apple’ 排在 ‘orange’ 之前,因为 ‘a’ 在字母表中排在 ‘o’ 之前)。
让我们看一个例子:
#Create a list of strings
fruit = ['banana', 'pineapple', 'orange', 'apple']
#Sort the list of strings
s_fruit = sorted(fruit)
#Print sorted list
print(s_fruit)
你应该得到:
['apple', 'banana', 'orange', 'pineapple']
如你所见,字符串列表已经根据字符串的第一个字符按字母顺序(升序)排序了。
你还可以通过将可选的 reverse 参数设置为 True 来按降序对字符串列表进行排序。
注意: 你可以将上述功能扩展到其他可迭代对象,如 元组、集合,以及其他对象。
使用带有 key 函数的 sorted()
对于更复杂的排序任务,我们可以在 sorted() 中使用 key 函数,这将作为排序的关键。
使用 key 函数有两种方式:
-
使用 lambda 函数作为 key 函数
-
使用自定义函数作为 key 函数
使用 lambda 函数与 sorted()
让我们创建一个包含单词的示例列表:
['Python', 'programming', 'tutorial', 'code']
现在,在这个示例中,我们希望根据元素的长度对列表进行排序,这意味着单词将按从短到长的顺序排列。
如你所料,我们将不得不使用 len() 函数来计算每个元素的长度,使用 lambda 函数可以将其作为排序的 key 函数:
#Create a list of words
words = ['Python', 'programming', 'tutorial', 'code']
#Sort the list of words based on length of each word
s_words = sorted(words, key=lambda x: len(x))
#Print sorted list
print(s_words)
你应该得到:
['code', 'Python', 'tutorial', 'programming']
使用自定义函数与 sorted()
让我们重用前面示例中的相同单词列表:
['Python', 'programming', 'tutorial', 'code']
现在,我们希望基于列表中每个元素的长度进行相同的排序,但使用自定义函数来计算每个单词的长度。
我们可以定义一个简单的函数来计算单词的长度,并将其作为 key 函数传递给 sorted():
#Create a list of words
words = ['Python', 'programming', 'tutorial', 'code']
#Define a function to calculate length of a word
def calc_len(word):
len_w = len(word)
return len_w
#Sort the list of words based on length of each word
s_words = sorted(words, key=calc_len)
#Print sorted list
print(s_words)
你应该得到:
['code', 'Python', 'tutorial', 'programming']
这与我们使用 len() 和 lambda 函数作为 sorted() 的 key 函数时的结果是相同的。
使用 sorted() 对自定义对象进行排序
Python sorted() 函数的功能可以扩展到自定义对象(只要我们排序的是可迭代对象)。
例如,让我们创建一个具有两个属性 name 和 age 的自定义类 Person:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __repr__(self):
return repr((self.name, self.age))
这个类将创建一个包含每个人信息的元组列表:
#Create a list of tuples
persons = [
Person('Mike', 20),
Person('John', 35),
Person('David', 23),
]
#Print list of tuples
print(persons)
你应该得到:
[('Mike', 20), ('John', 35), ('David', 23)]
如你所见,这现在是一个元组的列表,这是一个 Python 可迭代对象,可以使用 sorted() 函数进行排序。
在这个例子中,我们希望根据每个人的 age 属性对列表进行排序:
#Sort the list of tuples based on age attribute
s_persons = sorted(persons, key=lambda person: person.age)
#Print sorted list
print(s_persons)
你应该得到:
[('Mike', 20), ('David', 23), ('John', 35)]
结论
在本文中,我们探讨了如何使用 Python sorted() 函数。
现在你了解了基本功能,你可以在其他可迭代的 数据结构 中练习使用它,以应对更复杂的用例。
如果你有任何问题或对某些修改有建议,请随时在下方留言,并查看更多我的Python 函数教程。
最初发表于 https://pyshark.com 于 2023 年 1 月 16 日。
Python 字符串数据类型解释
原文:
towardsdatascience.com/python-string-data-type-explained-ff81a363fe08
在本文中,我们将探索 Python 字符串数据类型
·发表于 Towards Data Science ·阅读时间 6 分钟·2023 年 1 月 30 日
--

照片由 Gaelle Marcel 拍摄,来自 Unsplash
在本文中,我们将探索 Python 字符串数据类型。
目录
-
介绍
-
在 Python 中创建字符串
-
在 Python 中访问字符串中的字符
-
在 Python 中查找字符串中的字符
-
在 Python 中切片字符串
-
在 Python 中迭代字符串
-
在 Python 中连接字符串
-
在 Python 中拆分字符串
-
结论
介绍
在 Python 中,字符串是不可变的字符序列,用于处理文本数据。
你应该了解关于字符串的关键点如下:
-
有序的
-
不可变的
-
可迭代的
学习每种编程语言中的数据类型对于理解代码和程序至关重要。
字符串数据类型在许多编程和机器学习解决方案中广泛使用,特别是在 Python 中用于存储一些格式化的文本数据。
在 Python 中创建字符串
在 Python 中,你可以通过 4 种不同的方式创建字符串:
-
通过用单引号括起字符
-
通过用双引号括起字符
-
通过用三重引号括起字符
-
通过使用 str() 构造函数
使用单引号创建字符串
这是在 Python 中创建字符串的最常见方式之一,非常简单:
#Single quotes
my_string1 = 'Hello World!'
print(my_string1)
你应该得到:
Hello World!
使用双引号创建字符串
这种创建字符串的方式与之前的方法相同,只是现在我们将使用双引号:
#Double quotes
my_string2 = "Hello World!"
print(my_string2)
你应该得到:
Hello World!
使用三重引号创建字符串
这种创建字符串的方式可能是最少见的,因为只有少数几种情况需要使用它。
用三重引号括起字符将产生与前两种方法相同的输出:
#Double quotes
my_string3 = '''Hello World!'''
print(my_string3)
你应该得到:
Hello World!
然而,使用三重引号的一个主要区别是,当你想创建一个多行字符串时,字符串的不同部分会在输出中显示在不同的行上。
例如:
#Double quotes
my_string4 = '''Hello
World!'''
print(my_string4)
你应该得到:
Hello
World!
使用 str() 构造函数创建一个字符串
在 Python 中,你也可以通过使用str()构造函数来创建字符串。
str(object) 构造函数接受任何对象并返回其字符串表示形式。
它返回:
让我们来看几个使用str()的不同数据类型的示例:
#String of int
str_int = str(5)
#String of float
str_float = str(1.5)
#String of complex
str_complex = str(1+3j)
#String of bool
str_bool = str(True)
#Print values
print(str_int)
print(str_float)
print(str_complex)
print(str_bool)
你应该得到:
5
1.5
(1+3j)
True
在 Python 中访问字符串中的字符
Python 列表的一个重要且非常有用的属性是它是一个带索引的序列,这意味着对于一个包含n个元素的列表,第一个元素的索引 = 0,第二个元素的索引 = 1,一直到n-1。
字符串中的字符可以通过其索引访问,索引也可以反转,这意味着第一个元素的索引 = — n,第二个元素的索引 = — n+1,一直到 -1。
为了更容易展示,请看下面的可视化图:

图片由作者提供
我们可以看到字符串中的‘P’字符有两个索引:0 和 -6。
让我们在 Python 中创建这个字符串,并使用上述索引打印出它的第一个字符:
#Create a string
my_string = 'Python'
#Print first character
print(my_string[0])
print(my_string[-6])
你应该得到:
P
P
在 Python 中查找字符串中的字符
使用索引,我们还可以找到字符串中字符的位置。
让我们重用之前示例中的字符串:‘Python’,并尝试找到‘y’字符在字符串中的位置。
使用 Python 字符串的.index() 方法,我们可以通过将字符作为参数传递给它来找到字符的位置:
#Create a string
my_string = 'Python'
#Find character
i = my_string.index('y')
#Print index
print(i)
你应该得到:
1
在 Python 中切片字符串
在前一节中,我们探讨了如何通过其精确索引从 Python 字符串中访问一个字符。
在本节中,我们将探讨如何访问一系列字符,例如前两个或最后两个。
记住,若要使用索引从字符串中检索字符,我们将索引放在方括号[]中。
切片使用相同的方法,但我们传递的是一个范围,而不是单一的索引值。
Python 中的范围是使用以下语法传递的[from : to]。
使用范围我们可以切片字符串以访问多个字符:
#Create a string
my_string = 'Python'
#First two characters
first_two = my_string[:2]
#Second to fourth characters
mid_chars = my_string[1:4]
#Last two characters
last_two = my_string[-2:]
#Print characters
print(first_two)
print(mid_chars)
print(last_two)
你应该得到:
Py
on
yth
注意,指定的字符在to索引处不包括在内,因为在 Python 切片算法中,它会遍历字符直到指定的to索引,并包括所有到达该索引但不包括to索引下的字符。
在 Python 中迭代字符串
Python 字符串是一个可迭代对象,这意味着我们可以遍历字符串中的字符。
可以使用 for() 循环执行简单的迭代:
#Create a string
my_string = 'Python'
#Iterate over a string
for char in my_string:
print(char)
你应该得到:
P
y
t
h
o
n
在 Python 中连接字符串
在 Python 中,我们也可以将多个字符串连接(组合)在一起以创建一个单一字符串。
在 Python 中连接字符串的两种最流行的方法是:
-
使用 ‘+’ 操作符
-
使用 .join() 方法
使用 ‘+’ 操作符
使用 ‘+’ 操作符是连接多个字符串的最常见方法之一。
让我们看一个例子:
#Create strings
s1 = 'Python'
s2 = 'Tutorial'
sep = ' '
#Concatenate strings
new_string = s1 + sep + s2
#Pring new string
print(new_string)
你应该得到:
Python Tutorial
使用 .join() 方法
Python 字符串 .join() 方法允许将一个字符串列表连接起来以创建一个新字符串。
Python 字符串 .join() 方法的语法是:
separator.join([list of strings])
让我们看一个例子:
#Create strings
s1 = 'Python'
s2 = 'Programming'
s3 = 'Tutorial'
sep = ' '
#Concatenate strings
new_string = sep.join([s1, s2, s3])
#Pring new string
print(new_string)
你应该得到:
Python Programming Tutorial
在 Python 中拆分字符串
在 Python 中,正如我们可以连接多个字符串一样,我们也可以将一个字符串拆分成多个字符串。
有多种方法可以做到这一点,但最常用的方法是使用字符串的 .split() 方法,它根据分隔符(默认分隔符是:‘ ’)将字符串拆分成一个字符串列表。
Python 字符串 .split() 方法的语法是:
string.split(separator)
让我们看一个例子:
#Create a string
long_string = 'Apple Banana Orange Pineapple'
#Concatenate strings
new_strings = long_string.split()
#Pring new string
print(new_strings)
你应该得到:
['Apple', 'Banana', 'Orange', 'Pineapple']
你还可以根据你想要拆分字符串的内容指定自定义分隔符。
例如:
#Create a string
long_string = 'Apple, Banana, Orange, Pineapple'
#Concatenate strings
new_strings = long_string.split(', ')
#Pring new string
print(new_strings)
你应该得到:
['Apple', 'Banana', 'Orange', 'Pineapple']
结论
在本文中,我们探讨了 Python 布尔数据类型,包括它在布尔表达式和控制结构中的使用。
作为学习 Python 的下一步,考虑阅读以下文章,了解 Python 数据类型和数据结构:
最初发表于 https://pyshark.com 2023 年 1 月 30 日。
从 Python 到 Rust:破解 3 大障碍
原文:
towardsdatascience.com/python-to-rust-breaking-down-3-big-obstacles-094eb99e331d
Python 高手到 Rust 新手——一名数据科学家的过渡故事
·发表于 Towards Data Science ·阅读时间 8 分钟·2023 年 12 月 12 日
--

图 1:蛇和螃蟹。(螃蟹:Romina BM;蛇:Mohan Moolepetlu;由作者编排)。
我周围的每个人都知道我是一个忠实的🐍 Python 粉丝。我大约 15 年前开始使用 Python,当时我对Mathworks Matlab感到厌倦。尽管 Matlab 的想法看起来不错,但在掌握 Python后,我再也没有回头。我甚至在我的大学成为了 Python 的一种布道者,并“传播这个消息”。
编写代码的能力并不代表你是一个软件开发人员。
在我目前的雇主TenneT——荷兰和德国的大型传输系统运营商——我们正在与约 10 人的团队一起构建一个文档解析和验证解决方案。构建这样的解决方案,尤其是在团队中,比我想象的要困难得多。这也让我对软件工程的正确范式更感兴趣。我一直认为我的代码还不错,但在看到我软件工程师朋友的工作后:天哪,还有很多需要学习的地方!
当我学习强类型、SOLID 原则和一般编程架构等主题时,我也略微了解了其他语言以及它们如何解决问题。特别是Rust吸引了我的注意,因为我经常看到基于 Rust 的 Python 包(例如:Polars)。
为了更好地了解 Rust,我跟随了官方 Rustlings 课程,这是一个包含 96 个小编程问题的本地 Git 仓库。虽然它是完全可以做到的,但 Rust 与 Python 非常不同。Rust 编译器非常严格,无法接受也许的答案。以下是我认为 Rust 和 Python 之间的三个主要区别。
免责声明:虽然我对 Python 非常熟练,但我的其他语言有点生疏(双关语)。我仍在学习 Rust,并且可能有些部分理解不完全。

图 2:我们成功抵达终点(截图由作者提供)。
1. 所有权、借用和生命周期
所有权和借用可能是 Rust 编程语言中最基本的方面。它旨在确保内存安全,无需所谓的垃圾回收器。这是 Rust 的独特概念,我还没有在其他语言中看到过。
让我们从一个例子开始,我们将值42赋给变量answer_of_life。Rust 现在将在内存中分配一些空间(这稍微复杂一些,但我们现在保持简单),并将“所有权”附加到该变量。重要的是要知道一次只能有一个所有者。一些操作“转移所有权”,使得之前的变量引用无效。这通过防止双重释放内存、数据竞争和悬挂引用等问题来确保内存安全。
来源 1:所有权、所有权转移和作用域。
在其他语言中也使用的一个术语是作用域。这可以被视为代码“存在”的某种区域。每次代码离开一个作用域时,所有拥有所有权的变量都会被解除分配。这是 Python 中根本不同的东西。Python 使用垃圾回收器,在没有对变量的引用时解除分配变量。在来源 1的例子中,从变量s1到s2的所有权转移之后,变量s1就不再可用了。
作为 Python 用户,所有权可能会让人感到困惑,特别是在开始时确实是一个挑战。
在来源 1的例子中有些过于简单。Rust 强制要求你思考变量是如何创建的以及它应该如何被转移。例如,当你将参数传递给函数时,所有权可以被转移,如来源 2中所示:
来源 2:一个函数获取所有权,从而使原始变量无效。
仅仅转移所有权可能会很麻烦,甚至对于一些使用场景来说可能无法实现,因此 Rust 提出了一个所谓的借用系统。变量通过借用同一个变量来避免转移所有权,而原变量仍然是所有者。默认情况下,借用的变量是不可变的,即只读,但通过添加 mut 关键字,借用可以变成可变的。虽然可以有无限多个不可变借用,但只允许有一个可变借用。在源 3中,我展示了两个不可变借用和一个可变借用的例子。当函数超出作用域时,所有变量将被移除。
源 3:两个不可变和一个可变借用。
生命周期是 Rust 中与借用和所有权相关的概念,帮助编译器强制执行引用有效的时长。你可能会遇到创建一个使用两个借用的结构或函数的情况。这意味着现在函数或结构的结果可能依赖于之前的输入。为了使这一点更明确,我们可以通过注释生命周期来表达关系。在源 4中查看示例:
源 4:生命周期语法初看可能令人困惑,但最终会有所帮助。
所有权、借用和生命周期虽然不易处理,但确实迫使你编写更好的代码。至少,当你能够通过编译器的检查时(-:
2. Rust 不接受 None 作为答案
在 Python 中非常常见的事情在 Rust 中是不可能的:设置一个值为 None。这是一个与 Rust 的安全性、可预测性和零成本抽象目标一致的设计选择。
安全性方面类似于 Rust 的所有权、借用和生命周期方面:防止引用指向未分配的内存。通过不允许返回 None,将导致更高的可预测性,因为它迫使开发者显式处理可能缺少数字的情况。由于内存安全和可预测行为,Rust 可以实现所有高级语言功能而不会牺牲性能。
“None shall not pass” — 甘道夫灰袍
仅仅拒绝 None 会使 Rust 成为一个糟糕的语言,因此创作者提出了一个不错的替代方案:枚举 Option 和 Result。通过这些枚举,我们可以显式地表示值的存在或缺失。这也使得错误处理非常优雅。让我们考虑源 5中的 Option 示例。
源 5:使用 Option 返回可选答案并处理特殊情况。
等一下! 你不是说没有 None 吗?这也是第一次让我感到困惑的地方,不过这里的 None 是一个特殊的枚举结构体,不接受参数。Some 也是一个特殊的结构体,但可以接受参数。我们的函数divide()返回这些可能的枚举值之一,随后我们可以检查它是什么,并据此采取行动。
没有
None并且强制返回值使得 Rust 非常可预测。
主函数使用 match 结构来处理结果,这非常方便。它有点类似于其他语言中的 switch/case 结构(参见图 2 中 Guido 的回应)。match 检查是否是 Enum Some 或 Enum None 并执行相关操作。
图 3:Guido van Rossum 对 switch/case 的推文/反应。
Option 枚举是一种特殊的结构,用于处理可能返回值或不返回值的函数。对于可以返回值或错误的函数,Rust 有一个更为明确的枚举,称为 Result。它们的思想完全相同,主要区别在于 Option 有一个默认的“错误”值 None,而 Result 需要一个明确的“错误”类型。这个类型可以是简单的字符串,也可以是更明确的结构体来标识错误。在 来源 6 中,divide 函数使用 Result 重新编写。
来源 6:Result 枚举是一种很好的返回值或错误的方式。
Rust 开发者发现 match 结构有时可能有些繁琐,因此添加了 if let 和 while let 操作符。这些操作符类似于 match,但提供了一些漂亮的语法糖和丰富的装饰。甚至还有一个非常酷的 ? 操作符(这里未展示),它为丰富的装饰添加了一个樱桃在上面!
来源 7:if let 和 while let 正在创造美丽的语法糖!
使用 Python 时,我学会了使用 Optional 关键字来为结果类型定义值或 None。但我不得不承认 Rust 在这方面处理得非常精妙。我可以想象,Python 社区也会朝着这种风格发展,类似于强类型化的趋势。
3. 类在哪里?
Python 和 Rust 都可以用于两种编程范式:函数式编程(FP)和面向对象编程(OOP)。然而,Rust 实现这些所谓的对象的方式有所不同。在 Python 中,我们有一个典型的 class 对象,可以关联变量和方法。像许多其他语言(如 Java)一样,我们现在可以将这个方法作为基础,通过创建继承父类方法和变量的新对象来扩展功能。
在 Rust 中,没有 class 关键字,对象与 Python 的根本不同。Rust 使用特质系统来实现代码重用和多态,这可以带来与多重继承相同的好处,但没有多重继承所带来的问题。多重继承通常用于结合或共享多个类的各种功能,但它可能使代码变得复杂和模糊。一个著名的问题是 钻石 问题,如 来源 8 所示:
来源 8:钻石问题:不清楚使用了哪个方法。
虽然我认为我们可以很容易地解决这个问题,但如果我创建一种新的语言,我也会尝试以不同的方式来做。对于多重继承,目标主要是与其他对象共享相似的功能。在 Rust 中,通过使用 Trait 系统,这种方法做得更优雅。这种方法并不是 Rust 独有的,类似的系统也在 Scala、Kotlin 和 Haskell 中使用。
Rust 中的类是由枚举(Enums)和结构体(Structs)创建的。单独来看,这些只是数据结构,但我们可以向这些类添加功能。我们可以直接这样做,然而,通过使用特征,这些功能可以与多个“类”共享。使用特征的一个大好处是我们可以提前检查某个特征是否被实现。请参见以下示例:
Source 9: 为两个结构体添加一个共享特征。
在这个例子中,我们有一个Speaker特征,表示能够说话的角色。我们为两种类型Jedi和Droid实现了这个特征。每种类型都提供了自己对speak方法的实现。
introduce函数接受任何实现了Speaker特征的类型,并调用speak方法。在main函数中,我们创建了Jedi(Obi-Wan Kenobi)和Droid(R2-D2)的实例,并将它们传递给introduce函数,展示了多态性。
对我来说,作为一个 Pythonista 🐍,Rust 的特征系统非常令人困惑。我花了一段时间才欣赏到其语法的优雅。
总结
Rust 是一门非常酷的语言,但绝对不是一门容易学习的语言。Rustlings 课程让我了解了一些基础知识,但我远远没有足够熟练来承担大型项目。但我真的很喜欢 Rust 如何迫使你编写更好、更安全的代码。
Python 仍然是我的日常使用语言。在工作中,我们的文档管道完全用 Python 构建,而且在机器学习领域,我看不到所有的东西都换成另一种语言。Python 实在是太容易学习了,即使你是一个糟糕的开发者(当然不是我 (-😉),你也可以完成工作。
然而,Rust 的势头正在小幅上升。当然,一些包如 Polars 和 Pydantic 是使用 Rust 构建的,但 HuggingFace 也发布了他们自己的第一个用 Rust 构建的机器学习框架版本,名为 Candle。所以我认为学习一点 Rust 并不是一个坏主意!
我下一步(或实际上是当前)Rust 的旅程是使用 Rust 参与Advent of Code 2023。我还在研究 Leptos,并计划创建一个个人网站。还有很多东西需要学习!
如果你有任何意见,请告诉我!欢迎在LinkedIn上联系。
从 Python 到 Rust:你必须了解的虚拟环境的一切
从 Python 专家到 Rust 新手——一位数据科学家的过渡故事
·发表于 Towards Data Science ·7 分钟阅读·2023 年 12 月 26 日
--

图 1:货物仓库里的蛇和螃蟹。 (螃蟹; 蛇; 集装箱; 由作者编排)
从 Python 转到 Rust 的旅程就像把一个可靠的光剑换成一种新的刀刃——既令人兴奋又略显令人生畏。作为一个对 Python 的特性非常熟悉的数据科学家,进入 Rust 的世界是一个令人激动的新挑战。在本文中,我将分享我的经历和见解,比较这两种强大语言如何处理软件开发的一个关键方面——特别是关注(虚拟)环境和依赖管理。
在使用 Python 时,你首先学到的事情之一就是在所谓的虚拟环境中工作。这是一个管理依赖关系和隔离项目特定包的关键工具,以避免它们干扰其他项目或系统范围的 Python 安装。我几年前写了一篇关于如何管理 Python 的文章,但它仍然适用(它稍微变化了一些,涉及到micromamba和poetry ,如果需要,我可以写一篇关于这方面的文章)。
TLDR: 只需使用cargo,大多数情况下你就会没问题——Dennis
在使用rustup安装 Rust 之后,我的第一个问题是:我应该如何创建一个虚拟环境? 对我来说,这是一个非常有意义的问题,因为 Rust 也可以使用许多包(称为 Crates)作为依赖项。事实上,cargo 非常优雅地解决了这个问题。以下是我在比较虚拟环境和 pip 与 Rust 的 cargo 构建系统时的发现。
免责声明:在我探索这些 Rust 领域时,我对语言的熟练程度可能仍有些生疏(玩笑话)。加入我,在这个学习冒险中揭开 Cargo 的细微差别,告别虚拟环境的熟悉拥抱。
1. 包的单一全局位置
Python 中的虚拟环境是使用像venv、virtualenv或conda这样的工具按项目创建的。在底层,这些系统创建一个单独的文件夹,该文件夹包含 Python 发行版及其所有包。现在,当我们使用 pip 或 conda 安装一个包时,该包及其所有依赖项会被安装在这个隔离的文件夹中。这些虚拟环境工具所做的事情类似于“chroot”,但针对 Python 安装。
解释型语言如 Python,依赖关系解析通常发生在运行时。
对于像 Python 这样的解释型语言,依赖关系解析通常发生在运行时。这意味着当 Python 脚本执行时,解释器需要动态解析和加载所需的依赖项。虚拟环境帮助管理这些依赖关系,为项目提供了一个干净的隔离,以避免冲突。以下是 Python 中的典型工作流程:
# create a virtual environment with a specific Python version
conda create -n my_environment python=3.12
# activate the virtual environment
conda activate my_environment# Install a package
pip install pandas
另一方面,Rust 有一个叫做cargo的包管理器,它使用一个全球唯一的位置,即没有用户特定的虚拟环境。它之所以能做到这一点,是因为 cargo 构建系统。当你使用 cargo 创建一个项目时,它围绕 Cargo.toml 文件展开。这是所谓的项目文件,定义了项目的详细信息,包括其依赖项及语义版本控制。使用 cargo add <crate> 你将依赖项添加到这个项目文件中,这些依赖项会在构建过程中下载。由于我们使用 cargo 来构建,并且 cargo 负责选择/下载正确的依赖项,因此不需要像 Python 虚拟环境中的 chroot-like 机制。
使用 cargo,一切都已经在虚拟环境中。
使用 cargo 创建新项目的工作流程看起来与 Python 非常相似,但在底层,它确实要聪明一些:
# create a new project folder using cargo
cargo new my_project
# go into the new project folder
cd my_project# Install a package
cargo add rand
在使用 cargo 构建期间,需要的正确版本的包从 Cargo.toml 读取并从全局注册表中加载(默认情况下在 $HOME/.cargo)。这主要是因为 Rust 是编译语言,而在运行时需要解析依赖的 Python 实现起来要困难得多。
2. 内置的依赖解析
看看 Python,没有内置的依赖解析系统。是的,使用 pip freeze 你可以获得已安装包的概述,但没有保证它也能捕获所有间接依赖。这意味着它不能捕获环境的完整复杂性。
Pip freeze 可能不足以捕获完整的环境
为了解决这个问题,其他语言如 Ruby 和 JavaScript 的依赖解析器开始使用所谓的 锁文件。这些锁文件捕获了所有依赖项及其依赖项的版本信息。Python 通过 Pipenv 或我个人最喜欢的 Python Poetry 获得类似的功能,但在下载 Python 时没有内置工具。
Rust 的 Cargo 通过使用锁文件具有内置的依赖解析功能。当你使用 cargo build 或 cargo run 时,它会检查 Cargo.lock 文件,以确保使用所有依赖项的确切版本。这个锁文件捕获了整个依赖树,包括传递依赖,形成了项目环境的全面且确定性的表示。
Cargo.lock 文件作为特定时间点的依赖快照。它包含了不仅是 Cargo.toml 文件中指定的直接依赖的准确版本信息,还有所有传递依赖的版本信息。
例如,如果项目 A 依赖于库 B 版本 1.0.0,而库 B 依赖于库 C 版本 2.1.0,则这两个版本都会记录在 Cargo.lock 文件中。这确保了所有参与项目的人员,无论其环境如何,都得到完全相同的依赖集合。Cargo 非常灵活,可以支持即使在同一编译目标中也能有多个版本的相同依赖。

图 2:在构建阶段,Cargo 收集所有必需的依赖。在运行阶段,依赖项已被链接到可执行文件中。(图由作者提供)。
使用 Cargo 的锁文件消除了开发者手动管理和同步不同环境中依赖版本的需要。它提供了一个一致且可重现的构建环境,使得协作和部署更加可靠。这是编译语言的一大优势,我们可以认为这是一种不公平的比较。
3. 包和 Rust 自身的兼容性
在软件工程中,兼容性是确保项目在各种环境下顺利运行的基石。当我们比较 Rust 的 cargo 与 Python 的 pip 时,可以清楚地看到 Rust 在这方面经过了精心考虑,而 Python 则是随着时间的推移逐渐发展到现在的状态。
Rust 中的兼容性不仅仅是一个考虑因素,它是一种文化承诺。社区非常重视主要版本的应用程序编程接口(API)兼容性。这在 cargo 包管理器中得到了清晰体现,它强制执行 语义化版本控制。这使得开发环境可靠且可预测,其中依赖项预期能够良好配合。
与此相比,Python 生态系统中的兼容性有时可能是一个微妙的问题。升级 Python 或其依赖项可能会导致意外的问题,这些问题可能只在运行时显现。与 Rust 不同,Rust 在构建时更容易识别潜在问题,而 Python 开发者通常只有在部署后才会发现这些问题。
示例场景:将 Python 3.7 升级到 Python 3.9
想象一下你有一个运行在 Python 3.7 上的 Python 项目。该项目包含一个严重依赖字典的脚本。在 Python 3.7 中,字典的插入顺序作为实现细节被保留,但这并没有得到正式保证。你决定将 Python 环境升级到 Python 3.9,以便获得性能改进和新语言特性。
升级后,你会注意到你的脚本表现不同。在 Python 3.7 中,你可能无意中依赖了字典中项目的顺序来进行某些操作,即使这并未正式成为语言规范的一部分。如果你的代码依赖于字典中元素的顺序,并且在编写时没有意识到这种行为在 3.7 中并不被保证,那么如果在 Python 3.9 中实现有任何细微变化,它可能会表现得不可预测或中断。
这个例子说明了即使在同一主要版本的 Python(Python 3.x)内升级也可能导致意外的问题,特别是当代码依赖于未正式指定在语言中的行为时,而这些行为只是某一特定实现的副产品。在这个例子中,我们忽略了在次版本中添加的许多功能,这些功能常常改变了首选的工作流程。同时,也忽略了被弃用的函数。例如,一些方法和函数在 Python 中被移除,即使在次版本中。
Rust 对次版本中稳定 API 维护的强烈关注确保了兼容性,并减少了与升级相关的问题。其严格的语义版本控制和 Cargo 的依赖管理最小化了意外变化。这使得 Rust 的更新对于开发者来说更具可预测性和较少干扰。
总结
学习和使用 Rust 真的突显了每种语言在环境和依赖管理上的巨大差异。Python 的悠久历史促成了各种工具的发展,如 venv 和 Poetry,它们都在应对语言的动态特性和运行时依赖解决挑战。尽管这些工具有效,但它们往往更像是必要的变通方法,而不是语言的集成组件。
相比之下,Rust 通过 Cargo 的简化方法展示了其对更集成和用户友好体验的承诺。Cargo 高效的依赖管理,无需外部工具或‘PATH’操作,展示了 Rust 现代化的软件开发方法。
学习 Python 和 Rust 确实突显了每种语言的独特之处,并让我们窥见了软件开发的未来。我认为 Python 和 Rust 仍然有不同的目标,但可以看到它们越来越趋同。同时,随着机器学习社区逐渐向 Rust 迈进,Rust 的语言特性也被引入 Python,用于更成熟的产品。我对 Python 和 Rust 的未来充满期待!
我很期待听到你对从 Python 到 Rust 的这段旅程的看法和反馈。让我们在LinkedIn上联系,并继续交流!
Python 元组,真相大白,只有真相:你好,元组!
原文:
towardsdatascience.com/python-tuple-the-whole-truth-and-only-the-truth-hello-tuple-12a7ab9dbd0d
PYTHON 编程
学习元组的基础知识及其使用方法
·发布在Towards Data Science ·阅读时长 16 分钟·2023 年 1 月 21 日
--

元组通常被视为记录。照片由Samuel Regan-Asante提供,来自Unsplash
元组是 Python 中的一种不可变集合类型。它是 Python 中三种最流行的集合类型之一,另外两种是列表和字典。虽然我认为许多初学者和中级开发者对这两种类型了解颇多,但他们可能在真正理解元组是什么以及如何工作上存在问题。即使是高级 Python 开发者也不必了解所有关于元组的知识——鉴于这种类型的特殊性,我对此并不感到惊讶。
作为一个初学者甚至中级 Python 开发者,我对元组了解不多。让我给你一个例子;想象一下我写了一段类似于以下的代码:
from pathlib import Path
ROOT = Path(__file__).resolve().parent
basic_names = [
"file1",
"file2",
"file_miss_x56",
"xyz_settings",
]
files = [
Path(ROOT) / f"{name}.csv"
for name in basic_names
]
如你所见,我使用了列表字面量来定义basic_names列表——但为什么不使用元组字面量呢?它看起来会是下面这样:
basic_names = (
"file1",
"file2",
"file_miss_x56",
"xyz_settings",
)
关于元组,我们知道的主要事情是它是不可变的——代码本身表明basic_names容器将不会改变。因此,元组在这里似乎比列表更自然,对吧?那么,两种方法之间是否存在实际差异?比如性能、安全性或其他方面?
知识上的这些空白使我们成为更差的程序员。本文旨在通过帮助你了解 Python 中一个非常重要但许多人不了解的数据类型:元组,从而帮助你成为更好的程序员。我的目标是使这篇文章从实际角度尽可能详尽。因此,例如,我们不会讨论元组的 C 语言实现细节,但会讨论在 Python 中使用元组的细节。
元组是一个丰富的话题。因此,我将把关于它的知识分为两部分——和两篇文章。以下是我将在第一部分中覆盖的主题——也就是这里:
-
元组的基础。
-
使用元组:元组解包和元组方法。
因此,我们将在这里专注于基础知识。在第二部分,我将覆盖元组的更多高级主题,例如继承自元组、元组性能和元组推导。你可以在这里找到它:
[## Python 元组,完全的真相和唯一的真相:让我们深入探讨]
了解元组的复杂性
towardsdatascience.com
元组的基础知识
元组是一个值的容器,类似于列表。在他伟大的著作《流畅的 Python》中,L. Ramalho 解释说,元组是为了成为不可变的列表而创建的,这个术语很好地描述了元组的本质。但他也提到,元组不仅仅是不可变的列表;它们远不止于此。
特别是,元组可以用作没有字段名称的记录。这意味着我们可以有一个包含几个未命名字段的记录。当然,这种基于元组的记录只有在每个字段的含义明确时才有意义。
当你想在 Python 中使用元组字面量创建元组时,你需要使用圆括号 () 而不是方括号 [],就像创建列表时一样¹:
>>> x_tuple_1 = (1, 2, 3)
>>> x_tuple_1
(1, 2, 3)
>>> x_tuple_2 = ([1, 2], 3)
>>> x_tuple_2
([1, 2], 3)
这里,x_tuple_1 = (1, 2, 3) 创建了一个包含数字 1、2 和 3 的三元素元组;x_tuple_2 = ([1, 2], 3) 创建了一个包含两个值的两元素元组:一个列表 [1, 2] 和数字 3。如你所见,你可以在元组中使用任何类型的对象。你甚至可以创建一个空元组的元组:
>>> tuple((tuple(), tuple()))
((), ())
尽管,说实话,我不知道你为什么会想这样做。
好的,我们上面使用了元组字面量。创建元组的第二种方法是使用内置的 tuple() 类。只需提供一个可迭代对象作为参数,这将把可迭代对象转换为元组:
>>> tuple([1, 2, 5])
(1, 2, 5)
>>> tuple(i for i in range(5))
(0, 1, 2, 3, 4)
要访问元组中的值,你可以使用典型的索引:x_tuple_1[0] 将返回 1,而 x_tuple_2[0] 将返回一个列表 [1, 2]。注意,因为 x_tuple_2[0] 是一个列表,所以你可以使用它的索引来访问它的元素——因此,你将使用多个(在这里是双重)索引;例如,x_tuple_2[0][0] 将返回 1,而 x_tuple_2[0][1] 将返回 2。
列表和元组之间最大的区别在于列表是可变的,所以你可以改变它们,而元组是不可变的,所以你不能改变它们:
>>> x_list = [1, 2, 3]
>>> x_tuple = (1, 2, 3)
>>> x_list[0] = 10
>>> x_list
[10, 2, 3]
>>> x_tuple[0] = 10
Traceback (most recent call last):
...
TypeError: 'tuple' object does not support item assignment
如你所见,你不能对元组进行项赋值。这一特性使得元组比列表更不容易出错,因为你可以确定(实际上,几乎可以确定,我们将下文讨论)元组不会改变。然而,你可以确定的是,它们的长度不会改变。
有一个关于元组的常见面试问题:由于元组是不可变的,你不能改变它们的值,对吗? 对这个问题的回答是:嗯…
这是因为你可以改变元组中可变元素的值:
>>> x_tuple = ([1, 2], 3)
>>> x_tuple[0][0] = 10
>>> x_tuple
([10, 2], 3)
>>> x_tuple[1] = 10
Traceback (most recent call last):
...
TypeError: 'tuple' object does not support item assignment
所以,尽管元组是不可变的,但如果它们的元素不是,你可以改变这些元素,因此,至少间接地,你可以改变元组。这使得改变一个不可变的东西成为可能…
如果你感到困惑,至少要意识到你并不孤单。你只是其中之一。然而,这种不可变性至少在理论上是有意义的,所以让我解释一下这里发生了什么。
整个真相在于以下几点。像其他集合一样,元组不包含对象,而是包含对它们的引用;不可变意味着在这些引用方面是不可变的。因此,一旦创建,元组将始终包含相同的引用集合。
-
理论上,当一个元组引用的对象发生变化时,元组保持不变:它仍然是完全相同的元组,具有完全相同的引用。
-
实际上(也就是说,从我们典型/自然的角度来看),当一个元组引用的对象发生变化时,元组似乎已经改变:尽管引用完全相同,一个对象发生了变化,因此,从实际情况来看,元组看起来与变化前不同。但在理论上,元组(一个引用的集合)没有发生任何变化。
像其他集合一样,元组不包含对象,而是包含对它们的引用;不可变意味着在这些引用方面是不可变的。
好了,现在我们知道了元组的不可变性是如何工作的,我们应该记住也要以这种方式来看待元组。但知道某件事并不意味着习惯它会很容易。以这种方式思考不可变性并不容易。记住,从现在开始,你应该记住元组是对对象的不可变引用集合,而不是对象的不可变集合。元组包含的对象的值实际上可以改变——但对象必须保持不变……已经觉得头疼了吗?这只是开始…
让我们考虑一下典型的元组长度。然而,为了增加一些背景,我们应该考虑它在列表中的表现。我认为可以安全地说,短列表和长列表都经常使用。你可以通过多种方法创建列表,比如字面量、for循环、list()方法和列表推导。
元组是不可变的,它们并不像那样工作。你不能在for循环中更新它们(除非你在更新它们的可变元素)或在推导式中更新它们。你可以用两种方式创建一个元组,使用元组字面量,比如这里:
>>> x = (1, 56, "string")
或调用tuple()类(tuple()是一个可调用类)对一个可迭代对象:
>>> x = tuple(x**.5 for x in range(100))
我猜前一种用法要频繁得多。也许元组最常见的用法是从函数中返回值,特别是当返回两个或三个值时(你很少(如果有的话)会为十个值这么做)。
当元组字面量很短时,通常会省略括号:
>>> x = 1, 56, "string"
这种方法通常与 return 语句一起使用,但不仅限于此。带括号和不带括号的两种方式中哪一种更好?一般来说,没有哪一种更好;但这要视情况而定。有时,括号会使代码更清晰,有时则不需要括号。
请记住非括号元组,因为它们可能成为难以发现的错误来源;见这里:
即使是最小的字符也可能引发大问题
简而言之,当你忘记在行末添加逗号时,你可能会将一个对象作为元组而不是单独的对象来使用:
>>> x = {10, 20, 50},
你可能认为 x 是一个包含三个元素的集合,但实际上它是一个包含一个元素的元组:
>>> x
({10, 20, 50},)
正如你所见,这一个单独的逗号放在右大括号后面,而不是前面,使得 x 成为了一个一元素的元组。
元组的实际应用
元组提供的方法比列表少,但仍然有不少。有些方法比其他方法更为人所知;有些方法甚至非常少为人知晓且使用得不频繁。在本节中,我们将探讨使用元组的两个重要方面:元组方法和元组解包。
解包
元组的一个极好的特性是 元组解包。你可以用它将一个元组的值一次性赋给多个变量。例如:
>>> my_tuple = (1, 2, 3,)
>>> a, b, c = my_tuple
在这里,a 将变为 1,b 将变为 2,而 c 将变为 3。
考虑以下示例:
>>> x_tuple = ([1, 2], 3)
>>> x, y = x_tuple
>>> x
[1, 2]
>>> y
3
你还可以使用带有星号 * 的特殊解包语法:
>>> x_tuple = (1, 2, 3, 4, 5)
>>> a, b* = x_tuple
>>> a
1
>>> b
[2, 3, 4, 5]
>>> *a, b = x_tuple
>>> a
[1, 2, 3, 4]
>>> b
5
>>> a, *b, c = x_tuple
>>> a
1
>>> b
[2, 3, 4]
>>> c
5
正如你所见,当你将星号 * 附加到一个变量名时,就像是在说:“将这个项及所有接下来的项解包到这个名字中。”所以:
-
a, b*意味着将第一个元素解包到a,所有剩余的元素解包到b。 -
*a, b意味着将最后一个元素解包到b,所有之前的元素解包到a。 -
a, *b, c意味着将第一个元素解包到a,最后一个元素解包到c,所有中间的元素解包到b。
当元组中的元素更多时,你可以考虑更多场景。想象一下你有一个包含七个元素的元组,而你对前两个和最后一个感兴趣。你可以用解包的方式将它们获取并赋值给变量,如下所示:
>>> t = 1, 2, "a", "ty", 5, 5.1, 60
>>> a, b, *_, c = t
>>> a, b, c
(1, 2, 60)
这里还要注意一点。我使用了 *_,因为我只需要提取这三个值,其他值可以忽略。这里,下划线字符 _ 正是表示这一点:我不关心这些元组中的其他值,因此让我们忽略它们。如果你使用名称,代码的读者会认为该名称在代码中被使用——但你的 IDE 也会对分配给一个在作用域中未被使用的名称而发出警告²。
元组解包可以用于各种场景,但当你赋值时,特别是从返回元组的函数或方法中获得值时,它特别有用。下面的例子展示了从函数/方法返回值中解包的有用性。
首先,让我们创建一个 Rectangle 类:
>>> @dataclass
... class Rectangle:
... x: float
... y: float
... def area(self):
... return self.x * self.y
... def perimeter(self):
... return 2*self.x + 2*self.y
... def summarize(self):
... return self.area(), self.perimeter()
>>> rect = Rectangle(20, 10)
>>> rect
Rectangle(x=20, y=10)
>>> rect.summarize()
(200, 60)
如你所见,Rectangle.summarize() 方法返回两个组织在元组中的值:矩形的面积和周长。如果我们想将这些值分配给名称,我们可以这样做:
>>> results = rect.summarize()
>>> area = result[0] # poor!
>>> perimeter = result[1] # poor!
然而,上述方法并不是一个好的选择,尤其是出于清晰性考虑,我们可以使用元组解包更有效地完成这个任务:
>>> area, perimeter = rect.summarize()
>>> area
200
>>> perimeter
60
如你所见,它更加清晰简洁:只需一行而不是三行。此外,它不使用索引来从元组中获取值。索引降低了可读性,使用名称而非位置会更好。我们将在下面的部分讨论,从 tuple 类继承和命名元组。但请记住,当一个函数/方法返回一个元组——这是一种相当常见的情况——你应该解包这些值,而不是直接使用元组索引分配它们。
另一个例子,也使用 dataclass³:
>>> from dataclasses import dataclass
>>> KmSquare = float
>>> @dataclass
... class City:
... lat: float
... long: float
... population: int
... area: KmSquare
... def get_coordinates(self):
... return self.lat, self.long
>>> Warsaw = City(52.2297, 21.0122, 1_765_000, 517.2)
>>> lat, long = Warsaw.get_coordinates()
>>> lat
52.2297
>>> long
21.0122
上述示例展示了元组解包的最常见用例。然而,有时我们可能需要从基于元组的嵌套数据结构中解包值。考虑以下例子。假设我们有一个如上所示的城市列表,每个城市由一个字典中的列表表示,而不是 dataclass:
>>> cities = {
... "Warsaw": [(52.2297, 21.0122), 1_765_000, 517.2],
... "Prague": [(50.0755, 14.4378), 1_309_000, 496],
... "Bratislava": [(48.1486, 17.1077), 424_428_000, 367.6],
... }
如你所见,我们将城市的坐标组织成了列表中的元组。我们可以使用嵌套解包来获取这些坐标:
>>> (lat, long), *rest = cities["Warsaw"]
>>> lat
52.2297
>>> long
21.0122
或者我们可能还需要面积:
>>> (lat, long), _, area = cities["Warsaw"]
>>> lat, long, area
(52.2297, 21.0122, 517.2)
再次,我使用了下划线字符 _ 来分配我们不需要的值。
请注意,我们对 *args 所做的正是解包。通过将 *args 放在函数的参数中,你让用户知道他们可以使用任何参数:
>>> def foo(*args):
... return args
>>> foo(50, 100)
(50, 100)
>>> foo(50, "Zulu Gula", 100)
(50, 'Zulu Gula', 100)
在这里,*args 将所有位置参数(而非关键字参数)收集到 args 元组中。这个 return 语句使我们能够查看 args 元组中的这些参数。
还有一点:解包不仅限于元组,你也可以将它用于其他可迭代对象:
>>> a, *_, b = [i**2 for i in range(100)]
>>> a, b
(0, 9801)
>>> x = (i for i in range(10))
>>> a, b, *c = x
>>> c
[2, 3, 4, 5, 6, 7, 8, 9]
元组方法
Python 初学者很快就会了解元组。随着时间的推移,他们会多了解一些,主要是它们的不变性及其后果。但许多开发者不知道tuple类提供的所有方法。说实话,在写这篇文章之前,当我认为自己是一个相当高级的开发者时,我也不知道这些方法。不过了解这些方法是好的——这一小节旨在帮助你学习这些方法。
这并不意味着你需要使用所有这些操作。但例如,记住可以在元组上使用就地操作及其结果是好的。这些知识足以让你回忆起,元组只有两种就地操作:就地拼接和就地重复拼接。
为了学习这些方法,我们再看看《流畅的 Python》。我们将找到一个比较列表和元组方法的漂亮表格,从中我们可以提取出元组的方法。因此,下面你将找到tuple类的完整方法列表,每个方法附有一个或多个简单示例。
获取长度:len(x)
>>> len(y)
7
拼接:x + y
>>> x = (1, 2, 3)
>>> y = ("a", "b", "c")
>>> z = x + y
>>> z
(1, 2, 3, 'a', 'b', 'c')
重复拼接:x * n
>>> x = (1, 2, 3)
>>> x * 3
(1, 2, 3, 1, 2, 3, 1, 2, 3)
反向重复拼接:n * x
>>> x = (1, 2, 3)
>>> 3 * x
(1, 2, 3, 1, 2, 3, 1, 2, 3)
就地拼接:x += y
>>> x = (1, 2, 3)
>>> y = ("a", "b", "c")
>>> x += y
>>> x
(1, 2, 3, 'a', 'b', 'c')
就地拼接的语法可能会暗示我们在处理相同的对象:我们从等于(1, 2, 3)的元组x开始;在拼接y之后,x仍然是一个元组,但包含了六个值:(1, 2, 3, "a", "b", "c")。由于我们讨论了元组的不变性,我们知道x之前和x之后是两个不同的对象。
我们可以通过以下简单测试轻松检查这一点。它使用两个对象的id:如果它们有相同的id,那么它们是同一个对象;但如果id不同,那么在就地拼接之前和之后的x是两个不同的对象。我们来做一下测试:
>>> x = (1, 2, 3)
>>> first_id = id(x)
>>> y = ("a", "b", "c")
>>> x += y
>>> second_id = id(x)
>>> first_id == second_id
False
两个id不同,这意味着在就地操作之后的x与之前的x是不同的对象。
就地重复拼接:x *= n
>>> x = (1, 2, 3)
>>> x *= 3
>>> x
(1, 2, 3, 1, 2, 3, 1, 2, 3)
我上面写的同样适用在这里:尽管我们在这里看到的只有一个名字,x,但实际上有两个对象:x之前的和x之后的。
包含:in
>>> x = (1, 2, 3)
>>> 1 in x
True
>>> 100 in x
False
计算元素出现的次数:x.count(element)
>>> y = ("a", "b", "c", "a", "a", "b", "C")
>>> y.count("a")
3
>>> y.count("b")
2
获取指定位置的项:x[i](x.__getitem__(i))
>>> y[0]
'a'
>>> y[4], y[5]
('a', 'b')
查找第一次出现的 element 的位置:x.index(element)
>>> y = ("a", "b", "c", "a", "a", "b", "C")
>>> y.index("a")
0
>>> y.index("b")
1
获取迭代器:iter(x)(x.__iter__())
>>> y_iter = iter(y)
>>> y_iter # doctest: +ELLIPSIS
<tuple_iterator object at 0x7...>
>>> next(y_iter)
'a'
>>> next(y_iter)
'b'
>>> for y_i in iter(y):
... print(y_i, end=" | ")
a | b | c | a | a | b | C |
支持使用 pickle 优化序列化:x.__getnewargs__()
这个方法不像上面那些方法那样直接使用。相反,它在 pickle 序列化过程中用于优化元组的序列化,如下面的玩具示例所示:
>>> import pickle
>>> with open("x.pkl", "wb") as f:
... pickle.dump(x, f)
>>> with open("x.pkl", "rb") as f:
... x_unpickled = pickle.load(f)
>>> x_unpickled
(1, 2, 3)
在他那本精彩的书《流畅的 Python》(第 2 版)中,Luciano Ramalho 列出了 15 个列表有而元组没有的方法——但这个优化序列化的方法是元组独有的,是列表没有的唯一方法。

“元组”一词在不同语言中的表达。图片由作者提供。
结论
在这篇文章中,我们讨论了 Python 中最常见的集合类型之一——元组的基础知识。希望你喜欢这篇文章——如果喜欢,请注意,我们讨论的不仅仅是基础知识,还可以说是非争议性的。
然而,元组还有更多内容,其中一些内容并不像我们从这篇文章中学到的那样清晰。我们将在这篇文章的后续部分讨论这些内容。你会看到,元组并不像你读完这篇文章后可能想象的那样简单。在我看来,元组比任何其他内置类型都更具争议性。也许元组甚至被过度使用了——但在读完下一篇文章后,我让你自己决定。老实说,我对元组有些不满。实际上,我会对元组有点苛刻……甚至可能有些过头?
我希望我已经足够引起你的兴趣,让你阅读这篇文章的续集。你可以在这里找到它:
Python 元组,全面的真相与唯一真相:让我们深入探讨
了解元组的复杂性
towardsdatascience.com
感谢阅读。如果你喜欢这篇文章,你可能也会喜欢我写的其他文章;你可以在这里查看。如果你想加入 Medium,请使用下面的推荐链接:
## 通过我的推荐链接加入 Medium — Marcin Kozak
阅读 Marcin Kozak 的每一个故事(以及 Medium 上的其他数千位作者的故事)。你的会员费直接支持……
脚注
¹ 请注意,在许多代码块中,如上面所示,我使用了doctest测试,以确保示例正确运行。你可以在模块的文档和这篇在Towards Data Science上发布的介绍文章中了解更多关于doctest的信息。
² 请注意,我写的是“在范围内”,而不是“在代码中”。这是因为虽然我们在这里不需要这些值,但我们可能在代码的其他地方,在某个其他范围内需要它们(例如,在另一个函数中)。在特定范围内使用特定解包只会影响这个范围;因此,我们可以在另一个范围内再次解包相同的可迭代对象,这种解包可能会有所不同。
在代码块中,你会发现 KmSquare 类型别名。我使用它来提高定义城市时浮点数的可读性。你可以在这里阅读更多关于类型提示和类型别名的内容。
资源
## Python 文档测试,使用 doctest:简单的方法
doctest 允许进行文档、单元和集成测试,以及测试驱动开发。
了解列表推导(listcomps)、集合推导(setcomps)、字典推导等的复杂性…
## 找到 Python 代码中的 bug:小细节产生大问题
即使是最小的字符也可能引入大问题
Python 的简洁性使你可以迅速变得高效,但这通常意味着你并没有充分利用它的所有功能…
## Python 文档测试,使用 doctest:简单的方法
Python 元组,真相和唯一的真相:深入探讨
原文:
towardsdatascience.com/python-tuple-the-whole-truth-and-only-truth-lets-dig-deep-24d2bf02971b
PYTHON PROGRAMMING
学习元组的复杂性。
·发表于 Towards Data Science ·阅读时间 24 分钟 ·2023 年 1 月 27 日
--

元组的不可变性可能令人困惑且令人头痛。照片由 Aarón Blanco Tejedor 提供,来源于 Unsplash
在上一篇文章中,我们讨论了元组的基础知识:
## Python Tuple, the Whole Truth, and Only the Truth: Hello, Tuple!
学习元组的基础知识及其使用方法
towardsdatascience.com
我向你展示了元组是什么,它提供了哪些方法,以及最重要的是,我们讨论了元组的不可变性。但元组远不止这些,这篇文章将对上一篇文章进行扩展。你将在这里学习元组类型的以下方面:
-
元组的复杂性:不可变性对复制元组的影响以及元组类型提示。
-
从元组继承。
-
元组性能:执行时间和内存。
-
元组相较于列表的优势(?):清晰度、性能以及元组作为字典键的使用。
-
元组推导(?)
-
命名元组
元组的复杂性
元组最重要的复杂性可能就是它的不可变性。但由于这定义了这种类型的本质,即使是初学者也应该了解这种不可变性是如何工作的,以及它在理论和实践中的意义。因此,我们在上述提到的上一篇文章中讨论了这一点。在这里,我们将讨论元组的其他重要复杂性。
不可变性对复制元组的影响
这将会很有趣!
一位理论家可能会对我大喊,称只有一种元组的不可变性,那就是我们在上一篇文章中讨论的那个。好吧,这是事实,但……但 Python 本身区分了两种不同的不可变性!而 Python 必须 做出这种区分。这是因为只有真正不可变的对象才是可哈希的。在下面的代码中,你会看到第一个元组是可哈希的,而第二个元组则不是:
>>> hash((1,2))
-3550055125485641917
>>> hash((1,[2]))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'
一个对象是否可哈希会影响到各种事情——这也是为什么 Python 区分可哈希和不可哈希的元组;前者是我称之为真正不可变的元组。我将展示 Python 如何处理这两种元组,包括元组复制的工作原理和将元组用作字典键的情况。
首先,让我们看看在元组复制中它是如何工作的。为此,我们创建一个完全不可变的元组,并使用所有可用的方法进行复制:
>>> import copy
>>> a = (1, 2, 3)
>>> b = a
>>> c = tuple(a)
>>> d = a[:]
>>> e = copy.copy(a) # a shallow copy
>>> f = copy.deepcopy(a) # a deep copy
由于 a 是一个完全不可变的元组,原始元组 (a) 及其所有副本应该指向同一个对象:
>>> a is b is c is d is e is f
True
正如预期的那样——也应该是完全不可变类型的情况——所有这些名称都指向同一个对象;它们的 id 是相同的。这就是我所称的真正或完全不可变性。
现在我们用第二种类型的元组做同样的事情;也就是说,一个包含一个或多个可变元素的元组:
>>> import copy
>>> a = ([1], 2, 3)
>>> b = a
>>> c = tuple(a)
>>> d = a[:]
>>> e = copy.copy(a) # a shallow copy
>>> f = copy.deepcopy(a) # a deep copy
从 b 到 e 的副本是浅复制,因此它们将引用与原始名称相同的对象:
>>> a is b is c is d is e
True
这就是我们需要深度复制的原因。深度复制应该覆盖所有对象,包括嵌套在内部的对象。由于 a 元组内部有一个可变对象,因此与之前不同的是,这次深度复制 f 将不会指向相同的对象:
>>> a is f
False
元组的第一个元素(索引 0)是 [1],所以它是可变的。当我们创建 a 的浅复制时,元组 a 到 e 的第一个元素指向相同的列表:
>>> a[0] is b[0] is c[0] is d[0] is e[0]
True
但创建深度复制意味着创建一个新的列表:
>>> a[0] is f[0]
False
现在让我们看看这两种不可变性在将元组用作字典键时的工作差异:
>>> d = {}
>>> d[(1, 2)] = 3
>>> d[(1, [2])] = 4
Traceback (most recent call last):
...
TypeError: unhashable type: 'list'
所以,如果你想将一个元组用作字典键,它必须是可哈希的——也就是说,它必须真正不可变。
所以,如果有人告诉你 Python 元组只有一种不可变性,你会知道这并不完全正确——因为在不可变性方面有两种类型的元组:
-
完全不可变的元组,仅包含不可变元素;这在引用和值两个方面都表现为不可变性;
-
从引用角度看不可变但从值角度看可变的元组,即包含可变元素的元组。
如果不区分这两者,你将无法理解元组复制的工作原理。
元组类型提示
类型提示在 Python 中变得越来越重要。有些人说现代 Python 代码中没有类型提示是不可能的。正如我在另一篇文章中所写的那样,我不会在这里重复。如果你感兴趣,请阅读它:
[## Python 的类型提示:朋友、敌人,还是只是个头疼的问题?
类型提示在 Python 社区中的受欢迎程度不断上升。这会将我们引向何处?我们能做些什么来利用它……
在这里,我们简要讨论如何处理元组的类型提示。我将展示现代版本的元组类型提示,即 Python 3.11。由于类型提示在不断变化,请注意,并非所有旧版本的 Python 都能以相同的方式工作。
从 Python 3.9 开始,事情变得更简单,因为可以使用内置的 tuple 类型,并用方括号 [] 指示字段。以下是你可以做的几个示例。
tuple[int, ...]、tuple[str, ...] 等等
这意味着对象是 int / str / 等等元素的元组,长度不限。省略号 ... 表明元组可以有任意长度;无法固定长度。
tuple[int | float, ...] 如上所述,但元组可以包含 int 和 float 类型的元素。
tuple[int, int] 与上述不同,这个元组是两个整数的记录。
tuple[str, int|float] 再次是一个两项记录,第一项是字符串,第二项是整数或浮点数。
tuple[str, str, tuple[int, float]] 一个包含三项的记录,前两项是字符串,第三项是包含一个整数和一个浮点数的二元素元组。
tuple[Population, Area, Coordinates]
这是一个特定的记录,包含三种特定类型的元素。这些类型,Population、Area、Coordinates,是命名元组或先前定义的数据类型,或类型别名。正如我在上述文章中所解释的,使用这些类型别名比使用内置类型如 int、float 等更具可读性。
这些只是几个示例,但我希望它们能帮助你了解你可以用元组的类型提示做些什么。我只提到了 命名元组,因为我将在下面的另一个部分讨论这种类型。不过,请记住,在类型提示的背景下,命名元组也非常有用,因为借助命名元组,你可以获得一个自定义的类型别名,它也是一个数据容器——这是一个强大的组合。
从 tuple 继承
你可以从 list 继承,尽管有时从 collections.UserList 继承更好。那么,我们是否可以对元组做同样的事情?我们可以从 tuple 类继承吗?
基本上,忘掉创建类似元组的通用类型的想法。tuple没有自己的.__init__()方法,因此你不能像继承自列表那样调用super().__init__()。没有这一点,你几乎没有任何功能,因为tuple类继承的是object.__init__()。
然而,这并不意味着你不能从tuple继承。你可以,但不是为了创建通用类型,而是特定类型。你还记得City类吗?我们可以做类似的事情,但要注意,这可能并不有趣。
>>> class City(tuple):
... def __new__(self, lat, long, population, area):
... return tuple.__new__(City, (lat, long, population, area))
我们有一个类似元组的City类:
>>> Warsaw = City(52.2297, 21.0122, 1_765_000, 517.2)
>>> Warsaw
(52.2297, 21.0122, 1765000, 517.2)
>>> Warsaw[0]
52.2297
这个类确切地接受四个参数,既不多也不少:
>>> Warsaw = City(52.2297, 21.0122, 1_765_000)
Traceback (most recent call last):
...
TypeError: __new__() missing 1 required positional argument: 'area'
>>> Warsaw = City(52.2297, 21.0122, 1_765_000, 517.2, 50)
Traceback (most recent call last):
...
TypeError: __new__() takes 5 positional arguments but 6 were given
请注意,在当前版本中,我们可以使用参数名称,但不必这样做,因为它们是位置参数。
>>> Warsaw_names = City(
... lat=52.2297,
... long=21.0122,
... population=1_765_000,
... area=517.2
... )
>>> Warsaw == Warsaw_names
True
但是我们不能通过名称访问值:
>>> Warsaw.area
Traceback (most recent call last):
...
AttributeError: 'City' object has no attribute 'area'
我们可以通过两种方式来改变这一点。一种是使用collections或typing模块中的命名元组;我们稍后会讨论它们。但我们也可以使用我们的City类来实现相同的效果,感谢operator模块:
>>> import operator
>>> City.lat = property(operator.itemgetter(0))
>>> City.long = property(operator.itemgetter(1))
现在我们可以按名称访问lat和long属性:
>>> Warsaw.lat
52.2297
>>> Warsaw.long
21.0122
然而,由于我们只对lat和long进行了上述操作,我们将无法按名称访问population和area:
>>> Warsaw.area
Traceback (most recent call last):
...
AttributeError: 'City' object has no attribute 'area'
我们当然可以改变这一点:
>>> City.population = property(operator.itemgetter(2))
>>> City.area = property(operator.itemgetter(3))
>>> Warsaw.population
1765000
>>> Warsaw.area
517.2
不过,我从未做过这样的事情。如果你想要这样的功能,你应该使用命名元组。
元组性能
执行时间
为了基准测试使用元组的各种操作,以及作为比较的列表,我使用了附录中接近文章末尾的脚本。你还会在那里找到运行代码的结果。我提供代码不仅仅是为了记录,也为了让你可以扩展实验。
总体而言,无论其大小和执行的操作是什么,列表总是更快。我常听说创建元组的原因之一是它们较小的内存消耗。我们的这个小实验远未确认这一观点。虽然有时元组确实使用了稍少的内存,但通常它们使用的内存稍多。因此,我对 5 百万和 1000 万整数项的非常长的列表和元组进行了实验。结果是,列表通常消耗的内存更少……
那么,这些小内存消耗的元组在哪里呢?也许这与元组和相应列表所占的磁盘空间有关?让我们检查一下:
>>> from pympler.asizeof import asizeof
>>> for n in (3, 10, 100, 1000, 1_000_000, 5_000_000, 10_000_000):
... print(
... f"tuple, n of {n: 9}: {asizeof(tuple(range(n))):10d}"
... "\n"
... f" list, n of {n: 9}: {asizeof(list(range(n))):10d}"
... "\n"
... f"{'-'*33}"
... )
tuple, n of 3: 152
list, n of 3: 168
---------------------------------
tuple, n of 10: 432
list, n of 10: 448
---------------------------------
tuple, n of 100: 4032
list, n of 100: 4048
---------------------------------
tuple, n of 1000: 40032
list, n of 1000: 40048
---------------------------------
tuple, n of 1000000: 40000032
list, n of 1000000: 40000048
---------------------------------
tuple, n of 5000000: 200000032
list, n of 5000000: 200000048
---------------------------------
tuple, n of 10000000: 400000032
list, n of 10000000: 400000048
---------------------------------
仅在小元组及其相应列表的情况下,内存使用差异才是明显的——例如,152与168。但我认为你会同意,400_000_032与400_000_048实际上并没有小那么多,对吧?
我在过去的实验中观察到的另一件事(代码未展示)。Python 编译器以特殊方式处理元组字面量,因为它将它们保存在静态内存中——所以它们是在编译时创建的。其他方式创建的列表和元组都不能保存在静态内存中——它们总是使用动态内存,这意味着它们是在运行时创建的。这个话题复杂到足以值得单独的文章,因此我们就此停下。
我将这些基准留给你。如果你想扩展它们,请随意。如果你学到了新且意外的东西,请在评论中分享。
我学到的是,几乎不要仅仅因为性能而使用元组。但的确,如果我们需要一个简单的类型来存储非常小的记录,比如由两个或三个元素组成,元组可能是一个有趣的选择。如果字段名称有帮助,而且字段更多,我宁愿使用其他东西,命名元组是一个选择,dataclasses.dataclass是另一个选择。

一个列表和一个元组。作者提供的图像。
元组相对于列表的优势(?)
在流畅的 Python中,L. Ramalho 提到元组相对于列表的两个优势:清晰度和性能。老实说,我找不到其他优势,但这两个优势可能已经足够。因此,让我们逐一讨论它们,并决定它们是否确实在某些方面使元组优于列表。
清晰度
正如 L. Ramalho 所写,当你使用元组时,你知道它的长度永远不会改变——这增加了代码的清晰度。我们已经讨论过元组长度可能发生的情况。的确,由于不可变性带来的清晰度是很棒的,我们确实知道任何元组的长度永远不会改变,但……
正如 L. Ramalho 自己警告的那样,包含可变项的元组可能是难以发现的错误来源。你还记得我之前提到的与原地操作相关的内容吗?一方面,我们可以确定一个元组,比如x,它的长度永远不会改变。我同意这是一个在清晰度方面很有价值的信息。然而,当我们对x进行原地操作时,这个元组将不再是同一个元组,即便它仍然是一个名为x的元组——但,请让我重复,是一个不同的名为x的元组。
因此,我们应该按如下方式修订上述清晰度优势:
- 我们可以确定一个特定的
id的元组长度永远不会改变。
或者:
- 我们可以确定,如果我们定义一个特定长度的元组,它的长度不会改变,但我们应该记住,如果我们使用任何原地操作,那么这个元组就不是我们之前所指的那个元组。
听起来有点疯狂?我完全同意:这确实很疯狂。对我来说,这不是清晰;这是清晰的对立面。有人这样想吗?想象一下你在一个函数中定义了一个元组 x。然后你执行原地连接操作,例如 x += y,这看起来就像 y 保持不变但 x 发生了变化。我们知道这不是真的——因为这个原始的 x 已经不存在,我们有一个全新的 x——但这就是它看起来的样子,尤其是因为我们仍然有一个元组 x,其第一个元素与原始 x 元组中的元素完全相同。
当然,我知道从 Python 的角度来看这一切都是有意义的。但当我编码时,我不希望我的思维被这种方式占据。为了使代码清晰,我更倾向于让它在不需要做出这样的假设的情况下保持清晰。这正是为什么对我来说,元组并不意味着清晰;它们意味着比我在列表中看到的清晰度要低。
这还不是元组清晰度的全部。在代码方面,我特别喜欢列表中的一个特性,但不喜欢元组中的这个特性。用于创建列表的方括号 [] 使得它们在代码中显得突出,因为没有其他容器使用方括号。看看字典:它们使用大括号 {},集合也可以使用这些大括号。元组使用圆括号 (),而圆括号不仅在生成器表达式中使用,而且在代码中的许多不同地方使用,因为 Python 代码使用圆括号的目的非常多。因此,我喜欢列表在代码中显得突出——而不喜欢元组的不突出。
性能
L. Ramalho 写道,元组使用的内存比对应的列表少,Python 可以对这两者进行相同的优化。我们已经分析了内存性能,因为我们知道这并不总是如此——实际上,元组所用的磁盘内存确实比对应的列表要小,但这种差异可能微不足道。
这种知识,加上列表在执行时间上的更好性能,使我认为性能不使元组成为更好的选择。在执行时间方面,列表更好。在内存使用方面,元组确实可以更好——但现在,随着现代计算机的出现,这些差异真的很小。此外,当我需要一个真正节省内存的容器来收集大量数据时,我不会选择列表或元组——而是选择生成器。
另一件事:元组作为字典键
除了这两个方面,还有一个值得考虑的第三个方面,我们已经提到过——你不能将列表用作字典中的键,但可以使用元组。或者说,你可以使用真正不可变(即,可哈希)的元组。原因在于前者的可变性和后者的不可变性。
与前两个优势不同,这个优势在特定情况下可能非常显著,即使这种情况比较少见。
元组推导(?)
如果你希望从这一节中了解到 Python 中存在元组推导,或者希望学到一些能让你的 Python 爱好者同伴惊叹的惊人技巧——我很抱歉!我并不想制造虚假的希望。今天没有元组推导;没有让人震撼的语法。
你可能还记得,在我关于 Python 推导的文章中,我并没有提到元组推导:
学习列表推导(listcomps)、集合推导(setcomps)、字典推导的复杂性…
towardsdatascience.com
这是因为没有元组推导。但我不想让你空手而归,我为你准备了一个安慰奖。我会向你展示一些元组推导的替代方案。
首先,记住生成器表达式不是元组推导。我认为许多 Python 初学者会混淆这两者。我特别记得在学习列表推导后第一次看到我的生成器表达式。我的第一反应是,“嗯,这就是了。一个元组推导。”我很快意识到,虽然前者确实是列表推导,但后者不是元组推导:
>>> listcomp = [i**2 for i in range(7)] # a list comprehension
>>> genexp = (i**2 for i in range(7)) # NOT a tuple comprehension
我花了一些时间——如果不是浪费的话——才了解到有列表推导、集合推导、字典推导和生成器表达式——但没有元组推导。不要重蹈我的覆辙。不要花几个小时去寻找元组推导。它们在 Python 中不存在。
这就是我的安慰奖——两个元组推导的替代方案。
替代方案 1: tuple() + genexp
>>> tuple(i**2 for i in range(7))
(0, 1, 4, 9, 16, 25, 36)
你有没有注意到你不需要先创建一个列表推导然后是元组?确实,在这里我们创建了一个生成器表达式,并用tuple()类来转换它。这自然会给我们一个元组。
替代方案 2: genexp + 生成器解包
>>> *(i**2 for i in range(7)),
(0, 1, 4, 9, 16, 25, 36)
一个不错的小技巧,不是吗?它使用了扩展的可迭代解包,它返回一个元组。你可以用它来处理任何可迭代对象,既然生成器是其中之一,它就有效!让我们检查它是否也对列表有效:
>>> x = [i**2 for i in range(7)]
>>> *x,
(0, 1, 4, 9, 16, 25, 36)
你可以不赋值给x而做同样的事情:
>>> *[i**2 for i in range(7)],
(0, 1, 4, 9, 16, 25, 36)
它适用于任何可迭代对象——但别忘了行末的逗号;没有它,这个技巧将无法奏效:
>>> *[i**2 for i in range(7)]
File "<stdin>", line 1
SyntaxError: can't use starred expression here
让我们检查集合:
>>> x = {i**2 for i in range(7)}
>>> *x,
(0, 1, 4, 9, 16, 25, 36)
它有效!并且要注意,通常,解包提供一个元组。这就是为什么扩展的可迭代解包看起来有点像元组推导。虽然它确实像一个不错的小技巧,但其实不是:这是 Python 提供的工具之一,尽管它确实是一个边缘情况。
但我不会使用替代方案 2。我会选择替代方案 1,它使用tuple()。我们大多数人喜欢像第二个替代方案这样的技巧,但它们很少清晰——而且替代方案 2,与替代方案 1相比,远不如前者清晰。不过,任何 Python 爱好者都会看到替代方案 1中的内容,即使他们没有看到其中隐藏的生成器表达式。
命名元组
元组是未命名的——但这并不意味着 Python 中没有命名元组。恰恰相反,确实存在——而且,毫无意外,它们被称为……命名元组。
你有两种方法来使用命名元组:collections.namedtuple和typing.NamedTuple。命名元组顾名思义:它们的元素(称为字段)具有名称。你可以在附录中的基准测试脚本中看到前者的实际应用。
就个人而言,我认为它们在许多不同情况下都非常有帮助。它们不会提高性能;甚至可能会降低性能。但在清晰性方面,它们可以更清楚,无论是对开发者还是对代码的用户。
因此,尽管我经常使用常规元组,有时我会选择命名元组——这正是因为它的清晰性。
命名元组提供了丰富的可能性,值得专门为它们写一篇文章。因此,我在这里仅仅讲述这些——但我计划写一篇专门讨论这种强大类型的文章。

“元组”在各种语言中的表示。图像由作者提供。
结论
本文以及上一篇文章旨在提供关于元组、它们的用例、优缺点及其复杂性的深入信息。尽管元组的使用非常普遍,但在开发者中,尤其是那些经验较少的 Python 开发者中,它们并不那么知名。这就是为什么我想将关于这个有趣类型的丰富信息集中在一个地方——希望你从阅读中学到了一些东西,甚至像我从写作中学到的一样多。
说实话,在开始写关于元组的内容时,我以为会发现更多的优势。我从开始使用 Python 的第一天起就一直在使用元组。尽管我使用列表的频率要高得多,但我还是喜欢元组,尽管对它们了解不多——所以我在这篇文章中包含的一些信息对我来说是新的。
然而,在写完这篇文章后,我对元组的喜爱已经不那么强烈了。我仍然认为它们是处理小记录的有价值类型,但它们的扩展——命名元组——或数据类似乎是更好的方法。而且,元组似乎也不是特别有效。它们比列表要慢,而且只节省了少量内存。那么,我为什么还要使用它们呢?
也许是因为它们的不可变性?也许。如果你喜欢基于不可变性概念的函数式编程,你肯定会更喜欢元组而不是列表。我曾多次使用这个论点来说服自己在这种或那种情况下应该更喜欢元组而不是列表。
但元组所提供的不可变性,如我们讨论的那样,并不是那么明确。假设x是一个不可变类型的项的元组。我们知道这个元组确实是不可变的,对吗?如果是这样,我不喜欢以下代码,这在 Python 中是完全正确的:
>>> x = (1, 2, 'Zulu Minster', )
>>> y = (4, 4, )
>>> x += y
>>> x
(1, 2, 'Zulu Minster', 4, 4)
>>> x *= 2
>>> x
(1, 2, 'Zulu Minster', 4, 4, 1, 2, 'Zulu Minster', 4, 4)
我知道这在 Python 中是正确的,我知道这甚至是 Pythonic 的代码——但我不喜欢它。我不喜欢我可以用 Python 元组做这样的事情。它根本没有元组不可变性的感觉。依我看,如果你有一个不可变类型,你应该能够复制它,你应该能够连接两个实例等等——但你不应该能够通过就地操作将一个新元组赋给旧名称。你想让这个名称保持不变?你的选择。所以,我对以下情况没问题:
>>> x = x + y
因为这意味着将x + y赋值给x,这基本上意味着覆盖这个名称。如果你选择覆盖x的先前值,这是你的选择。但就我而言,就地操作至少没有不可变性的感觉。我更愿意在 Python 中不能做到这一点。
如果没有不可变性,那么也许其他的因素应该说服我更常使用元组?但是什么呢?性能?元组的性能较差,因此这并不能说服我。在执行时间方面,毫无争议;它们确实比相应的列表慢。你可以说在内存方面。确实,它们占用的磁盘空间更少,但差异微妙,对于长容器来说——完全可以忽略。RAM 内存使用?这个论点也没有太成功,因为通常列表的效率和元组一样——有时甚至更高。如果我们有一个巨大的集合,生成器在内存方面会表现更好。
尽管如此,元组在 Python 中确实有其存在的意义。它们非常频繁地被用来从函数或方法中返回两个或三个项——所以,作为小型未命名记录。它们被用作可迭代解包的输出。它们构成了命名元组的基础——collections.namedtuple和typing.NamedTuple——这些是元组的强大兄弟,可以用作具有命名字段的记录。
总的来说,我不再像写这篇文章之前那样喜欢元组了。我曾把它们视为一个重要的 Python 类型;现在在我眼中它们不再那么重要——但我接受它们在 Python 中的各种使用场景,甚至喜欢其中一些。
我对元组是否不公平?也许。如果你这么认为,请在评论中告诉我。我总是很享受与读者的有益讨论。
感谢阅读。如果你喜欢这篇文章,你可能也会喜欢我写的其他文章;你可以在这里查看。如果你想加入 Medium,请使用下面的推荐链接:
[## 使用我的推荐链接加入 Medium — Marcin Kozak
阅读 Marcin Kozak 的每一个故事(以及 Medium 上的其他成千上万的作者)。你的会员费直接支持…
资源
了解列表推导(listcomps)、集合推导(setcomps)、字典推导的细节…
towardsdatascience.com [## PEP 3132 — 扩展的可迭代解包
这个 PEP 提议对可迭代解包语法进行更改,允许指定一个“全能”名称来接收…
peps.python.org [## Fluent Python, 第 2 版
Python 的简洁性让你能够迅速提高生产力,但这通常意味着你没有充分利用它所具备的所有功能…
附录
在这个附录中,你将找到我用来基准测试元组与列表的脚本。我使用了perftester包,你可以在这篇文章中阅读相关信息:
## 轻松进行 Python 函数基准测试:perftester
你可以使用 perftester 轻松对 Python 函数进行基准测试
towardsdatascience.com
这是代码:
import perftester
from collections import namedtuple
from typing import Callable, Optional
Length = int
TimeBenchmarks = namedtuple("TimeBenchmarks", "tuple list better")
MemoryBenchmarks = namedtuple("MemoryBenchmarks", "tuple list better")
Benchmarks = namedtuple("Benchmarks", "time memory")
def benchmark(func_tuple, func_list: Callable,
number: Optional[int] = None) -> Benchmarks:
# time
t_tuple = perftester.time_benchmark(func_tuple, Number=number)
t_list = perftester.time_benchmark(func_list, Number=number)
better = "tuple" if t_tuple["min"] < t_list["min"] else "list"
time = TimeBenchmarks(t_tuple["min"], t_list["min"], better)
# memory
m_tuple = perftester.memory_usage_benchmark(func_tuple)
m_list = perftester.memory_usage_benchmark(func_list)
better = "tuple" if m_tuple["max"] < m_list["max"] else "list"
memory = MemoryBenchmarks(m_tuple["max"], m_list["max"], better)
return Benchmarks(time, memory)
def comprehension(n: Length) -> Benchmarks:
"""List comprehension vs tuple comprehension.
Here, we're benchmarking two operations:
* creating a container
* looping over it, using a for loop; nothing is done in the loop.
"""
def with_tuple(n: Length):
x = tuple(i**2 for i in range(n))
for _ in x:
pass
def with_list(n: Length):
x = [i**2 for i in range(n)]
for _ in x:
pass
number = int(10_000_000 / n) + 10
return benchmark(lambda: with_tuple(n), lambda: with_list(n), number)
def empty_container() -> Benchmarks:
"""List vs tuple benchmark: creating an empty container."""
return benchmark(lambda: tuple(), lambda: [], number=100_000)
def short_literal() -> Benchmarks:
"""List vs tuple benchmark: tuple literal."""
return benchmark(lambda: (1, 2, 3), lambda: [1, 2, 3], number=100_000)
def long_literal() -> Benchmarks:
"""List vs tuple benchmark: tuple literal."""
return benchmark(
lambda: (1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3,
1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3,
1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3,
1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3,
1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3,
1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3,
1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3,
1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3,),
lambda: [1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3,
1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3,
1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3,
1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3,
1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3,
1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3,
1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3,
1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3,],
number=100_000)
def func_with_range(n: Length) -> Benchmarks:
"""List vs tuple benchmark: func(range(n))."""
def with_tuple(n: Length):
return tuple(range(n))
def with_list(n: Length):
return list(range(n))
number = int(10_000_000 / n) + 10
return benchmark(lambda: with_tuple(n), lambda: with_list(n), number)
def concatenation(n: Length) -> Benchmarks:
"""List vs tuple benchmark: func(range(n))."""
def with_tuple(x: tuple):
x += x
return x
def with_list(y: list):
y += y
return y
number = int(10_000_000 / n) + 10
return benchmark(lambda: with_tuple(tuple(range(n))),
lambda: with_list(list(range(n))),
number)
def repeated_concatenation(n: Length) -> Benchmarks:
"""List vs tuple benchmark: func(range(n))."""
def with_tuple(x: tuple):
x *= 5
return x
def with_list(y: list):
y *= 5
return y
number = int(10_000_000 / n) + 10
return benchmark(lambda: with_tuple(tuple(range(n))),
lambda: with_list(list(range(n))), number)
if __name__ == "__main__":
n_set = (3, 10, 20, 50, 100, 10_000, 1_000_000)
functions = (
comprehension,
empty_container,
short_literal,
long_literal,
func_with_range,
concatenation,
repeated_concatenation,
)
functions_with_n = (
comprehension,
func_with_range,
concatenation,
repeated_concatenation,
)
results = {}
for func in functions:
name = func.__name__
print(name)
if func in functions_with_n:
results[name] = {}
for n in n_set:
results[name][n] = func(n)
else:
results[name] = func()
perftester.pp(results)
以下是结果:
{'comprehension': {3: Benchmarks(time=TimeBenchmarks(tuple=9.549e-07, list=8.086e-07, better='list'), memory=MemoryBenchmarks(tuple=15.62, list=15.63, better='tuple')),
10: Benchmarks(time=TimeBenchmarks(tuple=2.09e-06, list=1.94e-06, better='list'), memory=MemoryBenchmarks(tuple=15.64, list=15.64, better='list')),
20: Benchmarks(time=TimeBenchmarks(tuple=4.428e-06, list=4.085e-06, better='list'), memory=MemoryBenchmarks(tuple=15.64, list=15.65, better='tuple')),
50: Benchmarks(time=TimeBenchmarks(tuple=1.056e-05, list=9.694e-06, better='list'), memory=MemoryBenchmarks(tuple=15.69, list=15.69, better='list')),
100: Benchmarks(time=TimeBenchmarks(tuple=2.032e-05, list=1.968e-05, better='list'), memory=MemoryBenchmarks(tuple=15.7, list=15.7, better='list')),
10000: Benchmarks(time=TimeBenchmarks(tuple=0.002413, list=0.002266, better='list'), memory=MemoryBenchmarks(tuple=15.96, list=16.04, better='tuple')),
1000000: Benchmarks(time=TimeBenchmarks(tuple=0.2522, list=0.2011, better='list'), memory=MemoryBenchmarks(tuple=54.89, list=54.78, better='list'))},
'concatenation': {3: Benchmarks(time=TimeBenchmarks(tuple=3.38e-07, list=3.527e-07, better='tuple'), memory=MemoryBenchmarks(tuple=31.45, list=31.45, better='list')),
10: Benchmarks(time=TimeBenchmarks(tuple=4.89e-07, list=4.113e-07, better='list'), memory=MemoryBenchmarks(tuple=31.45, list=31.45, better='list')),
20: Benchmarks(time=TimeBenchmarks(tuple=5.04e-07, list=4.368e-07, better='list'), memory=MemoryBenchmarks(tuple=31.45, list=31.45, better='list')),
50: Benchmarks(time=TimeBenchmarks(tuple=7.542e-07, list=6.22e-07, better='list'), memory=MemoryBenchmarks(tuple=31.45, list=31.45, better='list')),
100: Benchmarks(time=TimeBenchmarks(tuple=1.133e-06, list=9.005e-07, better='list'), memory=MemoryBenchmarks(tuple=31.45, list=31.45, better='list')),
10000: Benchmarks(time=TimeBenchmarks(tuple=0.0001473, list=0.000126, better='list'), memory=MemoryBenchmarks(tuple=31.7, list=31.7, better='list')),
1000000: Benchmarks(time=TimeBenchmarks(tuple=0.04862, list=0.04247, better='list'), memory=MemoryBenchmarks(tuple=123.5, list=125.4, better='tuple'))},
'empty_container': Benchmarks(time=TimeBenchmarks(tuple=1.285e-07, list=1.107e-07, better='list'), memory=MemoryBenchmarks(tuple=23.92, list=23.92, better='list')),
'func_with_range': {3: Benchmarks(time=TimeBenchmarks(tuple=3.002e-07, list=3.128e-07, better='tuple'), memory=MemoryBenchmarks(tuple=23.92, list=23.92, better='list')),
10: Benchmarks(time=TimeBenchmarks(tuple=4.112e-07, list=3.861e-07, better='list'), memory=MemoryBenchmarks(tuple=23.92, list=23.92, better='list')),
20: Benchmarks(time=TimeBenchmarks(tuple=4.228e-07, list=4.104e-07, better='list'), memory=MemoryBenchmarks(tuple=23.93, list=23.93, better='list')),
50: Benchmarks(time=TimeBenchmarks(tuple=5.761e-07, list=5.068e-07, better='list'), memory=MemoryBenchmarks(tuple=23.93, list=23.94, better='tuple')),
100: Benchmarks(time=TimeBenchmarks(tuple=7.794e-07, list=6.825e-07, better='list'), memory=MemoryBenchmarks(tuple=23.94, list=23.94, better='list')),
10000: Benchmarks(time=TimeBenchmarks(tuple=0.0001536, list=0.000159, better='tuple'), memory=MemoryBenchmarks(tuple=24.67, list=24.67, better='list')),
1000000: Benchmarks(time=TimeBenchmarks(tuple=0.03574, list=0.03539, better='list'), memory=MemoryBenchmarks(tuple=91.7, list=88.45, better='list'))},
'long_literal': Benchmarks(time=TimeBenchmarks(tuple=1.081e-07, list=8.712e-07, better='tuple'), memory=MemoryBenchmarks(tuple=23.92, list=23.92, better='list')),
'repeated_concatenation': {3: Benchmarks(time=TimeBenchmarks(tuple=3.734e-07, list=3.836e-07, better='tuple'), memory=MemoryBenchmarks(tuple=31.83, list=31.83, better='list')),
10: Benchmarks(time=TimeBenchmarks(tuple=4.594e-07, list=4.388e-07, better='list'), memory=MemoryBenchmarks(tuple=31.83, list=31.83, better='list')),
20: Benchmarks(time=TimeBenchmarks(tuple=5.975e-07, list=5.578e-07, better='list'), memory=MemoryBenchmarks(tuple=31.83, list=31.83, better='list')),
50: Benchmarks(time=TimeBenchmarks(tuple=9.951e-07, list=8.459e-07, better='list'), memory=MemoryBenchmarks(tuple=31.83, list=31.83, better='list')),
100: Benchmarks(time=TimeBenchmarks(tuple=1.654e-06, list=1.297e-06, better='list'), memory=MemoryBenchmarks(tuple=31.83, list=31.83, better='list')),
10000: Benchmarks(time=TimeBenchmarks(tuple=0.0002266, list=0.0001945, better='list'), memory=MemoryBenchmarks(tuple=31.83, list=31.83, better='list')),
1000000: Benchmarks(time=TimeBenchmarks(tuple=0.09504, list=0.08721, better='list'), memory=MemoryBenchmarks(tuple=169.4, list=169.4, better='tuple'))},
'short_literal': Benchmarks(time=TimeBenchmarks(tuple=1.048e-07, list=1.403e-07, better='tuple'), memory=MemoryBenchmarks(tuple=23.92, list=23.92, better='list'))}
我决定对更大的n进行内存使用基准测试,即 500 万和 1000 万。我不会在这里展示代码,如果你有时间,可以基于上面的脚本写一个代码,这将是一个不错的练习。
如果你只想查看代码,你可以在这里找到。请注意,我可以改进代码,例如将两个实验的代码合并。我决定不这样做,以保持两个脚本的简单性。
这是结果:
{'comprehension': {5000000: MemoryBenchmarks(tuple=208.8, list=208.8, better='list'),
10000000: MemoryBenchmarks(tuple=402.2, list=402.2, better='tuple')},
'concatenation': {5000000: MemoryBenchmarks(tuple=285.4, list=247.2, better='list'),
10000000: MemoryBenchmarks(tuple=554.8, list=478.5, better='list')},
'func_with_range': {5000000: MemoryBenchmarks(tuple=400.4, list=396.4, better='list'),
10000000: MemoryBenchmarks(tuple=402.2, list=402.2, better='list')},
'repeated_concatenation': {5000000: MemoryBenchmarks(tuple=399.8, list=361.7, better='list'),
10000000: MemoryBenchmarks(tuple=783.7, list=707.4, better='list')}}
如你所见,对于我们研究的操作,元组要么占用相同的内存,要么占用更多的内存——有时甚至显著更多(例如,比较554.8与478.5或783.7与707.4)。
Python 类型提示:鸭子类型兼容性和与一致
原文:
towardsdatascience.com/python-type-hinting-duck-type-compatibility-and-consistent-with-72e8b348d8ac
PYTHON PROGRAMMING
当你在提示float时,你不必提示int,当你在提示tuple时,也不必提示namedtuple。为什么?
·发布于Towards Data Science ·阅读时间 8 分钟·2023 年 6 月 6 日
--

由Markus Winkler在Unsplash提供的照片
有时,Python 类型提示可以使事情变得更简单。确实,并不总是如此——但至少在我看来,通常它确实能做到——前提是明智地使用它。有些人不同意,但我不打算与他们争论:在我看来,这是一个相当主观的话题。
我在以下文章中写了我对 Python 类型提示的看法,如何使用它来提高代码可读性,以及如何不使用它以达到其他目的:
[## Python 的类型提示:朋友、敌人还是仅仅是个头疼的问题?
类型提示在 Python 社区中的受欢迎程度正在上升。这会将我们带到哪里?我们可以做些什么来使用它……
betterprogramming.pub](https://betterprogramming.pub/pythons-type-hinting-friend-foe-or-just-a-headache-73c7849039c7?source=post_page-----72e8b348d8ac--------------------------------)
今天,我们将讨论在 Python 类型中,与一致(consistent-with)和鸭子类型兼容性(duck-type compatibility)的含义。
想象一下你在提示使用float,就像下面的函数:
from collections.abc import Sequence
def sum_of_squares(x: Sequence[float]) -> float:
n, s = len(x), sum(x)
return sum((x_i - s/n)**2 for x_i in x)
这是一个典型的统计函数,用于计算一个变量的平方和。它接受一个浮点数的容器并返回一个浮点数。
正如你所见,为了注释这个函数,我使用了Sequence,这是一个从collections.abc(在 Python 3.9 之前,你需要使用typing.Sequence)中提供的通用抽象基类。这意味着你可以提供一个列表或一个元组——但你不能提供,例如,一个生成器¹。
好的,所以这是一个统计函数,它期望一个浮点数的序列。这是有道理的,对吧?但在实际生活中,相当多的定量变量是整数,比如每个芽的螨虫数量、销售的物品数量、人口数量,仅举几例。
那么我们是不是应该对函数做一些调整,以考虑到这个事实呢?我们都知道,动态情况下,函数对整数是完全有效的,而且动态情况下,我们可以轻松地将整数和浮点数结合在 x 中。但是类型提示和静态检查器呢?
对于这个函数,使用 int 是否合适,还是我们应该更清楚地说明它也接受 int 值?我们应该像下面这样做吗?
def sum_of_squares(x: Sequence[float | int]) -> float:
n, s = len(x), sum(x)
return sum((x_i - s/n)**2 for x_i in x)
这很明显:你可以使用浮点数或整数的序列,函数会返回一个浮点数。从类型提示的角度来看,这个版本不是更好吗?
为了回答这个问题,我们回到之前的版本,没有 int。静态类型检查器对此有何看法?
一句话也不说!看看 Pylance(在 Visual Studio Code 中)对它的说法:

来自 Visual Studio Code 的截图:Pylance 没有指出任何错误。图片由作者提供
没有!如果 Pylance 发现静态错误,我们会看到它被红色下划线标出。在这里,这是 mypy 的看法:

Mypy 说当你使用 int 进行浮点数注解时一切正常。图片由作者提供
为什么你可以用 int 代替 float?
我们已经进入了本文的主要话题。简而言之,当你提示 float 时,你可以使用 int 代替。
首先,我们来看看 mypy 文档中描述鸭子类型兼容性的网页:
[## 鸭子类型兼容性 - mypy 1.3.0 文档
在 Python 中,某些类型即使不是彼此的子类,仍然是兼容的。例如,对象是…
这就是我们将在那里看到的内容之一:
在 Python 中,某些类型即使不是彼此的子类,仍然是兼容的。例如,
int对象在期望float对象的地方是有效的。Mypy 通过 鸭子类型兼容性 支持这种惯用法。
哈!
不用担心,这不会过多扩展你对类型提示的知识:
这对于一小部分内置类型是被支持的:
int是与float和complex兼容的鸭子类型。
float是与complex兼容的鸭子类型。
bytearray和memoryview是与bytes兼容的鸭子类型。
所以现在我们知道了。当我们已经提示使用 float 时,不必再提示 int。这将和 float | int(或 Union[float, int])完全一样。这意味着提示中的 | int 部分是多余的。
就像 int 与 float 是鸭子类型兼容的,它也与 complex 是鸭子类型兼容的,float 与 complex 是鸭子类型兼容的,同时 bytearray 和 memoryview 也与 bytes 是鸭子类型兼容的。
好的,那是 mypy。现在,让我们看看我最喜欢的 Python 书籍,我在文章中经常提到的那本书:Fluent Python,第 2 版,由 Luciano Ramalho 编写:
一个无障碍友好的 Hugo 主题,从原始的 Cupper 项目移植过来。
www.fluentpython.com](https://www.fluentpython.com/?source=post_page-----72e8b348d8ac--------------------------------)
要了解这里发生了什么,我们应该转到 Luciano 解释 consistent-with 意思的地方。他写道,我们不需要将 int 添加到 float 类型提示中,因为 int 是 consistent-with float。
那么 consistent-with 是什么意思呢?(是的,Luciano 每次都使用连字符和斜体来表示 consistent-with,这与 PEP 484 不同。)
正如他解释的那样,当 T1 是 T2 的子类型时,T2 是 consistent-with T1。换句话说,一个子类是 consistent-with 所有它的超类——尽管有一些例外,这些例外扩展了 consistent-with 的定义。根据 PEP 484 的这一部分,Luciano 解释说,这一定义还包括了上述提到的数字场景。
当我们添加与类型 consistent-with bytes 的场景时,我们将有以下 consistent-with 的定义:

当 T2 是 consistent-with T1 时:
-
T1是T2的子类型,或者 -
T1与T2是鸭子类型兼容的。

我们需要记住的是,如果一种类型是 consistent-with 另一种类型,它要么是其子类型(子类),要么是与之鸭子类型兼容的——这归结为一个事实:只需对后者进行类型提示即可;你可以简单地省略前者。
说实话,我经常犯这样的错误——我的意思是,我做了这种多余的事情,类似如下:
from typing import Iterable
def sum_of_squares(x: Iterable[float | int]) -> float:
n, s = len(x), sum(x)
return sum((x - s/n)**2)
我一直认为通过澄清 x 可以包含整数和浮点数,我是在让用户的生活更轻松。
我吗?我不知道。确实,我使代码变得冗长。一个不知道int是float的鸭子类型的人可能会想,为什么只有float?另一方面,我们不应该以让那些不了解的人容易理解的方式编写代码。当然,有一些限制,但我认为这种情况并没有越界。此外,任何稍微懂一点 Python 的人应该知道,在期望float的地方,可以使用int;这是一种相当普遍的知识。无论如何,这也是我写这篇文章的原因之一——让我的读者知道,不仅int可以动态地代替float,从静态检查器的角度来看这也是可以的。
让我们回到sum_of_squares()函数。当你了解鸭子类型兼容性时,简洁版是一样清晰但更短,因此更干净:
from typing import Iterable
def sum_of_squares(x: Iterable[float]) -> float:
n, s = len(x), sum(x)
return sum((x - s/n)**2)
所以,我可以说,我对 Python 知识的缺乏让我认为我是在为我的代码用户做好事——现在我知道我不是。
命名元组
对于collection.namedtupes和typing.NamedTuples,情况类似,但有一点小差别。这两种类型都是常规tuple类型的子类型,这就是它们与…一致的原因。
这就是为什么下面的注释是……嗯,它不是最好的:
from collections import namedtuple
from typing import NamedTuple
def join_names(names: tuple | namedtuple | NamedTuple) -> str:
return " ".join(names)
这个函数本身在我写过的函数中不是最聪明的,但这不是重点。重点是,如果你想接受一个tuple、一个namedtuple和一个NamedTuple,你可以这样做:
def join_names(names: tuple) -> str:
return " ".join(names)
然而,如果你只想接受两种命名元组中的一个,你可以进行类型提示,例如:
from collections import namedtuple
def join_names(names: namedtuple) -> str:
return " ".join(names)
在这里,只能使用collections.namedtuple及其子类的实例。你当然可以以相同的方式指明typing.NamedTuple,这样collections.namedtuple就不能使用了。记住,如果T1与T2一致,并不意味着T2也一致于T1。
记住,如果
T1与T2一致,并不意味着T2也一致于T1。
结论
我们了解了与…一致和鸭子类型兼容性的含义。不要害怕在代码中使用这些知识。你知道如何回应以下问题:“为什么只有float?如果我想使用int呢?”
脚注
¹ sum_of_squares()以这种方式定义不接受生成器是有充分理由的。要理解原因,请分析函数的主体,并考虑生成器是如何工作的。
注意,计算len(x)会消耗生成器——所以,函数将无法计算x的和。看:
>>> sum_of_squares((i for i in (1., 2, 34)))
Traceback (most recent call last):
...
n, s = len(x), sum(x)
^^^^^^
TypeError: object of type 'generator' has no len()
Pylance大喊:

mypy也不喜欢:
error: Argument 1 to "sum_of_squares" has incompatible type
"Generator[float, None, None]"; expected "Sequence[Union[float, int]]"
[arg-type]
你是否看到使用静态类型检查器可以帮助你捕捉那些否则会在运行时被发现的错误?
所以,类型提示值得称赞?是的——但要称赞好的类型提示!
感谢阅读。如果你喜欢这篇文章,你可能也会喜欢我写的其他文章;你可以在这里看到它们。如果你想加入 Medium,请使用下面的推荐链接:
[## 使用我的推荐链接加入 Medium - Marcin Kozak
阅读 Marcin Kozak 的每个故事(以及 Medium 上的其他成千上万位作家的故事)。你的会员费将直接支持…
medium.com](https://medium.com/@nyggus/membership?source=post_page-----72e8b348d8ac--------------------------------)
Python 类型提示:从类型别名到类型变量和新类型
PYTHON 编程
查看类型别名、类型变量和新类型的实际应用
·发表于 Towards Data Science ·15 分钟阅读·2023 年 4 月 26 日
--

Python 提供了类型提示。选择权仍在你手中。图片来自 William Felker Unsplash
正如我在下面的文章中所写,如果你想在 Python 中使用类型提示,请以正确的方式进行:
[## Python 的类型提示:朋友、敌人还是只是个麻烦?
类型提示在 Python 社区中的受欢迎程度正在增加。这会把我们带向何方?我们可以做些什么来使用它……
betterprogramming.pub](https://betterprogramming.pub/pythons-type-hinting-friend-foe-or-just-a-headache-73c7849039c7?source=post_page-----a4a9e0400b6b--------------------------------)
什么是 正确的方式?简单来说,就是使你的代码从静态类型检查器的角度看起来 可读 和 正确 的方式。所以,两件事:可读 和 正确。
在上面的文章中我提到的事情之一是创建 类型别名 是提高可读性的好方法。我们将从它们开始讨论,重点讨论它们何时确实能提供帮助。然后,我们转向使用类型变量 (typing.TypeVar) 和新类型 (typing.NewType),这些将帮助我们实现常规类型别名无法实现的目标。
我将使用 Python 3.11 和 mypy 版本 1.2.0。
简而言之,使用类型别名的目的有两个:
-
以相对简单的方式让用户知道参数应该是什么类型(应该,因为我们仍在谈论 类型提示),以及
-
让静态检查器满意。
让静态检查器满意也应该让我们满意:一个不满意的类型检查器通常意味着错误,或至少是一些不一致性。
对于一些用户来说,第二点是唯一值得提及的——因为静态检查是他们使用类型提示的唯一原因。它帮助他们避免错误。
当然,这很棒——但这不是全部。类型提示可以帮助我们做更多的事情。并且请注意,如果我们的唯一目标是满足静态检查器,类型别名将没有用,因为它们根本不帮助静态检查器。它们帮助的是用户。
对我来说,这两个方面同样重要。如今,当我阅读一个函数时,我会特别注意其注释。注释写得好,它们能帮助我理解函数;注释写得不好——更不用说写得错误了——它们会使函数的可读性不如没有注释时那样好。
我们从类型别名开始。我会向你展示它们的两个主要用例。接着,我们将看到类型别名在相对简单的情况下如何提供帮助,有时我们需要更多的东西。在我们的案例中,类型变量和新类型将提供帮助。
复杂注释的类型别名
类型别名提供了一种简单而强大的工具,使类型提示更清晰。我将在这里重用Python 文档中的类型别名中的一个很好的且有说服力的例子:
from collections.abc import Sequence
ConnectionOptions = dict[str, str]
Address = tuple[str, int]
Server = tuple[Address, ConnectionOptions]
def broadcast_message(message: str,
servers: Sequence[Server]
) -> None:
...
正如文档所说,上述servers的类型签名正好等于下面使用的签名:
def broadcast_message(
message: str,
servers: Sequence[tuple[tuple[str, int], dict[str, str]]]
) -> None:
...
正如你所见,等价性并不完全:虽然这两个签名在代码上确实是等效的,但它们在可读性上有所不同。关键在于这个类型签名:
servers: Sequence[tuple[tuple[str, int], dict[str, str]]]
尽管阅读和理解起来比较困难,但通过使用几个类型别名将其重定义为Sequence[Server]后,已经变得更加清晰。类型别名在签名中传达的信息很有帮助。良好的命名可以产生奇迹。
请注意,我们可以通过添加一个更多的类型别名来使这个类型签名有所不同:
Servers = Sequence[Server]
servers: Servers
对我来说,Sequence[Server]比Servers要好得多,因为我立刻看到我处理的是一个实现了Sequence协议的对象。它可以是一个列表。例如,我们已经有了参数的名称servers,所以创建一个类型别名Servers似乎是多余的。
当然,理解这个签名的每一个细节,使用这些类型别名并不简单:
ConnectionOptions = dict[str, str]
Address = tuple[str, int]
Server = tuple[Address, ConnectionOptions]
servers: Sequence[Server]
但由于类型别名ConnectionOptions、Address和Server及其明确的含义,这比理解以下签名要简单得多:
servers: Sequence[tuple[tuple[str, int], dict[str, str]]]
简而言之,面对如此复杂的类型,原始的类型签名虽然让静态检查器满意——但不太可能让用户的生活变得更轻松。类型别名可以帮助实现这一点——它们有助于将关于变量、函数、类或方法的附加信息传达给用户。它们充当了一种沟通工具。
类型别名作为沟通工具:进一步的考虑
好吧,让我们跳到另一个例子。这次,我们将尝试利用类型别名来改善与用户的沟通,在一个比之前更简单的情况中。
正如我们所见,最重要的沟通工具是良好的命名。一个好的函数、类或方法名称应该明确表明其责任。当你看到 calculate_distance() 的名称时,你知道这个函数会计算距离;你会对看到一个返回两个字符串的元组的函数感到惊讶。当你看到 City 类时,你知道这个类会以某种方式表示一个城市——而不是一个动物、一辆车或一只海狸。
注释可以传达比函数(类、方法)及其参数名称更多的信息。换句话说,我们希望类型提示不仅能提示应该使用哪些类型,还能帮助用户理解我们的函数和类——并帮助他们提供正确的值。正如之前提到的,这可以通过命名良好的类型别名来实现。
让我们从一个简单的例子开始,这次使用变量类型提示。假设我们有如下的东西:
length = 73.5
当然,我们知道这个变量表示某物的长度。但这就是我们所知道的。首先,是什么长度?一个更好的名字可能会有所帮助:
length_of_parcel = 73.5
现在清楚了。想象一下你是一名送货员,你需要决定包裹是否能放进你的车里。那么,它能放进去吗?
如果有人根据上述知识做出了决定,他要么是那种“我会处理任何包裹”的人,要么是“最好不要冒险”的人。在这两种情况下,这都不是一个经过深思熟虑的决定。我们缺少单位,不是吗?
length_of_parcel = 73.5 # in cm
更好!但这仍然只是一个注释,如果代码本身提供这些信息会更好;上面没有提供,但这里提供了:
Cm = float
length_of_parcel: Cm = 73.5
我们再次使用了类型别名。但请记住,这只是一个类型别名,对于 Python 来说,length_of_parcel 仍然只是一个 float,别无其他。然而,对我们来说,这意味着很多——这个包裹的长度是 73.5 厘米。
让我们进入一个更复杂的情况,即从变量注释到函数注释。假设我们想实现一个计算矩形周长的函数。我们从没有注释开始:
def get_rectangle_circumference(x, y):
return 2*x + 2*y
简单。符合 Python 习惯¹。正确。
我们已经熟悉了这个问题:没有注释,用户不知道函数期望什么样的数据。厘米?英寸?米?公里?实际上,函数将处理字符串:
>>> get_rectangle_circumference("a", "X")
'aaXX'
嗯。确实,这有效——但没有意义。我们希望用户能够用我们的函数处理这样的东西吗?我们希望用户说:
嘿,他们的函数告诉我,当我用边长为
"a"和"X"的矩形时,这个矩形的周长是"aaXX",哈哈!
不,还是不行。确实,函数的名称说明了函数的作用,但如果能让用户知道函数期望什么样的数据会更有帮助。然后我们可以回应:
嘿,你不能读吗?难道你看不出这个函数期望浮点数吗?或者你认为字符串是浮点数,哈哈?
我认为最好避免这种哈哈式讨论。所以,类型提示是一个大好的选择。我们继续吧。
好的,我们有一个矩形,它有四条边,x和y是它们的长度。用户提供什么单位并不重要,因为函数适用于任何长度单位;它可以是厘米、英寸、公里,任何长度单位。真正重要的是——实际上,区别很大——是x和y都必须使用相同单位。否则,函数将无法正确工作。这是可以的:
>>> x = 10 # in cm
>>> y = 200 # in cm
>>> get_rectangle_circumference(x, y) # in cm
420
但这不是:
>>> x = 10 # in cm
>>> y = 2 # in m
>>> get_rectangle_circumference(x, y) # incorrect!
24
问题是,即使这个调用毫无意义,我们也知道这一点,但从 Python 的角度来看,它是正确的——两者都一样。
-
动态地:我们会得到
24;以及 -
静态地:
x和y都是浮点数。
问题是,我们没有让用户——以及 Python——知道两个参数x和y应该是相同单位的,只是他们应该使用浮点数。对于 Python 而言,浮点数就是浮点数,它不区分公里和英寸,更不用说千克了。
让我们检查一下是否可以使用类型提示来做些事情。换句话说:我们能否使用类型提示让用户知道他们应该为两个参数使用相同的类型,并且返回值也是这种类型呢?
最简单的注解是使用浮点数:
def get_rectangle_circumference(
x: float,
y: float) -> float:
return 2*x + 2*y
这个函数签名比没有注解的稍好,因为至少用户知道他们应该使用float。但还是,英寸?厘米?米?实际上,为什么不使用千克?
那么,让我们尝试一个类型别名:
Cm = float
def get_rectangle_circumference(x: Cm, y: Cm) -> Cm:
return 2*x + 2*y
清楚了吧?mypy会鼓掌:

Pylance也是如此。用户知道他们应该提供厘米,并且函数会以厘米为单位返回周长。Cm是一个类型别名,这基本上意味着它仍然是float,Cm和float之间没有区别。但关键是,用户知道。
然而,静态检查器在这种情况下不会太有帮助。你可以提供一个float的额外类型别名,它将与Cm以及任何float一样被对待:
Cm = float
M = float
def get_rectangle_circumference(x: Cm, y: Cm) -> Cm:
return 2*x + 2*y
x: Cm = 10
y: M = 10
get_rectangle_circumference(x, y)
类型检查器对此完全没问题,因为Cm和M只是相同类型的别名,即float。基本上,对于静态检查器而言,Cm不仅等同于float,也等同于M。因此,如果你想在这种情况下使用类型别名,你必须记住它们只是……别名——仅此而已!
我相信你已经注意到使用Cm类型别名的上面签名的另一个大缺点。为什么用户要用厘米提供x和y,而他们的单位是英寸或其他单位?转换?然后怎么办,转换回来?那简直疯狂!
嗯……也许我们可以创建一个与距离(或长度)相关的float别名?
DistanceUnit = float
def get_rectangle_circumference(
x: DistanceUnit,
y: DistanceUnit
) -> DistanceUnit:
return 2*x + 2*y
mypy将再次发出警告,因为我们唯一更改的是名称。但这并没有改变其他任何东西:用户仍然可以犯提供不同单位值的相同错误,这些值都将是DistanceUnit,如厘米和英寸。至少用户知道他们不应该提供千克。
正如你所见,类型别名无法帮助我们解决这个问题。一方面,我认为我们可以假设使用 Python 的人应该知道在计算矩形周长时,应该以相同的单位提供边的长度。这不是 Python 知识。这是简单的数学。
然而,在一些其他场景中,你可能想要让事情变得清晰,因为并非所有事情都像计算矩形周长那样清晰。我们知道类型别名没有帮助,所以让我们转向typing的其他两个工具:类型变量(TypeVar)和新类型(NewType)。它们会有帮助吗?
类型变量和新类型
如果你真的想实现如此详细的类型提示,你可以这么做。然而,请注意,这会使代码变得更复杂。为此,typing.NewType和typing.TypeVar可以提供帮助。
让我们从NewType开始。这是一个typing工具,用于创建具有最小运行时开销的新类型(参见附录 1)。以这种方式创建的类型提供的功能很有限,因此当你只需要明确的类型提示和将值转换到这种类型的可能性时,你应该优先使用它们。它的优点是它与静态检查工具兼容(正如我们稍后将看到的)。它的缺点——在我看来,这是一个相当大的缺点——是使用typing.NewType创建的类型不被isinstance视为类型(至少在 Python 3.11.2 中如此——我希望将来版本会有所改变):

Python 3.11.2 的截图:typing.NewType类型不被isinstance()视为类型。图片由作者提供。
对我来说,这是一个严重的问题。但正如你将看到的,typing.NewType类型仍然非常有用,开销较小(如附录 1 所示)。
因此,我们想要创建代表我们距离相关单位的类型。问题是,我们需要创建的类型数量与我们要考虑的单位数量相同。为了简化,让我们将它们限制为几个基于国际单位制(SI 单位)的最重要的长度单位。这是你在处理项目时的做法,其中类型数量有限。然而,当你在开发一个供他人使用的框架时,你应该创建更多的类型。
在我们的情况下,四种类型就足够了:
from typing import NewType
Mm = NewType("Mm", float)
Cm = NewType("Cm", float)
M = NewType("M", float)
Km = NewType("Km", float)
NewType 创建子类型——因此,Mm、Cm、M 和 Km 都是 float 的子类型。它们可以在任何 float 可以使用的地方使用,但静态检查器将不接受任何这些四种子类型应使用的普通 float 值。你需要将这样的 float 值转换为所需的类型;例如,你可以执行 distance = Km(30.24),意味着距离为 30 公里和 240 米。
让我们看看用于注解这个简单函数的类型:
def km_to_mm(x: Km) -> Mm:
return x * 1_000_000
Pylance 听到:

来自 VSCode 的 Pylance 截图。图片由作者提供
这是因为 x / 1_000_000 给出一个浮点数,而我们指明函数返回 Mm 类型的值。为实现这一点,我们需要将返回值转换为预期的类型:
def km_to_mm(x: Km) -> Mm:
return Mm(x * 1_000_000)
如你所见,使用 typing.NewType 创建的类型可以作为可调用对象(在 Python 3.10 之前它们是函数;现在它们是类)用于将值转换为它们的类型。这在这种情况下非常方便。
但这将如何帮助我们处理 get_rectangle_circumference() 函数?我们仍然有四种不同的 float 子类型,我们希望函数返回其 x 和 y 参数的确切类型。
现在是引入新 typing 工具——类型变量,或 typing.TypeVar 的时候了。事实证明,类型变量可以帮助我们实现所需的功能:
from typing import NewType, TypeVar
Mm = NewType("Mm", float)
Cm = NewType("Cm", float)
M = NewType("M", float)
Km = NewType("Km", float)
DistanceUnit = TypeVar("DistanceUnit", Mm, Cm, M, Km)
def get_rectangle_circumference(
x: DistanceUnit,
y: DistanceUnit) -> DistanceUnit:
t = type(x)
return t(2*x + 2*y)
与之前使用类型别名时不同,这次你不能混合不同的类型。让我们看看静态类型检查器 Pylance 如何处理此函数的三种不同调用:
浮点数无效:

(1) 浮点数无效。图片由作者提供
你不能混合不同的类型:

(2) 两种不同的类型无效。图片由作者提供
函数通过静态检查的唯一方法是对两个长度使用相同类型:

(3) 仅相同类型的两个参数有效。图片由作者提供
当然,返回值的类型将与两个参数的类型匹配——例如,当你提供米时,你会得到米。这就是为什么我们需要 t = type(x) 行的原因。我们可以使函数稍微简短一些:

更短版本的函数。图片由作者提供
对于中级和高级 Python 使用者,两种版本的可读性可能相当;然而,对于初学者来说,前者可能更容易理解。
注意,DistanceUnit 类型别名不会以相同方式工作:

DistanceUnit 的类型别名无法按要求工作。图片由作者提供
在这里,你可以在调用 get_rectangle_circumference() 时混合不同类型,这正是我们想要避免的;而类型变量帮助我们实现了这一点。
所以,我们达到了我们想要的目标。尽管任务看起来不算复杂,但类型别名并不足以实现我们的目的。然而,typing 的类型变量(TypeVar)和新类型(NewType)提供了帮助。
结论
类型提示在 Python 中不是必需的;它们是可选的。有时最好完全省略它们。然而,当你不得不使用它们时,应该明智地使用它们:让它们对你和你的代码用户有所帮助,而不是成为障碍。
我希望你现在已经准备好在自己的项目中使用 typing 的类型别名、类型变量和新类型,至少在类似的、相对简单的场景中使用。在这样做时,请记住不要过度使用这些工具。老实说,我很少决定使用类型变量和新类型。因此,在决定打开这些门之前,请三思。你的代码肯定会变得复杂得多,所以你必须有充分的理由去做这个决定。
我们已经涵盖了在 Python 类型提示系统中使用类型别名、类型变量和新类型的基本概念。这个话题还有很多内容,因为 Python 的静态检查系统仍在发展,但这种更多会带来更大的复杂性。今天就先说到这里,我们以后会在准备好专注于 Python 类型提示的更高级方面时再回到这个话题。
脚注
¹ 如果有人想对我大喊这不是Pythonic,因为函数没有注解,那么请让我提醒这个人,类型提示在 Python 中是可选的。如果某样东西是可选的,它不能作为声明代码是否 Pythonic 的决定性因素。
附录 1
与例如基于浮点数的自定义类相比,typing.NewType 的时间开销明显更小。下面的简单代码片段使用 perftester 来基准测试这两个方面:
-
使用
typing.NewType或自定义类创建新类型哪个更快? -
哪种类型的使用更快(具体来说,将浮点值转换为该类型)?
import perftester
from typing import NewType
def typing_type_create():
TypingFloat = NewType("TypingFloat", float)
def class_type_create():
class ClassFloat(float): ...
TypingFloat = NewType("TypingFloat", float)
class ClassFloat(float): ...
def typing_type_use(x):
return TypingFloat(x)
def class_type_use(x):
return ClassFloat(x)
if __name__ == "__main__":
perftester.config.set_defaults("time", Number=1_000_000)
t_typing_create = perftester.time_benchmark(typing_type_create)
t_class_create = perftester.time_benchmark(class_type_create)
t_typing_use = perftester.time_benchmark(
typing_type_use, x = 10.0034
)
t_class_use = perftester.time_benchmark(
class_type_use, x = 10.0034
)
perftester.pp(dict(
create=dict(typing=t_typing_create["min"],
class_=t_class_create["min"]),
use=dict(typing=t_typing_use["min"],
class_=t_class_use["min"]),
))
这是我在我的机器上得到的结果:

基准测试结果:基于 typing 的方法更快。图片作者提供。
显然,typing.NewType 创建新类型的速度显著比自定义类快一个数量级。然而,它们在创建新类实例方面的速度差异不大。
上面的基准测试代码很简单,表明 perftester 提供了一个非常简单的 API。如果你想了解更多,阅读下面的文章:
基准测试 Python 函数的简单方法:perftester [## 基准测试 Python 函数的简单方法:perftester
你可以使用 perftester 以简单的方式基准测试 Python 函数
你当然可以使用 timeit 模块进行这种基准测试:
最受欢迎的 Python 代码时间基准测试工具,内置的 timeit 模块提供了超出大多数工具的功能…
towardsdatascience.com
感谢阅读。如果你喜欢这篇文章,你可能也会喜欢我写的其他文章;你可以在这里看到。如果你想加入 Medium,请使用我下面的推荐链接:
[## 使用我的推荐链接加入 Medium - Marcin Kozak
阅读 Marcin Kozak 的每一个故事(以及 Medium 上成千上万其他作家的故事)。你的会员费直接支持…
medium.com](https://medium.com/@nyggus/membership?source=post_page-----a4a9e0400b6b--------------------------------)
Python 类型提示在数据科学项目中:必须、可能还是不推荐?
PYTHON 编程
我们应该在 Python 实现的数据科学项目中使用类型提示吗?
·发表于 Towards Data Science ·阅读时间 6 分钟·2023 年 9 月 26 日
--

无论你是否是 Python 类型提示的满意用户,你都需要了解这些概念以及如何使用它们。照片由 Kerin Gedge 拍摄,来源于 Unsplash
我们应该在 Python 实现的数据科学项目中使用类型提示吗?
想要免责声明吗?请看这里:这要看情况。在概念验证类型的项目中,通常是不必要的。在生产项目中,至少在 2023 年,这还是有必要的。但再次说明,这要看情况。
我会尽量简明扼要,尽快切入重点。我不想花费数小时考虑所有的利弊,原因很简单,因为数据科学市场对我们的工作有明确的期望。我的目标是向你展示这些期望,而不是详细讨论它们。
让我们从显而易见的事情开始。首先,Python 中的类型提示是 可选的。可选的意思是 你不必在 Python 中使用类型提示。如果是这样,那么我们主要问题的唯一答案是:你可以,但不,您不必在数据科学项目中使用类型提示!
那么…就这样吗?我们完成了吗?
等一下。我们确实陈述了显而易见的内容,但我们并没有触及任何超出显而易见的内容。
我们应该在 Python 实现的数据科学项目中使用类型提示吗?这要看情况。在概念验证类型的项目中,这并不是必要的。在生产项目中,至少在 2023 年,这还是有必要的。
举个例子。假设你是一个在私人公司工作的 Python 开发者。公司有自己关于 Python 开发的规则和建议。其中一条规则是:使用类型提示。就这么简单——不管你偏好什么,你都必须使用它们。如果这只是一个建议,你可能不需要使用它们。然而,由于这是一个规则,你必须使用可选类型提示。
好的,这点说得很好。但我们讨论的是一般数据科学项目中的类型提示,而不是某个特定公司里的情况。那么,是可选的,对吗?你不一定非得使用它们?
在回答之前,让我告诉你我在 Python 中使用类型提示的方式、时间和原因。
我在这里写了我对类型提示的看法:
[## Python 的类型提示:朋友、敌人还是仅仅是个麻烦?
类型提示在 Python 社区中的受欢迎程度不断上升。这会把我们带向何方?我们可以做些什么来使用它……
简而言之,我尝试以一种使代码更具可读性的方法来使用类型提示。此外,多亏了类型提示,静态检查器可以帮助保持代码的正确性。
我们应该记住,Python 的核心在于动态类型。
同时,我们应该记住,Python 的核心在于动态类型。当我们在实现类型提示上花费大量精力时,我们有点像是在剥离 Python 的动态类型。那么剩下的是什么?剩下的就是没有核心的 Python。
当我们在实现类型提示上花费大量精力时,我们有点像是在剥离 Python 的动态类型。那么剩下的是什么?剩下的就是没有核心的 Python。
我认为,有些情况下不应该使用类型提示。例如快速原型设计。我经常这样做:为了看看某些东西如何工作,我实现一个简单的原型。有时我可能会使用一些类型提示,只是为了展示所需的参数类型,尤其是当这些类型是自定义类型时。记住,我说的是原型设计,如果说明一个特定的函数返回dict[str, tuple[int, str]]类型的对象很重要,那我会在类型提示中说明它。不是为了让静态检查器满意,而是为了展示需要展示的内容。
但在原型设计阶段,我更常忽略类型提示。到时自然会用到它们。现在重要的是代码能够动态运行。这时静态类型并不那么有用。
但当我编写数据科学软件产品时,现在我总是使用类型提示。我会坦白地说。是的,它们可以(动态地)帮助捕捉一些静态错误——但它们也可能成为很大的障碍。有时候,我觉得实现良好的类型提示比其他所有工作加起来还要花费更多的时间。代码会变得更长、更复杂。在高级项目中,良好的类型提示可能很难实现,主要是由于代码的复杂性。更糟糕的是,许多类型检查器仍然远未达到最佳状态,可能会在类型提示正确时显示错误。确实,你可以保持类型提示过于简单(x: dict),但通常不应该这样做。在生产项目中,你应该更详细,因此,不应做如下操作:
from typing import Optional
def foo(x: dict) -> Optional[dict]:
...
比如,你可能需要做如下操作:
def foo(x: dict[str, dict[str, float]]) -> Optional[dict[str, str]]:
...
或者,相当于:
from typing import Optional
Params = dict[str, dict[str, float]]
Descriptions = dict[str, str]
def foo(x: params) -> Optional[Descriptions]:
...
没有类型提示,代码变成了:
def foo(x):
...
哪种更好?你不会惊讶于我的答案:好吧,这要看情况。
让我们总结一下上述提供的选项:
-
无类型提示:基于鸭子类型的快速编码。
-
过于简化的类型提示:编码稍慢,但这种类型提示的优势相当有限。
-
更详细的类型提示:静态检查器提供了很大的帮助,但编码速度大幅降低,同时静态检查器失败的风险也大大增加;通常,鸭子类型变得隐蔽,如果不是被遗忘的话;代码可读性下降。
-
基于类型别名的更详细类型提示:与上述类似,但代码更易读。
-
极其详细的类型提示,细节到最深层次:对我来说,绝对是一种过于狂热的方法,没有优势且有许多缺点,和详细的类型提示一样——但在一种夸张的形式中。
-
专用工具如
[pylance](https://pypi.org/project/pylance/)或[typeguard](https://pypi.org/project/typeguard/)。但请记住,这些工具可能会导致运行时效率成本,如果你决定在运行时类型检查中使用它们——而类型提示本身是一个静态检查工具,对运行时没有影响。
如常,最佳选择通常在这些选项之间的某个地方,你可以称之为黄金选择。但这个选择的位置取决于项目的多个方面,如客户、公司、项目负责人以及——最重要的——项目的类型。
在原型设计过程中,你通常会选择不使用类型提示,特别是当你不是类型提示的忠实粉丝时。然而,当你是时,你会发现自己更经常地使用它们——除非截止日期太紧迫,甚至无法再多花一分多余的时间。那是多余的吗?好吧,你已经知道答案了……这要看情况。
上述方法是我在日常数据科学工作中遵循的做法。一些细节因项目而异,但大体上几乎都是一样的。我认为这些是大多数进行数据科学项目的公司遵循的规则,因此你也可能希望遵循这些规则——除非公司或项目的规则非常严格。否则,你可能没有选择的余地。
无论你是否是一个快乐的类型提示用户,你都应该了解它是如何工作的以及如何使用它。如今,一个不知道如何使用类型提示的 Python 开发者… 已经不再是 Python 开发者了。接受这一点:你必须了解类型提示,并且要了解得很透彻。
不过请记住,Python 的类型提示系统仅在一定复杂度下才有帮助。即使你作为代码的作者了解你实现的复杂类型提示的所有细节,其他开发者也可能需要花费很多时间来理解代码。如果我被迫越过这一界限,我可能会认为是时候换一种语言了。被迫走到这一步,我会认为像 Go 这样的静态类型语言可能会更好。
感谢阅读。如果你喜欢这篇文章,你也可能喜欢我写的其他文章;你可以在这里查看。如果你想加入 Medium,请使用下面的推荐链接:
## 通过我的推荐链接加入 Medium - Marcin Kozak
作为 Medium 的会员,你的一部分会员费用会流向你阅读的作者,你可以完全访问每一个故事…
Python 类型提示与字面量
原文:
towardsdatascience.com/python-type-hinting-with-literal-03c60ce42750
PYTHON 编程
比看起来更强大:使用typing.Literal创建字面类型
·发布于 Towards Data Science ·15 min read·Nov 28, 2023
--

typing.Literal创建具有选定选项的类型。图片由Caleb Jones提供,来源于Unsplash
我承认:我并不总是喜欢typing.Literal,这是在 Python 中创建字面类型的一种形式。实际上,我不仅低估了字面类型,还完全忽视了它们,拒绝使用它们。出于某种原因,即使今天我也不太明白,我找不到字面类型的实际价值。
我有多么错误。我没有认识到这个简单工具的强大,我的代码因此受到了影响。如果你像我一样忽略了字面类型,我敦促你阅读这篇文章。我希望说服你,尽管它很简单,typing.Literal可以成为你 Python 编程工具库中的一个非常有用的工具。
即使你已经认识到字面类型的价值,也不要停止阅读。虽然我们不会深入探讨typing.Literal的所有细节,但这篇文章将提供比官方 Python 文档更全面的介绍,而不会像PEP 586那样陷入细节。
字面类型非常直接,可以使代码比没有字面类型的代码更清晰、更易读。这种简单性既是typing.Literal的优点,也是其缺点,因为它不提供任何额外的功能。然而,我将向你展示如何自行实现附加功能。
这篇文章的目标是介绍typing.Literal并讨论其在 Python 编程中的价值。在过程中,我们将探讨何时使用typing.Literal——同样重要的是,何时不要使用它。
字面类型
字面量类型是通过PEP 586引入到 Python 类型系统中的。这个 PEP 提供了对字面量类型提案的全面探讨,是一个丰富的信息来源。相比之下,typing.Literal类型的官方文档故意简洁,反映了它的直接性质。本文弥补了这两个资源之间的差距,提供了关于字面量类型的基本信息,同时深入探讨了我认为对所讨论用例至关重要的细节。
如PEP 586中所述,字面量类型在 API 根据参数值返回不同类型的场景中特别有用。我会进一步扩展这一说法,指出字面量类型允许创建一个涵盖特定值的类型,这些值不一定都是同一类型的。这并不排除所有值具有相同类型的可能性。
字面量类型提供了一种极其简单的方法来定义和利用具有特定值的类型,这些值是唯一可能的值。这种简单性远远超过任何替代方法。虽然确实可以使用其他方法实现相同的结果,但这些替代方案通常会带来更复杂的实现和潜在的更丰富功能。例如,创建你自己的类型(类)需要仔细考虑设计和实现,而创建字面量类型时可以完全忽略这些问题。
使用typing.Literal通常提供了一个更简单的解决方案,往往简单得多,但功能可能有所减少。因此,在做出决定之前,必须仔细权衡两种方法的优缺点。本文可以帮助你做出明智的选择。
字面量中可接受的类型
要创建一个typing.Literal类型,可以使用以下值:
-
一个
int、bool、str或bytes的字面量值 -
一个枚举值
-
None
像float或自定义(非枚举)类的实例是不接受的。
字面量类型:用例
现在我们将探讨几个我认为字面量类型是绝佳选择(往往是最佳选择)的用例。我们还将检视一些可能更合适的替代解决方案。每个用例都假设需要一个只接受特定值的类型,这些值不一定都是同一类型的。typing.Literal不会创建空类型,因此Literal[]是无效的。然而,它可以创建具有单一值的字面量类型。
下述讨论的用例并不构成情境的详尽列表,而是作为示例,其中一些可能会重叠。这个非排他性列表旨在展示typing.Literal提供的机会范围,并增强对这个有趣且有价值工具的理解。
示例 1:仅一个值
如前所述,当变量只接受单一值时,可以使用字面量类型。虽然这乍一看可能不符合直觉,文档提供了相关示例:
def validate_simple(data: Any) -> Literal[True]:
...
这个函数旨在进行数据验证,并始终返回 True。换句话说,如果验证失败,函数会引发错误;否则,它会返回 True。
理论上,如下所示的 bool 类型的返回值类型签名,对于静态检查器来说是可以接受的:
def validate_simple(data: Any) -> bool:
...
然而,该函数从未返回 False,使得这个类型提示具有误导性和不准确性。使用 bool 表示函数根据情况可以返回两个布尔值中的任意一个。当函数始终只返回其中一个值而从不返回另一个时,使用 bool 是误导性的。
这正是字面量类型发挥作用的地方。它不仅满足静态检查器的要求,还为用户提供了有价值的信息。
示例 2:需要静态类型
当运行时类型检查不需要时,静态类型通常提供最有效的解决方案。因此,如果你需要一个接受一个或多个特定值的类型,并且你的主要目标是通知静态检查器,创建相应的字面量类型是一个极好的方法。
示例 3:多个字符串
此用例包含了一系列字符串,例如模式、产品或颜色。以下是一些示例:
Colors = Literal["white", "black", "grey"]
Grey = Literal["grey", "gray", "shades of grey", "shades of gray"]
Mode = Literal["read", "write", "append"]
如你所见,此用例中的字面量类型可以包含两个或更多的字符串。重要的是,使用 Literal 不允许我们建立个别值之间的关系。例如,我们可以创建以下字面量类型:
Days = Literal[
"Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday", "Sunday"
]
值的提供顺序是否重要?在 Python 3.9.1 之前,是的:

在 Python 3.9.1 之前,字面量类型中的值的顺序是重要的。图片由作者提供
但自那以后就不再重要了:

从 Python 3.9.1 开始,字面量类型中值的顺序不再重要。图片由作者提供
因此,重要的是可能的选择,而不是它们之间的关系。如果利用值的顺序是关键,考虑使用其他类型,而不是字面量类型。一个解决方案是利用枚举类型,使用 enum.Enum 类;我们将很快在专门的文章中深入探讨这个概念。
谨慎提醒:Python 3.11 及更新版本引入了typing.LiteralString。这是一个不同的工具,因为与typing.Literal不同,它作为一种类型存在,而不是创建类型的工具。在本文中,我们探讨了字面量类型的创建,我不希望引入与这个略有不同但相关的工具的混淆。如果你有兴趣了解更多,访问文章末尾的附录。不过,让我们现在将这个话题搁置。关键是,typing.LiteralString不是typing.Literal的字符串替代品。
typing.LiteralString不是typing.Literal的字符串替代品。
示例 4:相同类型的多个值
这个示例扩展了前一个示例,涵盖了更广泛的数据类型。就像我们为字符串使用字面量类型一样,我们也可以将它们应用于大多数其他数据类型。这里是一些示例:
Literal[1, 5, 22] # integers
Literal["1", "5", "22"] # strings
如上所述,你可以使用int、bool、str或bytes的字面量值、枚举值和None。
示例 5:组合各种类型的值
这代表了字面量类型的最通用形式。你可以组合任何类型的对象,它将正常工作。这有些类似于使用typing.Union类型,但与典型的Union使用情况不同,我们是在组合对象而不是类型。
注意区别:一个常见的 Union 使用案例可能如下所示:
Union[int, str]
而一个组合了int和str类型对象的字面量类型可能如下:
Tens = Literal[10, "10", "ten"]
这里是一些其他示例:
Positives = Literal[True, 1, "true", "yes"]
Negatives = Literal[False, 0, "false", "no"]
YesOrNo = Literal[Positives, Negatives]
你可以创建以下类型:Literal[True, False, None]。它类似于这里描述的OptionalBool类型。
## An OptionalBool Type for Python: None, False or True
使用 OptionalBool 而不是 Optional[bool]。
上述文章中描述的OptionalBool类型比基于Literal的对应类型要复杂得多,后者既易于使用和理解,又具有显著较差的功能。
上述代码块中的三个例子也很有趣。它们显示了你可以创建两个(或更多)字面量类型的组合。这里,YesOrNo是一个将两个其他字面量类型,即Positives和Negatives组合在一起的字面量类型:

在 Python 3.9.1 及更高版本中连接两个字面量类型。作者想象
但请记住,这在 Python 3.9.1 之前的版本中不会以相同的方式工作(我们之前讨论了类型定义中字面量的顺序):

在 Python 3.9.1 之前连接两个字面量类型。作者想象
示例 6:运行时 membership 检查
在前面的例子中,我们专注于字面量类型的静态应用。然而,这并不排除它们在运行时的使用,即使这偏离了 Python 类型提示的原意。在这里,我将演示当需要时,你可以对字面量类型进行运行时成员检查。换句话说,你可以验证一个给定的值是否属于字面量类型的可能选择集合。
坦白说,我认为这一单一能力使 typing.Literal 成为一个更强大的工具。虽然它偏离了字面量类型的传统用法(静态代码检查),但这并不是一种黑客行为。这是类型模块的一个合法功能:typing.get_args()。
一个例子将最好地说明这个概念。首先,让我们定义一个字面量类型:
from typing import Any, get_args, Literal, Optional
Tens = Literal[10, "10", "ten"]
Tens 类型涵盖了数字 10 的各种表示形式。现在,让我们定义一个函数来验证一个对象是否具有 Tens 类型:
def is_ten(obj: Any) -> Optional[Tens]:
if obj in get_args(Tens):
return obj
return None
关于这个函数的几点说明:
-
它接受任何对象,并返回
Optional[Tens],这表明如果obj是Tens的有效成员,函数将返回它;否则,将返回None。这就是为什么使用typing.Optional(参见 这篇文章)。 -
使用
typing.get_args()函数进行检查。对于字面量类型,它返回所有可能的值。 -
在这里情况变得有趣。从动态的角度来看,函数的最后一行(
return None)是多余的,因为缺少的None返回值会被隐式解释为None返回值。然而,mypy不接受 隐式 None 返回值,如下图所示。

Mypy 不接受隐式的 None 返回值。截图来自 Visual Studio Code。图片由作者提供
根据官方文档中的[mypy](https://mypy.readthedocs.io/en/stable/kinds_of_types.html#disabling-strict-optional-checking),你可以使用[--no-strict-optional](https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-no-strict-optional)命令行选项来禁用严格的None检查。如果你打算使用这个选项,请三思。我更倾向于明确声明某种类型是否接受None。禁用严格检查意味着任何类型都假定接受None,这可能导致意外行为,使代码更难以理解和维护。虽然我不是非常喜欢非常详细的类型提示,但在我看来,使用[--no-strict-optional](https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-no-strict-optional)标志是一种过于简化的做法,因为None是一个非常重要的哨兵值,不应如此轻忽。
如果你确实需要在特定情况下禁用严格检查,请记住,当你这样做而其他人没有时,他们可能会在代码中遇到许多静态错误。在整个代码库中保持一致的类型检查设置是一个好的实践。
字面量与枚举
在阅读前一部分时,你是否注意到一些字面量类型与枚举类型相似?确实,它们确实有一些相似之处,但字面量类型缺乏枚举固有的自然值顺序。
比较这两种类型定义:
from typing import Literal
from enum import Enum
ColorsL = Literal["white", "black", "grey"]
class ColorsE(Enum):
WHITE = "white"
BLACK = "black"
GREY = "grey"
如果你主要注意到的是语法差异,要知道你也可以使用静态工厂方法来定义枚举类型:
ColorsE2 = Enum("ColorsE2", ["WHITE", "BLACK", "GREY"])
ColorsE3 = Enum("ColorsE3", "WHITE BLACK GREY")
因此,定义语法并不是字面量类型和枚举类型之间的关键区别。首先,字面量类型是具有少量动态功能的静态类型,而枚举类型则提供了静态和动态能力,使其更加多功能。如果你需要的功能超出了字面量类型的范围,枚举类型可能是更好的选择。
本文不会深入探讨 Python 枚举的复杂性。然而,以下表格比较了这两种工具。在继续之前,请分析表格并观察typing.literal提供了enum.Enum的一部分功能。

enum.Enum与typing.Literal的比较。图片由作者提供
尽管字面量类型在简洁性、简短性和可读性方面表现优异。虽然 Python 枚举类型也很简单和可读,但字面量类型提供了更高水平的清晰性和简洁性。
结论
本文的核心信息是 typing.Literal 和字面量类型是强大的工具,提供的功能超出了最初的假设。它们的简单性掩盖了它们的深度和多功能性。正如我在文章开头提到的,我曾经低估了这个工具的价值。然而,今天我认识到它——以及一般的字面量类型——是增强 Python 代码简洁性同时保持静态正确性的强大而简单的机制。
实际上,使用其他类型提示来表达与字面量类型相同的概念可能会导致混淆,即使静态检查器没有报错。当你只需要静态类型供静态检查器检查时,typing.Literal 应该是你的首选。它的使用方法很简单,不需要过多的代码:只需类型定义,这通常需要一行或多行,具体取决于类型中包含的字面量数量。
对于需要更多高级动态功能的场景,枚举可能是更好的选择。它们通过防止无效值分配,在运行时提供了额外的安全层。另一方面,字面量类型并没有提供这种固有的保护,尽管可以像上述 is_ten() 函数演示的那样实现。然而,这种保护需要在每次用户提供该类型的值时应用。
本质上,记住字面量类型和 typing.Literal。将它们融入你的 Python 代码中,以实现简洁和可读性。我认为在 Python 中,typing.Literal 实现了最高的实用性与复杂性的比率之一,使其既非常有用又极其简单。
附录 1
typing.LiteralString
Python 3.11 及更高版本引入了 typing.LiteralString 类型。尽管其名称如此,但它并不是 typing.Literal 在字符串方面的直接替代品。为了避免不必要的混淆,我们在这里不深入探讨此类型。相反,我们简要概述一下此类型的基本方面。
与用作创建字面量类型机制的 typing.Literal 不同,typing.LiteralString 本身就是一个类型。它可以用来指定变量应持有一个字面量字符串,如下例所示:
from typing import LiteralString
def foo(s: LiteralString) -> None
...
请注意文档中的说明:
任何字符串字面量都与
*LiteralString*兼容,另一个*LiteralString*也是如此。然而,单独标记为*str*的对象则不兼容。
而且
*LiteralString*对于敏感 API 很有用,在这些 API 中,任意用户生成的字符串可能会产生问题。例如,上述生成类型检查器错误的两个情况可能会受到 SQL 注入攻击的威胁。
这个简要概述应该足以满足我们当前的讨论。如果你有兴趣进一步探索此类型,请参阅 PEP 675,该 PEP 介绍了这个字面量类型。
附录 2
使用可迭代对象定义字面量类型
警告:本节展示了一个静态无法工作的技巧。因此,如果你的唯一目标是创建静态类型,请不要使用这个技巧。这更多的是一个有趣的信息,而非生产代码中的内容。
如果你不熟悉typing.Literal,Literal[]可能类似于索引,而Literal[1, 2, 3]可能类似于列表。因此,你可能会被诱导使用列表推导式,如下所示:
>>> OneToTen = Literal[i for i in range(1, 11)]
File "<stdin>", line 1
OneToTen = Literal[i for i in range(1, 11)]
^^^
SyntaxError: invalid syntax
错误消息表明这不是有效的语法。这是因为typing.Literal不应该用作列表推导式。相反,它用于指定类型接受的特定值。
但看看这里:
>>> OneToTen = Literal[[i for i in range(1, 11)]]
没有错误?那么,我们没问题,对吧?
不,我们不是。看看OneToTen是什么:
>>> OneToTen
typing.Literal[[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]]
>>> get_args(OneToTen)
([1, 2, 3, 4, 5, 6, 7, 8, 9, 10],)
如你所见,这个定义有效,但并不是我们想要的方式。OneToTen是一个字面量类型,只有一个值:一个从 1 到 10 的整数列表。列表不仅不是一个可接受的字面量类型,这也不是我们期望的!
但别担心,我们还没完成。还有一个技巧可以帮助我们实现预期结果。我们可以通过两种方式访问字面量类型的可能值。一种方法是我们已经看到的get_args()函数。另一种方法是使用类型的.__args__属性:
>>> get_args(OneToTen)
([1, 2, 3, 4, 5, 6, 7, 8, 9, 10],)
>>> OneToTen.__args__
([1, 2, 3, 4, 5, 6, 7, 8, 9, 10],)
>>> get_args(OneToTen) == OneToTen.__args__
True
虽然get_args()允许我们获取字面量类型的值,但我们可以利用.__args__属性来更新类型。看看:
>>> OneToTen.__args__ = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> OneToTen
typing.Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
哈!这就是我之前提到的技巧。我们可以称之为.__args__技巧。
上面我使用了一个列表,但你使用什么类型的可迭代对象并不重要:
>>> OneToTen == Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
True
>>> OneToTen.__args__ = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
>>> OneToTen == Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
True
>>> OneToTen.__args__ = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
>>> OneToTen == Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
True
我将一个列表字面量赋值给了OneToTen.__args__,但你可以用其他方式实现,比如使用列表推导式或另一种推导式:
>>> OneToTen.__args__ = [i for i in range(1, 11)]
>>> OneToTen == Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
True
>>> OneToTen.__args__ = list(range(1, 11))
>>> OneToTen == Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
True
>>> OneToTen.__args__ = {i for i in range(1, 11)}
>>> OneToTen == Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
True
然而,你确实需要小心,因为Literal并不总是表现得可预测。例如,它在range()中像上面那样有效,但在生成器表达式中则不行:
>>> OneToTen.__args__ = range(1, 11)
>>> OneToTen == Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
True
>>> OneToTen.__args__ = (i for i in range(1, 11))
>>> OneToTen == Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
False
>>> OneToTen.__args__
<generator object <genexpr> at 0x7f...>
实际上,在使用Literal进行生成器表达式实验时,我发现它确实有几次有效……我不知道为什么:通常它不这样工作,所以在我尝试的二十多次中,只有效了 2 或 3 次。这让我担心,因为我讨厌编程语言表现出不可预测的行为——即使是在技巧中。
难以相信这一点?看看这张来自 Python 3.11 的截图:

使用生成器表达式时typing.Literal.__args__的不可预测行为。截图来自 Python 3.11。图片由作者提供
仅供参考,之前没有使用A,但使用过OneToTen——不过,这应该不影响结果。此外,下次我尝试这个时,换了个新名称B,结果也没有成功:

typing.Literal.__args__ 与生成器表达式的行为不同于之前。截图来自 Python 3.11。图像由作者提供
因此,除非你准备好接受 Python 的不可预测行为,否则在这个问题解决之前,不要将 typing.Literal 与生成器表达式一起使用。不过没什么好担心的,因为生成器表达式通常用于克服内存问题——创建字面量类型似乎不会导致这样的问题。因此,你可以将其转化为一个列表并使用,而不是用生成器创建字面量类型。
如本节开头所述,你应该避免使用 .__args__ hack。它会动态工作,但 mypy 不会接受它。了解这一点是好的,因为它扩展了你对 typing 类型提示的知识,但这不是你应该在生产代码中使用的东西。
感谢阅读。如果你喜欢这篇文章,你也可能喜欢我写的其他文章;你可以在这里查看。如果你想加入 Medium,请使用下面我的推荐链接:
[## 使用我的推荐链接加入 Medium - Marcin Kozak
作为 Medium 会员,你的一部分会员费用将会分配给你阅读的作者,并且你可以完全访问每一个故事……
medium.com](https://medium.com/@nyggus/membership?source=post_page-----03c60ce42750--------------------------------)
Python 类型:可选的可以是强制的
原文:
towardsdatascience.com/python-types-optional-can-mean-mandatory-8e3b7ac2e805
PYTHON 编程
了解如何避免对 typing.Optional 的常见误用和误解。
·发表于Towards Data Science ·8 分钟阅读·2023 年 11 月 21 日
--

照片由Caroline Hall拍摄,发布在Unsplash
根据Python 文档,typing.Optional 是一种方便的方式来表示一个对象可以是 None。这是一种简洁而优雅的方式来表达这个概念,但它是否也非常清晰?
让我换一种说法:当你在 Python 环境中看到“optional”这个词时,你认为它意味着什么?假设你看到一个名为 x 的参数,它的类型是 Optional[int]。int 部分相当明确,因为它很可能表示一个整数,但 Optional 代表什么呢?你的第一反应是什么?
我们来考虑以下两个选项:
-
我不需要提供
x的值,因为它是可选的。 -
x的值可以是int或None。
如果你对 Python 类型提示足够了解,你会知道选项 2 是正确的。但当你不了解时……也许我错了,但我无法想象任何一个不懂 Python 的人会选择选项 2。选项 1 似乎更有意义。当我看到信息说某物是可选的,我会认为……嗯,就是说它是可选的……
这个问题导致了 typing.Optional 类型的常见误用。本文旨在揭示这种误用,并引导你正确理解这个类型。
typing.Optional 的含义
这三种类型提示是等效的:
from typing import Optional, Union
x: Union[str, None]
x: Optional[str]
x: str | None
它们都传达了相同的信息:x 可以是字符串或 None。虽然完全有效,但第一个 (Union[str, None]) 代表了 Python 类型提示的早期阶段:这是最初的方法,但现在不一定是首选方法。随后,Optional 被添加到 typing 模块中,提供了一种更简洁和直接的方式来表达这一概念。根据 the [mypy](https://mypy.readthedocs.io/en/stable/kinds_of_types.html?highlight=union#optional-types-and-the-none-type) documentation:
你可以使用
[Optional](https://docs.python.org/3/library/typing.html#typing.Optional)类型修饰符来定义允许None的类型变体,例如Optional[int](Optional[X]是Union[X, None]的首选简写)。
最终,在 Python 3.10 中,引入了 | 运算符用于类型提示。正如 mypy 文档 所述,
PEP 604 引入了一种拼写联合类型的替代方式。在 Python 3.10 及更高版本中,你可以将
Union[int, str]写作int | str。
如你所见,这是一种用于联合类型的一般运算符,并非专门设计用于表示变量可以为 None。
尽管这三种版本都是有效的,但选择应取决于几个因素。首先,如果你使用的 Python 版本低于 3.10,则 | 运算符不可用。即使使用 __future__ 导入:
from __future__ import annotations
在某些情况下,它仍然可能会失败。你可以在 the [mypy](https://mypy.readthedocs.io/en/stable/runtime_troubles.html#using-x-y-syntax-for-unions) documentation 中阅读相关内容。
尽管如此,我建议不要使用 Union 类型来表示可能为 None,因为这不必要地冗长,而且正如上面引用的文档所述,这已经不是首选选项了。Mypy 推荐使用 typing.Optional (quote:“Optional[X] 是 Union[X, None] 的首选简写”)。我同样支持这一点,原因很简单:Optional 类型正是为这种用例创建的,并且它还适用于旧版本的 Python,区别于 | 运算符。
以下是几个正确的类型提示示例,这些示例使用了 Optional:

在 Python 3.12 中使用 typing.Optional 的类型提示示例。来自 Visual Studio Code 的截图,作者提供
如你所见,Optional可以用于简单和复杂的类型提示。我们只分析其中间的一个。dict[str, Optional[int]]类型表示一个变量应该是一个字典,键是字符串,值可以是整数或None。
但是,这篇文章并不是关于做出这种选择的。我想讨论typing.Optional类型的一个常见误用——并展示如何避免它。在此过程中,我将解释我认为这种误用的来源、如何纠正它,以及如何理解typing.Optional类型。
对Optional的误解
请考虑以下函数签名:
from typing import Optional
def foo(s: str, n: Optional[int] = 1) -> list[str]:
...
让我们分析一下这个函数签名中的类型提示。但是,不要过于依赖这个分析!因为这些类型提示可能(虽然不一定)是错误的。这里是:
-
s是一个字符串(str)参数;它可以是位置参数或关键字参数,并且是必需的; -
n是一个可选的整数(int)参数,可以是位置参数或关键字参数,默认值是1; -
foo()函数返回一个字符串列表(list[str])。
现在有一个问题:上述分析有什么问题?
第二个要点是错误的。它说n是一个可选的整数。从某种程度上说,这是一句完全有效的英语句子。n参数确实是可选的,因为你不必提供它的值;当你省略它时,将使用默认值1。
另一方面,这不一定是一个有效的typing句子。我的意思是,这种说法对typing.Optional的理解是不正确的。上面,我们用Optional[int]来表示以下含义:你不必提供n的值。这意义是不正确的。typing语法中Optional[int]的正确含义是:n可以是int或None。
下图总结了这两种理解:

正确和不正确理解typing.Optional。图像由作者提供
让我们改进函数签名。我们有四个选项可以选择,每个选项代表不同的情况。选择适合你特定场景的选项。
如你所见,将有选项 0,其中签名保持不变。是的,这种类型提示可以是正确的——但它的含义很少是你需要的。
选项 0:保持原样
from typing import Optional
def foo(s: str, n: Optional[int] = 1) -> list[str]:
...
类型提示n: Optional[int] = 1是完全正确的。重点是,它的含义与许多人认为的不同,因为它表示
-
n可以是int或None,并且 -
n的默认值是 1。
所以,默认值是1,但用户仍然可以提供None。
虽然技术上是正确的,但我只会在非常特定且罕见的情况下使用这种类型提示,因为这些情况非常少见,我从未遇到过需要这种类型提示的情况。这对我来说听起来不自然。
我对这个选项非常苛刻,因为在我看来,正是这种类型提示使得typing.Optional被频繁误用:它暗示n是可选的,因为它有一个默认值,因此你根本不必为这个变量或参数提供值。
我包括这个选项是因为它在技术上是正确的——但实际上你几乎不应该考虑它。至少,记住许多经验较少的 Python 用户很可能会误解这种类型提示。
选项 1:使用 Optional 并且 None 作为默认值
from typing import Optional
def foo(s: str, n: Optional[int] = None) -> list[str]:
if n is None:
...
...
当你需要None作为整数或其他任何类型的默认值时,这是一种最常见的情况。这里使用了默认值(None),作为触发特定操作的某种情感标志。因此,如果用户提供了一个整数,则会进行一些基于整数的处理。但当n是None时,这种处理可以完全关闭。当然,这只是一个示例场景,但它非常常见。
注意if块,它旨在进行显式的None检查。也许在所有这种情况下都不需要,但根据the [mypy](https://mypy.readthedocs.io/en/stable/kinds_of_types.html?highlight=union#optional-types-and-the-none-type) documentation:
对于未加保护的
None或[Optional](https://docs.python.org/3/library/typing.html#typing.Optional)值,大多数操作是不允许的[…] 相反,需要进行显式的None检查。Mypy 具有强大的类型推断功能,可以使用常规 Python 习惯来防范None值。
选项 2:不要使用 Optional
def foo(s: str, n: int = 1) -> list[str]:
...
在这个选项中,你真正需要的是n的默认值,但n不能是None。在这里,你完全不需要提供n的值,因为有了默认值,你在调用foo()时不必提供它的值。因此,这就是英语中optional的正确含义(参数是可选的,因为你不必提供它的值),但在typing语法中是不正确的(参数不是Optional,因为n不能是None)。
选项 3. 使用 Optional 但要求其值
这个用例展示了为什么使用typing.Optional并不会使参数optional。如上所述,typing.Optional类型意味着一个变量可以是None,但这并不意味着当它用于参数时,你不必提供它的值。因此,这段代码是完全有效的:
def foo(s: str, n: Optional[int]) -> list[str]:
if n is None:
...
...
尽管n是Optional,但它不是可选的——你必须提供它的值。因此,你必须提供n,但它仍然可以是None。在这里,可选意味着n是可选的int,因为它也可以是None。
如同选项 1 一样,你通常应该对n使用显式的None检查,因为我所写的关于使用None的内容在这里也适用。

对于使用 typing.Optional 处理必需参数的肯定。图片作者
结论
我们讨论了一个与typing.Optional类型相关的常见错误。这个错误源于这样一个事实:尽管名称暗示typing.Optional处理可选参数,但它实际上指的是一个变量是否可以是None——与是否必须在函数调用中提供参数值无关。
在我看来,“optional”一词并不能准确传达typing.Optional的含义。然而,这是一个已经存在一段时间的公认 Python 术语,因此我不预期会有任何变化——无论如何。因此,意识到这种潜在的误解很重要。希望随着时间的推移,Python 代码库会减少对typing.Optional的误用和误解。
感谢阅读。如果你喜欢这篇文章,你可能也会喜欢我写的其他文章;你可以在这里看到它们。如果你想加入 Medium,请使用下面的推荐链接:
## 通过我的推荐链接加入 Medium - Marcin Kozak
作为 Medium 会员,你的会员费用的一部分将会给你阅读的作者,而你可以全面访问每一个故事……
Python 水质 EDA 和可饮性分析
理解数据分析和可视化技术
·
关注 发表在 Towards Data Science · 11 分钟阅读 · 2023 年 7 月 8 日
--
图片来源:Amritanshu Sikdar 在 Unsplash
能够提供足够的饮用水是一个核心要求。在气候变化辩论中,最大的挑战之一是确保足够的淡水供生存使用。水质是一个影响所有生物的重要问题。地球上只有约三百分之一的水是淡水。而其中只有 1.2%可以作为饮用水,其余的被锁在冰川、冰盖和永久冻土中,或深埋在地下。使用数据驱动的方法来评估影响水质的特性,可以大大改善我们对使水可饮用的理解。
在最基本的层面上,水的可饮用性与水的安全性相关。数据技术可以用来审查这个目标特性。还有一些问题超出了我们目前的审查范围:
我们可以饮用所有类型的淡水吗?
世界上可以获取的淡水比例是多少?
随着海平面上升,地下水位是否也增加了?
在本文中,我们将与一个小型水质数据集一起进行探讨。我们将通过使用 pandas 和 numpy 的数据分析技术,从数据中寻找隐藏的见解。对于数据可视化,将使用 matplotlib 和 seaborn 库。将采用一系列探索性数据分析(EDA)技术,以进一步明确数据质量。
每个数据可视化都旨在突出数据的不同特征。它们还将为用户提供模板,以应用于其他挑战。
数据集
对于这项分析,水质数据集取自 Kaggle¹。
饮用水的可饮用性
使用了带有 Python 代码的 jupyter notebook 实例进行处理。
import sys
print(sys.version) # displays the version of python installed
运行上述脚本后,输出将显示使用了版本 3.7.10 的 Python。为了能够复制接下来的结果,用户应确保在工作环境中使用 Python 3。
理解数据
首先,我们需要了解我们正在使用的数据。由于文件格式为 csv 文件,将使用标准的 pandas 导入语句 read_csv。
# Import the dataset for review as a DataFrame
df = pd.read_csv("../input/water-potability/water_potability.csv")
# Review the first five observations
df.head()
导入数据后,代码将变量 df 分配为 pandas 方法生成的 DataFrame 输出结果。
与任何数据集一样,查看样本记录将帮助你获得信心。DataFrame 具有大量与之相关的方法,pandas API 是一个很好的资源。在 API 中,可以使用 head 方法。输出 1.1 默认显示 DataFrame 的前 5 行。为了显示更多的行,需要在括号内输入一个数值。可以应用两种替代方法来对 DataFrame 进行采样:i) sample (df.sample()) 从索引中选择随机行,或者 ii) tail (df.tail()) 从索引中选择最后的 n 行。

输出 1.1 DataFrame 的前五条记录详细信息
运行任何方法时,括号会在方法名后面出现,以便 Python 解释器产生结果。
显示 DataFrame 的内存可能是一个常见的任务,特别是在涉及内存限制时。例如,当要导入的数据集可能大于 Python 会话中可用的内存时。通过使用 pandas 库,DataFrame 会在内存中创建,因此用户应该了解在执行这些处理步骤时可以使用的内存。
# Display information about the DataFrame - contains memory details
df.info(memory_usage="deep")
上面的代码可以用作显示输出 1.2 的方法。通过包含关键字 memory_usage,Python 解释器会进行更深入的搜索,以了解下面显示的内存使用情况。默认选项会执行一般搜索,因此如果需要评估的准确性,请确保应用上述关键字短语。

输出 1.2 提供了内存使用情况的功能和详细信息概述
从输出 1.2 显示的结果中,可以查看一系列详细信息,从列名称和数据类型,到确认变量的类别和非空值的数量。我们可以看到整个表格中显示了 3,276 行。然而,对于 Sulfate 列,只有 2,495 个非空值。因此,可以查看一些缺失值,以了解这些缺失条目是否与其他列存在模式。我们将在文章后面回顾一种数据可视化技术,可以帮助识别模式。
根据之前的导入语句,用户可以调整列的 Dtype,如果默认选项不符合预期的话。上述结果显示,对于十进制数字,应用了 float Dtype,而整数显示为 int。此外,还包括了这些数字列的最大字节内存类型,以提供潜在输入值的全面覆盖。用户应当评估这些 Dtypes 是否保持了正确的值范围,如果未来预期的范围较小,则可以分配较小的字节值。应用这一逻辑将有助于提高 DataFrame 的内存效率,并在处理时提升性能。
上述 info 方法展示的一个特性是 DataFrame 的结构,这可以通过许多其他方法进行查看。这样的元数据可以让程序员查看行数和列数等基本组件。
# Shape of the DataFrame - shows tuple of (#Rows, #Columns)
print(df.shape)
# Find the number of rows within a DataFrame
print(len(df))
# Extracting information from the shape tuple
print(f'Number of rows: {df.shape[0]} \nNumber of columns: {df.shape[1]}')
在 Python 中调用诸如 shape 这样的属性时,不需要加上括号。属性是可以通过类及其对象访问的数据结果。之前我们回顾了一个方法,它是包含在类中的一个函数。要进一步了解 Python 类语句的细节,需要深入研究。然而,我们可以继续使用所展示的代码,并且展示了输出 1.3 中显示的一些值。

输出 1.3 显示了 DataFrame 的结构的元数据
第一行显示了形状输出,这是一个元组,由两个值组成的括号表示。从上面展示的代码中,我们能够访问该元组中的相对位置,以显示第一个和第二个位置的值。由于 Python 使用 0 索引约定,应用 0 在方括号内将返回第一个值。我们可以看到,元组中包含了第一位置的行数,第二位置的列数。找到行数的另一种方法是使用 len 函数,它显示 DataFrame 的长度。
摘要统计
在这一部分,我们开始回顾 DataFrame 列的摘要细节。一个简单的 describe 方法可以用于对数字列进行高层次的数据分析。由于我们的 DataFrame 仅包含数字列,因此所有摘要属性都被生成。当存在字符和数字列的混合时,需要包含其他关键字参数以显示相关输出。
# Review the high level summary details for each variable
df.describe()
输出 1.4 显示每列的默认总结值。计数值可以解释为非空值的计数。任何总计小于 DataFrame 中行总数的显示列都有缺失值。对于每个变量,我们可以看到一系列值。我们可以使用四种矩方法来理解 i) 平均值,ii) 方差,iii) 偏度,和 iv) 峰度,基于显示的数据。

输出 1.4 标准细节显示 DataFrame 中每个特征的高级指标
在回顾总结细节时,具有外部视角来理解特征属性也是至关重要的。我们从经验中知道,pH 值应在 0 到 14 之间。如果值超出此范围,则必须检查和更正特征值。用于评估水质的数据中,平均值和中位数(由第 50 百分位数显示)接近 7 是水的中性属性的适当值。
如果 DataFrame 中有更多特征,则前一个代码块的输出可能会很难解释。输出可能会横向扩展到比没有滚动显示的范围更宽的范围。
# Transpose the summary details - easier to review larger number of features
df.desribe().T
能够转置输出是一种有用的方法。在上面的代码块中,链式调用 T 方法生成了下面的输出 1.5。现在用户可以更容易地查看行索引上显示的列名以及以列标题显示的总结指标。这一小的调整使得 describe 方法在列数较多时效果很好。

输出 1.5 总结细节转置以显示沿行索引的特征名称
要进一步了解 describe 方法的详细信息,我们可以通过使用 jupyter notebook 的问号魔法函数来解释文档字符串。
# Magic function in jupyter to display docstring
df.describe?
使用这种方法将帮助用户查看任何方法的默认参数值(关键字和位置)。

输出 1.6 在 jupyter 控制台中显示关于文档字符串和方法参数的详细信息
输出 1.6 提供了方法的内部工作原理,供用户查看。每个参数的默认值范围以及定义,有助于方法的应用。提供了一系列 jupyter 魔法函数,可以帮助提高程序员的生产力。
缺失值
如前所述,从元数据和总结统计数据中可以看到 DataFrame 中存在一些缺失值。为了确认这一点,我们可以应用下面的代码块。
# Check for the missing values by column
df.isnull().sum()
代码将第一个 isnull 方法与 sum 方法链在一起,以创建每列的缺失值数量。isnull 评估将检查列中的非空值。sum 方法用于执行计数。输出 1.7 突出显示三列显示缺失值。

输出 1.7 每列的缺失值计数
拥有缺失值总行数是一个很好的起点。然而,更好的是审查每列中缺失值的比例。
# Proportion of missing values by column
def isnull_prop(df):
total_rows = df.shape[0]
missing_val_dict = {}
for col in df.columns:
missing_val_dict[col] = [df[col].isnull().sum(), (df[col].isnull().sum() / total_rows)]
return missing_val_dict
# Apply the missing value method
null_dict = isnull_prop(df)
print(null_dict.items())
创建 isnull_prop 用户定义函数使我们能够为每列创建一个值字典。通过此函数,我们生成了上述计数值,并使用 shape 属性理解缺失值的百分比。

输出 1.8 将缺失值百分比函数应用于每列
输出 1.8 显示的结果难以可视化。为确保不遗漏最终信息,可以生成一个 DataFrame。
# Create a dataframe of the missing value information
df_missing = pd.DataFrame.from_dict(null_dict,
orient="index",
columns=['missing', 'miss_percent'])
df_missing
将字典变量应用于 pandas DataFrame 方法将使我们更容易理解每列的差异。输出 1.9 现在包括 miss_percent 列。我们现在可以应用阈值来评估缺失值的百分比是否在我们期望的范围内。如果值过高,例如硫酸盐值大于 20%,可以设置用户定义的控制,以突出显示此列需要排除在未来使用之外或更详细地审查。

输出 1.9 创建 DataFrame 以了解每列的缺失百分比
另一种通过缺失值检查是否存在模式的方法是应用来自 seaborn 数据可视化库的热图方法。
# Display missing values using a heatmap to understand any patterns
plt.figure(figsize=(15,8))
sns.heatmap(df.isnull());
应用上述代码块将生成输出 1.10。此可视化提供了进一步的上下文,以查看是否有多个行对所有三个变量都有缺失值。这可能是因为填充数据的用户在原始数据集中一致地显示缺失值。获得这一见解将使我们能够生成数据驱动的洞察,以更有效地减少缺失值数量。

输出 1.10 使用 seaborn 数据可视化生成每列每行的缺失值热图
理解 pH 变量分布
最终评估将是对我们已知的变量进行审查。使用 seaborn 库,我们能够生成 pH 变量的直方图。
# set the histogram, mean and median
sns.displot(df["ph"], kde=False)
plt.axvline(x=df.ph.mean(), linewidth=3, color='g', label="mean", alpha=0.5)
plt.axvline(x=df.ph.median(), linewidth=3, color='y', label="median", alpha=0.5)
# set title, legends and labels
plt.xlabel("ph")
plt.ylabel("Count")
plt.title("Distribution of ph", size=14)
plt.legend(["mean", "median"]);
print(f'Mean pH value {df.ph.mean()}
\n Median pH value {df.ph.median()}
\n Min pH value {df.ph.min()}
\n Max pH value {df.ph.max()}')
与之前的打印语句类似,f 字符串语句允许我们添加均值、中位数、最小值和最大值,以便更容易地查看分布。

输出 1.11 使用直方图查看 pH 变量分布
输出 1.11 显示大多数 pH 值接近中间。其分布类似于正态分布,我们可以利用这一见解在向外部用户展示细节时提供帮助。
结论
在本文中,我们旨在回顾 EDA 评估的早期阶段。最初审查了导入数据的元数据,以展示早期洞察。深入挖掘摘要统计数据让我们关注缺失值。最后,我们审查了 pH 变量的直方图,以确保该变量符合外部预期。
后续文章将继续这一旅程,并寻求开发旨在预测水质的模型。将使用分类机器学习技术提供基线模型。
留下你的评论,非常感谢你的阅读!
你可以通过 LinkedIn 与我联系,进行关于数据的一次友好聊天。其他我分享的故事包括:
确保在 SQL 代码的开头声明关键变量,可以帮助自动化代码的重用。
[towardsdatascience.com ## 高级 SQL 操作
审查更高级的 SQL 操作,以从爱尔兰天气数据集中提取更多的数据洞察。
[towardsdatascience.com ## 开发 SQL 表
只有通过创建和开发 SQL 表,我们才能理解如何最佳利用可用内存。
[towardsdatascience.com ## Python 中的 NLP 入门
开始进入自然语言处理领域
[towardsdatascience.com
[1] : Kaggle 数据集水质来自 www.kaggle.com/datasets/adityakadiwal/water-potability,许可证协议见 creativecommons.org/publicdomain/zero/1.0/
Python 水印:旧 vs 新,笨重 vs 干净 — 你会选择哪个?

图片由Siegfried Frech提供,来源于Pixabay
Python 水印制作简化:OpenCV、PIL 和 filestools 的全面比较
·发表于数据科学前沿 ·8 分钟阅读·2023 年 3 月 28 日
--
对图像进行水印处理是摄影师、艺术家以及任何希望保护其视觉内容免受未经授权使用的人的重要任务。在 Python 世界中,有许多库可以让你为图像添加水印。在本文中,我们将比较三种流行的 Python 图像水印方法:OpenCV、PIL(Python Imaging Library)和filestools。对于最后一种方法,你只需一行代码!
在这篇文章中,我将演示使用我在澳大利亚维多利亚州菲利普岛拍摄的照片的水印功能。原始照片在这里。请随意下载以便使用。

作者拍摄的照片
1. OpenCV — 小任务的大工具

OpenCV 是一个综合性的计算机视觉库,提供了广泛的图像处理功能,包括向图像添加文本水印的能力。虽然 OpenCV 并非专门为添加水印设计,但它仍然提供了实现这一目标的灵活性和控制力。然而,使用 OpenCV 添加水印可能会有些挑战,尤其是对于那些不熟悉该库的人来说。此外,使用 OpenCV 实现基于图像的水印需要一些手动处理。
无论如何,让我们看看 OpenCV 如何为我们完成这项任务。
在一切之前,请确保如果你还没有安装库,需要安装它。只需使用 pip 如下。
pip install opencv-python
要在 Python 代码中使用 OpenCV,我们需要导入 cv2 模块。为了使这个示例更简单,我还想导入 matplotlib,这样我就可以即时显示图像。
import cv2
import matplotlib.pyplot as plt
OpenCV 使从本地路径读取图像变得非常简单。你只需使用 imread 函数即可。
img = cv2.imread("my_photo.jpeg")
以下函数是可选的,我创建了这个函数以方便在 Jupyter Notebook 环境中内联显示图像。如果你想查看图像对象的样子,可以随意使用它。
def show_image(img, is_cv=False):
if is_cv:
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
plt.figure(figsize=(16, 9))
plt.imshow(img)
plt.axis("off")
plt.show()
在上述函数中,我添加了 is_cv 来指定这个图像对象是否来自 OpenCV。我们需要这样做,因为我们可能希望以后将这个函数用于 PIL 库。OpenCV 图像对象默认使用 BGR 而不是 RGB。因此,我们需要使用 cvtColor() 函数来转换编码方法。
之后,使用 matplotlib 来显示图像。在我的例子中,我指定了一个适合浏览器窗口的大小。此外,由于我们只是显示图像,可以关闭坐标轴。imshow() 是显示图像对象的关键函数。
因此,我们可以简单地通过调用我们刚刚创建的函数来显示图像。
show_image(img, is_cv=True)

现在,让我们创建一个字符串,这个字符串就是我们想要添加到图像上的水印文本。接下来,我们需要配置字体。有几种 OpenCV 内置的字体样式可以选择。font_scale 将决定水印文本的大小。最后,我们可以创建一个元组作为颜色。(255, 255, 255) 将使水印文本为白色。
watermark_text = "Christopher Tao @TDS"
# Set the font, font scale, and color of the text
font = cv2.FONT_HERSHEY_TRIPLEX
font_scale = 5
color = (255, 255, 255)
接下来是决定水印位置。getTextSize() 方法将帮助我们获取文本的大小。同时,我们可以从图像的 shape 属性中获取图像的维度。
# Get the size of the text
text_size, _ = cv2.getTextSize(watermark_text, font, font_scale, thickness=20)
# Calculate the position of the text
x = int((img.shape[1] - text_size[0]) / 2)
y = int((img.shape[0] + text_size[1]) / 2)
然而,需要强调的是,图像的大小是“H x W”,而文本的大小是“W x H”。因此,当我们计算坐标时,需要使用图像形状中的第二项(宽度)减去文本大小中的第一项(宽度),反之亦然。

最后,我们可以使用 putText() 方法将水印文本添加到图像中,使用我们定义的所有参数。
# Add the text watermark to the image
cv2.putText(img, watermark_text, (x, y), font, font_scale, color, thickness=2)
让我们看看结果。成功!

2. PIL — 简化水印处理

PIL(Python Imaging Library)是一个流行的第三方图像处理库,它提供了比 OpenCV 更简单直接的方式来给图像添加水印。然而,它仍然需要一些步骤来实现水印。PIL 是那些需要可靠且相对简单的方式来给图像添加水印的用户的不错选择,无需复杂的计算机视觉能力。
同样,在使用 PIL 库之前,我们需要按照如下方式安装它。
pip install pillow
对于 PIL 库,我们需要以下 3 个模块。
-
Image模块:提供了一个用于表示和操作 PIL 中图像的类。 -
ImageDraw模块:提供了一组用于在图像上绘制的函数,包括线条、矩形、圆形和文本。 -
ImageFont模块:提供了一个用于加载和操作字体的类,包括设置字体大小、样式和颜色。
from PIL import Image, ImageDraw, ImageFont
然后,我们可以使用Image模块打开图像,如下所示。我们也可以重用之前定义的show_image()方法来显示原始图像。
img = Image.open('my_photo.jpeg')
show_image(img)

要操作图像,我们需要从图像对象创建一个ImageDraw实例。
# Create an ImageDraw object
draw = ImageDraw.Draw(img)
下一步有点棘手。与 OpenCV 内置的字体样式不同,PIL 只能使用单独的“.ttf”文件。虽然所有操作系统都有一些字体样式,但我们仍需要了解现有的字体样式,以便可以使用它们。
在这种情况下,我建议最简单的方法是使用matplotlib来显示可用的字体,如下所示,除非你有特定的字体样式需要使用。
import matplotlib
matplotlib.font_manager.findSystemFonts(fontpaths=None, fontext='ttf')
以下是我所使用的一些可用字体。

现在,我们可以开始设置参数。
# Prepare watermark text
font = ImageFont.truetype('Humor-Sans.ttf', size=150)
# Calculate the size of the watermark text
t_width, t_height = draw.textsize(watermark_text, font)
# Calculate the x and y coordinates for the text
x = int((img.size[0] - t_width) / 2)
y = int((img.size[1] - t_height) / 2)
我们可以使用ImageFont.truetype创建一个特定大小的水印字体。之后,我们可以通过draw对象使用textsize()方法获取文本大小。之后,计算坐标的方式与我们在 OpenCV 演示中做的一样。
最后,我们可以使用draw对象的text()方法添加水印。
# Add the text as a watermark on the image
draw.text((x, y), watermark_text, font=font, fill=(255, 255, 255))

3. Filestools — 一行代码奇迹

filestools 是一个第三方 Python 库,提供了一系列有用的文件和图像处理工具。它包括显示目录结构的功能,如 Linux 中的 tree 命令,比较文件差异的功能,如 diff 命令,以及使用 marker 命令给图像添加水印。此外,filestools 还可以用于将 curl 请求转换为 Python 请求代码。尽管该库由一位中国开发者创建,但它仍然被广泛访问和使用,尽管一些日志是中文的。
同样,要使用这个库,我们可以按如下方式安装它。
pip install filestools
然后,我们将水印文字添加到图像中。我们可以按如下方式从库的 watermarker 模块中导入 add_mark() 函数。然后,这个函数将完成我们需要的一切。
from watermarker.marker import add_mark
add_mark(file="my_photo.jpeg",
out="watermarked",
mark=watermark_text,
size=60,
color="#ffffff",
opacity=0.5,
angle=30,
space=60)
out 参数是一个目录名称,因此带水印的图像将被输出到这个目录中。opacity 指定了水印的透明度。我们确实可以使用 OpenCV 和 PIL 实现这一点,但需要更多的步骤和复杂的逻辑。除此之外,水印还会被渲染为图像上的“图案”。因此,我们可以给文字指定一个 angle,以及定义文本实例之间间距的 space。

运行这个函数后,会显示“成功保存”。现在,我们可以检查我们的工作目录。我们应该能够找到一个包含带水印图像的新子目录。

这是我们打开后的带水印照片。

作者拍摄的照片
总结

图片由 Nikolett Emmert 提供,来自 Pixabay
在这篇文章中,我们比较了三种流行的 Python 库用于给图像加水印:OpenCV、PIL(Python Imaging Library)和 filestools。OpenCV 是一个综合的计算机视觉库,提供广泛的图像处理功能,而 PIL 提供了一种更简单直接的方法来给图像加水印。然而,这两个库都需要多个步骤和一些手动处理才能实现水印。另一方面,filestools 提供了一行代码的解决方案来添加水印,使其成为三者中最简单和最流线型的库。总体而言,虽然 OpenCV 和 PIL 提供了更高级的图像处理功能,但在水印添加的易用性方面,filestools 是明显的赢家。
[## 使用我的推荐链接加入 Medium - Christopher Tao
感谢你阅读我的文章!如果你不介意,请请我喝杯咖啡 😃 你的会员费用支持成千上万的…
如果你觉得我的文章有帮助,请考虑加入 Medium 会员来支持我和其他成千上万的作者!(点击上面的链接)
除非另有说明,所有图片均由作者提供
对 Python 3.12 的期待
即将发布的 Python 3.12 版本中的新特性、更新和移除项
·发表于Towards Data Science ·阅读时间 6 分钟·2023 年 1 月 4 日
--

摄影:由Yulia Matvienko提供,来源于Unsplash
尽管开发者和团队仍在将项目升级到具有革命性(在性能方面)的Python 3.11,但新版本的发布正在进行中。
Python 3.12 预计将在接下来的几个月内发布,但 alpha 版本(当前为3.12.0a3)已发布,预发布用户现在可以访问它以测试新功能、报告错误并提供进一步建议——如果需要的话。
在这篇文章中,我们将深入探讨根据相关 PEP 预计将引入的一些更改,包括 Python 3.12 的更新和新增内容。
移除和弃用
从 Python 3.10 开始,distutils被标记为弃用——根据PEP 632——Python 3.12 将彻底移除该模块。请注意,不提供向后兼容性,这意味着任何来自distutils的导入都将导致错误。
多年来,作为标准库的一部分的distutils是 Python 中首选的包管理模块,但由于setuptools的出现改变了这一格局,即使是Python 包装用户指南也会推荐setuptools,因为它旨在克服一些distutils的限制。
尽管 setuptools 仍然使用 distutils 的功能,但前者现在已经整合了后者的副本,这意味着它不再依赖于标准库,而 pip 长期以来一直在用 setuptools 替代 distutils。因此,将遗留的 distutils 从 Python 3.12 版本中移除是有意义的。
此外,在 Python 3.12 中,wstr 和 wstr_length 成员将从 Unicode 中移除,如 PEP-623 所示。这一变更与 PEP-393 中某些 Unicode API 的弃用一致。这些成员的移除使得 64 位平台上的对象大小减少了 8 或 16 字节。
改进的错误消息
Python 3.12 还将对某些错误报告的消息进行改进。
每当在顶层模块中引发 NameError 时,Python 3.12 解释器将会在错误信息中报告建议:
>>> sys.version
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'sys' is not defined. Did you forget to import 'sys'?
同样,如果在方法中发生 NameError 并且实例具有与异常同名的属性,纠正错误的建议将是 self.<NAME>,而不是方法范围内的最接近匹配。例如,
class Foo:
def __init__(self):
self.my_var = 'Hello'
def bar(self):
another_var = my_var
>>> Foo().bar()
File "<stdin>", line 1
another_var = my_var
^^^^^^
NameError: name 'my_var' is not defined. Did you mean: 'self.my_var'?
此外,每当因无效的导入语法(遵循模式 import x from y 而不是有效的语法 from y import x)引发 SyntaxError 时,错误信息中将会报告一个提示,告知用户这个潜在的问题,如下例所示。
>>> import mymodule.a from anothermodule.b
File "<stdin>", line 1
import mymodule.a from anothermodule.b
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
SyntaxError: Did you mean to use 'from ... import ...' instead?
最后,错误信息的另一个改进与 ImportError 相关,以及当 from <module> import <name> 因名称未解析而失败时引发的错误。在 Python 3.12 中,这些错误信息将包括有关未解析名称 <name> 的建议,基于模块 <module> 中实际包含的名称。
>>> from collections import ordereddict
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ImportError: cannot import name 'ordereddict' from 'collections'. Did you mean: 'OrderedDict'?
现在,Linux perf profile 可用于 Python 函数
Linux perf profiler 是一个有用的工具,可以帮助用户分析应用程序并获取与其性能相关的信息。在较早的 Python 版本中,profile 能够报告原生函数和过程的信息,这些函数和过程用 C 编写。从 Python 3.12 开始,解释器能够以一种特殊模式运行,使得 perf profiler 也可以报告 Python 函数的信息。
perf性能分析支持可以通过环境变量[PYTHONPERFSUPPORT](https://docs.python.org/3.12/using/cmdline.html#envvar-PYTHONPERFSUPPORT)或[-X perf](https://docs.python.org/3.12/using/cmdline.html#cmdoption-X)选项启用,也可以通过动态方式使用[sys.activate_stack_trampoline()](https://docs.python.org/3.12/library/sys.html#sys.activate_stack_trampoline)和[sys.deactivate_stack_trampoline()](https://docs.python.org/3.12/library/sys.html#sys.deactivate_stack_trampoline)启用。
sqlite3 命令行接口
Python 3.12 还将引入一个 sqlite3 的命令行接口。这意味着 sqlite3 模块也可以通过使用语言解释器的 -m 标志作为脚本调用,从而启动 SQLite Shell。
python -m sqlite3 [-h] [-v] [filename] [sql]
此外,sqlite3 默认适配器和转换器 现在已被弃用。
移除已弃用的 unittest 特性
作为 Python 3.12 发布的一部分,一些在早期版本(主要是 v3.1 和 3.2)中已被弃用的 unittest 模块特性将被移除。移除的特性包括:
-
许多
TestCase方法别名,包括failUnless、failIf、failUnlessEqual、failIfEqual、failUnlessAlmostEqual、failIfAlmostEqual、failUnlessRaises、assert_、assertEquals、assertNotEquals、assertAlmostEquals、assertNotAlmostEquals、assertRegexpMatches、assertRaisesRegexp和assertNotRegexpMatches -
TestCase 方法
assertDictContainsSubset -
TestLoader.loadTestsFromModule参数*use_load_tests* -
TextTestResult的别名_TextTestResult
其他语言添加和改进
尽管不可能分享 Python 3.12 中每一个修改的详细信息,这里有一些我个人觉得相当有趣的额外改进和变化的高层概述:
-
在标准库的
os模块中引入了[os.path.isjunction()](https://docs.python.org/3.12/library/os.path.html#os.path.isjunction)成员,允许用户检查给定路径是否为连接点 -
引入了一个新的
[pathlib.Path.walk()](https://docs.python.org/3.12/library/pathlib.html#pathlib.Path.walk)方法,允许遍历目录树,类似于os.walk()
要全面了解作为版本 3.12 正式发布的一部分计划引入的所有更改,可以参考相关文档,在 官方文档中。
结束语
Python 3.12 alpha 版本最近已向预发布用户公开。在本文中,我们讨论了一些最有趣的新特性和更新,基于目前分享的发布细节。有关即将发布的完整详细信息,可以参考 官方变更日志。本文讨论的一些变化包括:
-
移除
distutils模块 -
从 Unicode 中移除了
wstr和wstr_length -
改进了
NameError、SyntaxError和ImportError的错误消息 -
增加对 Linux perf profiler 的支持
-
添加了 sqlite3 命令行界面,并弃用了默认适配器和转换器。
-
移除了一些之前被弃用的
unittest功能。
如 官方 Python 文档 所述,讨论的新功能和更新目前处于草稿阶段,预计在实际发布时会有更多更新。
预发布用户应注意,本文件目前处于草稿阶段。随着 Python 3.12 接近发布,它将会有 substantial 更新,因此即使阅读了早期版本,也值得回访查看最新内容。
成为会员 并阅读 Medium 上的每一个故事。您的会员费直接支持我和您阅读的其他作家。您还将获得对 Medium 上每一个故事的完全访问权限。
使用我的推荐链接加入 Medium — Giorgos Myrianthous
作为 Medium 会员,您的会员费的一部分将用于支持您阅读的作家,并且您可以完全访问每一个故事……
您可能还喜欢的相关文章
Python 中的代码图示 [## Python 中的代码图示
使用 Python 创建云系统架构图。
如何编写 Python 中的 Switch 语句 [## 如何编写 Python 中的 Switch 语句
理解如何使用模式匹配或字典在 Python 中编写 Switch 语句。
如何使用 Docker 本地运行 Airflow [## 如何使用 Docker 本地运行 Airflow
在本地机器上使用 Docker 运行 Airflow 的逐步指南。
PyTorch 初学者图像分类教程
原文:
towardsdatascience.com/pytorch-image-classification-tutorial-for-beginners-94ea13f56f2
在 Python 中微调预训练的深度学习模型
·发布于 Towards Data Science ·22 min 阅读·2023 年 5 月 9 日
--

“不确定这应该是狮子还是猎豹……”
这个实用教程将展示如何使用 PyTorch 框架中的预训练深度学习模型进行图像分类。
这个面向初学者的图像分类教程与其他教程的不同之处在于,我们并不会从头开始构建和训练深度神经网络。在实际操作中,只有少数人会从头训练神经网络。相反,大多数深度学习从业者会使用预训练模型,并将其微调以适应新任务。
在实际操作中,只有少数人会从头训练神经网络。
特定的问题设置是构建一个二分类图像分类模型,以根据小型数据集对猎豹和狮子的图像进行分类。为此,我们将使用 PyTorch 微调一个预训练的图像分类模型。

数据集的示例图像 [1]。
本教程遵循基本的机器学习工作流程:
-
准备和探索数据
-
建立基准
-
运行实验
-
做出预测
你可以在 我的相关 Kaggle 笔记本中跟随教程。
前提条件和设置
理想情况下,你应该对 Python 有一定的了解。
由于这是一个实用教程,我们只会在高层次上覆盖如何构建图像分类模型。我们不会涵盖很多理论,比如卷积层或反向传播的工作原理。一旦你对这个话题感到舒适,我会用这个标志 ⚒️ 标记你可以深入了解的部分。
如果你想为本指南补充一些理论背景信息,我推荐免费的 Kaggle Learn 课程,内容包括深度学习和计算机视觉。
让我们开始导入 PyTorch 和其他相关库:
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
import cv2
import albumentations as A
from albumentations.pytorch import ToTensorV2
import numpy as np # data processing
import matplotlib.pyplot as plt # Data visualization
from tqdm import tqdm # Progress bar
关键的库包括用于深度学习的 PyTorch(版本 1.13.0)、用于图像处理的 OpenCV(版本 4.5.4)以及用于数据增强的 Albumentations(版本 1.3.0)。
第一步:准备和探索数据
第一步是熟悉数据。对于本教程,我们将简短地进行探索性数据分析。
首先,我们将加载数据。示例数据集[1]包含两个文件夹,每个文件夹中都有图像——每个类别一个文件夹。

二分类图像分类的示例数据集[1]。
以下代码遍历所有子文件夹,并创建一个包含文件名及其标签的 Pandas 数据框。
import os
import pandas as pd
root_dir = ... # Insert your data here
sub_folders = ["Cheetahs", "Lions"] # Insert your classes here
labels = [0, 1]
data = []
for s, l in zip(sub_folders, labels):
for r, d, f in os.walk(root_dir + s):
for file in f:
if ".jpg" in file:
data.append((os.path.join(s,file), l))
df = pd.DataFrame(data, columns=['file_name','label'])
在此处插入你的数据! — 为了跟上本文的内容,你的数据集应该类似于此:

二分类图像分类的示例数据集[1]。在此处插入你的数据。
我们大约有 170 张照片:大致 85 张狮子照片和 85 张猎豹照片(见[1]中的备注)。这是一个非常小但平衡的数据集,非常适合微调!
import seaborn as sns
sns.countplot(data = df, x = 'label');

使用 seaborn 绘制的图像分类样本数据集的类别分布
为了对数据集有一个感觉,绘制一些样本总是个好主意:
fig, ax = plt.subplots(2, 3, figsize=(10, 6))
idx = 0
for i in range(2):
for j in range(3):
label = df.label[idx]
file_path = os.path.join(root_dir, df.file_name[idx])
# Read an image with OpenCV
image = cv2.imread(file_path)
# Convert the image to RGB color space.
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
# Resize image
image = cv2.resize(image, (256, 256))
ax[i,j].imshow(image)
ax[i,j].set_title(f"Label: {label} ({'Lion' if label == 1 else 'Cheetah'})")
ax[i,j].axis('off')
idx = idx+1
plt.tight_layout()
plt.show()

数据集[1]中的样本图像。
通过探索这样的数据集,你可以获得一些见解。例如,正如你在这里看到的,图像不仅限于动物,还包括雕像。
在我们进一步操作之前,让我们将数据集拆分为训练数据和测试数据。训练数据将用于构建我们的模型,而测试数据将作为保留数据集来评估最终模型在未见数据上的性能。在本示例中,我们将把 10%的数据留作测试用。
from sklearn.model_selection import train_test_split
train_df, test_df = train_test_split(df,
test_size = 0.1,
random_state = 42)

将数据拆分为训练集和测试集(灵感来源于scikit-learn)
第二步:建立基线
接下来,我们将构建一个基线。基线包括三个关键组件:
-
一个加载图像的数据管道
-
一个模型,包含损失函数和优化器
-
一个训练管道,包括一个交叉验证策略
在本节中,我们将逐个组件进行讲解,并最终整理好。
由于训练深度学习模型涉及大量实验,我们希望能够快速切换代码的特定部分。因此,我们将尽可能使以下代码模块化,并使用配置进行调优:
from types import SimpleNamespace
cfg = SimpleNamespace(**{})
我们将随着进展添加可配置参数。
构建用于加载图像的数据管道
首先,你必须构建一个管道,以批次方式加载、预处理和喂入图像到神经网络中(而不是一次性加载)。PyTorch 提供了两个核心类供你使用:
-
Dataset类:加载和预处理数据集。你需要根据你的需求自定义这个类。 -
Dataloader类:将数据样本批次加载到神经网络中。
首先,你需要自定义 Dataset 类。其关键组件是:
-
构造函数:用于加载数据集,例如 Pandas Dataframe
-
__len__():获取数据集的长度。这通常只需要对数据集传递方式进行最小的调整。 -
__getitem__():通过索引从数据集中获取样本。这通常是你根据需要执行预处理时修改代码的地方。
下面你可以找到一个用于自定义 Dataset 类的模板。
class CustomDataset(Dataset):
def __init__(self, df):
# Initialize anything you need later here ...
self.df = df
self.X = ...
self.y = ...
# ...
# Get the number of rows in the dataset
def __len__(self):
return len(self.df)
# Get a sample of the dataset
def __getitem__(self, idx):
return [self.X[idx], self.y[idx]]
在加载数据集时,你也可以执行任何需要的预处理,如变换或图像标准化。这发生在 __getitem__() 中。
在这个示例中,我们首先使用 OpenCV 从根目录 (cfg.root_dir) 加载图像,并将其转换为 RGB 颜色空间。然后我们将应用基本转换:调整图像大小 (cfg.image_size),并将图像从 NumPy 数组转换为张量。最后,我们将图像的值标准化到 [0, 1] 范围,通过除以 255 实现。
cfg.root_dir = ... # Insert your data here
cfg.image_size = 256
class CustomDataset(Dataset):
def __init__(self,
cfg,
df,
transform=None,
mode = "val"):
self.root_dir = cfg.root_dir
self.df = df
self.file_names = df['file_name'].values
self.labels = df['label'].values
if transform:
self.transform = transform
else:
self.transform = A.Compose([
A.Resize(cfg.image_size, cfg.image_size),
ToTensorV2(),
])
def __len__(self):
return len(self.df)
def __getitem__(self, idx):
# Get file_path and label for index
label = self.labels[idx]
file_path = os.path.join(self.root_dir, self.file_names[idx])
# Read an image with OpenCV
image = cv2.imread(file_path)
# Convert the image to RGB color space.
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
# Apply augmentations
augmented = self.transform(image=image)
image = augmented['image']
# Normalize because ToTensorV2() doesn't normalize the image
image = image/255
return image, label
接下来,我们需要一个 Dataloader 来将 Dataset 的样本批次喂入神经网络,因为我们(可能)没有足够的 RAM 一次性喂入所有图像。
你需要提供 Dataloader 你要遍历的 Dataset 实例、批次大小 (cfg.batch_size),以及是否打乱数据的信息。
cfg.batch_size = 32
example_dataset = CustomDataset(cfg, df)
example_dataloader = DataLoader(example_dataset,
batch_size = cfg.batch_size,
shuffle = True,
num_workers=0,
)
批次大小应在训练过程中保持固定,不应调整 [2]。因为训练速度与批次大小相关,我们希望使用尽可能大的批次大小。首先使用 32 的批次大小,然后按二的幂次增加(64, 128 等),直到出现内存错误,然后使用最后的批次大小。
当你遍历 Dataloader 时,它会给你来自自定义 Dataset 的样本批次。让我们取出第一个批次进行验证:
for (image_batch, label_batch) in example_dataloader:
print(image_batch.shape)
print(label_batch.shape)
break
torch.Size([32, 3, 256, 256])
torch.Size([32])
Dataloader 返回图像批次和标签批次。image_batch 是形状为 (32, 3, 256, 256) 的张量。这是一个包含 32 张图像的批次,每张图像的形状为 (3, 256, 256)(color_channels, image_height, image_width)。label_batch 是形状为 (32) 的张量。这些是与 32 张图像对应的标签。

自定义数据集的 Dataloader 示例输出
本节解释了如何构建数据管道。在后面的章节中(见 设置训练管道),我们将使用 Dataset 和 Dataloader 创建用于训练、验证和测试的独立管道。
在训练模型之前,我们需要将训练数据再次拆分为训练集和验证集。在一个数据集上训练模型然后在相同数据上评估模型是一种方法上的错误,因为模型只需记住已见样本的标签。因此,模型会过拟合训练数据,而不是进行泛化。
为了避免过拟合,我们暂时使用 train_test_split() 函数将训练数据随机分割为训练集和验证集。本节稍后将被 交叉验证策略 替代。
X = df
y = df.label
train_df, valid_df, y_train, y_test = train_test_split(X,
y,
test_size = 0.2,
random_state = 42)

将训练数据再次拆分为训练集和验证集(灵感来源于 scikit-learn)
有了这个分割,我们现在可以为训练和验证数据创建 Datasets 和 Dataloaders:
train_dataset = CustomDataset(cfg, train_df)
valid_dataset = CustomDataset(cfg, valid_df)
train_dataloader = DataLoader(train_dataset,
batch_size = cfg.batch_size,
shuffle = True)
valid_dataloader = DataLoader(valid_dataset,
batch_size = cfg.batch_size,
shuffle = False)
准备模型
这部分将学习如何在 PyTorch 中构建神经网络。当我开始学习深度学习时,我认为构建神经网络是训练深度学习模型的重要部分。但实际上,这是研究人员为我们完成的工作。我们这些从业者只需使用最终模型即可。
研究人员尝试不同的模型架构,例如卷积神经网络(CNN),通常会在大型基准数据集(如 ImageNet [3])上训练图像分类模型。我们称这些模型为 骨干网。

期望与现实:实际上,只有少数人从头开始训练用于图像分类的神经网络
微调预训练神经网络之所以有效,是因为前几层通常会学习到通用特征(如边缘检测)。
⚒️ 当然,你应该了解神经网络的一般工作原理,包括反向传播,以及不同层(如卷积层)的工作方式。然而,为了跟上这个实际教程,你现在不需要理解这些细节。完成本教程后,你可以通过免费的 Kaggle Learn 课程填补一些理论空白,课程包括 深度学习 和 计算机视觉。
绝妙的骨干网及其获取途径 — 现在,你应该选择哪些预训练模型,以及从哪里获得这些模型?
在本教程中,我们将使用 [timm](https://timm.fast.ai/) — 一个包含由 Ross Wightman 创建的先进计算机视觉模型集合的深度学习库 — 来获取预训练模型。(你可以使用 torchvision.models 来获取预训练模型,但我个人觉得在实验中使用 timm 更容易更换骨干网络。)
import timm
cfg.n_classes = 2
cfg.backbone = 'resnet18'
model = timm.create_model(cfg.backbone,
pretrained = True,
num_classes = cfg.n_classes)
这段代码包含很多内容。让我们一步步来解读:
backbone = 'resnet18'— 在这个例子中,我们使用一个 18 层的 ResNet [5]。ResNet 代表残差网络,它是一种使用所谓的残差块的 CNN。
⚒️我们将跳过 ResNet 和残差块的详细内容。如果你对技术细节感兴趣, 你可以深入了解这篇文章,例如。
ResNet 系列中有许多不同的模型,如 ResNet18、ResNet34 等,其中的数字表示网络的层数。一个(非常粗略的)经验法则是:层数越多,性能越好。你可以打印 timm.list_models('*resnet*') 来查看其他可用的模型。
⚒️ 了解不同的计算机视觉/图像分类骨干网络,如 ResNet、DenseNet 和 EfficientNet。
pretrained = True — 这意味着我们希望使用在 ImageNet [3] 上训练的模型权重。如果设置为 False,你将只得到模型的结构而没有权重 [6]。
num_classes = cfg.n_classes — 由于模型是在 ImageNet [3] 上预训练的,你将得到一个包含 ImageNet 中 1000 个类别的分类器。因此,你需要移除 ImageNet 分类器并定义你问题中的类别数量 [6]。如果你设置 num_classes = 0,你将得到没有分类器的模型 [6]。
要检查输出大小,你可以传入一个具有随机值的 3 通道样本批次 X,尺寸为 [6]。
X = torch.randn(cfg.batch_size, 3, cfg.image_size, cfg.image_size)
y = model(X)
它将输出 torch.Size([1, cfg.n_classes]) [6]。

模型的输入和输出
准备损失函数和优化器
接下来,训练一个模型有两个关键要素:
-
一个损失函数(准则),
-
一个优化算法(优化器),和
-
可选的学习率调度器。
损失函数 — 常见的损失函数有:
-
二分类交叉熵(BCE)损失用于二分类。
-
分类交叉熵损失用于多分类。
-
均方损失用于回归。
虽然我们有一个二分类问题,但你也可以使用分类交叉熵损失。如果你愿意,可以将损失函数更换为 BCE。
criterion = nn.CrossEntropyLoss()
优化器 — 优化算法通过最小化损失函数(在我们的例子中是交叉熵损失)来优化模型。有很多不同的优化器可以选择。我们使用一个流行的优化器:Adam。
cfg.learning_rate = 1e-4
optimizer = torch.optim.Adam(
model.parameters(),
lr = cfg.learning_rate,
weight_decay = 0,
)
学习率调度器— 学习率调度器会在训练过程中调整学习率的值。虽然你不必使用学习率调度器,但使用它可以帮助算法更快收敛。这是因为如果学习率保持不变,如果学习率过大,它可能会阻碍你找到最佳解,如果学习率过小,则可能需要很长时间才能收敛。
有许多不同的学习率调度器可用,但 Kaggle 大师建议将余弦衰减作为微调的学习率调度器 [2]。
cfg.lr_min = 1e-5
cfg.epochs = 5
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
optimizer,
T_max = np.ceil(len(train_dataloader.dataset) / cfg.batch_size) * cfg.epochs,
eta_min = cfg.lr_min
)
T_max 定义了半周期,应该等于最大迭代次数(np.ceil(len(train_dataloader.dataset) /cfg.batch_size)*cfg.epochs)。
训练过程中,学习率的变化情况如下所示:

余弦衰减学习率调度器
指标 — 既然我们在谈论它,我们还需要定义一个指标来评估模型的整体性能。同样,有许多不同的指标。对于这个示例,我们将使用准确率作为指标:
from sklearn.metrics import accuracy_score
def calculate_metric(y, y_pred):
metric = accuracy_score(y, y_pred)
return metric
不要将指标与损失函数混淆。损失函数用于在训练过程中优化学习函数,而指标则在训练后衡量模型的性能。
⚒️ 了解不同的指标以及哪些指标适用于哪些问题。
设置训练管道
这可能是本教程中最复杂但也是最有趣的部分。你准备好了吗?
模型通常以迭代的方式进行训练。一轮迭代称为一个 epoch。从头开始训练通常需要许多 epochs,而微调只需要几个(大约 5 到 10)epochs。
在每个 epoch 中,模型会在完整的训练数据上进行训练,然后在完整的验证数据上进行验证。我们现在将定义两个函数:一个用于训练(train_an_epoch()),另一个用于在一个 epoch 上验证模型(validate_an_epoch())。
下面你可以看到训练函数:
cfg.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
def train_one_epoch(dataloader, model, optimizer, scheduler, cfg):
# Training mode
model.train()
# Init lists to store y and y_pred
final_y = []
final_y_pred = []
final_loss = []
# Iterate over data
for step, batch in tqdm(enumerate(dataloader), total=len(dataloader)):
X = batch[0].to(cfg.device)
y = batch[1].to(cfg.device)
# Zero the parameter gradients
optimizer.zero_grad()
with torch.set_grad_enabled(True):
# Forward: Get model outputs
y_pred = model(X)
# Forward: Calculate loss
loss = criterion(y_pred, y)
# Covert y and y_pred to lists
y = y.detach().cpu().numpy().tolist()
y_pred = y_pred.detach().cpu().numpy().tolist()
# Extend original list
final_y.extend(y)
final_y_pred.extend(y_pred)
final_loss.append(loss.item())
# Backward: Optimize
loss.backward()
optimizer.step()
scheduler.step()
# Calculate statistics
loss = np.mean(final_loss)
final_y_pred = np.argmax(final_y_pred, axis=1)
metric = calculate_metric(final_y, final_y_pred)
return metric, loss
让我们一步步来看:
-
将模型设置为训练模式。模型也可以处于评估模式。这种模式会影响模型中
[Dropout](https://pytorch.org/docs/stable/_modules/torch/nn/modules/dropout.html)和[BatchNorm](https://pytorch.org/docs/stable/_modules/torch/nn/modules/batchnorm.html)层的行为。 -
以小批量迭代训练数据。如果你使用 GPU 进行更快的训练,则需要将样本和标签移动到 GPU (
cfg.device)。 -
清除优化器的最后一个误差梯度。
-
通过模型进行一次前向传播。
-
计算模型输出的损失。
-
通过模型反向传播误差。
-
更新模型以减少损失。
-
步进学习率调度器。
-
计算损失和指标以获取统计数据。由于预测将是 GPU 上的张量,就像输入一样,我们需要分离张量以便将它们从自动微分图中分离,并调用 NumPy 函数将其转换为 NumPy 数组。
接下来,我们定义如下所示的验证函数:
def validate_one_epoch(dataloader, model, cfg):
# Validation mode
model.eval()
final_y = []
final_y_pred = []
final_loss = []
# Iterate over data
for step, batch in tqdm(enumerate(dataloader), total=len(dataloader)):
X = batch[0].to(cfg.device)
y = batch[1].to(cfg.device)
with torch.no_grad():
# Forward: Get model outputs
y_pred = model(X)
# Forward: Calculate loss
loss = criterion(y_pred, y)
# Covert y and y_pred to lists
y = y.detach().cpu().numpy().tolist()
y_pred = y_pred.detach().cpu().numpy().tolist()
# Extend original list
final_y.extend(y)
final_y_pred.extend(y_pred)
final_loss.append(loss.item())
# Calculate statistics
loss = np.mean(final_loss)
final_y_pred = np.argmax(final_y_pred, axis=1)
metric = calculate_metric(final_y, final_y_pred)
return metric, loss
让我们再一步步看一遍:
-
将模型设置为评估模式。
-
对验证数据进行小批量迭代。如果使用 GPU 进行更快的训练,样本和标签需要移动到 GPU 上。
-
通过模型进行前向传播。
-
计算损失和指标以获取统计数据。
初看,训练和验证一个时期看起来很相似。让我们看看代码比较,以使差异更清晰:

在 PyTorch 中训练和验证代码的对比截图
你可以看到以下差异:
-
模型必须处于训练或评估模式。
-
训练模型时,我们需要一个优化器和一个可选的调度器。对于验证,我们只需要模型。
-
梯度计算仅在训练时激活。对于验证,我们不需要它。
交叉验证策略
现在,我们还没有完成训练管道。之前,我们将训练数据分为训练数据和验证数据。但是,将可用数据分为两个固定的集合限制了训练样本的数量。
相反,我们将使用交叉验证策略,将训练数据分为k个折叠。然后,模型将在k次独立的迭代中训练,其中每次迭代模型在k-1 个折叠上训练,并在一个折叠上进行验证,每次迭代折叠都会切换,如下所示:

将训练数据再次分为训练和验证(灵感来源于scikit-learn)
在这个例子中,我们使用了[StratifiedKFold](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.StratifiedKFold.html)来创建分割。你也可以使用KFold,但StratifiedKFold的优点是它保持了类分布。
from sklearn.model_selection import StratifiedKFold
cfg.n_folds = 5
# Create a new column for cross-validation folds
df["kfold"] = -1
# Initialize the kfold class
skf = StratifiedKFold(n_splits=cfg.n_folds)
# Fill the new column
for fold, (train_, val_) in enumerate(skf.split(X = df, y = df.label)):
df.loc[val_ , "kfold"] = fold
for fold in range(cfg.n_folds):
train_df = df[df.kfold != fold].reset_index(drop=True)
valid_df = df[df.kfold == fold].reset_index(drop=True)
添加数据增强
当训练和验证指标之间的差异显著时,这表明模型正在过拟合训练数据。过拟合发生在模型仅在少量示例上进行训练,并从训练数据中学习无关的细节或噪声。这会对模型在呈现新示例时的表现产生负面影响。结果,模型在新图像上的泛化能力受限。
为了在训练过程中克服过拟合,你可以使用数据增强。数据增强通过随机变换现有图像生成额外的训练数据。这种技术让模型接触到数据的更多方面,从而帮助它更好地泛化。
我们可以使用albumentations包中的一些准备好的数据增强方法,例如:
-
图像旋转(
A.Rotate()) -
水平翻转(
A.HorizontalFlip()) -
切割 [4](
A.CoarseDropout())
之前,我们定义了一个基本的变换来调整图像大小并将其转换为张量。我们将继续在验证和测试数据集中使用它,因为它们不需要任何增强。对于训练数据集,我们创建了一个新的变换transform_soft,它在调整大小和转换为张量之外,还包含了上述三种增强。
transform_soft = A.Compose([A.Resize(cfg.image_size, cfg.image_size),
A.Rotate(p=0.6, limit=[-45,45]),
A.HorizontalFlip(p = 0.6),
A.CoarseDropout(max_holes = 1, max_height = 64, max_width = 64, p=0.3),
ToTensorV2()])
你可以通过参数p来控制增强应用到图像的百分比。
如果我们可视化从增强数据集中提取的一些样本,我们可以看到三种增强成功应用:
-
图像 0、1、2、4 中的旋转
-
水平翻转很难检测,如果你不知道原始图像,但我们可以看到图像 2 必须是水平翻转的
-
图像 1 和 4 中的切割(粗略丢弃)

增强后的训练数据集
⚒️ 接下来,你可以审查并添加其他图像增强技术,例如 Mixup 和 Cutmix,到你的管道中。
## Cutout, Mixup, and Cutmix: 在 PyTorch 中实现现代图像增强
在 Python 中实现的计算机视觉数据增强技术
towardsdatascience.com
将所有内容整合在一起
现在我们已经讨论了从 data pipeline 到 model 的基线的每个组件,包括 loss function and optimizer,到 training pipeline,以及 cross-validation strategy,我们可以将它们综合起来,如下图所示:

基线代码的流程图
我们将遍历交叉验证策略的每一个折叠。在每个折叠中,我们为训练和验证数据设置一个 data pipeline 和一个 model,并配备 loss function and optimizer。然后,对于每个时期,我们将训练和验证模型。
在动手之前,让我们为成功做好准备,固定随机种子以确保可重复的结果。
import random
def set_seed(seed=1234):
random.seed(seed)
os.environ["PYTHONHASHSEED"] = str(seed)
np.random.seed(seed)
# In general seed PyTorch operations
torch.manual_seed(seed)
# If you are using CUDA on 1 GPU, seed it
torch.cuda.manual_seed(seed)
# If you are using CUDA on more than 1 GPU, seed them all
torch.cuda.manual_seed_all(cfg.seed)
# Certain operations in Cudnn are not deterministic, and this line will force them to behave!
torch.backends.cudnn.deterministic = True
# Disable the inbuilt cudnn auto-tuner that finds the best algorithm to use for your hardware.
torch.backends.cudnn.benchmark = False
接下来,我们将编写一个fit()函数,该函数为所有周期拟合模型。该函数会迭代周期数,而训练和验证函数包含内循环,这些内循环会迭代训练和验证数据集中的批次,如训练管道部分所述。
cfg.seed = 42
def fit(model, optimizer, scheduler, cfg, train_dataloader, valid_dataloader=None):
acc_list = []
loss_list = []
val_acc_list = []
val_loss_list = []
for epoch in range(cfg.epochs):
print(f"Epoch {epoch + 1}/{cfg.epochs}")
set_seed(cfg.seed + epoch)
acc, loss = train_one_epoch(train_dataloader, model, optimizer, scheduler, cfg)
if valid_dataloader:
val_acc, val_loss = validate_one_epoch(valid_dataloader, model, cfg)
print(f'Loss: {loss:.4f} Acc: {acc:.4f}')
acc_list.append(acc)
loss_list.append(loss)
if valid_dataloader:
print(f'Val Loss: {val_loss:.4f} Val Acc: {val_acc:.4f}')
val_acc_list.append(val_acc)
val_loss_list.append(val_loss)
return acc_list, loss_list, val_acc_list, val_loss_list, model

拟合函数的日志
为了可视化目的,我们还将创建训练和验证集上的损失和准确性图:
def visualize_history(acc, loss, val_acc, val_loss):
fig, ax = plt.subplots(1,2, figsize=(12,4))
ax[0].plot(range(len(loss)), loss, color='darkgrey', label = 'train')
ax[0].plot(range(len(val_loss)), val_loss, color='cornflowerblue', label = 'valid')
ax[0].set_title('Loss')
ax[1].plot(range(len(acc)), acc, color='darkgrey', label = 'train')
ax[1].plot(range(len(val_acc)), val_acc, color='cornflowerblue', label = 'valid')
ax[1].set_title('Metric (Accuracy)')
for i in range(2):
ax[i].set_xlabel('Epochs')
ax[i].legend(loc="upper right")
plt.show()

绘制的指标和损失随周期变化的历史
当我们将所有内容结合起来时,它将如下所示:
for fold in range(cfg.n_folds):
train_df = df[df.kfold != fold].reset_index(drop=True)
valid_df = df[df.kfold == fold].reset_index(drop=True)
train_dataset = CustomDataset(cfg, train_df, transform = transform_soft)
valid_dataset = CustomDataset(cfg, valid_df)
train_dataloader = DataLoader(train_dataset,
batch_size = cfg.batch_size,
shuffle = True,
num_workers = 0,
)
valid_dataloader = DataLoader(valid_dataset,
batch_size = cfg.batch_size,
shuffle = False,
num_workers = 0,
)
model = timm.create_model(cfg.backbone,
pretrained = True,
num_classes = cfg.n_classes)
model = model.to(cfg.device)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(),
lr = cfg.learning_rate,
weight_decay = 0,
)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer,
T_max= np.ceil(len(train_dataloader.dataset) / cfg.batch_size) * cfg.epochs,
eta_min=cfg.lr_min)
acc, loss, val_acc, val_loss, model, lrs = fit(model, optimizer, scheduler, cfg, train_dataloader, valid_dataloader)
visualize_history(acc, loss, val_acc, val_loss)
步骤 3:运行实验
数据科学是一门实验科学。因此,这一步的目的是找到实现最佳性能的超参数配置、数据增强、模型主干和交叉验证策略(或任何你的目标,例如,性能和推理时间之间的最佳权衡)。
设置实验跟踪
在进入这一步之前,花点时间考虑一下你将如何跟踪实验。实验跟踪可以简单到用笔和纸记录一切。或者,你可以在电子表格中跟踪所有内容,甚至使用实验跟踪系统来自动化整个过程。
这为什么重要,以及你可以用笔和纸、电子表格等三种不同方式记录和组织你的 ML 实验。
如果你是绝对初学者,我建议一开始简单地手动在电子表格中跟踪你的实验。打开一个空电子表格,并为所有输入创建列,例如:
-
主干,
-
学习率,
-
周期,
-
增强方式,以及
-
图像大小
以及你想要跟踪的输出,如训练和验证的损失和指标。
结果电子表格可能会如下所示:

初学者跟踪实验的示例电子表格
⚒️ 一旦你对深度学习技术感到舒适,你可以通过 实现实验跟踪系统 来提升你的水平,以自动化实验跟踪,例如 Weights & Biases, Neptune,或 MLFlow。
实验和超参数调整
现在你有了实验跟踪系统,让我们开始进行一些实验。你可以从调整以下超参数开始:
-
训练步骤数:范围为 2 到 10
-
学习率:范围为 0.0001 到 0.001
-
图像大小:范围为 128 到 1028
-
主干网络:尝试不同的主干网络。首先,尝试 ResNet 家族的更深模型(打印
timm.list_models('*resnet*')查看其他可用模型),然后尝试不同的主干网络家族,如timm.list_models('*densenet*')或timm.list_models('*efficientnet*')
⚒️ 当你对深度学习技术感到熟练时,可以通过使用 Optuna 或 Weights & Biases* 来自动化这一步,进一步提升自己。*
现在轮到你了!— 调整几个参数,看看模型的性能如何变化。一旦你对结果满意,就可以进入下一步。

实验日志示例
第四步:进行预测(推断)
请敲鼓!现在我们已经找到能够给我们最优模型的配置,我们希望将其充分利用。
首先,让我们用最佳配置在完整数据集上微调模型,以利用每个数据样本。在这一步中,我们不会将数据拆分为训练数据和验证数据。相反,我们只有一个大的训练数据集。
train_df = df.copy()
train_dataset = CustomDataset(cfg, train_df, transform = transform_soft)
train_dataloader = DataLoader(train_dataset,
batch_size = cfg.batch_size,
shuffle = True,
num_workers = 0,
)
但其余的训练流程保持不变。
model = timm.create_model(cfg.backbone,
pretrained = True,
num_classes = cfg.n_classes)
model = model.to(cfg.device)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(),
lr = cfg.learning_rate,
weight_decay = 0,
)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer,
T_max= np.ceil(len(train_dataloader.dataset) / cfg.batch_size) * cfg.epochs,
eta_min=cfg.lr_min)
acc, loss, val_acc, val_loss, model = fit(model, optimizer, scheduler, cfg, train_dataloader)
推断 — 最后,我们将使用模型来预测保留的测试集。
test_dataset = CustomDataset(cfg, test_df)
test_dataloader = DataLoader(test_dataset,
batch_size = cfg.batch_size,
shuffle = False,
num_workers = 0,
)
dataloader = test_dataloader
# Validation mode
model.eval()
final_y = []
final_y_pred = []
# Iterate over data
for step, batch in tqdm(enumerate(dataloader), total=len(dataloader)):
X = batch[0].to(cfg.device)
y = batch[1].to(cfg.device)
with torch.no_grad():
# Forward: Get model outputs
y_pred = model(X)
# Covert y and y_pred to lists
y = y.detach().cpu().numpy().tolist()
y_pred = y_pred.detach().cpu().numpy().tolist()
# Extend original list
final_y.extend(y)
final_y_pred.extend(y_pred)
# Calculate statistics
final_y_pred_argmax = np.argmax(final_y_pred, axis=1)
metric = calculate_metric(final_y, final_y_pred_argmax)
test_df['prediction'] = final_y_pred_argmax
下面你可以看到我们模型的结果:

预测

总结与下一步
本教程向你展示了如何微调预训练的图像分类模型以适应你的特定任务,评估它,并使用 Python 中的 PyTorch 框架对未见数据进行推断。
当你感到熟练后,可以通过查看标有 ⚒️ 的部分来提升到中级水平。
深度学习模型在计算机视觉和自然语言处理中的微调实用指南
towardsdatascience.com
喜欢这个故事吗?
免费订阅 以便在我发布新故事时获得通知。
[## 每当 Leonie Monigatti 发布新内容时获取电子邮件通知。
每当 Leonie Monigatti 发布新内容时获取电子邮件通知。通过注册,如果你还没有的话,你将创建一个 Medium 账户……
medium.com](https://medium.com/@iamleonie/subscribe?source=post_page-----94ea13f56f2--------------------------------)
在 LinkedIn,Twitter,以及 Kaggle上找到我!
参考文献
数据集
[1] MikołajFish99 (2023). 狮子还是猎豹——图像分类 在 Kaggle 数据集中。
许可: 根据原始图像来源 (开放图像数据集 V6),注释由 Google LLC 根据CC BY 4.0许可授权,图像的许可列为CC BY 2.0。
注意,原始数据集包含 200 张图像,每个类别各 100 张图像。但数据集需要一些清理,包括移除其他动物的图像;因此,最终数据集略小。为了保持教程简短,我们将跳过数据清理过程。
图像
如果没有其他说明,所有图像均由作者创作。
文献
[2] S. Bhutani 与 H20.ai (2023). 训练 ML 模型的最佳实践 | @ChaiTimeDataScience #160 在 2023 年 1 月于 YouTube 上发布。
[3] Deng, J., Dong, W., Socher, R., Li, L. J., Li, K., & Fei-Fei, L. (2009 年 6 月). Imagenet:一个大规模的分层图像数据库。见 2009 年 IEEE 计算机视觉与模式识别会议(第 248–255 页)。Ieee。
[4] DeVries, T., & Taylor, G. W. (2017). 使用 cutout 改进卷积神经网络的正则化。 arXiv 预印本 arXiv:1708.04552。
[5] K. He, X. Zhang, S. Ren, & J. Sun (2016). 深度残差学习用于图像识别。见 IEEE 计算机视觉与模式识别会议论文集(第 770–778 页)。
[6] timmdocs (2022). Pytorch 图像模型(timm)(访问日期:2023 年 4 月 10 日)。










浙公网安备 33010602011771号