Fluent python -- 13 correctly overloading operators

13, Overload operators correctly

Function of operator overload: let user-defined objects use infix operator or unary operator.

Python imposes some limitations and balances flexibility, availability, and security:

1 operators of built-in types cannot be overloaded

2 cannot create a new operator, only existing operators can be overloaded

3 some operators cannot be overloaded -- is, and, or, not (bitwise operators &, |, ~ can)

Unary operator

- : __neg__

Unary negative arithmetic operator.

+ : __pos__

Unary positive arithmetic operator.

~ : __inveret__

Bitwise negation of integers, ~ x == -(x+1)

abs : __abs__

Take absolute value

Unary operator, only one parameter: self, return: a new object of the same type

def __abs__(self):
    return math.sqrt(sum(x * x for x in self))
def __neg__(self):
    return Vector(-x for x in self)
def __pos__(self):
    return Vector(self)

When are x and + X not equal

Although each + one_third expressions use one_ The value of third creates a new Decimal instance, but uses the precision of the current arithmetic operation context. Different precision leads to inequality.

In [8]: import decimal

In [9]: ctx = decimal.getcontext()  # Gets the context reference of the current global operator

In [10]: ctx.prec = 40  # Set the arithmetic operator context precision to 40

In [11]: a = decimal.Decimal('1')/decimal.Decimal('3')  # Find 1 / 3

In [12]: a
Out[12]: Decimal('0.3333333333333333333333333333333333333333')

In [13]: a == +a  # True at this time
Out[13]: True

In [14]: ctx.prec = 28  # Adjustment accuracy

In [15]: a == +a  # Is False
Out[15]: False

In [16]: +a
Out[16]: Decimal('0.3333333333333333333333333333')

In [17]: a
Out[17]: Decimal('0.3333333333333333333333333333333333333333')

collections.Counter clears the values of the number of negative and zero numbers in the counter, resulting in inequality.

In [36]: from collections import Counter

In [37]: c = Counter('abcde')

In [38]: c
Out[38]: Counter({'a': 1, 'b': 1, 'c': 1, 'd': 1, 'e': 1})

In [39]: c['a'] = -1

In [40]: c
Out[40]: Counter({'a': -1, 'b': 1, 'c': 1, 'd': 1, 'e': 1})

In [41]: c['b'] = 0

In [42]: c
Out[42]: Counter({'a': -1, 'b': 0, 'c': 1, 'd': 1, 'e': 1})

In [43]: +c
Out[43]: Counter({'c': 1, 'd': 1, 'e': 1})

In [44]: c == +c
Out[44]: False

Overloaded vector addition operator+

Effect achieved:

>>> v1 = Vector([3, 4, 5, 6])
>>> v3 = Vector([1, 2])
>>> v1 + v3
Vector([4.0, 6.0, 5.0, 6.0])

code:

def __add__(self, other):
    pairs = itertools.zip_longest(self, other, fillvalue=0)  # Fill 0 according to the length, generator
    return Vector(a + b for a, b in pairs)  # Generator, which returns a new instance of Vector without affecting self or other

Special methods that implement unary and infix operators must not modify operands. Expressions that use these operators expect the result to be a new object. Only incremental assignment expressions may modify the first operand (self).

In order to support operations involving different types, Python provides a special dispatch mechanism for infix operator special methods. For expressions a + b, the interpreter performs the following steps

(1) If a yes__ add__ Method, and the return value is not NotImplemented, call a__ add__ (b) , and then return the result.

(2) If a no__ add__ Method, or call__ add__ Method returns NotImplemented to check if b has__ radd__ Method, and if it does not return NotImplemented, call b__ radd__ (a) , and then return the result.

(3) If b not__ radd__ Method, or call__ radd__ Method returns NotImplemented, throws TypeError, and indicates in the error message that the operand type is not supported.

__ radd__ Yes__ add__ The reflected or reversed version of. I like to call it the "reverse" special method. The three technical revisers of this book, Alex, Anna and Leo, told me that they like to call it a "right" special method because they call it on the right operand.

Reflection special method, reverse special method, right special method:__ radd__

We want to implement vector__ radd__ method. This is a backup mechanism if the left operand is not implemented__ add__ Method, or implemented, but returning NotImplemented indicates that it does not know how to handle the right operand, then Python will call__ radd__ method.

NotImplemented

Special singleton value. If the infix operator special method cannot handle the given operand, it should be return ed to the interpreter.

NotImplementedError

An exception that is raise d by the placeholder method in the abstract class to remind the subclass that it must be overridden.

Realize__ radd__

def __add__(self, other):  # Implementation of forward special methods
    pairs = itertools.zip_longest(self, other, fillvalue=0.0)
    return Vector(a + b for a, b in pairs)
def __radd__(self, other):  # Implement the reverse special method and delegate directly__ add__.  Any exchangeable operator can do this.
    return self + other

When dealing with numbers and vectors, + can be exchanged, but not when splicing sequences. Will throw an exception that does not have much effect.

If the types are incompatible, it returns NotImplemented, and the reverse method also returns NotImplemented, throwing a standard error message:

"unsupported operand type(s) for +: Vector and str"

In order to follow the duck type spirit, we cannot test the type of other operand or the type of its element. We'll catch the exception and return NotImplemented.

realization:

def __add__(self, other):
    try:
        pairs = itertools.zip_longest(self, other, fillvalue=0.0)
        return Vector(a + b for a, b in pairs)
    except TypeError:
        return NotImplemented
def __radd__(self, other):
    return self + other

Overloaded scalar multiplication operator*

The practical application of white goose type -- explicitly checking abstract types.

def __mul__(self, scalar):
    if isinstance(scalar, numbers.Real):
        return Vector(n * scalar for n in self)
    else:
        return NotImplemented
def __rmul__(self, scalar):
    return self * scalar

Comparison operator

The processing of many comparison operators (= =,! =, >, <, > =, < =) by the Python interpreter is similar to the above, but there are significant differences in two aspects.

1 forward and reverse calls use the same series of methods. The rules in this regard are shown in table 13-2. For example, for = =, both forward and reverse calls are__ eq__ Method, just switch the parameters; And positive__ gt__ Method calls are reversed__ lt__ Method and swap the parameters.

2 pairs = = and= For example, if the reverse call fails, Python compares the ID of the object without throwing a TypeError.

class Vector:
    def __eq__(self, other):
        return (len(self) == len(other) and all(a == b for a, b in zip(self, other)))

The list can also be compared with the above methods.

"Python Zen" says:

​ If there are many possibilities, don't guess.

Over tolerance of operands can lead to surprising results, and programmers hate surprises.

Looking for clues from Python itself, we found that the result of [1,2] = = (1,2) is False. Therefore, we should be conservative and do some type checks. If the second operand is an instance of Vector (or an instance of a subclass of Vector), use__ eq__ Method. Otherwise, return NotImplemented and let Python handle it.

After improvement:

def __eq__(self, other):
    if isinstance(other, Vector):
        return (len(self) == len(other) and all(a == b for a, b in zip(self, other)))
    else:
        return NotImplemented
>>> va = Vector([1.0, 2.0, 3.0])
>>> vb = Vector(range(1, 4))
>>> va == vb
True
>>> vc = Vector([1, 2])
>>> from vector2d_v3 import Vector2d
>>> v2d = Vector2d(1, 2)
>>> vc == v2d
True
>>> t3 = (1, 2, 3)
>>> va == t3
False
(1) In order to calculate vc == v2d,Python call Vector.__eq__(vc, v2d). 
(2) through Vector.__eq__(vc, v2d) Confirm, v2d no Vector Instance, so return NotImplemented. 
(3) Python obtain NotImplemented As a result, an attempt was made to call Vector2d.__eq__(v2d, vc). 
(4) Vector2d.__eq__(v2d, vc) Turn both operands into tuples and compare them. The result is True
(1) In order to calculate va == t3,Python call Vector.__eq__(va, t3). 
(2) through Vector.__eq__(va, t3) Confirm, t3 no Vector Instance, so return NotImplemented. 
(3) Python obtain NotImplemented As a result, an attempt was made to call tuple.__eq__(t3, va). 
(4) tuple.__eq__(t3, va) hear nothing of Vector What is it, so return NotImplemented. 
(5) yes == For example, if the reverse call returns NotImplemented,Python Will compare objects ID,Make a last shot.

Incremental assignment operator

If a class does not implement the in place operators listed in table 13-1, the incremental assignment operator is just a syntax sugar: the function of a += b is exactly the same as that of a = a + b. For immutable types, this is the expected behavior, and if defined__ add__ Method, you can use + = without writing additional code.

The result is as expected, and a new Vector instance is created.

However, if an in place operator method is implemented, for example__ iadd__, The in place operator method is called when calculating the result of a += b. The names of such operators indicate that they modify the left operand in place without creating a new object as a result.

Immutable types must not implement in place special methods. This is an obvious fact, but it is worth mentioning.

Note that the + = operator is more tolerant of the second operand than the ++ The two operands of the operator must be of the same type (AddableBingoCage here). Otherwise, the type of the result may be confusing. The case of + = is more clear, because the left operand is modified in place, so the type of result is determined.

By observing how the built-in list type works, I determined what restrictions to make on the behavior of + and + =. my_list + x can only be used to add two lists together, not my_list += x the list on the left can be extended with the elements in the iteratable object x on the right. The behavior of list.extend() is the same, and its parameters can be any iteratable object.

import itertools
from tombola import Tombola
from bingo import BingoCage

class AddableBingoCage(BingoCage):
    def __add__(self, other):
        if isinstance(other, Tombola):
            return AddableBingoCage(self.inspect() + other.inspect())
        else:
            return NotImplemented
    def __iadd__(self, other):
        if isinstance(other, Tombola):
            other_iterable = other.inspect()
        else:
            try:
                other_iterable = iter(other)
            except TypeError:
                raise TypeError(msg.format(self_cls))
        self.load(other_iterable)
        return self  # Important: the special method of incremental assignment must return self

__add__

Call the constructor to build a new instance and return it as a result.

__iadd__

Return the modified self as the result.

Generally speaking, if the forward method of infix operator (such as _mul__) only processes operands of the same type as self, there is no need to implement the corresponding reverse method (such as _rmul__), because by definition, the reverse method is to process operands of different types.

Python imposes some restrictions on operator overloading: overloading operators of built-in types is prohibited, and overloading existing operators is limited, with several exceptions (is, and, or, not).

If the types of operands are different, we need to detect operands that cannot be processed. This chapter uses two methods to deal with this problem: one is duck type, which directly attempts to execute the operation. If there is a problem, catch the TypeError exception; The other is to explicitly use isinstance testing.

Both methods have advantages and disadvantages: duck type is more flexible, but explicit inspection can predict the results better. If you choose to use isinstance, be careful not to test specific classes, but to test the abstract base class of numbers.Real, such as isinstance(scalar,numbers.Real). This makes a good compromise between flexibility and security, because current or future user-defined types can be declared as real or virtual subclasses of the abstract base class,

Python special handling = = and= Backup mechanism: never throw an error, because Python will compare the ID of the object and make a last ditch attempt.

In Python programming, operator overloading is often tested with isinstance. Generally speaking, the library should take advantage of dynamic types (improve flexibility) to avoid explicitly testing types, but directly try operations and then handle exceptions, so that as long as the object supports the required operations, it does not have to be a type. However, python abstract base classes allow a more strict duck type, which Alex Martelli calls "white goose type", which is often used when writing code for overloaded operators.

Tuple: only when used, not when defined.

Posted by celeb-x2 on Thu, 04 Nov 2021 09:20:20 -0700