Muspi Merol

Muspi Merol 的个人主页

马上订阅 Muspi Merol RSS 更新: https://muspimerol.site/feed

实现一个类型安全的 (f1, f2) -> f2(f1()) 函数

2024年12月29日 19:33
def compose(first, second):
    return lambda: second(first())

有了这个函数,当你需要 串联 两个函数的时候,就可以很方便了,下面我举例一个使用场景

例子:序列化并用 bzip2 压缩

大家都知道 python 对象的序列化用pickle.dumps,压缩用***.compress(***替换成各种库,比如标准库的 gzip,bz2,lzma 都是这样)

前者是(Any)->bytes,后者是bytes->bytes,如果你要封装一个“序列化并压缩”的函数,你得这么写:

def dumps(object: Any):
    serialized = pickle.dumps(object)
    compressed = bz2.compress(serialized)
    return compressed

当然这种一次性的代码一般会缩写成一行:

def dumps(object: Any):
    return bz2.compress(pickle.dumps(object))

再加上反序列化的代码:

def loads(data: bytes):
    return pickle.loads(bz2.decompress(data))

这样,再加上空行,相当于你代码加了8行。不能忍吧

用我们刚刚的compose函数,只要这样就行了:

dumps = compose(pickle.dumps, bz2.compress)
loads = compose(bz2.decompress, pickle.loads)

只要两行,而且清清楚楚。

但是类型呢?

我们刚刚的compress函数,没有类型注解,你可能觉得这很简单,这样就行了:

def compose(first: Callable, second: Callable):
    return lambda: second(first())

这样只能确保输入compose的两个参数都是可调用对象,但是并没有保证:

  1. 第二个函数可以接受第一个函数的返回值为参数
  2. compose后的函数的返回值与second的返回值相同

我们希望h()的类型推断是Literal[42]或者int对吧

泛型

Python 有勉强够用的泛型支持,而且早在 Python 3.12 就开始支持 typescript 式的更方便的声明泛型的方式:

PEP 695: Type Parameter Syntax —— What's New In Python 3.12

这么写就可以了:

def compose[T1, T2](first: Callable[[], T1], second: Callable[[T1], T2]):
    return lambda: second(first())

你看我甚至返回值都不用标,pyright能推算出来返回的是Callable[[], T2]

你看,这样错误就能检查出来啦☀️

修复了g之后,类型也正确推断啦:

如果 first()有参数呢

上面的这个例子用途不够广泛,因为它用于无参调用。而有时候我很需要这个 compose 出来的函数,仍然有着原来的 first 函数的参数定义

其实我们需要的就是一个“输入 first 的参数,返回 second 的返回值”这么一个函数对吧,那就简单了:

def compose[T1, T2, **P](first: Callable[P, T1], second: Callable[[T1], T2]) -> Callable[P, T2]:
    return lambda *args, **kwargs: second(first(*args, **kwargs))

我只是把Callable[[], T1]改成了Callable[P, T1],加上明确最后返回Callable[P, T2]就完事儿啦。

当然,如果你不喜欢 lambda,你可以这样写一个函数,这样甚至能少写-> Callable[P, T2]这个返回值声明

右边的返回值声明是它自己生成的,这个显示是 IDE 的 inlay hint,不是我打上去的

这样你就可以让这个 compose 出的函数具有和first一样的入参签名啦:

这个例子其实挺没用的,但是我一时间想不到好例子。总之使用场景就是,可能前面那个函数有很多参数,而且你是会用到这些参数的。

后话:为什么做这个

因为我想要一个类型安全的json.dumpsjson.loads...

剩余内容已隐藏

查看完整文章以阅读更多