@driscollis: Learn how to create module-level lazy assignments in #Python in my friend Anthony's new video:

X AI KOLs Timeline 工具

摘要

Anthony 介绍了一个名为 lazy_static 的 Python 库,它利用惰性导入机制实现模块级惰性赋值,避免 import 阶段的开销。视频展示了如何使用装饰器推迟计算和导入,并解释了其内部工作原理。

Learn how to create module-level lazy assignments in #Python in my friend Anthony's new video: https://t.co/Tid2m920B2
查看原文
查看缓存全文

缓存时间: 2026/07/31 06:51

Learn how to create module-level lazy assignments in #Python in my friend Anthony’s new video:

https://t.co/Tid2m920B2


@driscollis: Learn how to create module-level lazy assignments in #Python in my friend Anthony’s new video:

Channel: @driscollis Source: https://www.youtube.com/watch?v=abjnA3fQCWE

Transcript

我知道,哇,我们又在谈惰性导入了。但实际上,今天我们要谈的是惰性赋值,这是一个新想法,我希望能够说服你们它很有用,并且展示我是如何让它成为可能的。但在那之前,我们先聊聊惰性导入,给大家简单回顾一下,因为稍后它会用到。

我这里做了一个小例子,我正在惰性导入 asyncio。这里的想法是,我想把这个使用推迟到绝对必要的时候。所以,如果我们需要显示类似参数解析器的帮助信息,我们可以很快做到。然后当我们真正运行程序时,它会惰性解析这个导入,并实际执行功能。

你可以看到,我们做了……怎么说呢,我们跑 20 次。然后 python3 t.py --help。我要把这个包起来,然后发送到 dev null,因为我们实际上并不需要……好吧,其实先让我们确认它能正确工作。好了。

所以,发送到 dev null 只是为了展示,当我们推迟导入时,大约需要 34 毫秒。当我们真正运行程序时,需要 56 毫秒。这基本上就是导入 asyncio 和不导入的差别。实际上,我的意思是,会多一点,因为它真的在运行并启动了一个事件循环,但请相信,推迟这个导入会带来很大的不同。我想我们也可以直接导入这个模块。好吧,我们就这么做吧。

[笑声] 做一个更好的例子,Anthony。

所以,我不知道为什么我在那里放了分号。如果我们用非延迟导入来做这个,需要 50 毫秒。如果我们用延迟导入,只需要 16 毫秒。所以你可以看到,在花费的时间上,有非常显著的差异。

通过推迟导入,我们可以更快地显示帮助信息。这就是惰性导入背后的基本想法。天啊,我今天真是到处跑题。现在,我想介绍第二个想法,那就是……我想惰性导入 YAML,而且我经常在使用 PyYAML 的时候,还会包装 loads 函数,写上 loads = 哦,我们还需要 functools。functools.partial(yaml.load, Loader=yaml.CSafeLoader)。因为默认情况下,safe_load 使用慢速的纯 Python 加载器,所以我通常想使用快速的但仍然安全的用 C 实现的加载器。

现在,你会注意到,如果我惰性导入 YAML,然后我们看看导入这个模块需要多长时间,它仍然是 15 毫秒。一旦我引入了这个 loads 辅助函数,我们就回到了慢速的情况。我们不再惰性导入 YAML 了,因为我们在模块作用域中使用了 YAML 符号。所以这会导致惰性导入立即触发,于是我们就失去了惰性的好处。

现在,在理想情况下,我希望能够直接说 lazy loads = this,然后推迟这个全局变量,这个模块变量,直到需要它的时候。你知道,如果我能这样做就太好了。这就是我的目标。我希望能够启用这个功能,这样我就可以在模块作用域惰性计算东西。

显然,这个语法行不通。这不是真实存在的东西。所以我们要尝试让它生效。

好吧,我们直接用 loads 来加载点东西。不知道,xy。当然。

嗯,我们要从基线开始,我向你们展示我是如何通过一些小技巧让它工作的,然后我再向你们展示我做的这个东西在幕后是如何工作的。

那么,让我们运行一下,确认它能做我们想做的事。

[笑声] 什么? 段错误。嗯,我想 PyYAML 可能在 Python 3.15 里坏了。那看起来不太好。我们暂时使用纯 YAML 安全加载器吧。我的意思是它不完全匹配我想要的例子,但没关系。好的。

[笑声] 什么? 那看起来不太好。好吧,不管怎样,呃,我刚才在分心之前说到哪了?哦,对了。好的,那么让我给你们展示我做的这个东西,来尝试启用这种惰性模块级赋值。我有点希望代码的工作方式能完全像急切求值一样,并且我想在这里保留惰性导入。所以,我做了这个东西,叫做 lazy_static。

它的名字有点像是借鉴了那个已废弃的 Rust crate,后者做的也是同样的事情,允许你惰性实例化模块级的东西。它提供了一个单一 API。

这个单一 API 是 from lazy_static import lazy。就是这个 lazy 辅助工具,它是一个装饰器,而且……是的,我们必须稍微改变一下它的工作方式。我们没有那种漂亮的赋值语法。但我们不需要改变程序的其他部分。这会导致这个赋值,也就是这里的这个函数,在访问这个符号时被惰性求值,就像如果我们在下面访问 YAML,这个符号会惰性触发导入一样。就是这样。它能工作。让我展示它确实能工作,然后我会展示它是如何工作的,我还会展示来自 README 的另一个例子,用来说明这一点。我想另一件事是我们可以用这个导入示例来证明,是的,我们仍然是在 15-16 毫秒,而如果我们没有惰性做这个,我们会在 30 毫秒。或者其实,我还没试过,但我认为如果我们也这样做,会导致它……是的,它会导致这里也急切求值。就像惰性导入一样。所以,这实际上是

[笑声] 这其实是一个穿着风衣的惰性导入。

但只是为了向你们展示,哦,抱歉,只是为了向你们展示这个程序即使这样也仍然能工作,你可以看到它仍然在求值那个符号。

嗯,而且我们之前已经展示了它把那个赋值推迟了。

另一个很酷的地方是,我不需要做任何特殊的事情。这里没有花哨的 mypy 插件。它应该可以直接与类型检查器一起工作,不仅适用于 mypy,也适用于其他类型检查器。

嗯,如果我们看一下……哦,其实,你知道吗?呃,可能不能和类型检查器一起工作,因为我没有 PEP 451,但是,因为单文件包被打包的方式,它会抱怨。是的,它找不到它。但是如果我们把文件复制到虚拟环境中,我真希望有一个单文件版的 PEP 451。哦,typeshed 的 pi.ml。哦,因为这个。我不知道。算了,我们用另一个例子来展示 MyPy 的东西,因为这个的类型注解很复杂。

好的。总之,这就是基本想法。我稍后给你们看类型注解。

让我们使用 README 中一个更简单的例子来展示同样的事情。

Lazy static。 是的。这就是 README 中的例子,我觉得它比这里的例子更简单。

我们把这个去掉。在这个例子中,我们有一个返回 int 的函数。它只是一个非常简单的 prints computing,所以你可以看到它什么时候进行计算。你可以看到你只需要像访问全局变量一样访问这个变量。你不需要调用它。你不需要 functools.cache。这些都和这里无关。

嗯,所以,再说一次,如果你用 --help 运行这个,你可以看到它推迟了运行那个函数,直到我们真正在这里访问它。所以,这就是这里发生的事情。

然后在这里我们可以用 reveal_type 来展示它,因为类型只是 int。很简单。只计算一次。如果我们在这个文件上运行 MyPy,你可以看到它能工作。很好。所以,这就是这个想法。你可以推迟昂贵的计算。这对于保留惰性导入,或者避免在模块作用域编译正则表达式很有用,因为那会拖慢导入速度。呃,任何其他你可能会构建为全局变量的笨重对象,你都可以把它包在这个小小的 lazy 东西里,它就能正常工作。你不需要使用任何额外的 functools.cache。你不需要为了访问它们而多调用一个函数。你也不会有访问它们的函数调用开销。它使用的是与惰性导入相同的机制。事实上,它实际上就是一个惰性导入。所以,我待会儿会告诉你们它是如何工作的。但就是这样。这就是 lazy static。我对这个非常激动,因为我一直希望它是可能的,而且我做了些巧妙的处理让它成功。所以,现在我要向你们展示它在幕后是如何工作的。

嗯,实际上调试和弄清发生了什么非常容易。lazy_static 本身也就是 50 行 Python 代码,而且其中很大一部分,嗯,相当一部分是类型注解和空白。那么,让我们看看它是如何工作的。

首先,我们要看实际的 lazy 装饰器本身,然后从那里向上看。

这是一个泛型函数。它接受一个可调用对象,注解说明它接受一个可调用对象并返回一个值,该值的类型是这个可调用对象返回值的类型。

嗯,这只是装饰器部分,然后这是告诉类型检查器该怎么做的技巧。

现在,它在这里做的事情是,我们实际上要从它包装的那个函数构建一个惰性导入。所以,我们需要给那个惰性导入一个唯一的模块名。我们通过给它加上 lazy_static 前缀来实现。这就是我们这里的双下划线名称的作用。然后是那个函数的模块,以及那个函数的名字。所以,这只是给我们一个要使用的模块名,然后我们要把那个 lazy 装饰器变成一个真正的模块。

现在,构建实际的惰性导入类型相当困难。它确实存在于 types.LazyImportType 中。但你完全不能构造它。你甚至不能使用通常的技巧,当某些东西不可构造时,你可以通过 object.__new__ 把它们偷渡过去,但你确实无法构建这些。没有好的方法来构建它们,至少我没找到不用 C API 的好方法,但那看起来太作弊了。

所以,最简单的方法是实际使用 exec。我们将用 exec 来构建一个惰性导入对象。我们在这里要做的是,这里有 globals 字典。我们已经在这里写好了惰性导入的语法。然后我们要导入这个大写字母 V。这个稍后会出现。但这里就是我们要存放被求值函数值的地方。

我们执行这个。这会返回我们的惰性导入对象。这样做的好处是,它真的很容易调试。所以如果我们在这里放一个断点,然后运行我们的例子,嗯,你可以看到这是在求值……这,嗯,这是 3.15 当前版本的一个 bug。你可以看到 lazy 装饰器在 t.py 的模块作用域被求值了。我们已经创建了这个 G 字典。忽略其余部分。这些只是被注入进去的内置函数。但重要的是,我们创建了这个 V 对象,它是 lazy_static.t._____main_____ .computed_once.v 的一个惰性导入。所以,这有点像我们在这里创建的模块名。

嗯,我讨厌这个。我非常讨厌这个。愚蠢到我还必须确认这个。我讨厌它。我做了一个不同的视频来谈我有多讨厌它。嗯,不管怎样,现在我们有了那个惰性对象,这就是我们要从这个函数返回的东西,稍后允许 Python 运行时在解析全局名称时解析它。

接下来我们必须做的第二件事是,我们必须让这个模块可解析。所以,我们做了一个模块查找器。我说过没有 functools cache。我想这里确实有 functools cache,但我只是用它来插入一次,然后就不用再做了。

我们在做一个查找器。所以 Python 中的导入系统完全可以通过 sys.meta_path 和 PEP 451 的查找器和加载器来插拔,我为这些惰性模块做了一个自定义查找器。我们要把它放到 sys.meta_path 中。这就是这个东西在这里做的事情。然后我们要赋值一个函数。我们基本上存储了一个引用,指向我们之前包装的那个函数,这样当它真正需要被导入时,我们就可以调用它。

最后,我们返回那个惰性导入对象,然后让我们看看这个 LazyStaticFinder。这就是这些所谓高明的部分所在。

嗯,它非常直接。实际上只是有一个函数映射。这些是我们从装饰器捕获的函数。它既是一个元路径查找器,又是一个……哦,我滚动得不够远。该死。还是一个加载器。这个是找模块的,这个是执行它们的。通常它们是两个独立的类,但很容易把它们合在一起。

嗯,这是上面的查找器部分,这是下面的加载器部分。查找器所做的只是说,如果我们知道这个函数,我们就说:“是的,我们找到了。这是我们的模块规格说明。”否则,我们就说:“不,我们没有找到它。”然后让导入系统继续到其他查找器,并可能在别处解析这个导入。所以,这就是查找器部分的工作方式。

加载器部分有两部分。这个函数看起来非常蠢,但你返回 None 来使用默认行为,因为我们并没有真正定制模块创建。我们实际所做的只是把 V 属性附加到这个新创建的模块上,我们使用我们捕获的那个函数来做到这一点。仅此而已。就像,非常简单。

它实际上只是捕获函数,然后把它们推迟到模块创建的时候。

而且,是的,它确实会创建一堆占位模块来表示所有这些惰性延迟的函数,但谁在乎呢?我的意思是,它只是一个空模块,本质上是 sys.modules 里的空模块。

这就是它的工作原理。

嗯,你实际上可以看到,如果我们在这里放一个断点……这也是我非常喜欢这个工作的另一部分,那就是它全部是纯 Python。所有东西都非常容易单步调试。全局变量 computed_once。你可以看到这里……哦,什么?我们不是……哦,我忘了移除那个断点。

[笑声] 我忘了移除这个断点了。我还以为,什么?这不是我以为的地方。Globals computed_once。你可以看到这里,它就是我们之前看到的那个惰性导入对象。

如果我们访问 computed_once,它会把这个全局变量转换成实际的值,并运行那个函数。这只是惰性导入的工作方式。这是同样的功能,同样的机制。就是这样。我对于这个东西感到非常非常兴奋。而且我觉得……

[笑声] 当我能用上 Python 3.15 的时候,我想我可能会大量使用它。嗯,因为它让惰性导入用起来容易多了。它还有助于把很多非常非常昂贵的东西推迟到……嗯,你知道,稍后运行的时候。总之,希望你们觉得 lazy static 有意思。我对这个东西非常兴奋。嗯,是的,安装它,试试看,看看它对你怎么样。

嗯,是的。

如果你有额外想让我解释的东西,请在下面留言,或者通过各种平台联系我。但谢谢大家的观看,我们下次再见。

相似文章

Typst: Designing for Incrementality

Lobsters Hottest

Typst 通过约束记忆化(comemo)和纯函数设计,使语言和编译器协同工作,实现高效的增量编译和实时预览。文章详细介绍了布局缓存、模块评估记忆化、函数纯度以及内省系统的设计思路。

Haskell惰性求值不完全指南 (2015)

Lobsters Hottest

本文提供了关于Haskell中惰性求值的指南,解释了其机制、对模块化代码的好处,以及分析空间和时间使用的方法。

@AYi_AInotes: 做LLM生产落地的开发老哥们,可以看Andrew Ng刚出的这门课,免费版可以看所有视频和基础代码。 这个课程不是又一遍Attention is All You Need的数学推导, 也不是又一套调prompt的玄学技巧, 更不是又一个从…

X AI KOLs Timeline

Andrew Ng 推出了一门新的 LLM 生产落地课程,免费版可观看所有视频和基础代码。课程深入讲解 LLM 内部机制、推理优化(如量化、KV Cache、Flash Attention、投机解码)以及硬件感知优化,由 AMD 工程副总裁主讲,旨在帮助开发者将 Transformer 从学术概念转化为可调试、可优化的工程工具。