{"id":15638707,"url":"https://github.com/borzunov/plusplus","last_synced_at":"2025-04-14T00:37:47.299Z","repository":{"id":57453699,"uuid":"405489464","full_name":"borzunov/plusplus","owner":"borzunov","description":"Enables increment operators in Python using a bytecode hack","archived":false,"fork":false,"pushed_at":"2023-05-26T19:10:19.000Z","size":42,"stargazers_count":93,"open_issues_count":0,"forks_count":5,"subscribers_count":5,"default_branch":"main","last_synced_at":"2024-11-01T13:36:23.770Z","etag":null,"topics":["bytecode","decrement","increment","python"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/borzunov.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2021-09-11T21:47:21.000Z","updated_at":"2024-07-02T03:32:36.000Z","dependencies_parsed_at":"2023-10-20T18:35:10.816Z","dependency_job_id":null,"html_url":"https://github.com/borzunov/plusplus","commit_stats":{"total_commits":31,"total_committers":2,"mean_commits":15.5,"dds":"0.032258064516129004","last_synced_commit":"5e215cd294dab6d1af77f7e258a9d6c12811bd5d"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/borzunov%2Fplusplus","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/borzunov%2Fplusplus/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/borzunov%2Fplusplus/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/borzunov%2Fplusplus/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/borzunov","download_url":"https://codeload.github.com/borzunov/plusplus/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248803806,"owners_count":21164122,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bytecode","decrement","increment","python"],"created_at":"2024-10-03T11:22:33.684Z","updated_at":"2025-04-14T00:37:47.267Z","avatar_url":"https://github.com/borzunov.png","language":"Python","funding_links":[],"categories":["Python"],"sub_categories":[],"readme":"plusplus\n========\n\n[![PyPI version](https://img.shields.io/pypi/v/plusplus.svg?color=blue)](https://pypi.org/project/plusplus/)\n![Codecov](https://img.shields.io/codecov/c/github/borzunov/plusplus?token=SCAU424JFE)\n\nEnables increment operators in Python with a bytecode hack\n\nWhat's this?\n------------\n\nBy default, Python supports neither pre-increments (like `++x`) nor post-increments (like `x++`).\nHowever, the first ones are _syntactically correct_ since Python parses them as two subsequent `+x` operations,\nwhere `+` is the [unary plus operator](https://docs.python.org/3/reference/datamodel.html#object.__pos__)\n(same with `--x` and the [unary minus](https://docs.python.org/3/reference/datamodel.html#object.__neg__)).\nThey both have no effect, since in practice `-(-x) == +(+x) == x`.\n\nThis module turns the `++x`-like expressions into `x += 1` at the bytecode level.\nIncrements and decrements of collection items and object attributes are supported as well, for example:\n\n```python\nsome_dict = {'key': 42}\n++some_dict['key']\nassert some_dict['key'] == 43\n```\n\nUnlike the `x += 1` statement, `++x` remains to be an _expression_, so it works fine inside other expressions,\n`if`/`while` conditions, lambda functions, and list/dict comprehensions:\n\n```python\narray[++index] = new_value\n\nif --connection.num_users == 0:\n    connection.close()\n\nbutton.add_click_callback(lambda: ++counter)\n# No need for the `global counter` statement inside lambda\n\nindex = 0\nindexed_cells = {++index: cell for row in table for cell in row}\n```\n\nSee [tests](tests/test_plusplus.py) for more sophisticated examples.\n\n[[How it works](#how-it-works)] [[Installation](#how-to-use-it)]\n\nWhy?\n----\n\nI don't claim that allowing increments is good for real projects: such code may become less readable,\nconfuse new developers, and behave differently if copied to environments without this module.\nI've made this module for fun, as a demonstration of Python flexibility and bytecode manipulation techniques.\n\nHowever, some situations where increments simplify code do exist\n(see [examples](docs/stdlib_examples.md) from the Python's standard library).\nAlso, having the increment expressions seems consistent with\n[PEP 572 \"Assignment Expressions\"](https://www.python.org/dev/peps/pep-0572/)\nthat introduced the `x := value` expressions in Python 3.8+.\n\nHow it works?\n-------------\n\n### Patching bytecode\n\nPython compiles all source code to a low-level [bytecode](https://docs.python.org/3.7/library/dis.html)\nexecuted on the Python's stack-based virtual machine. Each bytecode instruction consumes a few items from the stack,\ndoes something with them, and pushes the results back to the stack.\n\nThe `++x` expressions are compiled into two consecutive\n[`UNARY_POSITIVE`](https://docs.python.org/3.7/library/dis.html#opcode-UNARY_POSITIVE) instructions\nthat do not save the intermediate result in between (same with `--x` and two\n[`UNARY_NEGATIVE`](https://docs.python.org/3.7/library/dis.html#opcode-UNARY_NEGATIVE) instructions).\nNo other expressions produce a similar bytecode pattern.\n\n`plusplus` replaces these patterns with the bytecode for `x += 1`, then adds the bytecode for storing\nthe resulting value to the place where the initial value was taken.\n\nThis is what happens for the `y = ++x` line:\n\n![](docs/images/plusplus_bytecode_load_fast.svg)\n\nA similar but more complex transformation happens for the code with subscription expressions\nlike `value = ++dictionary['key']`. We need the instructions from the yellow boxes to save the initial location and\nrecall it when the increment is done (see the explanation below):\n\n![](docs/images/plusplus_bytecode_binary_subscr.svg)\n\nThis bytecode is similar to what the string `dictionary['key'] += 1` compiles to. The only difference is that it\nkeeps an extra copy of the incremented value,\nso we can return it from the expression and assign it to the `value` variable.\n\nArguably, the least clear part here is the second yellow box. Actually, it is only needed to reorder\nthe top 4 items of the stack. If we need to reorder the top 2 or 3 items of the stack, we can just use\nthe [`ROT_TWO`](https://docs.python.org/3.7/library/dis.html#opcode-ROT_TWO) and\n[`ROT_THREE`](https://docs.python.org/3.7/library/dis.html#opcode-ROT_THREE) instructions (they do a circular shift\nof the specified number of items of the stack). If we had a `ROT_FOUR` instruction, we would be able to just\nreplace the second yellow box with two `ROT_FOUR`s to achieve the desired order.\n\nHowever, `ROT_FOUR` was removed in Python 3.2\n(since it was [rarely used](https://bugs.python.org/issue929502) by the compiler) and\nrecovered back only in Python 3.8. If we want to support Python 3.3 - 3.7, we need to use a workaround,\ne.g. the [`BUILD_TUPLE`](https://docs.python.org/3.7/library/dis.html#opcode-BUILD_TUPLE) and\n[`UNPACK_SEQUENCE`](https://docs.python.org/3.7/library/dis.html#opcode-UNPACK_SEQUENCE) instructions.\nThe first one replaces the top N items of the stack with a tuple made of these N items. The second unpacks the tuple\nputting the values on the stack right-to-left, i.e. _in reverse order_. We use them to reverse the top 4 items,\nthen swap the top two to achieve the desired order.\n\n[[Source code](src/plusplus/patching.py)]\n\n### The @enable_increments decorator\n\nThe first way to enable the increments is to use a decorator that would patch the bytecode of a given function.\n\nThe decorator disassembles the bytecode, patches the patterns described above, and recursively calls itself\nfor any nested bytecode objects (this way, the nested function and class definitions are also patched).\n\nThe bytecode is disassembled and assembled back\nusing the [MatthieuDartiailh/bytecode](https://github.com/MatthieuDartiailh/bytecode) library.\n\n[[Source code](src/plusplus/wrappers.py#L11)]\n\n### Enabling increments in the whole package\n\nThe Python import system allows loading modules not only from files but from any reasonable place\n(e.g. there was a [module](https://github.com/drathier/stack-overflow-import) that enables importing code\nfrom Stack Overflow answers). The only thing you need is to provide module contents, including its bytecode.\n\nWe can leverage this to implement a wrapping loader that imports the module as usual but patching its bytecode\nas described above. To do this, we can create a new\n[MetaPathFinder](https://docs.python.org/3/library/importlib.html#importlib.abc.MetaPathFinder) and install it\nto [sys.meta_path](https://docs.python.org/3/library/sys.html#sys.meta_path).\n\n[[Source code](src/plusplus/wrappers.py#L27)]\n\n### Why not just override the unary plus operator?\n\n- This way, it would be impossible to distinguish applying two unary operators consequently (like `++x`) from\n    applying them in separate places of a program (like in the snippet below).\n    It is important to not change behavior in the latter case.\n\n    ```python\n    x = -value\n    y = -x\n    ```\n\n- Overriding operators via magic methods\n    (such as [`__pos__()`](https://docs.python.org/3/reference/datamodel.html#object.__pos__) and\n    [`__neg__()`](https://docs.python.org/3/reference/datamodel.html#object.__neg__))\n    do not work for built-in Python types like `int`, `float`, etc.\n    unless you use other hacks like in [forbiddenfruit](https://github.com/clarete/forbiddenfruit) or\n    [dontasq](https://github.com/borzunov/dontasq#adding-methods-to-built-ins).\n    Using more hacks complicates porting this module to other Python versions and interpreters.\n\n- You would need to override these methods for each built-in/numpy/user-defined number type.\n    In contrast, `plusplus` works for all types automatically.\n\n### Caveats\n\n- `pytest` does its own bytecode modifications in tests, adding the code to save intermediate expression results\n    to the `assert` statements. This is necessary to show these results if the test fails\n    (see [pytest docs](https://docs.pytest.org/en/stable/assert.html#assertion-introspection-details)).\n\n    By default, this breaks the `plusplus` patcher because the two `UNARY_POSITIVE` instructions become\n    separated by the code saving the result of the first `UNARY_POSITIVE`.\n\n    We fix that by removing the code saving some of the intermediate results, which does not break\n    the pytest introspection.\n\n    [[Source code](src/plusplus/patching.py#L87)]\n\nHow to use it?\n--------------\n\nYou can install this module with pip:\n\n```\npip install plusplus\n```\n\n### For a particular function or method\n\nAdd a decorator:\n\n```python\nfrom plusplus import enable_increments\n\n@enable_increments\ndef increment_and_return(x):\n    return ++x\n```\n\nThis enables increments for all code inside the function, including nested function and class definitions.\n\n### For all code in your package\n\nIn `package/__init__.py`, make this call __before__ you import submodules:\n\n```python\nfrom plusplus import enable_increments\n\nenable_increments(__name__)\n\n# Import submodules here\n...\n```\n\nThis enables increments in the submodules, but not in the `package/__init__.py` code itself.\n\nOther ideas\n-----------\n\nThe same approach could be used to implement\nthe [assignment expressions](https://docs.python.org/3/whatsnew/3.8.html#assignment-expressions)\nfor the Python versions that don't support them.\nFor example, we could replace the `x \u003c-- value` expressions (two unary minuses + one comparison)\nwith actual assignments (setting `x` to `value`).\n\nSee also\n--------\n\n- [cpmoptimize](https://github.com/borzunov/cpmoptimize) \u0026mdash; a module that optimizes a Python code\n    calculating linear recurrences, reducing the time complexity from O(n) to O(log n).\n- [dontasq](https://github.com/borzunov/dontasq) \u0026mdash; a module that adds functional-style methods\n    (such as `.where()`, `.group_by()`, `.order_by()`) to built-in Python collections.\n\nAuthors\n-------\n\nCopyright \u0026copy; 2021 [Alexander Borzunov](https://github.com/borzunov)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fborzunov%2Fplusplus","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fborzunov%2Fplusplus","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fborzunov%2Fplusplus/lists"}