MoreRSS

site iconFrost Ming修改

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

Inoreader Feedly Follow Feedbin Local Reader

Frost Ming的 RSS 预览

友好的 Python:从其他语言移植

2025-12-30 08:00:00

我们总说 Pythonic,我自己也会说某段代码不够 Pythonic,但什么是 Pythonic 呢?不讲清楚,我会被人说老登。为了避免如此,我决定再写写。

要说一段代码是 Pythonic 还是 Rustic、Go-istic、Java-ish 最直观的方法就是把它们放一起来对比,这个场景就是代码移植。其实,代码在特定语言里怎么写,是由语言特性决定的,但肯定有些部分,什么语言都能这么用,那么这些移植代码最终会变成什么样子,就更多是由写代码的人(也不一定是人)决定的了。下面我就从其他语言找些例子,看看如果把它们用 Python 重写,应该是什么样的。

opendal.Operator

不多介绍这个库了,不影响本文的主旨,咱直接从它的官方文档里截取一段最原汁原味的 Rust 代码:

// Pick a builder and configure it.
let mut builder = services::S3::default();
builder.bucket("test");

// Init an operator
let op = Operator::new(builder)?
    // Init with logging layer enabled.
    .layer(LoggingLayer::default())
    .finish();

// Use operator

因为没有任何 Python 不支持的语言特性,这一段可以 1:1 地翻译成 Python,简单地去掉 defaultnew 即可:

builder = services.S3()
builder.bucket("test")

op = Operator(builder) \
    .layer(LoggingLayer()) \
    .finish()

我相信一个初学 Python 的人,就能写出这样的代码。但仔细咂摸,这好像不太像 Python,特别注意 operator 的构造方法。于是我们搬出 Xuanwo 著名的第一性原理,暂时忽略细节,思考这段代码本质在做什么。就像你摘掉眼镜看,如果不近视就眯缝着眼看,才会看到它的轮廓。

它其实就是在构造一个 S3 的 Operator 对象,并指定了一些参数。那么为什么需要一个 builder 对象呢?因为这些参数是可选的,而 Rust 不支持函数的可选参数。那 Python 没这限制,为啥还要搞个 builder 呢?实际上这个就是设计模式中的建造者模式(Builder Pattern),这也是它在 Python 里并不常见的原因。现在我们去掉 builder:

op = Operator(
    service="s3",
    bucket="test",
)
op.layer(LoggingLayer())
# Use operator

这就对味了(你怎么知道 opendal 的 Python binding 是这么写的?)。

其实 Javascript 也不支持可选参数,但它可以利用对象啊,所以如果用 Javascript 写的话,可能是这样:

const op = new Operator({
  service: "s3",
  bucket: "test",
});

因此在从 Javascript 移植到 Python 时也要注意这个区别,不能把这种对象也照搬成字典。

downloadFileuploadFile

继续上面提到的 JS 语言,这门语言有一个突出的特征是喜欢用回调函数,特别是在 ES5 时代。因为 JS 是单线程,所以任何一个耗时的任务都必须异步执行,而在没有 Promise 的时代,这就意味着必须靠回调。比如这个例子:

function downloadFile(
  url: string,
  onSuccess: (data: string) => void,
  onError: (err: Error) => void,
  onComplete: () => void
)

一个函数接受三个回调,这也是由于在 JS 中有 function,有箭头函数,定义回调相当方便和自然,那在 Python 中呢?因为缺少花括号,并没有一种原地定义函数的快捷方式,lamda 关键字又很鸡肋。虽说硬写也能写,但很丑,调一个函数要先 def 定义三个。其实,上面的函数在有 Promise 之后大概会像这样调用:

downloadFile(url)
  .then((data) => {
    // onSuccess
  })
  .catch((err) => {
    // onError
  })
  .finally(() => {
    // onComplete
  });

// Or using async/await
try {
  const data = await downloadFile(url);
  // onSuccess
} catch (err) {
  // onError
} finally {
  // onComplete
}

Python 可以借鉴这个风格:

try:
    data = await download_file(url)
    # onSuccess
except Exception as err:
    pass # onError
finally:
    pass # onComplete

好,现在假如再增加一个回调 on_progress 呢?try/except/finally 都用完了,没有更多的关键字了啊,难道又把这个回调塞回函数参数吗:

def download_file(
    url: str,
    on_progress: Callable[[bytes], None]
) -> bytes:
    ...

还有没有办法可以避免回调呢?有的,可以用异步生成器

downloaded: bytes = b""
try:
    async for chunk in download_file(url):
        # process chunk
        downloaded += chunk
    # onSuccess
except Exception as err:
    pass # onError
finally:
    pass # onComplete

这就很 Pythonic 了。但是接下来思考一个问题,反过来的函数怎么写?刚才是下载读取数据,那写入上传数据呢?JS 完全可以用一样的回调写法。但 Python 里就不一样了,首先我们要思考输入参数是什么,当然我们可以提供一个生成器,在被读取时加入逻辑:

async def file_chunks():
    async for chunk in read_file_in_chunks("large_file"):
        # process chunk
        yield chunk

try:
    await upload_file(file_chunks())
    # onSuccess
except Exception as err:
    pass # onError
finally:
    pass # onComplete

但我觉得这里有一点不好,在于在生成器中还是由我们(生产者)控制了生成数据块的节奏,这里本应该由上传函数(消费者)根据现在的网络传输状况来控制才对。所以在这样的场景里,典型的做法应该是传入一个 file-like 对象,然后利用装饰器传入处理函数:

class UploadFile:
    def __init__(self, fp):
        self._fp = fp
        self._callback = None

    def callback(self, func):
        self._callback = func
        return self

    def read(self, size=-1):
        chunk = self._fp.read(size)
        if self._callback:
            self._callback(chunk)
        return chunk

with open("large_file", "rb") as f:
    upload_fp = UploadFile(f)

    @upload_fp.callback
    def on_progress(chunk):
        pass  # process chunk

    try:
        await upload_file(upload_fp)
        # onSuccess
    except Exception as err:
        pass # onError
    finally:
        pass # onComplete

这个 UploadFile 定义只是为了更加通用,如果一次性使用也可以不用装饰器方式而是直接写在类里。这里我原地定义了一个函数传递给了 UploadFile,其实这是另一种传回调的方式,但这样无疑更加 Pythonic。

useEffect

最后我们再看看大名鼎鼎的 React 框架中的 useEffect。这个函数大家再熟悉不过了:

const useEffect = (effect: () => (void | (() => void)), deps: any[]) => void

光写这个函数签名我就有点晕了,输入一个函数,返回一个函数,函数里还可能互相引入变量,各种闭包,这就是 JS,就是任性。要是写成 Python 呢?要是原封不动的话,可能是这样:

def my_effect():
    # effect
    def cleanup():
        # cleanup
    return cleanup

use_effect(my_effect, [])

虽然语言特性完全支持这么写,但这太不 Pythonic 了,我们不喜欢 nested,我们喜欢展平(谁叫展平?)。可是怎么展平呢?我们回到第一性原理考虑,这里的本质就是在事件发生时,执行某个动作,在事件结束时,执行清理动作。这个过程,熟悉 Python 的同学很快就想到了上下文管理器:

from contextlib import contextmanager

@contextmanager
def my_effect():
    # effect
    try:
        yield
    finally:
        # cleanup

use_effect(my_effect, [])

注意到 use_effect 第一个参数是个函数,在 Python 里凡是这样的结构,都可以换成用装饰器,两个装饰器再合并一下:

@use_effect(deps=[])
def my_effect():
    # effect
    try:
        yield
    finally:
        pass  # cleanup

可以说这样的写法就是 native 地不能再 native 的 Python 了,那真叫一个地地弟弟道道到到,你可以在简历中写自己精通 Python 了。

结语

在文中我假想了一些移植的场景,实际上我相信大家没有多少机会真的做移植这件事,再说了,并没有人要把 useEffect 改用 Python 写。这只是一种思维训练,如果同样的功能在 Python 实现并封装成库,会是什么样子,文中侧重展示的是调用方的写法。你得先想好要怎么调,才能知道 API 怎么写,这是一种自顶向下的思考,关键是设计调用的方式,而(有意)略过了具体实现,这是我认为库的作者应该采用的思考方式。在这个时代,任何一个 AI 都能实现地很好,所以好的 API 设计越来越关键,人并不是一个只会复制粘贴的动物。第一性原理的思考方式也能帮助你跳出局部最优,达到全局最优。希望大家都能写出更 Pythonic 的代码。

珍重

2025-12-22 08:00:00

我曾经做过一个梦,很奇怪的是,这个梦过了近二十年我还依稀有印象。那是在 2006 年春节后的一天午睡,我梦见我睁开眼发现日历已经是 2006 年 8 月了。我一下惊醒,感叹时间怎么过得这样快,崭新的 2006 年已经要过去了。

直到 2006 年的暑假真的到来了,那时我即将进入高三,却没有什么学业负担,每天都在巷子口等陈何,一起去网吧打魔兽 2v2。陈何是我同学兼同桌,他名字是四个字,我们只称他前两个字,他家住在离我家 100 米处的地方。这情景我现在还时常想起时常怀念,感叹那时我怎么可以这么无忧无虑。他数学很好,我们时常在一起讨论数学,以及攀比。男生之间的攀比往往是互对喷对方菜,不管是打游戏,还是考数学。我们单挑过魔兽,仅有那么几次,是我比较菜,但也只有那么一点儿,只能说伯仲之间吧。但说到数学,在整个高中来说,是他比较菜。很感激他,让我能像打游戏一样,学数学。

大一暑假后他就跟随父母去了广东,我后来路过他家门前,总往里张望,里面再也没有人。等等,这并不是一个失联的故事,我们现在还有联系,只是没那么多了而已。

那年暑假还有另外一件事,我跟另外三个同学因为学校演讲的关系去了桂林旅游,总共是两个男生两个女生,其中有肖鹏。我跟肖鹏是另一个「竞争伙伴」,我们比的是化学。用现在的话说,他建模优秀,又高又帅。在桂林时有个插曲是有个骑行活动,我当时不咋会骑自行车,但在女生面前没有怂,果不其然,当一辆车掠我身边而过时我太过紧张而摔倒,膝盖擦破,半个月才好。

高考结果出来,我比肖鹏考得好,听说他比估的分低许多,应该很是郁闷。我说应该,因为自那之后我们就再没有真正地交流过。他大学去了哈尔滨,后来去了武大,去了韩国交换,去了法国读博士。这些我都是通过朋友圈(不是微信那个)知道的,他一直都很优秀,很努力,但我却觉得我们距离越来越远了。很想回到高中时,被他一拳捶我后背的互怼的时光。

能看出来,我很重感情,只是非常不擅于联络感情。最近《山河故人》重新上映,这是一部我很喜欢的电影,在某种程度上甚至塑造了我的人生观。它让我接受了每个人都是孤独的这件事,「每个人只能陪你走一段路」。所以我能接受这些遗憾,只能道声珍重,对那些还在联系,或者不再联系的朋友们。

被嫌弃的松子的一生

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 差不多,但暴露的主要对象已经从类变成了实例,这个类已经不允许做实例化的操作了。这就是从代码层面控制了暴露的接口。