Python 的 block stack

网上关于 Python block stack 的文章少得可怜,中文文章更是基本搜不着。block stack 是 CPython 中的重要概念,但其实现又比较繁琐,因此我会先跳过细节,讲一讲为什么需要 block stack。之后会贴一些链接,里面有更细致的讲解。本文假定读者对 Python 的字节码有基本了解,且针对的是 Python 3.7 版本。

作为预备知识,我们知道 Python 的解释器是基于栈的。以 BINARY_ADD 举例,这个指令把栈顶元素和与栈顶相邻的元素出栈、相加,并把结果存到栈顶。这个栈的官方名称是 "value stack",但很多地方也将其称作 "evaluation stack" 或者 "data stack"。这里显示出 Python 在暴露实现细节时的混乱——文档中并正式定义这个术语,而它却又被在文档中广泛提及,并且是总是以 "stack" 这个含糊的名字出现(都想提个 bpo 去改进文档了 >_<)。

言归正传,为什么要强调 "value stack",是为了和解释器中的另外两个栈区分开:它们分别是 call stack 和 block stack。Call stack 大家比较熟悉这里就略过了。下面正式介绍 block stack,为了能更清晰地解释其来源,我们先看一个例子:

if a == 1:
  foo()
else:
  bar()

作为解释器,该如何实现 if 功能呢?很简单,只要知道分支所在的位置,根据 a == 1 的结果跳过去可以了,比如我们可以用行号表示位置:

if a == 1 is True, jump to line of "foo()"
if a == 1 is False, jump to line of "else:"

解释器有代码位置的信息,它知道 line of foo()line of else: 的值,因此完全可以把这两条规则直接写死。不论整个程序其它部分的代码有多复杂,这个规则都不会变。

再看这个例子:

for i in range(4):
  break
foo()

请问 break 应该如何实现?是否可以像 if 一样,直接跳到 line of foo() 呢?

答案是不行,因为 break 的跳转受到程序其它部分的影响。比如我们加一个 try...finally...

for i in range(4):
  try:
    break
  finally:
    bar()
foo()

这时程序的执行流程就变为了 break ⟶ bar() ⟶ foo()。可以看到,break 跳转是上下文相关的。如果解释器仍然想用 hard-code 的规则实现跳转,就需要对上下文进行全面分析,才能判断出跳转的位置。这会让解释器的实现变得复杂,性能开销也会很大。

为了解决这个问题,CPython 引入了 block stack。用一句话概括就是,CPython 使用 block stack 应对控制流和异常处理带来的跳转不确定性,它让解释器利用运行时信息和少量的固定规则即可实现正确跳转。我将解释 block stack 在这个例子中是如何发挥作用的,为了方便读者理解会将一些地方简化。

首先,解释器在遇到循环 for i in range(4) 会 push 一个 Block(LOOP, line of foo(), b_level) 的结构到 block stack。我们称这个结构为 block,因为它对应了一个代码块。block 包含的第一个重要信息是它的类型,此处为 LOOP,表示它是一个循环结构。第二个重要信息是下一个指令的位置,即“当代码退出 block 的时候,接下来要执行的代码在哪”,显然,这里是 line of foo()

对于 try...finally... 同理,Block(FINALLY, line of bar(), b_level) 会被 push 到 block stack。这时候的 block stack 如下图所示。

现在程序执行到了 break,解释器会根据当前执行的操作和栈顶 block 的类型判断它接下来要做什么。比如这里,当前执行的操作是 break,且 block 类型是 FINALLY,操作就是弹出栈顶元素 Block(FINALLY, line of bar(), b_level),并跳转到该 block 的下一个指令继续执行,也就是开始执行 bar

bar() 执行完之后来到 finally 的结尾,解释器发现当前 block 的类型是 LOOP,且当前执行的操作是 break(这里比较 tricky,break 会被 push 到 value stack 上,所以现在还是可以读取的。我们只要知道解释器仍然知道当前的操作是 break 即可),则同样弹出栈顶元素 Block(LOOP, line of foo(), b_level),并跳转到该 block 的下一个指令 line of foo() 继续执行。此时 block stack 已被清空。

这里面有非常多的细节,我们可以先不去深究。关键是理解一点,即有了 block stack 之后,解释器只需要根据运行时的信息(当前执行的指令)以及先前 push 到 block stack 上的元素的信息(类型、下一个指令的位置),加上一些规则,就能够实现正确跳转。除了上面提到的几个 statements,try...except, continue, with 的实现都涉及 block stack,原因也是显而易见的。

本文仅为介绍 block stack 的概念,有意忽略了很多细节。因此下面列出一些材料,希望做深入了解的读者可以前往阅读。

  • dis moduel
    Python 的 dis 模块。需要注意一下,Python 3.8 对 block 相关的 bytecode 做了较大改动,取消和新增了若干指令,但 block stack 的概念和作用没有变。

  • 《Inside The Python Virtual Machine》 Section 10. The Block Stack
    我学习 block stack 的主要阅读材料。顺带一提本书可能是被低估的 Python 书籍,没有之一。

  • byterun 项目
    用 < 1000 Python 行代码部分实现了一个 Python 解释器(主要是 evaluation loop 的部分)。建议首先阅读概述文章《A Python Interpreter Written in Python》,以便对字节码的工作方式和该项目有一个整体性的认识。之后可以阅读代码中和 block stack 相关的部分,基本上和 CPython 中对应的部分是等价的。

  • ceval.c
    CPython 的 evaluation loop 实现。请重点关注 fast_block_end,它包含了 stack unwinding 的代码。限于篇幅我们没有过多介绍 stack unwinding。这个概念来源于 C++,在 Python 里指的是跳出一个 block 时需要做的清理工作(包括从 block stack 出栈、跳转到下一个指令)。清理工作其实还有另一部分,就留作思考题吧:请说明 UNWIND_BLOCK 这个宏的含义。我个人花了好长时间才看懂,要怪就怪 b_level 这个变量名起得实在是太烂了。

相似的人物,各异(?)的故事

如果你读过一些漫画,可能会发现有些漫画家会执着于某种特定人设,比如安达充。这个现象在百合漫领域更为普遍。一个漫画家笔下的人物在性格、身高、发色、长相上总是呈现某种相似性,甚至连关系都经常直接照葫芦画瓢,导致看的时候常常串戏。因为觉得很有意思,所以打算聊一聊。

儿玉直子,黑发小恶魔角色

首先要说的是儿玉直子(コダマナオコ)老师。由于《捏造陷阱》的动画化,想必熟悉她的人更多了。捏造中的 水科萤 就是这类角色的代表——美丽、脆弱、捉摸不透、反复无常,并且是永远的黑长直。她们总是故事里激发冲突的一方,会闯入另一个浅发角色的世界,让另一个角色为之烦恼。比如《海猫庄days》的斋藤 ,《残光噪音》中的亚梨美 。这就是小玉老师写故事的基本架构,我甚至不需要重看就能概括,但她就是能用这种简单的架构让读者着迷。小玉老师并不是最会讲故事的,但她笔下的人物足以弥补故事的不足。

小玉老师并非每次都会安排这种角色,虽然死鱼眼是一定会出现的(笑)。

大沢やよい,土气短发 × 开朗型角色

大沢やよい 很钟情这种组合。大家最为熟知的应该是名作《2DK、G笔、闹钟。》中的 本渡枫 × 香月奈奈美,但其实远不止于此。《你好,忧郁少女!》 的 浅野湊 × 须川响生 《任性的Fuzz与闪亮小姐》 中的 花 × 望 《呛辣女孩》中的 店员小姐 × 一条 ,都是这个类型。其中开朗型角色往往是更为主动的一方,并且会或多或少地引导土气角色做出改变。

另一个大沢老师常画的题材是乐器少女,我猜测她多半有玩乐器类社团的经历。自从低音号火了之后,描绘吹奏部或是乐队的漫画明显多了起来。但我们不要忘了,百合和音乐自古就有很深的联系。什么你说没有?建议重看《NANA》。

不过,我最吃的还是《你好,忧郁少女!》中的 稻垣千华 × 有田咲子,这对从各种意义上都太棒了,而且独特到几乎很难在其它漫画中找到类似。现在评论区全是恳求老师多画她们的(笑)。

伊藤ハチ,身高差 + 年龄差 + 兽耳

伊藤ハチ 老师的画风最人畜无害,故事最清水,但某种意义上也是最糟糕的一位。伊藤老师和安达充很像,笔下的人物基本长的一样。问题在于,年下的这位不论从哪个角度看年龄都不会超过幼女的范围,这就让人不禁怀疑萌萌画风是不是为了掩盖什么。更糟糕的是,年龄差还不是一两岁的那种。给一张图你们自己体会吧。。。

森永みるく,黑色短发乖乖女 × 黄褐色长发伪现充 + 推倒好友

好了,按惯例我要讲到 森永みるく 老师。森永老师的漫画里,乖乖女角色的人设基本上不会变,但是 CP 的另一方就比较难概括。黄褐色长发没什么疑问,虽然其实这很大程度上是为了在黑白漫画中区分角色。“伪现充”是我能想到最接近的词,因为这一方从气质外形上很符合一般印象中的现充,但又不是真正的现充。至于故事,则是遵循“我喜欢上了我最好的朋友,我最好的朋友也喜欢我”这个公理展开。

森永老师对黑色短发乖乖女的执着超乎想象,我高度怀疑她在创作的时候是把自己带入这一方的。而且角色形象性格极为接近,比如 真理(《GIRL FRIENDS》)和 奈奈(《唇瓣 叹惜 樱色/奈奈与瞳》)基本就是同一个人把刘海换了个方向,然后把头发卷一下就是 花(《放学后的花和绯奈》)。

这是真理

这是奈奈

这是花

对了,我有提到老师还有个短篇吗?这是夏目

老师,够了啊!请多来点!

知乎为什么能成功

在我看来主要有三点

  • 中心化答案排序机制

  • 多层次,有效的传播机制

  • 早期极为优秀的运营

中心化答案排序机制

提到知乎,我们脑中首先出现的概念就是“问答网站”。但大部分人其实并不理解“问答”这个形式对知乎的重要性。问答不是另一种论坛,而是全新的玩法,是知乎的心脏。它一方面带给优秀答主正反馈,激励他们源源不断地输出;同时限制了另一部分人能获得的正反馈,缓解了社区的毒化

由于答案排序机制的存在,加上对算法持续不断的微调,知乎成功使他的用户相信,答案质量高 = 排名靠前。在刚混知乎没多久的时候,我写过一篇文章漫谈Quora,知乎和StackOverflow,里面描述了被点赞带来的那种成就感。这种成就感,相信非三零用户都体会过。如果不考虑机器人刷赞、恶意抱团踩答案、管理员删除&折叠答案等特殊情况,排序确实能在很大程度上反映答案质量。那些肚子里有货,能写出好答案(我们暂且不深究“好”的定义)的用户容易拿赞,容易排在上面,受此激励,他们更喜欢回答问题。这些答主源源不断地生产优质内容,构成了知乎的基础。

答案排序机制还有不那么为人所知另一面。咪蒙曾是公众号界呼风唤雨的女王,她在知乎却并不活跃。假设咪蒙拿出同样的劲头经营知乎,是否能取得一样的成绩呢?我认为不能。不光咪蒙不能,很多别的 KOL 也不能。为什么?这就涉及到答案排序的另外一个特性:它限制了那些观点不符合社区“主流审美”的用户。

首先我们必须搞清楚,知乎的主流审美是什么。虽然知乎上的话题范围很广,但观察那些排名靠前的答案和粉丝众多的答主,我们不难发现某种共性的存在。我把知乎社区的审美总结为四点:专业、理性、真实、幽默。限于篇幅,这里就不一一展开了。总之,知乎是不是“985遍地走”我不知道,但社区的整体审美确实是精英化的,毕竟这几点都非常符合大众对精英的印象。社区审美的建立并非一朝一日。知乎在早期首先通过邀请制和运营培养出了萌芽,之后随着社区氛围的确立,越来越多喜好相似的用户加入进来。当然,我们不能把知乎这家公司、知乎用户同知乎社区混为一谈。我这里并没有说知乎公司,或者知乎用户如何,而只是说,社区形成了这种文化。

说回咪蒙。咪蒙粉丝众多,只要一出手就是 10w+。这在微信里玩得转,在知乎就行不通了。想当知乎大V,必然要参与一些全站热点话题的讨论,并至少拿下若干个高票答案。这种情况下,一个答案会被知乎用户集体审阅。一旦用户达到一定规模,他们的赞和踩在统计上总是会呈现之前所说的倾向。尤其是知乎中“踩”的权重很大,经常能看到一些高赞答案排名靠后,多半就是被很多人踩的缘故。因此,你能讨好多少粉丝不重要,关键在于不可惹怒大众。你得能写出符合社区主流审美的答案,而这一点咪蒙就做不到。实际上,咪蒙的文章恰恰是反着来的:不专业,情绪化,编故事,不幽默。她的文章或许读起来很爽,在小圈子内极易引起共鸣。但若是放在一个更加大众化的社区里,这类文章只会带来无休止的口水和战争,并且会分散用户对更有价值话题的关注。然而这一切并没有发生,因为答案排名机制限制了小圈子的存在,使得成百上千个潜在的咪蒙要么离开知乎,要么在知乎默默无闻。当然,我们不能说所有不符合社区主流审美的答主都是毒瘤,这显然过于武断。但我认为这些用户无法获得足够的影响力,的确延缓了知乎的毒化。事实上,即使知乎落到今天这般田地,我认为社区也始终保持着极强的自净能力——衰落更多的是由大环境导致,这里我们就不展开了。

总结起来,知乎的问答 + 排序机制,即是红细胞,又是白细胞。它一方面给社区带来了优质的内容,另一方面有效阻止了社区的毒化。知乎并不是一个恰好成功了的问答网站,问答正是它成功的核心。

多层次,有效的传播机制

NND 打了一大段 Typora 给我退出了。反正知乎能用很多方法让你看到一个答案就对了。

这里还想从答案曝光度的角度说一句。现在很多论坛,比如虎扑,也提供了某种帖子排名机制。虎扑中,被“亮”(基本等同于赞)得越多的帖子排名越靠前。然而,一个高赞答案的曝光度远比一个虎扑亮帖要多,因为在知乎,每一次点赞都是一次传播——一个万粉大V的点赞会把这个答案发到其关注者的时间线上,往往能间接带来几十几百个点赞。这种滚雪球般效应给优秀答案带来了可怕的曝光量,传统论坛无论如何也模仿不来。排名 + 传播,给优秀答主的正反馈是非常足的,也在一定程度上导致了知乎的马太效应。不过不要紧,虽然开头比较难,只要你有一个答案火了,一般能涨到千粉。随后不管你写什么,至少传播上的阻碍已经没有了。因此知乎虽然不会专门推荐新人,但新人只要有能力,起步并不算难。归根到底还是看一个人能不能输出符合知乎社区喜好的答案,这就又回到前一节说的审美问题了。

早期极为优秀的运营

虽然我并不是早期用户,但对知乎在起步阶段的运营也有所耳闻。优秀的运营带来的影响是非常深远的,像之前说的社区主流审美,想想也知道绝不可能自然形成。我们暂且不把知乎和更娱乐化的平台比如微博抖音对比,就说在很多人心目中高大上的 Quora。Quora 的运营在我看来不如知乎。一个很简单的观察是,Quora 上极少极少见到长答案——这里说的长,是指那种占几屏,几乎像一篇小论文一样的答案。长答案就一定优秀吗?未必。但长答案包含的内容往往更加丰富,更关键的是,写一个长答案付出的心力可能是短答案的几十倍。对于快节奏下的现代人来说,发微博写 tweet 才是符合人性的,花几个小时查资料写大段文字是反人性的。而知乎能够做到在 2011 年起步,花不长的时间就打造了一个大家乐于写长答案的社区,运营绝对居功至伟。实际上,我压根想不到除了知乎外,还有哪一个社区能看到如此多大段的文字。额,或许只有 Medium?然而 Medium 本身就是博客平台。

关于知乎的成功大概就说这么多。最后还是想感叹一句,纵然知乎已如此优秀,在时代的洪流面前,依然显得那么不堪一击。这就和人一样,能决定自己命运的,往往不是自己。

P. S. 前段时间写了一个项目,欢迎尝试。


top