序言
众所周知,Python 支持向函数传递关键字参数。比如 Python 的内置函数max
就接受名为key
的关键字参数,以决定如何获取比较两个参数时的依据
1 | max({'v': 1}, {'v': 3}, {'v': 2}, key=lambda o: o['v']) # 返回值为{'v': 3} |
自定义一个运用了关键字参数特性的函数当然也不在话下。例如模仿一下 Common Lisp 中的函数string-equal
1 | def string_equal(string1, string2, *, start1=None, end1=None, start2=None, end2=None): |
再以关键字参数的形式向它传参
1 | string_equal("Hello, world!", "ello", start1=1, end1=4) # 返回值为True |
秉承 Python 之禅中的 我甚至可以花里胡哨地、用关键字参数的语法向There should be one-- and preferably only one --obvious way to do it.
理念,string1
和string2
传参
1 | string_equal(string1='Goodbye, world!', string2='ello') # 返回值为False |
但瑜不掩瑕,Python 的关键字参数也有其不足。
Python 的不足
Python 的关键字参数特性的缺点在于,同一个参数无法同时以:
- 具有自身的参数名,以及;
- 可以从
**kwargs
中取得,
两种形态存在于参数列表中。
举个例子,我们都知道 Python 有一个知名的第三方库叫做 requests,提供了用于开发爬虫牢底坐穿的发起 HTTP 请求的功能。它的类requests.Session
的实例方法request
有着让人忍不住运用 Long Parameter List 对其重构的、长达 16 个参数的参数列表。(你可以移步request
方法的文档观摩)
为了便于使用,requests 的作者贴心地提供了requests.request
,这样只需要一次简单的函数调用即可
1 | requests.request('GET', 'http://example.com') |
requests.request
函数支持与requests.Session#request
(请允许我借用 Ruby 对于实例方法的写法)相同的参数列表,这一切都是通过在参数列表中声明**kwargs
变量,并在函数体中用相同的语法向后者传参来实现的。(你可以移步request 函数的源代码观摩)
这样的缺陷在于,requests.request
函数的参数列表丢失了大量的信息。要想知道使用者能往kwargs
中传入什么参数,必须:
- 先知道
requests.request
是如何往requests.Session#request
中传参的——将kwargs
完全展开传入是最简单的情况; - 再查看
requests.Session#request
的参数列表中排除掉method
和url
的部分剩下哪些参数。
如果想在requests.request
的参数列表中使用参数自身的名字(例如params
、data
、json
等),那么调用requests.Session#request
则变得繁琐起来,不得不写成
1 | with sessions.Session() as session: |
的形式——果然人类的本质是复读机。
一个优雅的解决方案,可以参考隔壁的 Common Lisp。
Common Lisp 的优越性
Common Lisp 第一次面世是在1984年,比 Python 的1991年要足足早了7年。但据悉,Python 的关键字参数特性借鉴自 Modula-3,而不是万物起源的 Lisp。Common Lisp 中的关键字参数特性与 Python 有诸多不同。例如,根据 Python 官方手册中的说法,**kwargs
中只有多出来的关键字参数
If the form “**identifier” is present, it is initialized to a new ordered mapping receiving any excess keyword arguments
而在 Common Lisp 中,与**kwargs
对应的是&rest args
,它必须放置在关键字参数之前(即左边),并且根据 CLHS 中《A specifier for a rest parameter》的说法,args
中含有所有未经处理的参数——也包含了位于其后的关键字参数
1 | (defun foobar (&rest args &key k1 k2) |
如果我还有另一个函数与foobar
有着相似的参数列表,那么也可以轻松将所有参数传递给它
1 | (defun foobaz (a &rest args &key k1 k2) |
甚至于,即使在foobaz
中支持的关键字参数比foobar
要多,也能轻松地处理,因为 Common Lisp 支持向被调用的函数传入一个特殊的关键字参数:allow-other-keys
即可
1 | (defun foobaz (a &rest args &key k1 k2 my-key) |
回到 HTTP 客户端的例子。在 Common Lisp 中我一般用drakma这个第三方库来发起 HTTP 请求,它导出了一个http-request
函数,用法与requests.request
差不多
1 | (drakma:http-request "http://example.com" :method :get) |
如果我想要基于它来封装一个便捷地发出 GET 请求的函数http-get
的话,可以这样写
1 | (defun http-get (uri &rest args) |
如果我希望在http-get
的参数列表中直接暴露出一部分http-request
支持的关键字参数的话,可以这样写
1 | (defun http-get (uri &rest args &key content) |
更进一步,如果我想在http-get
中支持解析Content-Type
为application/json
的响应结果的话,还可以这样写
1 | (ql:quickload 'jonathan) |
不愧是Dio Common Lisp,轻易就做到了我们做不到的事情。
题外话
曾几何时,Python 程序员还会津津乐道于 Python 之禅中的There should be one-- and preferably only one --obvious way to do it.
,但其实 Python 光是在定义一个函数的参数方面就有五花八门的写法了。甚至在写这篇文章的过程中,我才知道原来 Python 的参数列表中可以通过写上/
来使其左侧的参数都成为 positional-only 的参数。
1 | def foo1(a, b): pass |