序言
众所周知, Python 支持向函数传递关键字参数. 比如 Python 的内置函数 max 就接受名为 key 的关键字参数, 以决定如何获取比较两个参数时的依据
max({'v': 1}, {'v': 3}, {'v': 2}, key=lambda o: o['v']) # 返回值为 {'v': 3}
自定义一个运用了关键字参数特性的函数当然也不在话下. 例如模仿一下 Common Lisp 中的函数 string-equal
- def string_equal(string1, string2, *, start1=None, end1=None, start2=None, end2=None):
- if not start1:
- start1 = 0
- if not end1:
- end1 = len(string1) - 1
- if not start2:
- start2 = 0
- if not end2:
- end2 = len(string2) - 1
- return string1[start1:end1 + 1] == string2[start2:end2 + 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 传参
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, 这样只需要一次简单的函数调用即可
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 则变得繁琐起来, 不得不写成
- with sessions.Session() as session:
- return session.request(method=method, url=url, params=params, data=data, JSON=data, **kwargs)
的形式 -- 果然人类的本质是复读机.
一个优雅的解决方案, 可以参考隔壁的 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 中含有所有未经处理的参数 -- 也包含了位于其后的关键字参数
- (defun foobar (&REST args &key k1 k2)
- (list args k1 k2))
(foobar :k1 1 :k2 3) ;; 返回值为 ((:K1 1 :K2 3) 1 3)
如果我还有另一个函数与 foobar 有着相似的参数列表, 那么也可以轻松将所有参数传递给它
- (defun foobaz (a &REST args &key k1 k2)
- (declare (ignorable k1 k2))
- (cons a
- (apply #'foobar args)))
(foobaz 1 :k1 2 :k2 3) ;; 返回值为 (1 (:K1 2 :K2 3) 2 3)
甚至于, 即使在 foobaz 中支持的关键字参数比 foobar 要多, 也能轻松地处理, 因为 Common Lisp 支持向被调用的函数传入一个特殊的关键字参数: allow-other-keys 即可
- (defun foobaz (a &REST args &key k1 k2 my-key)
- (declare (ignorable k1 k2))
- (format t "my-key is ~S~%" my-key)
- (cons a
- (apply #'foobar :allow-other-keys t args)))
(foobaz 1 :k1 2 :k2 3 :my-key 4) ;; 打印 my-key is 4, 并返回 (1 (:ALLOW-OTHER-KEYS T :K1 2 :K2 3 :MY-KEY 4) 2 3)
回到 HTTP 客户端的例子. 在 Common Lisp 中我一般用 drakma 这个第三方库来发起 HTTP 请求, 它导出了一个 http-request 函数, 用法与 requests.request 差不多
(drakma:http-request "http://example.com" :method :get)
如果我想要基于它来封装一个便捷地发出 GET 请求的函数 http-get 的话, 可以这样写
- (defun http-get (uri &REST args)
- (apply #'drakma:http-request uri :method :get args))
如果我希望在 http-get 的参数列表中直接暴露出一部分 http-request 支持的关键字参数的话, 可以这样写
- (defun http-get (uri &REST args &key content)
- (declare (ignorable content))
- (apply #'drakma:http-request uri :method :get args))
更进一步, 如果我想在 http-get 中支持解析 Content-Type 为 application/JSON 的响应结果的话, 还可以这样写
- (ql:quickload 'jonathan)
- (ql:quickload 'str)
- (defun http-get (uri &REST args &key content (decode-JSON t))
;; http-request 并不支持 decode-JSON 这个参数, 但依然可以将整个 args 传给它.
- (declare (ignorable content))
- (multiple-value-bind (bytes code headers)
- (apply #'drakma:http-request uri
- :allow-other-keys t
- :method :get
- args)
- (declare (ignorable code))
- (let ((content-type (cdr (assoc :content-type headers)))
- (text (flexi-streams:octets-to-string bytes)))
- (if (and decode-JSON
- (str:starts-with-p "application/json" content-type))
- (jonathan:parse text)
- text))))
不愧是 Dio Common Lisp, 轻易就做到了我们做不到的事情.
题外话
曾几何时, Python 程序员还会津津乐道于 Python 之禅中的 There should be one-- and preferably only one --obvious way to do it., 但其实 Python 光是在定义一个函数的参数方面就有五花八门的写法了. 甚至在写这篇文章的过程中, 我才知道原来 Python 的参数列表中可以通过写上 / 来使其左侧的参数都成为 positional-only 的参数.
- def foo1(a, b): pass
- def foo2(a, /, b): pass
- foo1(a=1, b=2)
- foo2(a=1, b=2) # 会抛出异常, 因为 a 只能按位置来传参.
阅读原文
来源: https://segmentfault.com/a/1190000040507607