
On Sun, Oct 31, 2021 at 4:15 PM Steven D'Aprano <steve@pearwood.info> wrote:
On Sun, Oct 31, 2021 at 03:43:25PM +1100, Chris Angelico wrote:
There is a downside: it is possible to flat-out lie to the interpreter, by mutating bisect_left.__defaults__, so that help() will give a completely false signature.
>>> def func(arg="finest green eggs and ham"): ... pass ... >>> inspect.signature(func) <Signature (arg='finest green eggs and ham')> >>> >>> func.__defaults__ = ("yucky crap",) >>> inspect.signature(func) <Signature (arg='yucky crap')>
If help, or some other tool is caching the function signature, perhaps it shouldn't :-)
Yep, but with late-bound defaults, there is a slight difference. With early-bound ones, you do have a guarantee that the signature and the behaviour are synchronized; with late-bound, the behaviour is encoded in the function, and the signature has (or will have, once I write that part) some sort of snapshot, either the AST or a source code snippet. (At the moment, they all just show Ellipsis.) So you could reach in and replace the __defaults_extra__ and change how the signature looks:
def foo(a=[], b=>[]): ... ... dis.dis(foo) 1 0 QUERY_FAST 1 (b) 2 POP_JUMP_IF_TRUE 4 (to 8) 4 BUILD_LIST 0 6 STORE_FAST 1 (b) >> 8 LOAD_CONST 0 (None) 10 RETURN_VALUE foo.__defaults__ ([], Ellipsis) foo.__defaults_extra__ (None, '')
The first slot of __defaults_extra__ indicates that the first default is early-bound, but the second one will be the description - "[]" - that would get used in inspect/help. Replacing that would let you change what the default appears to be. I don't think this is a major problem. It's no worse than other things you can mess with, and if you do that sort of thing, you get only what you asked for; there's no way you can get a segfault or even an exception, as long as you use either None or a string. (I should probably have some validation to make sure that those are the only two types in the tuple. Will jot that down as a TODO.) Changing whether the extra slot is None or not is amusing, though still not particularly significant. If you have an early-bound default of Ellipsis, changing extra from None to a string will pretend that it's a late-bound with that value. (If the default value isn't Ellipsis, then according to the spec, the extra should be ignored; but it's possible that some tools will end up using extra first, which would mean they'd be deceived regardless of the actual value.) The behaviour will actually be UnboundLocalError, but inspecting the signature would show the claimed value. On the flip side, if you have a late-bound default and change the extra from a string to None, it will turn it into an early default of Ellipsis, with a small amount of dead code at the start of the function. All of this is implementation details though. What I'll document is that changing __defaults_extra__ requires a tuple of Nones and/or strings (and __kwdefaults_extra__ requires a dict mapping strings to None and/or strings), and I won't recommend actually changing it.
But if you want to shoot yourself in the foot, there are already plenty of gorgeous guns available.
Indeed. Beyond avoiding segmentation faults, I don't think we need to care about people who mess about with the public attributes of functions. You can touch, you can even change them, but keeping the function working is no longer our responsibility at that point.
If you change the defaults, you shouldn't get a seg fault when you call the function, but you might get an exception.
Exactly, and that's what happens. I suppose in some cases it might be nice to get the exception when you assign to __defaults_extra__, but it's not that big a deal if it results in an exception when you call the function. Generally, I would expect that most uses of these dunders will be read-only, or copying them from some other function. Not a lot else. ChrisA