Enable the ++x and --x expressions in Python
Project description
plusplus
Enable the ++x
and --x
expressions in Python
What's this?
By default, Python does not support neither pre-increments (like ++x
) nor post-increments (like x++
).
However, the first ones are syntactically correct since Python parses them as two subsequent +x
operations,
where +
is the unary plus operator
(same with --x
and the unary minus).
They both have no effect, since in practice -(-x) == +(+x) == x
.
This module turns the ++x
-like expressions into x += 1
at the bytecode level.
Increments and decrements of collection items and object attributes are supported as well, for example:
dictionary = {'key': 42}
assert ++dictionary['key'] == 43
Unlike x += 1
, ++x
is still an expression, so it works fine inside other expressions, if
/while
conditions, and
lambda functions:
array[++index] = new_value
if --connection.num_users == 0:
connection.close()
increment_and_return = lambda x: ++x
See tests for more sophisticated examples.
Why?
This module is made for fun, as a demonstration of Python flexibility. I agree that enabling increments in real projects may be risky: the code may become less readable, confuse new developers, and behave differently in new environments without this module.
However, there are a few situations where increments make code simpler and more readable. To demonstrate them, I list a number of real code snippets from the Python standard library here (instead of making up toy examples that may be unrealistic).
Also, having the increment expressions seems consistent with
PEP 572 "Assignment Expressions"
that introduced the x := value
expressions in Python 3.8+.
They can be used inside if
/while
conditions and lambda functions as well.
How it works?
Patching bytecode
Python compiles all source code to a low-level bytecode executed on the Python's stack-based virtual machine. Each bytecode instruction consumes a few items from the stack, does something with them, and pushes the results back to the stack.
The ++x
expressions are compiled into two consecutive
UNARY_POSITIVE
instructions
that do not save the intermediate result in between (same with --x
and two
UNARY_NEGATIVE
instructions).
No other expressions produce a similar bytecode pattern.
plusplus
replaces these patterns with the bytecode for x += 1
, then adds the bytecode for storing
the resulting value to the place where the initial value was taken.
This is what happens for the y = ++x
line:
A similar but more complex transformation happens for the code with subscription expressions
like value = ++dictionary['key']
. We need the instructions from the yellow boxes to save the initial location and
recall it when the increment is done (see the explanation below):
This bytecode is similar to what the string dictionary['key'] += 1
compiles to. The only difference is that it
keeps an extra copy of the incremented value,
so we can return it from the expression and assign it to the value
variable.
Arguably, the least clear part here is the second yellow box. Actually, it is only needed to reorder
the top 4 items of the stack. If we need to reorder the top 2 or 3 items of the stack, we can just use
the ROT_TWO
and
ROT_THREE
instructions (they do a circular shift
of the specified number of items of the stack). If we had a ROT_FOUR
instruction, we would be able to just
replace the second yellow box with two ROT_FOUR
s to achieve the desired order.
However, ROT_FOUR
was removed in Python 3.2
(since it was rarely used by the compiler) and
recovered back only in Python 3.8. If we want to support Python 3.3 - 3.7, we need to use a workaround,
e.g. the BUILD_TUPLE
and
UNPACK_SEQUENCE
instructions.
The first one replaces the top N items of the stack with a tuple made of these N items. The second unpacks the tuple
putting the values on the stack right-to-left, i.e. in reverse order. We use them to reverse the top 4 items,
then swap the top two to achieve the desired order.
The @enable_increments decorator
The first way to enable the increments is to use a decorator that would patch the bytecode of a given function.
The decorator disassembles the bytecode, patches the patterns described above, and recursively calls itself for any nested bytecode objects (this way, the nested function and class definitions are also patched).
The bytecode is disassembled and assembled back using the MatthieuDartiailh/bytecode library.
Enabling increments in the whole package
The Python import system allows loading modules not only from files but from any reasonable place (e.g. there was a module that enables importing code from Stack Overflow answers). The only thing you need is to provide module contents, including its bytecode.
We can leverage this to implement a wrapping loader that imports the module as usual but patching its bytecode as described above. To do this, we can create a new MetaPathFinder and install it to sys.meta_path.
Why not just override unary plus operator?
Overriding operators via magic methods
(such as __pos__()
and
__neg__()
)
do not work for built-in Python types like int
, float
, etc.
In contrast, plusplus
works with all built-in and user-defined types.
Caveats
-
pytest
does its own bytecode modifications in tests, adding the code to save intermediate expression results to theassert
statements. This is necessary to show these results if the test fails (see pytest docs).By default, this breaks the
plusplus
patcher because the twoUNARY_POSITIVE
instructions become separated by the code saving the result of the firstUNARY_POSITIVE
.We fix that by removing the code saving some of the intermediate results, which does not break the pytest introspection.
How to use it?
You can install this module with pip:
pip install plusplus
For a particular function or method
Add a decorator:
from plusplus import enable_increments
@enable_increments
def increment_and_return(x):
return ++x
This enables increments for all code inside the function, including nested function and class definitions.
For all code in your package
In package/__init__.py
, make this call before you import submodules:
from plusplus import enable_increments
enable_increments(__name__)
# Import submodules here
...
This enables increments in the submodules, but not in the package/__init__.py
code itself.
See also
- cpmoptimize — a module that optimizes a Python code calculating linear recurrences, reducing the time complexity from O(n) to O(log n).
- dontasq — a module that adds functional-style methods
(such as
.where()
,.group_by()
,.order_by()
) to built-in Python collections.
Authors
Copyright © 2021 Alexander Borzunov
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.