2025-05-11 08:00:00
这不是一篇正经的游记,事无巨细地记录行程、餐饮、住宿并非我所擅长,很容易写成流水账。我不希望文章变成那样,所以只是写下我想与大家分享的东西,记录下河南在我心中的印象。
五一假期我和老婆去了河南,尽管预料到人会很多,但在国家实行统一长假的框架内,还是只能如此。就算我是数字游民,但老婆不是啊,所幸女儿没有和我们一同前往,事后回想这次如果带上了她,想必会艰难加倍。
我看过一本书叫《中国八大古都》,可能大家只对七大古都熟悉,而这本书里提到的第八大古都,就是河南郑州,因为这里发掘了商早期的都城遗址,有郑州商城遗址。这里的道路很宽很平,太阳很大但并不觉热。我们去了两处适合网红拍照的景点瑞光路和油化厂创意园,差强人意。
晚上去吃了葛记焖饼 ,两个南方人只能吃一份,被服务员反复确认,我感觉受到了鄙视。
今天安排了重头戏,就是去郑州的「只有河南」戏剧幻城。这是一个有着 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 年的河南饥荒做线索,串起三大主剧场和各小剧场,其实每个剧场,单独拎出来,都属于卖不出票的情况,但合在一起他们就成立,且游客趋之若鹜,这就是「只有河南」在商业上成功的地方。感觉挺好的,既让大众有机会戏剧启蒙,又能养活一些十八线的演员们。园区入口和二层都种了大量的麦子,临近立夏,都长得很好,大片的青绿色,据说到了芒种麦子会变黄,那时去可能会看到不一样的景象。
这天没别的,就是河南博物院了。我喜欢逛博物馆,但又不带讲解,能接收到多少纯看缘分了,也不是走马观花,每个展馆都会认真去了解。今天人流自然是惊人,所以必须反着参观,进来就直上 4 楼然后看下来。 这里的几大镇馆之宝还是有点含金量的,楚文化展馆的云纹铜禁的镂空装饰非常精妙,武则天的金简也是拍照的人山人海。
顺带一提,藏在河南院的「妇好鸮尊」将自 5 月 19 日起在北京大运河博物馆展出,它本是一对,这次它将与另一只藏于国家博物馆的鸮尊合体展出,在北京对青铜器感兴趣的值得一去。
我们一直在博物馆参观到下午 2 点多才出去,步行去「合记烩面」吃了一碗烩面,那是真好吃。然后去「阿财咖啡」每人点了一杯,拿到咖啡时的我眼泪差点掉下来,13 块钱的美式足足有一升!
一大早就起床赶六点多的城际去开封,你问我为啥不提前一天去开封入住,那当然是因为贵啊。没想到开封已是河南第二大旅游目的地,仅次于洛阳,而宾馆酒店明显承载不了这么大的游客数量,只能一再涨价,一间小小的如家都要 800 多,这价钱能在郑州住大 house,它不香吗?
今天计划是清明上河园,别提了,这是比只有河南还要艰巨的一天。这是一个照搬《清明上河图》的全仿古建筑的主题乐园,内容依然是大大小小的表演,比如包公巡视汴河,岳飞枪挑小梁王之类的。看只有河南时还不觉得,现在才终于知道每年一大批表演专业的演员们毕业们都去哪了。他们每人从早演到晚,重复着一次又一次相同的走位相同的动作,还要上马战,只能感慨没有一个牛马是轻松的。对了,闭园时间是凌晨 1:00 哦。
除了人还是人,每场表演都里三十层外三十层,比肩接踵,重点演出你还必须提前占座,对我们这种单日的外地游客太不友好了。带小朋友的更是遭罪了,全程只能看屁股,所以这是为什么我庆幸没带女儿来,起早贪黑特种兵不说,还没有体验。
一天演出的重头戏就是晚上的打铁花表演和《岳飞郾城大捷》,我们毅然选择了提前两小时去占岳飞的座,然后看晚场的打铁花。事后证明这个选择无比正确,因为到晚上七点钟突然狂风大作电闪雷鸣,进而下起了大雨,而我们在的观看岳飞的场地是有顶棚的。我们等了两个小时,不仅看到了有上天赠送雷电特效的《郾城大捷》,还在之后的打铁花中坐到了第一排。
雷电加持的《岳飞郾城大捷》(来自小红书):http://xhslink.com/a/UhoRDNI06Fvcb
不得不提的是这里的物价,这么多演出随便看,一人只要 120,就连园内的可乐也只要 5 元一瓶,我从来没见过 5 元的可乐,如果你买了《东京梦华》的演出票,还能三天内随意进园。这价格放眼全国也是相当良心,难怪人都成山了。其实开封还有另一个类似的乐园叫万岁山武侠城,但据说人数比清园还多得多我们就放弃了。
今天不得不下榻开封,住的地方其实是一个洗浴中心,也算打开思路了。
今天参观了开封博物馆,吃到了宋园的灌汤包和化三驴肉火烧,下午逛了开封府。
总体来说,如果你是古建爱好者,喜欢寻找历史的遗迹,那开封作为七大古都之一是乏善可陈,古建全是现代仿的,博物馆展品也没有含金量,唯一一个开封府题名记碑还到处复制,我不知道看到多少块,其实只有博物馆里那块是真的。但是清明上河园和万岁山武侠城这两个沉浸式复原古代生活场景的园区,绝对是能值回票价(那场打铁花和烟火表演,在我看过的所有里面都算顶级的,单这一场表演就能值 120 块),推荐一去。
晚上实在不能再住洗浴中心了,坐高铁颠去安阳了。
古都之旅的最后一站是安阳,我们当然是慕殷墟之名而来。如果中国每个朝代能挑出一个博物馆来代表它,那么商代绝对是殷墟博物馆,19 世纪末因为一次抓药发现了甲骨文这个宝库,将中国信史上推一千年。 在文字发展的初期,文字还没有形成系统,很多字都是临时组合而成,比如商朝每代君主都有一个专属的合成字。而且人们还会充分利用象形文字的优势,随意改笔,表达特定的意思,比如下面这块龟甲:
圈出的两字其实都是「车(車)」,但仔细观察,上面的车轴断了,下面的车上下翻转了,这段卜辞其实讲的是王出猎,追犀牛,结果发生了轴断,车翻,人坠的事故。
另外还参观了很多的青铜器,现在我已经是青铜器小能手了,能准确辨别属于哪种青铜器型。
至于殷墟宫殿遗址就没什么好去了,房子不可能有真迹,妇好墓能下去,但什么也没有。大家以后要是来殷墟,只用 80 块的博物馆门票就行了,要是下午 5:30 以后进,是不要钱的。
不管之前网上大家对河南人是怎么调侃的,河南给我的印象就是一个厚道的北方人,你来他家玩,他恨不得把他家最好的东西拿来给你看给你吃。凉菜给你塞满,表演给你看足。 我上次来河南还是 2010 年的洛阳,那时洛阳远没有现在这样热门,当然那时我也穷得很,连龙门石窟都没去,我想可能还会再来吧。
这几天强度还是非常高的,有时候正餐都没法好好吃,谁让我五一出门来着了。
2025-03-05 08:00:00
起笔的原因是群里的一段聊天:
不禁感叹 Singleton(单例模式)作为一个经典的设计模式,是如何被滥用的,特别是在 Python 这门语言中。它竟然成了一个八股式的面试题,就像「茴字有几种写法」一样,一直被问个没完。但我敢说,绝大多数人回答的时候,都是照本宣科,他们参考的网上的答案,也很少有能讲正确的。
先说结论
至于原因,让我们来看看几种流行的 Singleton 实现方式:
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 来用,以及若被这样使用,是不是合理的要求。
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
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):
...
这个方案暴露的对象其实就是一个基类,它的标准用法就是让你用来继承的,它解决了上个方案的第一个问题,但第二个问题依然存在。
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
,这本身已经体现了问题所在。你可能会说没人在单例类的实例化中传参,这点我也不确定,但我认为,有人这么做,是因为你暴露的接口允许他这么做。
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-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 无疑受到了一些冲击,发展明显变缓。
但我不是那么狭隘,人不能沉湎在过去的辉煌中举步不前。好用的工具得用,我自己平时也会用用 uv。PDM 也在 2.19 中加入了对 uv 的支持。
开源了 Tetos,一个 TTS 的统一 SDK 库。
其他的一些娱乐项目比如:
2024 依然目睹了 GenAI 和 LLM 的火爆,我个人也体验了 Cursor 最后又回到了 VSCode。
今年继续去了挺多地方。
1 月份汕尾
2 月珠海——中山
春节马六甲——吉隆坡
清明节广西梧州
4 月份上海
五一普者黑——弥勒
6 月份泉州
8 月份太原
国庆日本九州
11 月潮汕
PyCon China 上海 Again
12 月香港
最喜欢的书是《隳三都》,在历史非虚构里属于非常优秀的了。另外还有刘震云的《我叫刘跃进》,高级的黑色幽默。 电影没有最喜欢的,相对好的是《走走停停》《从 21 世纪安全撤离》和《死侍与金刚狼》。
只是 10 月份以后就没有看书和电影了,提不起兴趣来。
明年会怎么样,我也不知道。2025 是个平方数,希望是个好年。
$$ 2025 = 45^2 = 3^4 \times 5^2 $$
本文题图由 Ideogram 生成,其余图片,除 PyCon China 合影,均为本人拍摄。
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)
2024-11-07 08:00:00
我女儿第一天上幼儿园回来,我就拿着群里他们班级的合照,挨个问她每个小朋友都叫什么名字。一方面是想看看她集体融入得怎么样,另一方面是自己想留个底,争取每个小朋友的脸和名字都能对上。 她当然不能每个都叫出名字来,但配合群里其他家长聊天的信息,三天后我就基本能全叫出名字了。有回我正在问,丈母娘见了在旁说,「知道这个有什么用,你又不是老师。」面对这种言论,我只能一笑了之。
我觉得,总会有用的。在我看来,至少有下面几个用处:
但丈母娘这个看法就见识短浅了,她才是接送孩子的主力。我已经不知多少次听她说路上碰见个幼儿园同学,做了什么举动,却不知道是谁。
今天去幼儿园参加家长开放日的活动,其实就是小朋友在上课活动时家长在旁观看。户外休息时,很多小朋友围到我身边来,因为他们发现我能叫对他们每一个人的名字,甚至还知道小名。于是就疯狂地来考我,乐意跟我聊天。 我突然觉得,两年前做过的功课没有白做,今天派上了用场,不是什么大用,能温暖我一阵子,也足够了。
所以,你看过的每一本书,玩过的游戏,走过的路,都不会浪费。只是你还没找到用处罢了,又或者它们已经内化在你的个人气质中了。
功不唐捐。
2024-10-20 08:00:00
import Tweet from '@components/Tweet';
我一直在思考要在博客写些什么,我能写什么。刚好看到了 @laike9m 的这篇推文:
<Tweet client:only="react" id="1847557409457524950" />
有很多观点我是不敢在网上表达的,只能和女朋友说说。有的是因为太个人化,有的是违反了普遍的认知,以及第三种情况:解释清楚要花太多时间,而我的时间本就不多。于是每次都是想想,很快便打消了这个念头了。
确实,很多时候阻挠我表达的,是四个字「大可不必」。我要表达的观点会有人认同吗?或至少,会引发一些思考吗?如果两者的答案都是否,那就「大可不必」发表了。或者即使,观点或许有一些价值,那么我要怎么表达呢?是不是得安排逻辑结构、旁征博引、收集数据案例,这些值不值得?「大可不必」,这四个字又在我脑子里冒了出来。
于是,很多想法可能就这样,只停留在了熄灯后的卧谈,甚至,更普遍的情况是,只停留在了脑海里,永远没有见天日的机会。
我们不知不觉中,给自己的表达设定了一个阈值,超过了这个值的,才会出现在对外分享的表达中。而要克服这个阈值,很需要一些能量。我死去的半导体课上学过的知识又突然袭击了我,价带电子需要一定能量,才能跃迁到导带。
所以我有时非常羡慕一些生活博主(特别是居住在国外的),坐了什么公共交通上班,吃了什么饭,日落时没云彩,下雨忘了带伞,都可以成为他们笔下津津乐道的话题。我心想要是按他们这个阈值来,那我每天不得发 24 条动态。
但有时候我又会想,大可不必。
感谢今天战胜了我的阈值的我的表达欲。