Home > Net >  Python code object, function, and default parameters
Python code object, function, and default parameters

Time:05-28

Investigation

I have this program

def gen_baz():
    return 2

def gen_bar(a, b, c=1):
    return a, b, c

# ---------- #

# Save the code objects
c1 = gen_bar.__code__
c2 = gen_baz.__code__

dis.dis(gen_bar) # [Disassambly 1]
print(inspect.getsource(gen_bar)) # [Inspect 1]

bar = gen_bar(1, 2) # bar: (1, 2, 1)
baz = gen_baz() # baz: 2
print(f"before: bar={bar}, baz={baz}")

# --------- #

gen_bar.__code__, gen_baz.__code__ = c2, c1 # swap code objects between 2 functions

dis.dis(gen_baz) # [Disassambly 2]
print(inspect.getsource(gen_baz))  # [Inspect 1]

bar = gen_bar() # bar: 2
baz = gen_baz(1, 2) # baz: [Error 1]
print(f"after: bar={bar}, baz={baz}")

Here are the outputs with Disassembly and Inspect

[Disassembly 1 & 2]
 17           0 LOAD_FAST                0 (a)
              2 LOAD_FAST                1 (b)
              4 LOAD_FAST                2 (c)
              6 BUILD_TUPLE              3
              8 RETURN_VALUE

[Inspect 1 & 2]
def gen_bar(a, b, c=1):
    return a, b, c

[Error 1]
Traceback (most recent call last):
  File "C:\Users\something\test.py", line 32, in <module>
    baz = gen_baz(1, 2)
TypeError: gen_baz() missing 1 required positional argument: 'c'

Question

  1. Why after I swapped the code objects between baz and bar, the program crashed? Even though they have identical byte code and inspect result
  2. Where is the default parameter in python stored? Does it come along with the function object but not the code object? If so, then how does Inspect module gives me c=1 on the second inspection?

Thanks a lot!

CodePudding user response:

Default values are not stored in the code object. They are stored directly in the function object.

>>> gen_bar.__defaults__
(1,)

The text returned by getsource comes, as implied by the name, from the source code itself, not the object generated by the source code. (At the object level, the signature is only implied by the values the function tries to load from the stack, global namespace, etc.)

You can use inspect.signature to see that gen_baz now takes three arguments, but that does not include the default value, which was not transferred to gen_baz along with the code object.

>>> inspect.signature(gen_baz)
<Signature (a, b, c)>

The use of a default argument value is buried in the evaluation of the CALL_FUNCTION opcode. Given

def foo(a=1):
    return a

You can see that neither its byte code

>>> dis.dis(foo)
  2           0 LOAD_FAST                0 (a)
              2 RETURN_VALUE

nor calls with or without explicit arguments

>>> dis.dis('foo(9)')
  1           0 LOAD_NAME                0 (foo)
              2 LOAD_CONST               0 (9)
              4 CALL_FUNCTION            1
              6 RETURN_VALUE
>>> dis.dis('foo()')
  1           0 LOAD_NAME                0 (foo)
              2 CALL_FUNCTION            0
              4 RETURN_VALUE

make use of the function's __defaults__ attribute. In the function, it is simply assumed that some value can pushed on to the stack from the local variable a, regardless of how that variable is set. In both calls, a value is simply loaded onto the stack or not before using CALL_FUNCTION.

CodePudding user response:

Check the docs:

  • __code__: code object containing compiled function bytecode
  • __defaults__: tuple of any default values for positional or keyword parameters
  • __kwdefaults__: mapping of any default values for keyword-only parameters
  • Related