预计阅读本页时间:-
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
广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元
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):
- At the call, all positional arguments appear before all keyword arguments.
- In the def, all nondefault arguments appear before all default arguments.
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:
- All N passed positional arguments in *pargs must match the first N expected arguments obtained from the function’s code object. This is true per Python’s call ordering rules, outlined earlier, since all positionals precede all keywords.
- To obtain the names of arguments actually passed by position, we can slice the list of all expected arguments up to the length N of the *pargs positionals tuple.
- Any arguments after the first N expected arguments either were passed by keyword or were defaulted by omission at the call.
- For each argument name to be validated, if it is in **kargs it was passed by name, and if it is in the first N expected arguments it was passed by position (in which case its relative position in the expected list gives its relative position in *pargs); otherwise, we can assume it was omitted in the call and defaulted and need not be checked.
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.