Enable ++x and --x expressions in Python

Overview

plusplus

PyPI version CI status Codecov

Enable ++x and --x expressions in Python

What's this?

By default, Python supports 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}
++dictionary['key']
assert dictionary['key'] == 43

Unlike x += 1, ++x is still an expression, so the increments work fine inside other expressions, if/while conditions, lambda functions, and list/dict comprehensions:

array[++index] = new_value

if --connection.num_users == 0:
    connection.close()

button.add_click_callback(lambda: ++counter)

index = 0
indexed_cells = {++index: cell for row in table for cell in row}

See tests for more sophisticated examples.

[How it works] [Installation]

Why?

This module is made for fun, as a demonstration of Python flexibility and bytecode manipulation techniques. Note that enabling increments in real projects may be risky: such code may confuse new developers and behave differently if copied to environments without the plusplus module. Also, this feature gives more opportunities to write unreadable code in general.

Nevertheless, there are situations where increments (if used with care) may allow to avoid repetitions or make code more readable. Some of them are listed here with the examples from the source code of the Python standard library.

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_FOURs 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.

[Source code]

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.

[Source code]

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.

[Source code]

Why not just override unary plus operator?

  • This way, it would be impossible to distinguish applying two unary operators consequently (like ++x) from applying them in separate places of a program (like in the snippet below). It is important to not change behavior in the latter case.

    x = -value
    y = -x
  • 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 the assert statements. This is necessary to show these results if the test fails (see pytest docs).

    By default, this breaks the plusplus patcher because the two UNARY_POSITIVE instructions become separated by the code saving the result of the first UNARY_POSITIVE.

    We fix that by removing the code saving some of the intermediate results, which does not break the pytest introspection.

    [Source code]

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.

Other ideas

The same approach could be used to implement the assignment expressions for the Python versions that don't support them. For example, we could replace the x <-- value expressions (two unary minuses + one comparison) with actual assignments (setting x to value).

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

Owner
Alexander Borzunov
Building hivemind for @learning-at-home // ex⁠-⁠research engineer at Yandex Self-Driving, ex⁠-⁠intern at Facebook
Alexander Borzunov
tade is a discussion/forum/link aggregator application. It provides three interfaces: a regular web page, a mailing list bridge and an NNTP server

tade is a discussion/forum/link aggregator application. It provides three interfaces: a regular web page, a mailing list bridge and an NNTP server

Manos Pitsidianakis 23 Nov 04, 2022
Blender 2.93 addon for loading Quake II MD2 files

io_mesh_md2 is a Blender 2.93 addon for importing Quake II MD2 files.

Joshua Skelton 11 Aug 31, 2022
A Python utility belt containing simple tools, a stdlib like feel, and extra batteries. Hashing, Caching, Timing, Progress, and more made easy!

Ubelt is a small library of robust, tested, documented, and simple functions that extend the Python standard library. It has a flat API that all behav

Jon Crall 638 Dec 13, 2022
Check username

Checker-Oukee Check username It checks the available usernames and creates a new account for them Doesn't need proxies Create a file with usernames an

4 Jun 05, 2022
Python based tool to extract forensic info from EventTranscript.db (Windows Diagnostic Data)

EventTranscriptParser EventTranscriptParser is python based tool to extract forensically useful details from EventTranscript.db (Windows Diagnostic Da

P. Abhiram Kumar 24 Nov 18, 2022
Aggregating gridded data (xarray) to polygons

A package to aggregate gridded data in xarray to polygons in geopandas using area-weighting from the relative area overlaps between pixels and polygons.

Kevin Schwarzwald 42 Nov 09, 2022
Small Python script to parse endlessh's output and print some neat statistics

endlessh_parser endlessh_parser is a small Python script that parses endlessh's output and prints some neat statistics about it Usage Install all the

ManicRobot 1 Oct 18, 2021
A Python class for checking the status of an enabled Minecraft server

mcstatus provides an easy way to query Minecraft servers for any information they can expose. It provides three modes of access (query, status and ping), the differences of which are listed below in

Nathan Adams 1.1k Jan 06, 2023
Color box that provides various colors‘ rgb decimal code.

colorbox Color box that provides various colors‘ rgb decimal code

1 Dec 07, 2021
Skywater 130nm Klayout Device Generators PDK

Skywaters 130nm Technology for KLayout Device Generators Mabrains is excited to share with you our Device Generator Library for Skywater 130nm PDK. It

Mabrains 18 Dec 14, 2022
a demo show how to dump lldb info to ida.

用一个demo来聊聊动态trace 这个仓库能做什么? 帮助理解动态trace的思想。仓库内的demo,可操作,可实践。 动态trace核心思想: 动态记录一个函数内每一条指令的执行中产生的信息,并导入IDA,用来弥补IDA等静态分析工具的不足。 反编译看一下 先clone仓库,把hellolldb

25 Nov 28, 2022
Retrying library for Python

Tenacity Tenacity is an Apache 2.0 licensed general-purpose retrying library, written in Python, to simplify the task of adding retry behavior to just

Julien Danjou 4.3k Jan 05, 2023
Standard implementations of FedLab and its provided benchmarks.

FedLab-benchmarks This repo contains standard implementations of FedLab and its provided benchmarks. Currently, following algorithms or benchrmarks ar

SMILELab-FL 104 Dec 05, 2022
ULID implementation for Python

What is this? This is a port of the original JavaScript ULID implementation to Python. A ULID is a universally unique lexicographically sortable ident

Martin Domke 158 Jan 04, 2023
password generator

Password generator technologies used What is? It is Password generator How to Download? Download on releases Clone repo git clone https://github.com/m

1 Dec 16, 2021
Python tool to check a web applications compliance with OWASP HTTP response headers best practices

Check Your Head A quick and easy way to check a web applications response headers!

Zak 6 Nov 09, 2021
A way to write regex with objects instead of strings.

Py Idiomatic Regex (AKA iregex) Documentation Available Here An easier way to write regex in Python using OOP instead of strings. Makes the code much

Ryan Peach 18 Nov 15, 2021
Group imports from Windows binaries

importsort This is a tool that I use to group imports from Windows binaries. Sometimes, you have a gigantic folder full of executables, and you want t

【☆ ゆう ☆ 】 15 Aug 27, 2022
Control-Alt-Delete - Help Tux Escape Beastie's Jail!

Control-Alt-Delete Help Tux escape Beastie's jail by completing the following challenges! Challenges Challenge 00: Drinks: Tux needs to drink less. Ch

NDLUG 8 Oct 31, 2021
Modeling Category-Selective Cortical Regions with Topographic Variational Autoencoders

Modeling Category-Selective Cortical Regions with Topographic Variational Autoencoders Getting Started Install requirements with Anaconda: conda env c

T. Andy Keller 4 Aug 22, 2022