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.