I tried to use dis module to print the opcode for explaining the difference between “a = a + b” and “a += b”, but sadly I got the same one: “BINARY_OP”.
It seems that displaying opcodes like INPLACE_ADD for __iadd__() and BINARY_ADD for __add__() is far more understandable than the only BINARY_OP.
AFAIK, INPLACE_ADD and BINARY_ADD just don’t exists as opcodes in the current version. They all got folded into BINARY_OP. dis does display the operation that is performed based on the argument for the opcode (i.e. 0 -> +, 13 -> +=) Otherwise there is no difference between the two. Or what do you mean?
I mean the two operations of + and += are very different from each other, the different opcode did indicate this difference.
def test_add():
l = [1, 2]
print(id(l))
l = l + [3, 4]
print(id(l))
def test_inplace_add():
l = [1, 2]
print(id(l))
l += [3, 4]
print(id(l))
if __name__ == "__main__":
# import dis
print("--- test for __add__ ---")
test_add()
print("--- test for __iadd__ ---")
test_inplace_add()
then the results respectively
--- test for __add__ ---
4543449408
4543807232
--- test for __iadd__ ---
4543807232
4543807232
Now they are scaffolded into BINARY_ADD. the difference was weaken.
I knew the different opcode(0 for +/13 for +=). Like I said above, the scaffolding may weaken the difference between add and inplace add.
I noticed these dramatical changes in ceval.c, the huge switch was moved into bytecodes.c, and many opcodes was grouped for better performance.
In terms of BINARY_OP:
switch (opcode) {
...
family(BINARY_OP, INLINE_CACHE_ENTRIES_BINARY_OP) = {
BINARY_OP_MULTIPLY_INT,
BINARY_OP_ADD_INT,
BINARY_OP_SUBTRACT_INT,
BINARY_OP_MULTIPLY_FLOAT,
BINARY_OP_ADD_FLOAT,
BINARY_OP_SUBTRACT_FLOAT,
BINARY_OP_ADD_UNICODE,
// BINARY_OP_INPLACE_ADD_UNICODE, // See comments at that opcode.
};
...
op(_BINARY_OP_INPLACE_ADD_UNICODE, (left, right --)) {
_Py_CODEUNIT true_next = next_instr[INLINE_CACHE_ENTRIES_BINARY_OP];
assert(true_next.op.code == STORE_FAST);
PyObject **target_local = &GETLOCAL(true_next.op.arg);
DEOPT_IF(*target_local != left, BINARY_OP);
STAT_INC(BINARY_OP, hit);
/* Handle `left = left + right` or `left += right` for str.
*
* When possible, extend `left` in place rather than
* allocating a new PyUnicodeObject. This attempts to avoid
* quadratic behavior when one neglects to use str.join().
*
* If `left` has only two references remaining (one from
* the stack, one in the locals), DECREFing `left` leaves
* only the locals reference, so PyUnicode_Append knows
* that the string is safe to mutate.
*/
assert(Py_REFCNT(left) >= 2);
_Py_DECREF_NO_DEALLOC(left);
PyUnicode_Append(target_local, right);
_Py_DECREF_SPECIALIZED(right, _PyUnicode_ExactDealloc);
ERROR_IF(*target_local == NULL, error);
// The STORE_FAST is already done.
SKIP_OVER(INLINE_CACHE_ENTRIES_BINARY_OP + 1);
}
...
}
I propose to add a switch like show_family=True to show the family detail like BINARY_OP, just like the show_caches=True did.
The opcode is BINARY_OP, the argument is 0 or 13. The dis module should not display anything different here, it should displaying exactly what is being executed.
Ok, but then you are wrong. The term “opcode” has a clear and well defined meaning here. For example, check the members of opcode.opmap. IMO it would be very confusing for dis.dis to display stuff that isn’t the opcode in the slot where the opcode normally goes, even if it’s behind an option. Nothing stops you from implementing your own display code for the opcode lists.