How to create operators in Python?

Asked

Viewed 883 times

10

I was searching the internet about why Python doesn’t have the operator ++ and I saw someone saying that you can create this operator.

How do you do this? How to create a Python operator?

  • Can’t just be '+= 1' ? or '+= value ', '+=' replaces '++' in certain cases

3 answers

14


There are some factors involved there - in short: it may be "possible" to create some form of "++" and "-" - but they will have limitations, and this particular operator violates one of the language’s premises - so even if it is possible, it doesn’t mean it’s a good idea.

Customization of operators

So, in parts: as it is in the other answers, in Python it is possible to define the behavior of operators existing in the classes you create for yourself.

To do so, simply put in your class some of the methods with a predefined name - as it is in Data Model of language.

This allows the objects you define, (and even classes), to do whatever they want with the operators, arithmetic or not, binaries already defined: - + * @ / // % ** ^ | & in << >> and perhaps some other I have forgotten - and allows to change the behavior for some unary operators (some of which are applied with the call of a function): not len + -, and even operators of "augmented assignment":+= -= *=`, etc... Operators that use other symbols than what are already mapped, as would be the case of new "++" and "-" cannot be defined this way.

Separation of expression and assignment

First, let’s understand why Python doesn’t have these operators to begin with: one of the premises of Python is to try to keep the code very readable and maintainable, with less error-prone. So from the beginning of Python, anything that assigns a value to a variable (assignment) is a "command" (statement), not an expression. This means that you cannot ordinarily change values of variables within an expression, either within a if or another command, within parentheses to calculate parameters for a function, or to the right of an equal sign.

To be clear, in C, and derived languages such as Java, Javascript, etc... this is valid here:

a = 0;
b = 1;
a = (b = 3) + 1;

and the third line will modify the variable b, and the a based on the new value of b. In Python this is illegal, because "=" is not an "operator", it is a "statement" and cannot be part of an expression. This requires an extra line when writing expressions of this type, on the other hand it avoids "errors" of the type:

if (user.gid = 0){
    // usuário é root, validar operação
}

See what was meant to be a test if the user id is 0, not only always works, but it turns that user into id 0 from now on, why typed = instead of ==.

So even in Augmented assignment operators - += *=, etc, which you can create for your objects - you can change the part that "makes the account" - but the assignment of the final result to the variable to the left of the operator is done by the language, and you can’t touch it. That is, in this code:

a  = MeuObjeto()
a += 5

Python will call the method MeuObjeto.__iadd__ with the parameters self (the instance itself a) and the value 5. This method does operations and returns a value - this value is assigned the variable a. If the object is "mutable" and it can do the operation "in itself" without creating a new instance, it must return self. Otherwise, it returns a new instance, the previous instance is "forgotten" and erased by the collector Garbage, and life goes on.

Nb: The "variables cannot be changed within expressions" rule is not a "magic" - it applies to the use of assignment operator syntax. Python allows most of the values it has associated with names to be changed by function calls and methods such as setattr and __setitem__ - Since these calls can be used within an expression, it is possible to change some values - with the not[exception of local and non-local variables. (For global variables, you can put, within an expression globals().__setitem__("nome_da_variavel", valor) )

Well, if you can understand why Python doesn’t allow changing values within variables within an expression, it’s easy to see that operators ++ and -- do just that - and so were not included in the language.

Immutable objects

There is still one another reason why these particular operators would not work with Python numbers: Python native numbers are immutable objects - that is, once created with a value, that value cannot be changed "inplace". The use of operators as += in a numeric variable they actually create another numeric object and assign the new object to the variable. Try doing:

a = 0
print (id(a))
a += 1
print(id(a))

In cpython interactive mode you will see distinct id’s, indicating that the name a points to different instances. This is very practical for many things in dynamic languages, allowing numeric types to be used as keys to dictionaries, etc... For calculations that use a lot of memory and need speed, you can use other types of data such as array.array and the various data structures provided by numpy to have native numeric types (with 1, 2, 4 or 8 bytes), which are changed "in place" in memory in the required operations. But an instance of an int or float in Python, as well as a string (str), are immutable.

That is, an operator ++ in a variable a could not change the number that is pointed out by a.

New entrants only for new types (classes)

Well, it must have already been understood that we can only modify the behavior of operators to new classes. For existing classes like int, float, Dict, etc..., only using things like the Forbidden Fruit, that allows you to change the native Python types by changing the process mempory and the behavior of Python’s Runtime, and then it can change the behavior of native types, as is done in the Ruby language.

If you do this in a larger program (and much more in production code), the chance that other Python modules your system uses will break, possibly with a Segmentation fault is 950%. In the next hack, we will not do that.

The scavenging! Hacking all this and doing the "++"

Realize that nothing prevents that you brood a new class that behaves as a numerical type. Simply implement the operations that your numeric type will support using the methods described in data model. Python itself implements extra numeric types like decimal.Decimal - which until recently was implemented in pure Python (now has a native code implementation for higher performance). And your numeric type can be changeable in place. To have a very indistinguishable type of other numbers, you can tag it as a subclass of numbers.Number.

To keep it simple, let’s create a type that supports operations + and - binary, but that is changeable "in place". A type of these cannot be used as dictionary key, but can be used on accounts. For this, we have to have an internal attribute that holds the real value of our number. If we implement __add__, __sub__, we can use our number with the operas + and -. If you want other operators, just implement them gradually - (__mul__, __div__, __pow__). If we implement the methods __iadd__ and __isub__ - ready, our number can be changed "inplace" with operators += and -=

class MutableNumber:
    def __init__(self, value):
        self.value = value

    def __add__(self, other):
        return MutableNumber(self.value + other)

    def __sub__(self, other):
        return MutableNumber(self.value + other)

    # define __mul__, __div__ and others if desired.

    def __iadd__(self, other):
        self.value += other
        return self

    def __isub__(self, other):
        self.value -= other
        return self

    def __repr__(self):
        return repr(self.value)

And we can test this in the interactive environment (I use ipython):

In [37]: a = MutableNumber(5)

In [38]: id(a)
Out[38]: 140138783472496

In [39]: a += 10

In [40]: a
Out[40]: 15

In [41]: id(a)
Out[41]: 140138783472496

And now the possible "hack" to implement the "++" and the "--" go through this path: the data model predicts the unary operators + and - and calls the methods __neg__ and __pos__ of the object class. The interpreter will simply call these methods twice in a row ++a, for example. Note that something like a++ will be a syntax error, or in case the code is something like a++ + b what’s going to happen is a + (++b). Why won’t the parser understand ++ he simply interprets each + as an operator.

In short, since we have our changeable type, it is enough to implement an internal state that knows whether __pos__ or __neg__ were called, and every second call changes the value.

Note further that for this to "work" cleanly well it would be necessary to have a way to detect that two calls in to __pos__ were made "in the same place". We will not do this here, so if you use +a in places other than the code, will cause the value increment. There are two ways to do this: to use "frame introspection" within the unary operators' code, and to check whether one call was in fact procoxime to another in the source code. And another is to "control" all uses of the class in all special methods (those defined in the data model) possible - any intermediate call between two calls to the unary method, reset the counter. We can do this for the methods we define, at least.

class MutableNumber:
    def __init__(self, value):
        self.value = value
        self.reset()

    def reset(self):
        self.inc_counter = 0
        self.dec_counter = 0

    def __add__(self, other):
        self.reset()
        return MutableNumber(self.value + other)

    __radd__ = __add__

    def __sub__(self, other):
        self.reset()
        return MutableNumber(self.value + other)

    def __rsub__(self, other):
        self.reset()
        return MutableNumber(other - self.value)

    # define __mul__, __div__ and others if desired.

    def __iadd__(self, other):
        self.reset()
        self.value += other
        return self

    def __isub__(self, other):
        self.reset()
        self.value -= other
        return self

    def __pos__(self):
        self.inc_counter += 1
        self.dec_counter = 0
        if self.inc_counter == 2:
            self.value += 1
            self.reset()
        return self

    def __neg__(self):
        self.dec_counter += 1
        self.inc_counter = 0
        if self.dec_counter == 2:
            self.value -= 1
            self.reset()
        return self

    def __repr__(self):
        self.reset()
        return repr(self.value)

(In this version I also implemented the __radd__ and __rsub__ - "right" versions for + e operations - this allows these numbers to be on either side of the binary operator).

And voila:

In [53]: a = MutableNumber(0)

In [54]: a
Out[54]: 0

In [55]: b = 2 + ++a

In [56]: b
Out[56]: 3

In [57]: a
Out[57]: 1

Remembering that just extend the implementation to have __mul__, __rmul__ and other operators to account for self.value as above and this class can be used as a normal numeric type. In particular implement also __int__ and __float__ returning __self.value__.

(as author, I declare for the proper purpose that this code is under the standard stackoverflow license, or can be used under LGPL v3.0)

  • By way of curiosity, I found the answer excellent. I think it is valid to comment that an implementation like this can occur a strange behavior, for calling +++a and then +a, the value of a will be incremented twice (https://repl.it/NVAB/0), although the "operator" ++ has been explicitly called only in +++a. Would there be some way to stop it?

  • oi Anderson - see the code there: this implementation takes care of this. It resets the counter after increment, there are no two increments with three unary operators.

  • ah yes, the third "+" increments the "a" another "half time". The only way to avoid this is to have insight into the source code from which the call was made - there’s a paragraph talking about it above. `sys. _getframe(). f_back, and then the attribute f_lineno gives the line number, and the attribute . f_code.co_filename the path to the source file. Voce opens the file and parses to see how the "++" are being used. .

  • Yes, it was :D It was just a test that came up reading the answer and I was curious how much. I had also thought about introspection, but I thought there might be another simpler way.

  • No - one thing that when it hit me was kind of that "wow" is that for any object in Python to be "consumed", it has to go through one of the magic methods. Be it representation, serialization, account...etc. . so if you put a call to reset in all methods (magicians and others you have in class), you close the thing well. But the "half increment" on separate lines, without any intermediate operation, it does not close.

9

Language does not provide this capability. It is possible to create a specific behavior for existing operators (see Math reply), but it would not allow creating the ++. The answer from jsbueno shows an interesting curiosity, but do not do this, is to look for scabies to scratch. If it was not enough that language deliberately did not want to have this operator, worse still force his creation with tricks to trick the parser.

Be careful not to abuse the operator overload. Never do anything in it other than the semantics expected for it. So don’t make a + or += behave as if it were a ++.

As far as I know it is not possible to modify the operators' behavior for primitive types of language, such as integers.

There are many questions whether the operators ++ and -- are good. I like them and knowing how to use it is okay, but they are not so intuitive for many people and few know all the characteristics and consequences of them. Probably why the language preferred to leave them out.

When you see somewhere you should ask for reasons.

If you put it where you saw it maybe I can improve the answer.

8

In Python you cannot create operators, but you can overwrite them, for that you need to reimplementate the magical methods also known as dunders (abbreviation of double underlines). See the list of all special methods on data model of the language.

An example of class overriding the addition method:

class Sobrecarga:
    val = 0

    def __init__(self, val):
        self.val = val

    def __str__(self):
        return '{}'.format(self.val)

    def __add__(self, other):
        if other == '+':
            self.val += 1
        else:
            return self.val + other


a = Sobrecarga(1)
print(a)    # saida: 1
a+'+'       # gambi :P
print(a)    # saida: 2
a+1         # resultado da operação não é armazenado
print(a)    # saida: 2
print(a+1)  # saida: 3

It’s not exactly what you want, because the operator + was overloaded for when the parameter is the literal string + add the self.val 1 and store the new value in itself, but if you try to do a++ will give syntax error because this cannot be interpreted in Python. For all other cases the sum operation will do the normal sum operation and return the value.

See working on Ideone

  • 2

    is legal by a link to the language data model document, listing all special methods: https://docs.python.org/3/reference/datamodel.html

  • @jsbueno good tip, done

Browser other questions tagged

You are not signed in. Login or sign up in order to post.