共计 3893 个字符,预计需要花费 10 分钟才能阅读完成。
序言
家喻户晓,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 只能按地位来传参。
浏览原文