MoreRSS

site iconFrost Ming修改

Python 开发工程师,pdm 作者,坐标深圳。平时喜欢折腾技术,写写技术文章。也是个开源爱好者。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

Frost Ming的 RSS 预览

被嫌弃的松子的一生

2025-12-02 08:00:00

题目是一部 2006 年的日本电影。主人公松子从小缺乏家人的爱,长期被忽视,因此形成了极度渴望被关注、被需要的性格。进入社会后,她为了获得爱而不断付出、不断迎合,结果却屡次被男人、家庭、工作、社会抛弃。为了别人而活的她,始终无法为自己活。

最近父母回老家参加表弟的婚礼,给我带回了一个松子的故事。我表弟小我两岁,从小就很纨绔和刁蛮,甚至从来不跟长辈说话,即便是在春节期间,用给 100 块的方式交易,也无济于事。成年以后似乎结识了一些朋友,倒是不再沉默寡言,但却变得满嘴社会嗑,生殖器不离口。做过一些工作,但大部分时间都在啃老。他向他妈我大姨要钱那是理直气壮,甚至不给就甩脸色看。就这样,他能每年换新 iPhone,这是我这个大城市牛马都难以达成的。

啃老就啃老吧,对社会没有危害就好了。年初听说他订了一门亲,女方是县医院的护士,圆圆脸蛋长得挺乖巧,就叫她玲子吧,我也只见过一次,给我的印象唯唯诺诺的,也不爱聊天。我只是奇怪他结识的那些社会朋友中怎么会有这一挂的,一问果然是相亲。我一开始就觉得这姑娘眼瞎,但没想到这么瞎。听说他们频繁吵架,表弟总是以非常恶毒的话语攻击,有次甚至把衣物全部丢出门外让她滚。我姨父也是直男癌晚期大男子主义毒瘤,表弟跟他是一个模子刻出来的,家里只有婆婆(我大姨)对她比较好。可想而知她面对的是什么家庭环境,于是有好几次都跑回了娘家。更令我震惊的事实是,她是收养的,我不知道养父母对她如何,但我估计也不怎么样,要么就是没有向父母诉苦,因为我不觉得有哪个正常父母听到这个能不提刀上门的。

我虽然跟表弟才是一家人,但我和父母都觉得他深受上世纪大男子主义遗毒,性格顽劣,根本不配结婚。玲子早前曾怀过孕,可惜着床位置不好打掉了。姨父和表弟知道后,对我大姨是一顿输出,怪她找了个不祥之人。但玲子仍非常积极备孕,总觉得有了孩子才有依靠,可是要让天天烟酒不离身的表弟备孕,那是完全不可能的。

这集齐了家暴、重男轻女两大毒瘤于一身的故事,就发生在我身边,我听完只觉胸中女拳之火熊熊燃烧,想告诉玲子,醒醒吧,生孩子不会好起来,好起来的办法只有一个,就是离开,立刻、马上。可惜我无能为力帮她改变命运,只能把它写下来,写在这里,告诉大家这个千千万万个一样的县城中的平凡的故事。原谅我没有第一手资料,只能用很多「我听说」这样没有力量的词语,写得乱七八糟。

电影的结尾,松子已下决心重新开始好好生活,却在回家的路上被几个熊孩子误会与嘲弄,最终丧命在河边草地。所幸我们的故事中,悲剧还没发生,但愿不会发生。

PyCome

2025-09-22 08:00:00

标题 Typo 致敬某粗鄙的大佬。

0x00

第二次来上海参加 PyCon China,也是我的第七个 PyCon,这次更多是 PyCon for friends。

最激动的是要第一次见到 yihong,当我得知他和我差不多时间到浦东,我就决定等他,哪怕他说下飞机要先拉屎。

结果这小子没同步消息,理解错误,屎没拉也不说一声,把我晾在那,好在等待市域铁发车让我堵到了他。他上来就给了一个拥抱,还把他刚看完的《多情剑客无情剑》送了我,然而是(下),(上)(中)都没有,离谱。

0x01

路上商量下午的节目,yihong 先是提议去看电影。我基于以下 3 点提出了反对:

  1. 一群大老爷们去看电影太怪了。
  2. 临近国庆档,没有好电影。
  3. 看电影聊不了天,太浪费了。

我说难道不是去咖啡馆集体 Coding 吗?我记得和程序员聚会都是这流程,就像过年回老家的固定节目是和同学连坐打 Dota 一样。

yihong 表示,连坐?什么连坐?卧槽咱可以去连坐!

大哥,你是不是搞错什么重点了。

事后才发现,我带的电脑,自始至终都没拿出来过。

0x02

和 jay 哥 piglei 接上了头,piglei 游戏上瘾,一听提议打 Dota 坚决支持,看来此事已不可逆转。

先去吃个饭,捡上了高先生、Alex 和马牛。老广高先生给我印象比网上好很多,难怪是妇女之友。

接着四个老年玩家就真去了网吧,等会我记得我们是来开会的是吧?您瞅瞅这是人干的事吗?大哥意识还是在线的,piglei 真是屠夫王,我对 Alex 寄予厚望,毕竟是专业在家打机的,结果最后还是 0-4 收场。

下次得抢白牛。

0x03

正打着游戏呢就被催去吃饭,我想起了在网吧还被家长揪的情景。马上马上,就打完这波。

晚上就是 筹划已久 的伊大。阵仗着实不小,我见到了许多网上只知道 ID 的朋友,盐粒带来了 Homebrew,真他妈好喝,我用自己杯子留了一杯给后来到场的老婆喝,大家一面敬仰,一面都主动去喝喜力了,搞得我挺不好意思。

一顿盛况空前,锣鼓喧天,伊大圆满结束,东北大哥差点挂了。

五人住了四个酒店这件事,震惊了我的 J 人老婆。

0x10

第二天的 PyCon,yihong 终于露出了他的真面目,原来他是个大佬啊,和我一样是个大佬。

(我建议 Copilot 不要乱补,但 yihong 要求别删。)

上来大哥的演讲真叫牛逼,是我梦想中的演讲效果。

接下来赞助商的演讲就兴趣不大了,溜出去 Social。粉丝围过来合影,要求签名,闪光灯一阵阵的并没有。

整体非常僵硬,碰到了 tygg 跟我撞了衫,相顾无话,愈加社死。还是羡慕卓燃,离开思为后整个放飞自我,我依稀记得去年他还挺正经的。

0x11

下午早早就等着听 Gray 的演讲,结果主持人迟到,这是拖堂的开始。

Gray 的演讲非常精彩,不用多说毫无疑问是全场(技术方向)最好的演讲,孩子都听得入迷了。

Gray 演讲完哗啦走了一堆观众。

Manjusaka 演讲前又哗啦进来一堆观众,好像比刚才还多了。Python 3.14 的 tail call 优化,听得我一愣一愣的。

Saka 演讲完教室都空了一半,我想着接下来我的演讲没压力了,那不是随便讲。

事实证明我错了,由于拖堂,大家都把其他会场的听完了来这汇合,我根本没预演过,果然是妥妥超时了,大概超了一倍。

本来还想去代码厨房逛逛,结果人满了。

0x12

晚上纠结是去晚宴还是吃面的问题差点闹分裂了,最后还是去了讲师的晚宴。

晚宴在能看到外滩全景的白玉兰广场,Amazing 啊,然而吃的很一般,看着吃面小组发来的照片,我明天高低得去吃一碗。

饭后二场我老婆找了一家精酿,大家相谈甚欢,慧姐忙完已近午夜仍仆仆赶来。这帮程序员虽不胜酒力,未能达到东坡「相与枕藉乎舟中 不知东方之既白」的程度,却也直到凌晨才阑珊而归。

明天将各奔东西,做牛马、带小孩,也不忘今晚在秋风中共饮。春节每年都越过越没劲,但我们找到了自己的春节。最后借用在和菜头文章里看到的一句话,愿大家都能:

肥而不腻,老而不登。

河南行拾遗

2025-05-11 08:00:00

这不是一篇正经的游记,事无巨细地记录行程、餐饮、住宿并非我所擅长,很容易写成流水账。我不希望文章变成那样,所以只是写下我想与大家分享的东西,记录下河南在我心中的印象。

五一假期我和老婆去了河南,尽管预料到人会很多,但在国家实行统一长假的框架内,还是只能如此。就算我是数字游民,但老婆不是啊,所幸女儿没有和我们一同前往,事后回想这次如果带上了她,想必会艰难加倍。

4/29

我看过一本书叫《中国八大古都》,可能大家只对七大古都熟悉,而这本书里提到的第八大古都,就是河南郑州,因为这里发掘了商早期的都城遗址,有郑州商城遗址。这里的道路很宽很平,太阳很大但并不觉热。我们去了两处适合网红拍照的景点瑞光路和油化厂创意园,差强人意。

DSCF0415

晚上去吃了葛记焖饼 ,两个南方人只能吃一份,被服务员反复确认,我感觉受到了鄙视。

DSCF0420

4/30

今天安排了重头戏,就是去郑州的「只有河南」戏剧幻城。这是一个有着 21 个剧场的戏剧主题园区,其中有 3 个主剧场,买的单日票是包含一个固定场次的其中一个主剧场,也不用贪心,因为要看完全部 3 个主剧场, 没有两天是看不下来的。事先我们做了周密的攻略,做了 A、B 计划防止时间赶不上。最后在 B 计划的大框架内做了一些小调整完成了一天的任务,看下来是 1 个主剧场 7 个小剧场。感觉已经到了极限,因为其中《苏轼的河南》和《红脸蛋儿》都是网红热门,都提前排队了。园子是 10:00 开放,我们上午 9:50 就进园,晚上 8:30 出园,已经拉满了。第二天是五一假,园区开放时间会大大延长,但我们惧怕人流,选择避开了。

剧场名 时间
麦子啊麦子 10:20-10:48
火车站(主) 11:00-12:05
天子驾六 12:30-12:52
候车大厅 13:30-13:55
苏轼的河南 15:30-16:00
红脸蛋儿 17:00-17:27
曹操的麦田 18:00-18:35
张家大院 19:20-19:50

这一整天的饭就别指望好好吃了,中午干粮应付,晚上坐园区的公交回市区(园区其实已经在中牟县了),回宾馆的时候已经 10 点多了,随便吃了点烧烤外卖。

剧场整体是以 1942 年的河南饥荒做线索,串起三大主剧场和各小剧场,其实每个剧场,单独拎出来,都属于卖不出票的情况,但合在一起他们就成立,且游客趋之若鹜,这就是「只有河南」在商业上成功的地方。感觉挺好的,既让大众有机会戏剧启蒙,又能养活一些十八线的演员们。园区入口和二层都种了大量的麦子,临近立夏,都长得很好,大片的青绿色,据说到了芒种麦子会变黄,那时去可能会看到不一样的景象。

IMG_7793

5/1

这天没别的,就是河南博物院了。我喜欢逛博物馆,但又不带讲解,能接收到多少纯看缘分了,也不是走马观花,每个展馆都会认真去了解。今天人流自然是惊人,所以必须反着参观,进来就直上 4 楼然后看下来。 这里的几大镇馆之宝还是有点含金量的,楚文化展馆的云纹铜禁的镂空装饰非常精妙,武则天的金简也是拍照的人山人海。

DSCF0430

DSCF0442

顺带一提,藏在河南院的「妇好鸮尊」将自 5 月 19 日起在北京大运河博物馆展出,它本是一对,这次它将与另一只藏于国家博物馆的鸮尊合体展出,在北京对青铜器感兴趣的值得一去。

DSCF0450

我们一直在博物馆参观到下午 2 点多才出去,步行去「合记烩面」吃了一碗烩面,那是真好吃。然后去「阿财咖啡」每人点了一杯,拿到咖啡时的我眼泪差点掉下来,13 块钱的美式足足有一升!

354858716f674d5857f7cbef2e20a9d5

5/2

一大早就起床赶六点多的城际去开封,你问我为啥不提前一天去开封入住,那当然是因为贵啊。没想到开封已是河南第二大旅游目的地,仅次于洛阳,而宾馆酒店明显承载不了这么大的游客数量,只能一再涨价,一间小小的如家都要 800 多,这价钱能在郑州住大 house,它不香吗?

今天计划是清明上河园,别提了,这是比只有河南还要艰巨的一天。这是一个照搬《清明上河图》的全仿古建筑的主题乐园,内容依然是大大小小的表演,比如包公巡视汴河,岳飞枪挑小梁王之类的。看只有河南时还不觉得,现在才终于知道每年一大批表演专业的演员们毕业们都去哪了。他们每人从早演到晚,重复着一次又一次相同的走位相同的动作,还要上马战,只能感慨没有一个牛马是轻松的。对了,闭园时间是凌晨 1:00 哦。

除了人还是人,每场表演都里三十层外三十层,比肩接踵,重点演出你还必须提前占座,对我们这种单日的外地游客太不友好了。带小朋友的更是遭罪了,全程只能看屁股,所以这是为什么我庆幸没带女儿来,起早贪黑特种兵不说,还没有体验。

一天演出的重头戏就是晚上的打铁花表演和《岳飞郾城大捷》,我们毅然选择了提前两小时去占岳飞的座,然后看晚场的打铁花。事后证明这个选择无比正确,因为到晚上七点钟突然狂风大作电闪雷鸣,进而下起了大雨,而我们在的观看岳飞的场地是有顶棚的。我们等了两个小时,不仅看到了有上天赠送雷电特效的《郾城大捷》,还在之后的打铁花中坐到了第一排。

雷电加持的《岳飞郾城大捷》(来自小红书):http://xhslink.com/a/UhoRDNI06Fvcb

IMG_7897

不得不提的是这里的物价,这么多演出随便看,一人只要 120,就连园内的可乐也只要 5 元一瓶,我从来没见过 5 元的可乐,如果你买了《东京梦华》的演出票,还能三天内随意进园。这价格放眼全国也是相当良心,难怪人都成山了。其实开封还有另一个类似的乐园叫万岁山武侠城,但据说人数比清园还多得多我们就放弃了。

今天不得不下榻开封,住的地方其实是一个洗浴中心,也算打开思路了。

5/3

今天参观了开封博物馆,吃到了宋园的灌汤包和化三驴肉火烧,下午逛了开封府。

总体来说,如果你是古建爱好者,喜欢寻找历史的遗迹,那开封作为七大古都之一是乏善可陈,古建全是现代仿的,博物馆展品也没有含金量,唯一一个开封府题名记碑还到处复制,我不知道看到多少块,其实只有博物馆里那块是真的。但是清明上河园和万岁山武侠城这两个沉浸式复原古代生活场景的园区,绝对是能值回票价(那场打铁花和烟火表演,在我看过的所有里面都算顶级的,单这一场表演就能值 120 块),推荐一去。

晚上实在不能再住洗浴中心了,坐高铁颠去安阳了。

5/4

古都之旅的最后一站是安阳,我们当然是慕殷墟之名而来。如果中国每个朝代能挑出一个博物馆来代表它,那么商代绝对是殷墟博物馆,19 世纪末因为一次抓药发现了甲骨文这个宝库,将中国信史上推一千年。 在文字发展的初期,文字还没有形成系统,很多字都是临时组合而成,比如商朝每代君主都有一个专属的合成字。而且人们还会充分利用象形文字的优势,随意改笔,表达特定的意思,比如下面这块龟甲:

19c03de0401e007be1344df6bf1e782a

圈出的两字其实都是「车(車)」,但仔细观察,上面的车轴断了,下面的车上下翻转了,这段卜辞其实讲的是王出猎,追犀牛,结果发生了轴断,车翻,人坠的事故。

另外还参观了很多的青铜器,现在我已经是青铜器小能手了,能准确辨别属于哪种青铜器型。

DSCF0475

DSCF0490

至于殷墟宫殿遗址就没什么好去了,房子不可能有真迹,妇好墓能下去,但什么也没有。大家以后要是来殷墟,只用 80 块的博物馆门票就行了,要是下午 5:30 以后进,是不要钱的。

河南印象

不管之前网上大家对河南人是怎么调侃的,河南给我的印象就是一个厚道的北方人,你来他家玩,他恨不得把他家最好的东西拿来给你看给你吃。凉菜给你塞满,表演给你看足。 我上次来河南还是 2010 年的洛阳,那时洛阳远没有现在这样热门,当然那时我也穷得很,连龙门石窟都没去,我想可能还会再来吧。

这几天强度还是非常高的,有时候正餐都没法好好吃,谁让我五一出门来着了。

再也别问 Singleton 了好吗?

2025-03-05 08:00:00

起笔的原因是群里的一段聊天:

20250305103123

不禁感叹 Singleton(单例模式)作为一个经典的设计模式,是如何被滥用的,特别是在 Python 这门语言中。它竟然成了一个八股式的面试题,就像「茴字有几种写法」一样,一直被问个没完。但我敢说,绝大多数人回答的时候,都是照本宣科,他们参考的网上的答案,也很少有能讲正确的。

先说结论

  • 在 Python 中,你不需要 Singleton。
  • 如果需要,就用模块级别的变量。

至于原因,让我们来看看几种流行的 Singleton 实现方式:

1. 装饰器

def singleton(cls):
    instances = {}
    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return get_instance

# Usage
@singleton
class MyClass:
    ...

如无特殊说明,代码均由 Copilot 提供

这个方案的问题很明显:应用装饰器后改变了对象的类型,由一个 class 变成了一个 function。假使有人要用 isinstance(obj, MyClass) 来判断对象类型,就会报错。

这又涉及另一个问题,你的代码将被如何使用,取决于你暴露了什么,上面的例子中暴露的就是 MyClass 这个对象,那就要考虑会不会被当成 class 来用,以及若被这样使用,是不是合理的要求。

2. 类变量

class Singleton:
    _instance = None
    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

这个方案的问题是,如果有多个单例类型,就得写多个这样的类,而且这个类也不能被继承,继承之后,实际上还是共享同一个 _instance 变量,产生冲突,这不是我们想要的。还是老问题,你暴露了一个类,别人就会用类的方式来用。

第二个问题,这个方案没有屏蔽 __init__ 的调用,实际上你如果多次实例化 Singleton 类,虽然返回的对象 id 唯一,__init__ 方法还是会被调用多次,这可能不是你想要的。

class Singleton:
    _instance = None
    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self):
        print('init called')

s1 = Singleton()
s2 = Singleton()
print(s1 is s2)

# Output
# init called
# init called
# True

3. 继承

class Singleton:
    _instances = {}

    def __new__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(Singleton, cls).__new__(cls, *args, **kwargs)
        return cls._instances[cls]

# Usage
class MyClass(Singleton):
    ...

class MyAnotherClass(Singleton):
    ...

这个方案暴露的对象其实就是一个基类,它的标准用法就是让你用来继承的,它解决了上个方案的第一个问题,但第二个问题依然存在。

4. 元类

class Singleton(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
        return cls._instances[cls]

# Usage
class MyClass(metaclass=Singleton):
    ...

这个方案用了元类,就是所有致力于掌握 Python 高级语言特性的人们会想到的终级方案。它在元类级别直接拦截了实例化的调用,而不仅仅是 __new__ 方法,因为实例化的调用包括了 __new____init__,这样就解决了上面提到的第二个问题。如果你再运行上面的测试代码,会发现输出符合期待了:

s1 = MyClass()
s2 = MyClass()
print(s1 is s2)

# Output
# init called
# True

很完美,不是吗?先别沾沾自喜,看下面的代码:

class MyClass(metaclass=Singleton):
    def __init__(self, name: str) -> None:
        self.name = name

s1 = MyClass('Alice')
s2 = MyClass('Bob')
print(s1.name, s2.name)
# Output
# Alice Alice

你认为这个输出符合编写者的意图吗?不能说是也不能说不是,只是值得商榷,这取决于调用者如何看待单例模式。一个考虑是如果用方案 2 和 3, name 属性的值就会被统一改成 Bob,这本身已经体现了问题所在。你可能会说没人在单例类的实例化中传参,这点我也不确定,但我认为,有人这么做,是因为你暴露的接口允许他这么做。

5. 模块级别的变量

class Singleton:
    ...

singleton = Singleton()

# Usage
from singleton_module import singleton

朴实无华,没有花里胡哨的东西,用这个答案如何能体现我精通 Python 呢?相反,我认为这个方案是最能实现原始需求的,相对来说问题最少,也最容易理解。就像人生的三重境界一样,最终还是要对花哨的东西袪魅,回到需求本身上去。这个方案里暴露的对象是唯一的 singleton 模块变量,你不可能用类的方式来用它,也不可能传参给它,这才是我们想要的。你甚至可以把类名写成 _Singleton 断了人的念想(当然这不是硬禁止,你想用还能用,别在这上面抬杠了)。

那如果,我还是想暴露 Singleton 类来做一些比如 isinstance 的操作呢?我承认有这个需求,那我们稍加改造一下:

class Singleton:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is not None:
            raise TypeError("Singleton class cannot be instantiated twice")

        cls._instance = super().__new__(cls)
        return cls._instance

singleton = Singleton()

看上去好像跟方案 2 差不多,但暴露的主要对象已经从类变成了实例,这个类已经不允许做实例化的操作了。这就是从代码层面控制了暴露的接口。

我的 2024

2024-12-24 08:00:00

工作与技术

  • 在 BentoML lead 了几个重要的 feature,包括 1.3 和 1.4 两个大版本,以及 Codespaces 和 Comfy-Pack。

  • 11 月参与 PyCon China 2024,见了许多网友,参加了代码厨房的活动,很开心。做了关于 Pydantic 的演讲。

  • 11 月参与了 NebulaGraph 深圳的线下活动,做了关于 BentoML 的分享。

  • 5 月份有荣幸与 Paul Romer 见面交流,并造就了 2024 年我最受关注的一篇文章。但于我来说会总结为机缘巧合,不必赋与我其他的光环。事实上从那以后我试图联系过 Romer 先生都没有得到回应。

  • 开源了 FxZhihu 被很多人使用,感受到贡献了一些价值。

  • 写作博客文章 15 篇。

  • 由于 uv 的横空出世,PDM 无疑受到了一些冲击,发展明显变缓。 star-history-20241224

    但我不是那么狭隘,人不能沉湎在过去的辉煌中举步不前。好用的工具得用,我自己平时也会用用 uv。PDM 也在 2.19 中加入了对 uv 的支持

  • 开源了 Tetos,一个 TTS 的统一 SDK 库。

  • 其他的一些娱乐项目比如:

  • 2024 依然目睹了 GenAI 和 LLM 的火爆,我个人也体验了 Cursor 最后又回到了 VSCode。

出行

今年继续去了挺多地方。

1 月份汕尾

IMG_4111.HEIC_compressed

2 月珠海——中山

20241224103554

春节马六甲——吉隆坡

20241224103332

清明节广西梧州

20241224103801

4 月份上海

20241224104551

五一普者黑——弥勒

2024122410464120241224104722

6 月份泉州

20241224104824

8 月份太原

20241224104920

国庆日本九州

jiuzhou_027.jpegjiuzhou_088.jpeg

11 月潮汕

20241224105516

PyCon China 上海 Again

20241224105552

12 月香港

20241224105633

生活

  • 房子做了几次提前还款,减少了一些负担。
  • 把手上相机卖了,换了个新的,富家子弟又支棱起来了。
  • 买了 Mac Mini M4,用来做开发机,体验还不错。
  • 被保险代理忽悠去香港考了个 IIQE(保险中介资格考试),很容易就过了,但不知道有什么用。
  • 呃啊我的高才明年就要续了,还不知道怎么搞,很焦虑。
  • 去年提到的精神内耗状况减轻了许多,在外界因素变化不大的情况下,我开始更多地放松和和解。

书影音

2024 年的书影音

最喜欢的书是《隳三都》,在历史非虚构里属于非常优秀的了。另外还有刘震云的《我叫刘跃进》,高级的黑色幽默。 电影没有最喜欢的,相对好的是《走走停停》《从 21 世纪安全撤离》和《死侍与金刚狼》。

只是 10 月份以后就没有看书和电影了,提不起兴趣来。

明年会怎么样,我也不知道。2025 是个平方数,希望是个好年。

$$ 2025 = 45^2 = 3^4 \times 5^2 $$

本文题图由 Ideogram 生成,其余图片,除 PyCon China 合影,均为本人拍摄。

一个 monkeypatch 引起的循环引用问题

2024-12-18 08:00:00

最近社区里有人提了一个PR^1,解决了一个潜在的内存泄漏问题。我认为这个问题在很多场景都容易忽略,所以分享在这里。

上代码

import gc

class Foo:
    def bar(self):
        print("bar")

foo = Foo()

print("Before", gc.get_count())
foo.bar = foo.bar
print("setattr", gc.get_count())
del foo
print("After", gc.get_count())

运行发现 foo 对象无法被正常回收,造成了内存泄漏。这是因为看似不起眼的一行 foo.bar = foo.bar,实际上创建了一个循环引用。

点解?

因为,foo.bar 这个表达式,实际上执行了 Foo.bar.__get__(foo, Foo),返回了一个绑定了 foo 的方法,此方法中持有 foo 的引用。而 foo.bar = foo.bar,相当于把这个结果缓存在了 foo.bar 属性上。之后的 foo.bar 只是从属性上取值,这样 foo 和该方法互相持有对方的引用,出现了循环引用。

现实中我们不太可能会写出 foo.bar = foo.bar 这样的代码,但是在 monkeypatch 场景下,可能很容易被坑到,比如:

import requests

resp = requests.get("https://example.com", stream=True)
resp._fp = CallbackFileWrapper(resp, callback)
...

上述代码 patch 了 requests 库的 Response 对象,让它在被读取完成中调用我们指定的 callback 函数。这里 CallbackFileWrapper 会持有 resp 的引用。

如何避免?

在上述例子中,我们可以使用 weakref 来避免循环引用:

import weakref

class CallbackFileWrapper:
    def __init__(self, resp, callback):
        self.resp_ref = weakref.ref(resp)
        self.callback = callback

    def read(self, size):
        resp = self.resp_ref()
        if resp is None:
            return b""
        read_bytes = resp.read(size)
        if not read_bytes:
            self.callback(resp)