Operators#

Like most other programming languages Python offers lots of operators to form new data from existing one. Important classes of operators are

  • arithmetic operators (+, -, *, /,…),

  • comparison operators (==, !=, <, >,…),

  • logical operators (and, or, not,…).

Operator Precedence#

Expressions containing more than one operator are evaluated in well-defined order. Python’s operators can be listed from highest to lowest priority. Operators with identical priority are evaluated from left to right.

Syntax

Operator

**

exponentiation

+, - (unary)

sign

*, /, //, %

multiplication, division

+, - (binary)

addition, substraction

==, !=, <, >, <=, >=

comparison

not

logical not

and

logical and

or

logical or

See Python’s documentation for a complete list of all operators.

Chained Comparisons#

We may write chained comparions like a < b < c. Python interprets them as single comparisons connected by and, that is, a < b and b < c.

Note

Unfortunate expressions like a < b > c are allowed, too. This example is equivalent to a < b and b > c. In a chain only neighboring operands are compared to each other! There is no comparison between a and c here.

Augmented Assignments#

For arithmetic binary operators there’s a shortcut for expressions like a = a + b. We may write such as a += b. The latter is called an augmented assignment. Although the result will look the same, there are two technical differences one should know:

  • augmented assignment may work in-place,

  • for augmented assignment the assignment target will be evaluated only once.

In-place Computations#

For usual binary operators the Python interpreter calls corresponding dunder methods, like __add__ for +. Augmented assignments have their own dunder methods starting with i. So += calls __iadd__, for instance. The intention is that += may work in-place, that is, without creating a new object. Of course, this is only possible for mutable objects. If there is no dunder method for augmented assignment the interpreter falls back to the usual binary operator’s dunder method.

Example: The + operator applied to two lists concatenates both lists. With a = a + b a new list object holding the result is created and then the name a is tied to the new object. With a += b list b is appended to the existing list object referred to by a.

a = [1, 2, 3]
b = [4, 5, 6]

print(id(a))

a = a + b

print(a)
print(id(a))
139941989941376
[1, 2, 3, 4, 5, 6]
139941989942272
a = [1, 2, 3]
b = [4, 5, 6]

print(id(a))

a += b

print(a)
print(id(a))
139941989933632
[1, 2, 3, 4, 5, 6]
139941989933632

Only One Evaluation#

If the assignment target is a more complex expression like for list items, the expression will be evaluated twice with usual binary operators, but only once if augmented assignment is used.

Example: In the following code an item of a list shall be incremented by 1. The item’s index is computed by some complex function get_index (which for demonstration purposes is very simple here). The two code cells show different implementations, resulting in a different number of calls to get_index.

def get_index():
    print('get_index called')
    return 2
    
a = [1, 2, 3, 4]

a[get_index()] = a[get_index()] + 1

print(a)
get_index called
get_index called
[1, 2, 4, 4]
def get_index():
    print('get_index called')
    return 2
    
a = [1, 2, 3, 4]

a[get_index()] += 1

print(a)
get_index called
[1, 2, 4, 4]

Note

If efficiency matters you should prefer augmented assignments. Even a += b with integers a and b is more efficient than a = a + b, because the name a will be looked up only once in the table mapping names to object IDs.

Operators as Member Functions#

All Python operators, == and + for instance, simply call a specially named member function of the involved objects, a so called dunder method. Lines 3 and 4 of the following code cell do exactly the same thing:

a = 5

b = a + 2
c = a.__add__(2)

print(b)
print(c)
7
7

Dunder methods allow to create new object types which can implement all the Python operators themselve. What an operator does depends on the operands’ object type. For instance, + applied to numbers is usual addition, but + applied to strings is concatenation.

Dunder Methods for Binary Operators#

For binary operators like + and == there is always the question which of both objects to use for calling the corresponding dunder method. In case of comparisons Python uses the dunder method of the left-hand side operand (up to one minor exception which we don’t discuss here). For arithmetic operations Python always tries the left operand first (again, we omit a minor exception). If it does not have the required dunder method, then Python tries the operand on the right-hand side. If both objects do not have the dunder method, then then interpreter stops with an error.

Binary arithmetic operations might be unsymmetric. Thus, there are two variants of most arithmetic dunder methods: one for applying an operation as the left-hand side operand and one for applying an operation as the right-hand side operand. For addition the methods are called __add__ and __radd__, for multiplication we have __mul__ and __rmul__. Others follow the same scheme.

Operands of Different Types#

Often binary operators shall be applied to objects of different types (adding integer and floating point values, for instance). Even if both objects have the corresponding dunder method, one or both of them could lack code for handling certain object types.

In such a case Python calls the dunder method and the method returns NotImplemented to signal that it doesn’t know how to handle the other operand. Then the interpreter tries the dunder method of the other operand. If it returns NotImplemented, too, then the interpreter stops with an error. The __add__ function of integer objects cannot handle float objects, but __add__ of float objects can handle integers, for example:

a = 2
b = 1.23
print(a.__add__(b))
print(b.__add__(a))
NotImplemented
3.23

Writing a + b in the example above first calls a.__add__(b), which returns NotImplemented, then b.__radd__. With b + a the first call goes to b.__add__ and no second call (to a.__radd__) is required.

An example of beneficial use of Python’s flexible mechanism for customizing operators is discussed in the project on Vector Multiplication.