同步阅读进度,多语言翻译,过滤屏幕蓝光,评论分享,更多完整功能,更好读书体验,试试 阅读 ‧ 电子书库
Implementation Details
This decorator’s code relies on both introspection APIs and subtle constraints of argument passing. To be fully general we could in principle try to mimic Python’s argument matching logic in its entirety to see which names have been passed in which modes, but that’s far too much complexity for our tool. It would be better if we could somehow match arguments passed by name against the set of all expected arguments’ names, in order to determine which position arguments actually appear in during a given call.
Function introspection
It turns out that the introspection API available on function objects and their associated code objects has exactly the tool we need. This API was briefly introduced in Chapter 19, but we’ll actually put it to use here. The set of expected argument names is simply the first N variable names attached to a function’s code object:
# In Python 3.0 (and 2.6 for compatibility):
>>> def func(a, b, c, d):
... x = 1
... y = 2
...
>>> code = func.__code__ # Code object of function object
>>> code.co_nlocals
6
>>> code.co_varnames # All local var names
('a', 'b', 'c', 'd', 'x', 'y')
>>> code.co_varnames[:code.co_argcount] # First N locals are expected args
('a', 'b', 'c', 'd')
>>> import sys # For backward compatibility
>>> sys.version_info # [0] is major release number
(3, 0, 0, 'final', 0)
>>> code = func.__code__ if sys.version_info[0] == 3 else func.func_code
The same API is available in older Pythons, but the func.__code__ attribute is spelled as func.func_code in 2.5 and earlier (the newer __code__ attribute is also redundantly available in 2.6 for portability). Run a dir call on function and code objects for more details.
Argument assumptions
Given this set of expected argument names, the solution relies on two constraints on argument passing order imposed by Python (these still hold true in both 2.6 and 3.0):
That is, a nonkeyword argument cannot generally follow a keyword argument at a call, and a nondefault argument cannot follow a default argument at a definition. All “name=value” syntax must appear after any simple “name” in both places.
To simplify our work, we can also make the assumption that a call is valid in general—i.e., that all arguments either will receive values (by name or position), or will be omitted intentionally to pick up defaults. This assumption won’t necessarily hold, because the function has not yet actually been called when the wrapper logic tests validity—the call may still fail later when invoked by the wrapper layer, due to incorrect argument passing. As long as that doesn’t cause the wrapper to fail any more badly, though, we can finesse the validity of the call. This helps, because validating calls before they are actually made would require us to emulate Python’s argument-matching algorithm in full—again, too complex a procedure for our tool.
Matching algorithm
Now, given these constraints and assumptions, we can allow for both keywords and omitted default arguments in the call with this algorithm. When a call is intercepted, we can make the following assumptions:
In other words, we can skip tests for arguments that were omitted in a call by assuming that the first N actually passed positional arguments in *pargs must match the first N argument names in the list of all expected arguments, and that any others must either have been passed by keyword and thus be in **kargs, or have been defaulted. Under this scheme, the decorator will simply skip any argument to be checked that was omitted between the rightmost positional argument and the leftmost keyword argument, between keyword arguments, or after the rightmost positional in general. Trace through the decorator and its test script to see how this is realized in code.
请支持我们,让我们可以支付服务器费用。
使用微信支付打赏