Skip to content
Advertisement

New operators in Python

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

Advertisement

Answer

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)
User contributions licensed under: CC BY-SA
7 People found this is helpful
Advertisement