New operators in Python

Question:

We can define intrinsic operators of Python as stated here. Just for curiosity, can we define new operators like $ or ***? (If so, then we can define ternary condition operators or rotate operators.)

Asked By: hola

||

Answers:

No, you can’t define new operators in Python.

Answered By: Ry-

As @minitech said you can’t define new operators. But check this hack that allows you to define infix operators http://code.activestate.com/recipes/384122-infix-operators/

Answered By: fasouto

Expanding on @fasouto answer, but adding a bit more code.

While you cannot define new operators AND you cannot redefine existing operators for built-in types, what you can do is to define a class (instantiated to any valid Python name, e.g. op) that act as an intermediate binding for two objects, thus effectively looking like a binary infix operator:

a | op | b

Non-binding Implementation

In short, one can define a class overriding forward and backward methods for an operator, e.g. __or__ and __ror__ for the | operator:

class Infix:
    def __init__(self, function):
        self.function = function
    def __ror__(self, other):
        return Infix(lambda x, self=self, other=other: self.function(other, x))
    def __or__(self, other):
        return self.function(other)
    def __call__(self, value1, value2):
        return self.function(value1, value2)

This can be used directly:

op = Infix(lambda a, b: a + b)  # can be any bivariate function

1 | op | 2
# 3

or as a decorator:

@Infix
def op(a, b):
    return a + b

1 | op | 2
# 3

The above solution works as is, but there are some issues, e.g. op | 2 expression cannot be used alone:

op = Infix(lambda a, b: a + b)
(1 | op)
#<__main__.Infix object at 0x7facf8f33d30>

# (op | 2)
# TypeError: <lambda>() missing 1 required positional argument: 'b'

(1 | op | 2)
# 3

Binding Implementation

To get proper bindings one would need to write a bit more complex code performing an intermediate binding:

class Infix(object):
    def __init__(self, func):
        self.func = func

    class RBind:
        def __init__(self, func, binded):
            self.func = func
            self.binded = binded
        def __call__(self, other):
            return self.func(other, self.binded)
        __ror__ = __call__

    class LBind:
        def __init__(self, func, binded):
            self.func = func
            self.binded = binded
        def __call__(self, other):
            return self.func(self.binded, other)
        __or__ = __call__

    def __or__(self, other):
        return self.RBind(self.func, other)

    def __ror__(self, other):
        return self.LBind(self.func, other)

    def __call__(self, value1, value2):
        return self.func(value1, value2)

This is used the same way as before, e.g. either:

op = Infix(lambda a, b: a + b)

or as a decorator:

@Infix
def op(a, b):
    return a + b

With this, one would get:

1 | op
# <__main__.Infix.LBind object at 0x7facf8f2b828>

op | 2
# <__main__.Infix.RBind object at 0x7facf8f2be10>

1 | op | 2
# 3

There is also a PyPI package (with which I have no affiliation) implementing substantially this: https://pypi.org/project/infix/

Timings

Incidentally, the binding solutions seems to be also marginally faster:

%timeit [1 | op | 2 for _ in range(1000)]

# Non-binding implementation
# 1000 loops, best of 3: 626 µs per loop

# Binding implementation
# 1000 loops, best of 3: 525 µs per loop

Notes

These implementations use |, but any binary operator could be used:

  • +: __add__
  • -: __sub__
  • *: __mul__
  • /: __truediv__
  • //: __floordiv__
  • %: __mod__
  • **: __pow__
  • @: __matmul__ (for Python 3.5 onwards)
  • |: __or__
  • &: __and__
  • ^: __xor__
  • >>: __rshift__
  • <<: __lshift__

The ** would require the binding implementation or adjusting the non-binding one to reflect that the operator is right-associative. All the other operators from above are either left-associative (-, /, //, %, @, >>, <<) or directly commutative (+, *, |, &, ^).

Remember that these would all have the same precedence as the normal Python operators, hence, e.g.:

(1 | op | 2 * 5) == (1 | op | (2 * 5)) != ((1 | op | 2) * 5)
Answered By: norok2

Yes, you can do it, you can add and/or change keywords, modify syntax and anything else you want.

Its Python, you can do whatever you want, you are only limited by your imagination.

Efficient way would of course be to modify the interpreter and compile it to include your operators. But as we want them added dynamically, what you do is the following:

You create a module that you import at the top of your program, this module intercepts execution of the rest of your code, instead it does re-analysis of your code, makes necessary changes to make your new syntax understandable to Python, then it executes the changed version instead of your program.

If you want it to work on scripts only, its not so hard. You can
do it with search and replace of the text code, you replace your operator with the function version of it your module has, then you use compile() to compile the correct code and you point the interpreter to that code object.

One example of doing this is the goto joke, which is a module that adds goto functionality to Python, so take a look to see how you dynamically inject changed code into the program flow.

If you want to support your new operators in byte compiled files as well, well, this is a bit harder.
You will have to use ast module to add your operator into the abstract syntax tree, describe how it translates into the bytecode using either precompiled sequences of code or other hacks, then pass the AST to the compile() function and force it to translate to bytecode.

There is a question of tracebacks as well, i.e. how do you want to present the exceptions when something is wrong with e.g. operands. You will have to write the correct line number that corresponds to the line in your code, not the one actually being interpreted.

The real question here is not whether it can be done, but whether it is affordable. If it is very important to you, you will do it my way, if you want to save yourself from headaches, you will use intermediate object as an operator, as presented here in other answers. If it is extremely important to you, then you will go about making the changes to Python itself, but then, your code will be useless on any other Python interpreter.

Answered By: Dalen