New operators in Python

前端 未结 3 1133
离开以前
离开以前 2021-01-13 05:58

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 defi

3条回答
  •  挽巷
    挽巷 (楼主)
    2021-01-13 06:08

    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: () 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 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)
    

提交回复
热议问题