Learning Python - Mark Lutz [397]
In more realistic classes where the class type may need to be propagated in results, things can become trickier: type testing may be required to tell whether it’s safe to convert and thus avoid nesting. For instance, without the isinstance test in the following, we could wind up with a Commuter whose val is another Commuter when two instances are added and __add__ triggers __radd__:
>>> class Commuter: # Propagate class type in results
... def __init__(self, val):
... self.val = val
... def __add__(self, other):
... if isinstance(other, Commuter): other = other.val
... return Commuter(self.val + other)
... def __radd__(self, other):
... return Commuter(other + self.val)
... def __str__(self):
... return ' ... >>> x = Commuter(88) >>> y = Commuter(99) >>> print(x + 10) # Result is another Commuter instance >>> print(10 + y) >>> z = x + y # Not nested: doesn't recur to __radd__ >>> print(z) >>> print(z + 10) >>> print(z + z) In-Place Addition To also implement += in-place augmented addition, code either an __iadd__ or an __add__. The latter is used if the former is absent. In fact, the prior section’s Commuter class supports += already for this reason, but __iadd__ allows for more efficient in-place changes: >>> class Number: ... def __init__(self, val): ... self.val = val ... def __iadd__(self, other): # __iadd__ explicit: x += y ... self.val += other # Usually returns self ... return self ... >>> x = Number(5) >>> x += 1 >>> x += 1 >>> x.val 7 >>> class Number: ... def __init__(self, val): ... self.val = val ... def __add__(self, other): # __add__ fallback: x = (x + y) ... return Number(self.val + other) # Propagates class type ... >>> x = Number(5) >>> x += 1 >>> x += 1 >>> x.val 7 Every binary operator has similar right-side and in-place overloading methods that work the same (e.g., __mul__, __rmul__, and __imul__). Right-side methods are an advanced topic and tend to be fairly rarely used in practice; you only code them when you need operators to be commutative, and then only if you need to support such operators at all. For instance, a Vector class may use these tools, but an Employee or Button class probably would not. Call Expressions: __call__ The __call__ method is called when your instance is called. No, this isn’t a circular definition—if defined, Python runs a __call__ method for function call expressions applied to your instances, passing along whatever positional or keyword arguments were sent: >>> class Callee: ... def __call__(self, *pargs, **kargs): # Intercept instance calls ... print('Called:', pargs, kargs) # Accept arbitrary arguments ... >>> C = Callee() >>> C(1, 2, 3) # C is a callable object Called: (1, 2, 3) {} >>> C(1, 2, 3, x=4, y=5) Called: (1, 2, 3) {'y': 5, 'x': 4} More formally, all the argument-passing modes we explored in Chapter 18 are supported by the __call__ method—whatever is passed to the instance is passed to this method, along with the usual implied instance argument. For example, the method definitions: class C: def __call__(self, a, b, c=5, d=6): ... # Normals and defaults class C: def __call__(self, *pargs, **kargs): ... # Collect arbitrary arguments class C: def __call__(self, *pargs, d=6, **kargs): ... # 3.0 keyword-only argument all match all the following instance calls: X = C() X(1, 2) # Omit defaults X(1, 2, 3, 4) # Positionals X(a=1, b=2, d=4) # Keywords X(*[1, 2], **dict(c=3, d=4)) # Unpack arbitrary arguments X(1, *(2,), c=3, **dict(d=4)) # Mixed modes The net effect is that classes and instances with a __call__ support the exact same argument syntax and semantics as normal functions and methods. Intercepting call expression like this allows class instances to emulate the look and feel of things like functions, but also retain state information for use during calls (we saw a similar example while exploring scopes in Chapter 17, but you should