天堂的另一面

英国“迷幻”摇滚 Glass Animals 发布了 Zaba 之后,开始了非常激进的巡回演唱会,一共办了将近 140 多场。在演唱会的旅行中,他们从不同的人那里听到了一些故事,有些甚至是亲身经历的。GA 将这些故事写成了新的专辑,叫做《如何做人》(How to Be a Human Being) 。

这张专辑一共 11 首歌,专辑的封面上有 11 个人,每一首歌的背后都是一个故事,虽然不是每一个故事都对剧情讲的很清晰,但是表达的感情却很强烈,悲伤,后悔,愤怒……

我最喜欢的一首是《天堂的另一面》( The Other Side Of Paradise ). 这首歌的故事我觉得讲的最清楚,具有非常强烈的愤怒和无力感。

这首歌以女生的口吻讲述,将故事的开头作为音乐的开头,心爱之人为了梦想背井离乡,并用非常安心的口吻安稳自己。

When I was young and stupid my love
Left to be a rock and roll star
He told me please don’t worry
Wise little smile that spoke so safely

男孩买了一张单程票去了西方(应该是加州),那个地方会让挤在一间屋子的 6 个人,有机会成为百万富翁。

He booked a one way ticket
Out west that’s where they make it
Six kids stuck in a bedsit
To sunswept poolside riches

然后有一天,他遇到了范思哲女孩,穿戴奢侈,珠光宝气,他也有机会成为明星了。

He met a girl who wore Versace
Pink feather coats and jumbo jewellery
Gonna be a hoop phenomenon
He’s gonna be Hakeem Olajuwan

然后是第二段 Verse,他在电话中告诉她,他终于成功了,每天都能都能赚很多钱。但是在女孩这里,却像电影里的慢镜头一样。

He’s got a gold Camaro
He said over the payphone
I try to keep my cool but
My life turns in slow motion

接下来就是 Chorus 了。大段的内容描述了女生内心的活动,对急速成功的渴望让他越来越远,他被欲望杀掉了,他成了 Ghost。

Bye bye baby blue
I wish you could see the wicked truth
Caught up in a rush it’s killing you
Screaming at the sun you blow into
Curled up in a grip when we were us
Fingers in a fist like you might run
I settle for a ghost I never knew
Superparadise I held on to
But I settle for a ghost

然后是第三段 Verse,依然是心里活动。这一段对后来的剧情至关重要。在“我出生的”新奥尔良,没有人会为了成为明星背井离乡,男人们会 stay and treat his lady, Give everything to his new baby

When I was from n.o.l.a no one
Left to be a rock and roll star
He’d stay and treat his lady
Give everything to his new baby

然后是 Bridge 的部分,她也变了一个人,变了自己的态度,她要用自己的愤怒毁灭他。但是这一段也开始变得模糊了,歌词中有枪,down,shake这些字眼,愤怒无疑是来自女生,但是也说不清是开枪打了谁,不知道歌词中的 Girl 指的是她,还是范思哲女孩,不知道她杀了别人,还是自杀。只能从歌词中明确的知道,她变得麻木了,“My body’s looking wrong”…

I know you don’t but I
I know you don’t but I still try
My thunder shook him down
My thunder came and shook him down
That girl is gone but I
That girl is gone but I still try
I think it’s over now
The bullet hit but maybe not
I feel so fucking numb
It hits my head and I feel numb
My body’s looking wrong

据说这个故事来自于一个真实的篮球运动员,搬到美国之后真实发生的。

 

P.S. 因为 Pork Soda 里面有一句 “Pineapples are in my head”,所以很多乐迷在演唱会现场会带菠萝,演唱会结束后让现场难以打扫。后来演唱会将烟花,武器,菠萝列为了违禁品

 

Use the Index, Luke! 笔记3:避免回表

你一定听说过一种加速查询的技术,使得只使用索引就可以得到查询的内容,而“避免回表”,至少应该在别人的面试经验中看过,或者自己面试中被问到过吧。这篇文章是 use the index luke! 中 clusting data 这一章节的笔记,解释了这样做的原理,实践。

Data Cluster

Data Cluster 的意思是将连续的,相关的数据尽量存储在一起,这样查找的时候 IO 的成本更低,因为顺序读比随机读速度快,磁头移动少。我们的索引也有这样的功能,帮我们安排数据的顺序,让读的速度更快。

我们通过索引可以达到这一目的。之前,我们介绍过通过执行计划的 filter 可以推测出哪些索引创建的不合理,因为 filter 是一个遍历操作,速度很慢,access 的速度很快。其实,filter 的信息信息很有用,还能帮助我们将数据聚集,来加速查询。

索引的第一大能力是 B-Tree 比那里,第二大能力就是数据聚集。

比如下面这个查询:

因为 LIKE 条件是以 % 开头的,所以无论是对 last_name 建立索引还是对 UPPER(last_name) 建立索引,查询都不会用到。

所以,这个查询的瓶颈在于,按照 subsidiary_id 过滤之后,找到符合 LIKE 条件的 row。这一步的流程是这样:取出一个 row,判断它的属性是否满足条件,然后再去取出下一个 row,再判断。对于这样的操作,如果 row 分散在不同的 block 上的话,一个一个取出来读的话,性能是很差的。如果这些 row 能尽量在同一个 block 上,或者相邻的 block 上,读起来就会快很多了。

有一种方式是将 row 按照索引查找的顺序排序,这样就可以将 filter 的这个操作从随机读,变成顺序读了。但是这样会影响整个表的顺序,只能根据一个索引优化,如果出现另一个查询,就无法根据另一个查询重新设定 row 的顺序了。并且数据库提供的这种 “row sequencing” 的技术和工具都颇为古老,很少使用。

更实用的一个解决方案是将 last_name 也放到索引中。这样就相当于将要查找的 column 放到了一起,我们遍历索引的时候,直接可以得到 last_name 这一利,不需要根据索引读出来 row 再进行判断。这时候索引中 last_name 这个数据不再是提供遍历树的索引功能,而是提供查询功能。

在执行计划中,可以看到 filter 这一步的成本显著降低。因为我们不需要去读表中的数据了,在索引的数据中就可以完成 access,判断是否符合 LIKE 这两步操作。

Covering Index

如果一个索引,能使一次查询避免 table access,那么这个索引就叫做 covering index.

比如我们建立下面这个索引:

那么这个查询就不需要 table access:

因为索引中已经有一份 eur_value 的 copy 了。Oracle 中的执行计划如下:

执行计划中的 INDEX ONLY SCAN 极大地提高了性能,执行计划说一共要聚合 40388 行,假如不是 INDEX ONLY SCAN 的话,这意味着数据库要读 40388(最多,如果这些 row 都没有在同一个 block 的话)次。

INDEX ONLY SCAN 是一种非常激进的优化策略,潜在的增加了数据的维护成本。比如会降低 update 操作的速度。使用 INDEX ONLY SCAN 需要三思。

另外,INDEX ONLY SCAN 也会带来潜在的性能问题。比如下面这个查询:

假如一个同事写了这样的查询,认为这个查询查到的 row 更少,理应更快才是。

但实际上却不是的,这个查询增加了一个不在索引中的 row,原来不需要 table access 的,现在需要了。所以查询的速度会慢很多。

Oracle 的查询计划:

在无法使用 INDEX ONLY SCAN 的时候,优化器会选择第二优的执行计划。在这个例子中,选择了 SALE_DATE 这个索引。这里的选择原因有2:1)通过 sale_date 过滤出来的 row 更少,执行计划显示是 ~10000 行,而上面的 subsidiary_id 有 ~40000 行。2)sale_date 这个索引,数据更加聚集。因为订单创建的顺序和时间是相关的,都是 append 在最后面。这可以可以看出,有些索引“自然”具有 cluster 的性质。

这个例子中使用 sale_date 这个索引正好是一个巧合,虽然它让 INDEX ONLY SCAN 失效了,但是使用它的这个索引,聚集效果也是很好的。但是通常,给 select 添加查询条件会导致聚集很差的查询路径,所以记得用 comment 或者其他的形式,来提醒自己这里的查询是一个 INDEX ONLY SCAN。避免后来添加查询条件导致性能急剧下降。

另外要格外注意,很多数据库多索引大小是有限制的。比如 Postgres 数据库,从 9.2 开始支持 index-only scans,B-Tree Entry 最大为 2137 bytes(硬编码)。如果插入或更新的时候超过了这个大小,会得到错误 “index row size … exceeds btree maximum, 2713” ,B-Tree 索引最多有 32 列。

Clustered Index

由上面一小节自然而然就会想到:那我们可以将整个表都保存在索引中吗?这样都不用回表查询了。答案是可以的。Oracle 将这种所有的数据都在索引中的形式,叫做 index-organized tables (IOT),名字很好的反应了它表达的意思,即表是完全按照索引组织的。另一些数据库将这个术语叫做 clustered index. 其实他们都是一回事。

简单说,clustered index 就是 B-Tree 索引中保存了所有数据,没有 heap table。

这样有两个好处:

  1. 省掉了表的空间,即 heap 的空间,因为所有的数据都在索引中;
  2. 每次查询都不需要回表操作,所有的查询都是 index-only scan;

坏处也显而易见:

因为 row 都是按照索引来组织的,所以 row 的顺序(物理位置)无法固定。正常情况下,我们在 heap table 中保存 row 的时候,我们随便找到一个空的地方保存下来就好了,然后在索引中更新 ROWID 的位置。这个 row 一旦保存好了,它的物理位置永远不会变了,我们只会更新索引中这个 ROWID 的位置。

然而这在 clusted index 中是无法做到的,因为 row 的顺序是按照索引来组织的,一个 row 的位置无法固定。一个 row 即使没有更新它,添加新的 row 的时候,它的位置也可能受到影响,跟着索引移动。

这是根本问题,导致的表面问题是什么呢?

假如我们现在有第二个查询语句,不是根据主索引来的(secondary index),按照有 heap table 的情况下,我们是这么查询的:先在 secondary index 中找到 ROWID 的位置,然后去 heap table 中找到这个 ROW。

但是在 clustered index 中,我们没有 ROWID,只有主索引,所以必须在 secondary index 中找到主索引,然后再去主索引中查找这个 row 的位置。

clustered index 中的这个查询(文中第二个)比 heap table 查询要慢很多,因为第一个查询是 INDEX RANGE SCAN + TABLE ACCESS 的操作,第二个操作需要经过两个索引,INDEX RANGE SCAN + INDEX UNIQUE SCAN 的操作。

当然,对于这个查询,其实可以针对 secondary index 再建立一个 index-only scan,这样针对这个查询又避免了回表操作。比起上面两个查询都是最优的。

总的来讲,clustering index 并不实用,clustering index 的 key 也比 ROWID 要长很多,所以占用更多的空间,适用场景非常有限。

如果 table 能确定,查询总是按照一个顺序的,绝对不会需要 secondary index 的话,可以考虑使用这种索引。

另外,不同的数据库对 index-organized tables 的支持差异很多。PG 不支持没有 heap table,但是可以用 CLUSTER 来将整个 table 放到 index 中。

 

Java 泛型初涉

这是一篇初学者对 Java 泛型的理解,如果不对欢迎指出。

Java 中的泛型,在编译时期提供类型检查。在运行时期,为了让泛型零开销,泛型都被擦除。擦除的方式

  1. 用 bound 替换泛型参数,如果泛型参数是 unbound 的,直接用 Object 类型替换。所以生成的字节码中,只包含原始的类,接口,和方法;
  2. 在必要的情况下生成类型的强制转换,来做到类型安全;
  3. Generate bridge methods to preserve polymorphism in extended generic types. (这句没看懂)

这种类型擦除的方式保证了没有新的 class 生成,使用泛型不会增加程序的开销。

在读了很多资料之后,我发现自己对泛型的误解主要是基于从 Python 来的印象。比如在 Animals 的一个 list 中,里面即可以有 Dog 又可以有 Cat,所以就以为在 Java 中 ArrayList<? extends Animal> 即可以有 Dog 又可以有 Cat,认为这样声明即表示这个 ArrayList 可以放任何 Animal 的子类。其实不是的,<? extends Animal> 是一个类型声明,最终只会表示一种类型。即这个 ArrayList 即可以是一个 DogArrayList,也可以是一个 CatArrayList,但不能有 Cat 又有 Dog

基于此,Bound 其实是比较好理解的。

Upper Bound

顾名思义,就是类型的上界被确定了。

在这个例子中,上界是 Animal,所以 ArrayList<? extends Animal> 这个 ArrayList 就表示一种 AnimalList。可能是 Cat,也可能是 Dog。 ArrayList<? extends Animal>ArrayList<Dog> 的子类,所以这个赋值可以成功。

对于写,因为 ArrayList 里面可以是任何 Animal 的子类,所以无法写入。不能 .add(new Dog()) 也不能 .add(new Animal()).

对于读,读出来的都是 bound 的类型,即 Animal

Lower Bound

确定的是类型的下界,即允许这个类型的任何父类。

下界是 Cat,即这个 ArrayList 允许任何 Cat 的父类。所以这个 ArrayList 可以是 AnimalArrayList,也可以是 Cat 的,也可以是 Object 的。

对于写,是允许的,因为下界是 Cat 了,那么任何 Cat 或者 Cat 的子类,都可以被转换成 <? super Cat>,所以允许写入 CatCat 的子类。

对于读,因为不知道 ArrayList 的类型可能是什么,所以读出来的都是 Object,即最上面的 Bound。

这段代码实际被编译器抹去泛型的字节码反编译如下:

本质上,Java 的泛型是通过编译器来实现的,编译器将我们写的有泛型的代码转换成没有泛型的代码。但是这个转换只是推导,增加类型转换,而不会生成新的类,也不会生成新的代码。在运行时不会有泛型的信息,没有额外的开销。

泛型是类型未确定,实际运行的时候,还是会确定到某一种类型上,时刻记住这是一门静态的语言。是否允许写入,读出来是什么类型这些问题,基于“保证”类型安全这个角度理解,就比较简单了。

更多阅读:

  1. Generics in the Java Programming Language
  2. Can’t add a ModuleInfo object to ArrayList<? extends ModuleInfo>
  3. Java generics type erasure: when and what happens?

感谢 messense 发我的资料和耐心解答。以上是个人理解,如果有误那肯定是我理解不到位。理解没有问题的话就是老师教得好。

 

Redis RESP3 的一些想法

在 Python 中使用 Redis,基本上都会选择 redis-py 这个库。本质上,它是封装了 Redis 命令,将它们变成 Python 的函数。比如 SET 这个命令,它的使用方式,在 redis 文档中是这么定义的:

然后 redis-py 对应的 Python 函数是这样的:

redis-py 中有几百个函数,封装了对应的 redis 命令。

这样的好处是你可以使用原生的函数来调用 Redis,返回结果也是 Python 的原生类型。在写代码的时候编辑器可以自动为你补全 Python 函数,调用的什么 Redis 命令也比较清楚,对 Python 代码静态分析就知道对 Redis 做了什么操作。

缺点也显而易见:

  • 如果 Redis 对已有命令支持了新的参数,需要 redis-py 支持才能使用。比如 让 HSET 支持多对 key value给 SET 添加 KEEPTTL 参数,这两个PR。合并之后要等 redis-py 发布新的版本,到应用能真的用的上 redis 的 Feature,将会是很漫长的一个过程;
  • 第二个问题是从 Redis 的命令,到 Python 的函数,相当于加了一层中间的转换。首先我要去看 Redis 的文档,知道了 SET 的用法,然后我还要去看 redis-py 的文档,去学习一下在 Python 中是如何抽象的,哪些是值是字面值,哪些值翻译成了 Python 的 bool;

在群里看到过网友这么吐槽,对这个问题总结的比较精辟:

所以,这一层抽象真的有必要吗?Redis 的命令如此简单,我们能否直接给 Redis 发送想要执行的命令呢?

我一直在想, 理想的 Redis Python 客户端应该是这样使用的:

而接口的定义,显然可以只需要一个,能够通过这个接口接收任何命令:

以目前的 RESP,实现起来还是有点难度的。我在 IRedis 中做了类似的事情,先分享一下 IRedis 里面是怎么做的,阻止我们实现一个这样的客户端的问题是什么。

IRedis 是一个 redis-cli 的替代品,一个命令行 REPL 工具。在 IRedis 中,如果使用 redis-py 这些函数的话,那么一个命令从用户输入,到真正到达 Redis server,要经过这样的转换:cli input -> 分解出来命令,和这些命令的参数 -> 调用相应的 redis-py 里面的函数 -> redis-py 打包成 RESP 协议的请求 -> 发送给 Redis Server。显然太复杂了。并且我认为上面的 问题2 对 IRedis 也是一个不小的限制:使用 IRedis 应该体验到 Redis 最新的 Feature,而不是等到 redis-py 发布之后。

redis-py 中 的 connection.py 是封装了 RESP 协议的内容的,这部分代码解耦的比较好,所以目前 IRedis 只依赖了打包和解析 Redis RESP 协议的部分代码。在 IRedis 中向 Redis 发送命令的代码是这样的(简化了重试之类的细节代码):

这样如果 Redis 支持了新的命令,IRedis 用户不需要升级 redis-py,甚至不需要升级 IRedis 本身,就可以直接使用新的命令。

So far, so good. 但是有一个我没提到的缺点是,RESP2(redis-py 是屏蔽了 RESP 协议的,如果不熟悉当前 RESP 的话,可以读一下这篇文章,解释的比较好:理解 Redis 的 RESP 协议)的类型太少,比如 LRANGE SMEMBERS HGETALL 三个命令,返回的都是 Array(术语中叫做 multi bulk reply),但其实这三个命令返回的结果分别是:list, set, dict。redis-py 里面是对这些命令的结果做了转换。所以,现在的 RESP 中是无法区分出返回的类型的,这就要求 Redis 的客户端必须记住每一个命令的返回类型。因此,redis-py 在当前的 RESP 协议中,对每一个命令封装是最合理的做法。

IRedis 中我也有同样的问题,所以我用一个 csv 文件整理了所有的命令返回的类型,以针对不同的类型做不同的渲染(redis-cli中并没有区分出类型,所以用户看到的都是 Array):

在 IRedis 中将不同类型分开解析了

redis-cli 中全部当成 list 显示

另外,当前的 RESP 协议也缺乏很多其他类型,比如浮点数是没有的,要用 string 来返回,boolean 也没有,要用 int 来返回。比如在写 Lua 代码的时候,因为 Redis 协议的本身并没有 Boolean 类型,如果我们要在 Lua 脚本中判断 True/False 的话,必须通过字符串相等来替代。比如这个PR

除此以外,当前的 RESP 还有其他一些限制。但是我觉得最严重的就是混用了一些类型,导致开发一个语言的客户端必须一个一个地按照 Redis 的文档来实现它的命令。

好在,这个问题在下一代的 Redis 协议 RESP3 里面彻底解决了。RESP3 有丰富的类型

  • Null: a single null value replacing RESP v2 *-1 and $-1 null values.
  • Double: a floating point number
  • Boolean: true or false
  • Blob error: binary safe error code and message.
  • Verbatim string: a binary safe string that should be displayed to humans without any escaping or filtering. For instance the output of LATENCY DOCTOR in Redis.
  • Map: an ordered collection of key-value pairs. Keys and values can be any other RESP3 type.
  • Set: an unordered collection of N other types.
  • Attribute: Like the Map type, but the client should keep reading the reply ignoring the attribute type, and return it to the client as additional information.
  • Push: Out of band data. The format is like the Array type, but the client should just check the first string element, stating the type of the out of band data, a call a callback if there is one registered for this specific type of push information. Push types are not related to replies, since they are information that the server may push at any time in the connection, so the client should keep reading if it is reading the reply of a command.
  • Hello: Like the Map type, but is sent only when the connection between the client and the server is established, in order to welcome the client with different information like the name of the server, its version, and so forth.
  • Big number: a large number non representable by the Number type

这样,我们可以实现这样一个客户端,完全不关心执行的命令,不需要对特定的命令进行支持(一些影响客户端行为比如 CLIENT REPLY OFF 依然需要特殊支持),就可以与 Redis Server 交互,无论 Redis 对已有命令做出了修改,或者支持了新的 Feature 新的命令,客户端都不需要做升级,应用就能直接使用。

我尝试写一个基于 RESP3 的这样的客户端,如果有兴趣可以在这里 follow:https://github.com/laixintao/resp3-py

RESP3 并没有采用基于现有数据打包结构(BSON 或者 msgpack)的方式实现,简单来说原因是 1)这些只是数据结构,依然要实现通讯层的东西,没有脱离要“设计一个通讯协议”的问题。2)引入了复杂性,用户本质上要和两个库相处 3)这些数据结构不是基于流处理的,比如 json,你要读完(至少读一定程度)才能开始处理,所以需要某种 buffer,可能是潜在的性能瓶颈。而 RESP3 是在 TCP 上的一种简单的协议,在协议上发送大的字符串的时候不需要全部读完,客户端就可以开始处理。

往远处想,RESP3 的意义不仅仅是适用于 Redis 的协议,甚至不局限于数据库,基于 RESP3 实现 RPC 也是完全可以的。

在 Unix 的哲学中,文本是优于二进制的,RESP3 中说道:

RESP 在过去的几年中工作的非常好。这说明,如果用心设计,那么一种简单的、用户可读的协议不会成为通信的性能瓶颈,而且这种对用户阅读友好的协议对客户端生态的建设大有好处。

最后,RESP3 是不兼容 2 的,而 Redis6 只支持 RESP3,这意味从 Redis5 迁移到 Redis6 可能要麻烦一些。这个设计选择可以看 Antirez 的博客:Why RESP3 will be the only protocol supported by Redis 6

RESP3 的 spec: RESP3

 

请不要再使用 __file__ 啦!

一般来说,Python 的 module 会有一个 __file__ 属性,定义了 module 的 path。在 Python 中,使用这个属性非常常见,比如获取 module 所在的目录地址,以便于读取这个 module 同级的,非 Python 脚本的其他文件,比如库需要依赖的数据等:module_dir = os.path.abspath(os.path.dirname(__file__)) ;或者用来获取脚本的位置,来进行魔法 import 操作。

但严格来说 __file__ 不总是有的。Python 文档中说:

__file__ 当 module 是从文件中加载的,才会有。如果是静态编译的 C Module,就不会有这个 __file__,extension module 中如果动态链接了 shared lib,那么 __file__ 的值就是动态链接库的位置。

另外,如果你的包是在一个 zip 里面的,__file__ 也没有。

所以,所有使用了 __file__ 的 Python 代码:

  • 要么是 broken 的;
  • 要么选择了不会支持其他的 module loader(不从文件系统 load module)

但是我们经常有读文件的需求,比如图片、json、csv 等,不用 __file__ 的话,怎么知道它们在哪里呢?

如果 Python 版本 < 3.7,解决方案会有点复杂。因为 3.7 之前一直没有一个完美的方案来读取文件资源。有一段时间 pkg_resources 是推荐的方式,Python3 后来引入了 ResourceLoader 接口,然后在 3.7 又 deprecated 了这个接口,并引入了 ResourceReader ,提供了对应的 API importlib.resources module 。即使最近的 ResourceReader,也有一些行为的不一致。但是到目前为止,应该是这些 API 中最好的选择了。

如果你使用 Python3.7+ 的话,可以直接使用内置的 importlib。如果是使用 3.5+ 的 Python 的,建议使用 importlib_resources,这个库是 importlib.resources 的向后兼容版本(实际上,这个库推荐 3.9 之前都使用这个库,3.9 使用标准库)。如果是用 3.5 以下的 Python 的话,应该尽快升级 Python 的版本。

当然了,一些小的脚本,可以不考虑兼容性直接使用 __file__ 的。

可能有人认为 Python 总是用脚本来跑的,可以依赖文件系统的路径,没有什么大不了的。但是我觉得现在 Python 的应用场景越来越多,Python 代码应该考虑运行在各种环境,最好不要对这些环境做一些“假设”。

这些想法起源与最近把 IRedis 打包成一个二进制来跑,希望用户在使用 IRedis 的时候,只要用 cURL 下载下来,然后解压,就可以运行了,不用装 pip,不用使用 pip 安装,甚至不装 Python 或者发行版的 Python 不支持 IRedis 都没问题,因为 IRedis 运行所需要的所有东西都已经包装在这个二进制里面了。在某些情况下是非常有用的,比如说 Redis 的 Docker 镜像,里面是没有 Python 的。

另外打包成 binary 体积也更小,现在用 docker 下载 Python 的 image 将近 1G 了,而打包好的 iredis.tar.gz 才 22M(包含Python解释器)。

这项打包工作是 Mac Chaffee 完成的,他写的打包脚本非常精彩,有兴趣的可以看一下这项工作的记录,很有意思:

Anyway,我们遇到的问题是,很多包都依赖了 __file__ ,去 patch 这些 package 几乎是不可能的:

所以就采用了一个这种的方案,将这些依赖放到一个 lib/ 下,依然让它们作为文件系统的文件存在。虽然这样让打包出来的东西并不是一个纯正的 binary,解压之后会有一个 iredis binary,还有一个 lib/ 目录,但综合考虑可能是性价比的方案了。

如果你正在开发的是一个 lib 的话,强烈建议不要在代码中依赖 __file__ 了。为 Python 更广阔的应用场景做一份贡献!