Unravelling unary arithmetic operators
In this entire blog series on Python's syntactic sugar, this might end up being the most boring post. ๐ We will cover the unary arithmetic operators: -
, +
, and ~
(inversion if you don't happen to be familiar with that last operator). Due to the fact that there is only a single object being involved, it's probably the most straightforward syntax to explain in Python.
The example we are going to use in this post is ~ a
.
What the data model says
If you look at the data model, you will see the documentation says that for inversion the method name is __invert__
(and the other operators) and has following details:
Called to implement the unary arithmetic operations (-
,+
,abs()
and~
).
That's it. That is literally all of the documentation for unary arithmetic operators in Python's data model. Now is that an over-simplification, or is it actually as simple as it sounds?
Looking at the bytecode
Let's look at what CPython executes for ~ a
:
It looks like UNARY_INVERT
is the opcode we care about here.
Looking at the C code
The opcode
Diving into Python/ceval.c
, you can see how UNARY_INVERT
is implemented:
The C API
The opcode implementaiton seems simple and PyNumber_Invert()
is the key function here.
What this code does is:
- Gets the
__invert__
method off ย of the object's type (which is typical for Python's data model; this is for performance as it bypasses descriptors and other dynamic attribute mechanisms) - If the method is there then call it and return its value
- Otherwise raise a
TypeError
Implementing in Python
In Python this all looks like:
import desugar.builtins as debuiltins
def __invert__(object_: Any, /) -> Any:
"""Implement the unary operator `~`."""
type_ = type(object_)
try:
unary_method = debuiltins._mro_getattr(type_, "__invert__")
except AttributeError:
raise TypeError(f"bad operand type for unary ~: {type_!r}")
else:
return unary_method(object_)
This can be generalized to create a closure for an arbitrary unary operation like so:
And that's it! As always, the code in this post can be found in my desugar project.
Aside: a bit of method name history
You may have noticed that for inversion there are four function names in the operator module:
inv
__inv__
invert
__invert__
It turns out that in Python 2.0, the special/magic method for inversion was renamed from the first two to the last two (I assume to be more self-explanatory). For backwards-compatibility the older names were left in the operator module.