Operator Overloading I Now that we have reviewed the fundamentals of classes, we are going to cover some new material about them: how we can write certain class methods that are used by Python in a special way: we don't call these methods directly, but Python does. All these special methods have their names writen between double underscores (just like __init__ which is also one of these special methods). Most of these methods (but not all) are invoked by Python operators (whose arguments are instances of the class they are defined in) so we call this technique "operator overloading": that means overloading (giving more than one meaning) to the semantics the standard operators for objects constructed from user classes (which really is a form of polymorphism). Overloading has a good, not bad connotation here. By studying operator overloading, we will better understand how Python executes our programs, and be able to write classes that are more powerful by exploiting this mechanism. Note that this material is also covered in Lutz, "Learning Python", in Chapter 29. While learning this material, we illustrate each overloaded operator simply with methods defined in a "simple" class, C. Later we will apply this technique to a bigger example; but, a typical class has only some of its operators overloaded (not all are meaningful for every class). ------------------------------------------------------------------------------ __init__ We'll start with __init__ because you have probably already written many of these methods. Recall that when you "call the class", you supply it with some arguments. Python creates an object (something that has a namespace/__dict__) for the instance of the class and then calls __init__ with a reference to the object it created and all the arguments in the "call of the class". So calling C(a,b,c) leads Python to call C.__init__(self,a,b.c) where self refers to the object it just created. Here is the beginning of this class, whose argument is a list of strings class C: def __init__(self,los): self.los = los Note that self.los = ... establishes a new name in the namespace of the constructed object and initializes it to ... Like the methods that we will discuss below, we don't call __init__ but Python does, automatically. Note that it is perfectly OK to call __init__(...) on some object to reinitialize it (although I have never done so; on the other hand I've not been using Python too long). ------------------------------------------------------------------------------ __len__ We can call the len function on any object. It translates into a call of the parameterless __len__ method on the object. len is defined (in the builtins) as def len(x): return x.__len__() Note that some objects (those of class/type int) do not defined a __len_ method. If we call len(1) Python reports: TypeError: object of type 'int' has no len() The following class illustrates this method. This class is initialized with a list of strings. The len method returns not the length of the list, but the sum of the length of all the strings in list. We write the method, so we can determine what the meaning of len is for objects from class C. class C: def __init__(self,los): self.los = los def __len__(self): print('Calling len') return sum((len(i) for i in self.los)) x = C(['ab','cd']) print(len(x)) This script prints 4, because 'ab' has 2 characters and 'cd' has 2 more. ------------------------------------------------------------------------------ __bool__ Whenever Python needs to interpret some object as a boolean value, it calls the parameterless method __bool__ on the object. For example in an if statement mentioning just an object, Python attempts to call the __bool__function on that object. For the following class (no __len__ is defined: we will see for a minute why this is important) the if statement would raise an exception. class C: def __init__(self,los): self.los = los x = C(['ab','cd']) if x: pass Strangely enough, the exception it raises is: TypeError: object of type 'C' has no len() The actual rule in Python for evaluating an object as if it were a boolean value is to first try to call __bool_, but it that method is not present, Pythong returns for the boolean value whether it is != 0. If there is no __len__ function Python raises the exception show above. So, if described C as we did in the previous section, writing if x : pass is equivalent to calling if len(x) != 0: pass which evaluates to True for this binding to x (whose len, recall, is 4) But if we defined C as follows, class C: def __init__(self,los): self.los = los def __len__(self): print('Calling len') return sum((len(i) for i in self.los)) def __bool__(self): return ''.join(self.los) != 'False' Then the boolean value for C(['ab','cd]) would return True but so would C([]), but C(['Fa','l','se]) would return False: all return their values because Python uses the __bool__ function supplied above, and doesn't have to (nor does it) call len to compute an answer. By the way, this also explains why we can test list, tuple, set, and dict objects as booleans: if one is empty (len = 0) it represents False, if it is not empty it represents false. Also, the object None has a boolean value of False (the NoneType class specifies __bool__ to always return False). -------------------- Interlude: Short-Circuit Logical Operators: and their real meanings The operators and/or are called short-ciruit operators, because unlike other operators (which evaluate both of their operands before being applied), short- circuit operators evaluate their left operand first, and sometimes don't need to evaluate their right operand. For example True or anything is True and False and anything is False. So if the or operator's left operand is True, it doesn't need to evalate its right operand: the result is True; and if the and operator's left operand is False, it doesn't need to evalate its right operand: the result is False. This rule saves time, but it is more important for another reason (discussed below). If the left operands of or/and is False/True it must evaluate the right operand in order to compute its result. So, for example, if we wrote the following if/test (assume d is a dict) if k in d.keys() and d[k] == 0: .... Python would first evaluate the expression: k in d.keys(). When False it would determine the value of the and operator is already known, and is False. It would not have to evaluate d[k] == 0 and that is a good thing, because that would throw a KeyError exception when k is not a key in d. So, while short-circuit operators can save a little time, that is not their most important purpose; avoiding exceptions is the primary reason that and/or operators are short-circuit. Even without short-circuit operators, we could write this same expression as if k in d.keys(): if d[k] == 0: .... But that requires a nested if statement and is much more cumbersome. It is better to spend some time learning about short-circuit operators now, and then understand how to use them and save time later by using them appropriately. Finally, the exact meaning of and/or is a bit more subtle. Let's look at and first. If the boolean value (computed by __bool__) of the left operand of and is False, we know that we don't have to evaluate the right operand. We said that the result is False, but realy the result is the actual value of left operand, not its boolean value. So [] and anything evaluates to [] (a value whose boolean value is False). Likewise if the boolean value (computed by __bool__) of the left operand of or is True, we know that we don't have to evaluate the right operand. We said that the result is True, but realy the result is the actual value of left operand, not its boolean value. So [1] or anything evaluates to [1] (a value whose boolean value is True). Similarly, if the boolean value of the left operand of or is False, we need to evaluate the right operand to compute the result, and the result will just be the value of the right operand: so (any value considered False) or [] is []; (any value considered False) or [1] is [1]. Likewise if the boolean value of the left operand of and is True, we need to evaluate the right operand to compute the result, and the result will just be the value of the right operand: so (any value considered True) and [] is []; (any value considered True) and [1] is [1]. Most programmers use logical operators where boolean values are needed (like in if and while loops), so regardless of their strange results, we are interested only in the boolean value of the result (which correponds to our intution about and/or). But the reality in Python is a bit different. Note that not has only one operand, so there is no way to short-circuit it. Also, note that not always returns a bool value (unlike what we learned above). That is, not [] if True; not [1] is False. ------------------------------------------------------------------------------ Relational operators: __lt__ (__gt__, __le__, __ge__, __eq__, __ne__): < (>, <=, >=, ==, and !=) In this section, the overloaded operators that we discuss are really operators, and we overload their meaning to compute values on new classes/types. Python translates any occurance of a relational operator into a call on the approprate method for its left operand: x < y is translated to x.__lt__(y) which by the Fundamental Equation of Object Oriented Programming is type(x).__lt__(x,y) or, assuming x is of type C, C.__lt__(a,b). Please note that int is just a class in Python, so 12 < x translates into int.__lt__(12,x). That is really what is happening in Python: usingit uses the method __lt__ defined in the int class. We will start our discussion by defining these relational operators to work when comparing two objects from class C. Later we will discuss how to expand them to compare against other types (here, just int). Back to C, first using the definition of C above, if we wrote x = C(['1','5']) y = C(['5','1']) print(x < y) Python raises the exception: TypeError: unorderable types: C() < C() likewise, for print(x < 10) Python raises the exception: TypeError: unorderable types: C() < int Note that there is no definition of __lt__ method (what tthe < operator translates to) in class C, so the translation of C.__lt__(anything) fails, for both operand combinations: C() and C() and C() and int. So, there is no built-in meaning for comparing an object of class/type C with any object of any other type. There is no C.__lt__ method. But, by defining an __lt__ method in C, there is a method for Python to call when it needs to compute the < operator on two operands, whose left is an instance of class C. So to start, let's add the following definition to the C class def __lt__(self,right): return self.los < right.los Notice the < operator in __lt__ is not recursive! Python calls the __lt__ method above when comparing two C() objects; inside this method the < operator is called on two lists, so it calls list.__lt__(self.los,right.los) to compute the results: an __lt__ in a different class. So, to compare two C values, we will use the standard < to compare the self.los instance variable (remember how list of strings are compared). So now, when evaluating x < y in the example below, the method call C.__lt__ is found and it is called with arguemtns (x,y): as C.__lt__(x,y). x = C(['1','5']) y = C(['5','1']) print(x < y) Python prints: True What if we changed the print to print(x > y) and executed the code? It turns out this works and it computes the correct answer. Python knows that if it has a __lt__ method it can reverse its arguments to compute the __gt__ method (if it doesn't already exist), that is x.__gt__(y) is the same as y.__lt__(x). So, we can let Python implicitly call __lt__ with the operands reversed, as above, or we can explicitly implement __gt__ either directly (as we did for __lt__) or we can implement __gt__ by having it explicitly call __lt__ with the arguments reversed. def __gt__(self,right): return self.los > right.los def __gt__(self,right): return right < self # or right.__lt__(self) or C.__lt__(right,self) So there are a variety of implicit and explicit ways that we can define the __gt__ method that compute the correct results. Finally, note that we can get the same behavior by defining only __gt__ which Python will call with reversed operands if it needs to compute the __lt__ if that method is undefined. So, writing one of these pairs allows Python to compute both relations. What if we changed the print to print(x <= y) and executed the code? Python again raises the exception: TypeError: unorderable types: C() <= C() In fact, the __le__ and __ge__ pair of methods are opposites like __lt__ and __gt__. If we define the __le__ method, and Python needs to compute x >= y, it will try the call to C.__le__(y,x), because x >= y when y <= x. And if we define the __ge__ and Python needs to computer x <= y it will translate the call to C.__ge__(y,x), because x <= y when y >= x. Finally, if we don't define an __eq__ method, Python substitutes the is operator; if we don't define a __ne__ method Python call __eq__ and negates the result. So unlike the other pairs, in this pair, an undefined __ne__ method calls __eq__ but an undefined __eq__ will not call __ne__. For example, with nothing defined x=C(['1']) y=C(['1']) z=x print(x == y, x is y, x == z, x is z) print(x != y, x is not y, x != z, x is not z) Prints False False True True True True False False So ==/is and !-/not is produce the same results. But if we define the __eq__ method as follows (True when the lists are same regardless of identity) def __eq__(self,right): return self.los == right.los print(x == y, x is y, x == z, x is z) print(x != y, x is not y, x != z, x is not z) Prints True False True True False True False False So in summary, to get all six relational operators to work correctly, we can define all six or choose one of __lt__/__gt__, one of __le__/__ge__, and __eq__ to define, and the others will work by calling one of these. Note that although Python doesn't do it, we can implement all the relational operators using only < (or > or <= or >= we show < bleow): although this approach might not be efficient in execution, the last 5 methods would be the same in every class, all using the meaning of __lt__. In fact, here is the equivalence (each proven by the law of trichotomy: x < y, x == y, or x > y) Relation | < only + logical operators ---------+------------------------------ x < y | x < y x > y | y < x x <= y | not (y < x) x >= y | not (x < y) x == y | not (x < y or y < x) x != y | x < y or y < x or we could write not(x == y), using == So, we could write each of these operators as illustrated above and be able to compare values from the C class in all possible ways. We will later see how to use a mix-in via class inheritence to simplify this process. Now on to what we must do if we also want to compare objects of type C with objects of another type, for simplicity we will use the standard int type as the other one for comparison Writing the code print (x < 15) won't magically work. As you would suspect, this will not end well, because Python raises the exception: AttributeError: 'int' object has no attribute 'los' (in the body of the __lt__ method). The current definition assumes type(right) is C, but here it is an int. Now, maybe it makes NO sense to do that comparison, so raising this exception is as good as any other (or mabye we should test whether type(right) is C and raise a special exception with a better message). The accepted one to raise is TypeError, which with the correct string would produce the same result as if the method were not defined (see below). But if it makes sense to compare a C and an int, we can write more code to fix the "problem", expressing how to compare Cs to integers. Suppose we decide to compare the C value with all the digits of the int captures in a list (compare 143 as if it were ['1','4','3']), and then do the standard list comparison. We could rewrite __lt__ as def __lt__(self,right): if type(right) is C: return self.los < right.los elif type(right) is int: return self.los < [d for d in str(right)] else: raise TypeError('unorderable types: C() < '+str(type(right))[8:-2]+'()') Here, if the type of right object is C, we do the standard list comparison we did before, but if the type is int, we perform the necessary conversion before doing the list comparison. If neither is the type of right, we raise the Type exception, meaning the trying to do the comparison is not valid. The magic having to do with the slice [8:-2] is because str(type(...)) produces a string of the form "" where the first ... starts at index 8 and ends 3 before the end of the string. But if we extend __lt__ to cover a right argument that is an int, and do not define an explicit __gt__ similarly, then Python will not be able to compute x > 15: remember if __gtr__ is not defined, x > 15 tries the equivalent of 15 < x, which won't work correctly (see below). What if we tried print(x > 15)? Here the call unable to find the __gt__ method for x, tries int.__lt__(15,x) and the int class (written/finalized before the C class) does not --and cannot be extended to- compare int values against C values. Python ultimately raises the exception: TypeError: unorderable types: int() > C() It is even more interesting to note that if we define an __lt__method and not __gt__ method, print(15 < x) works, because with no __gt__ method, this is translated into C.__lt__(x,15) whose definition above computes the right answer. So there is no solution to the problem of comparing Cs to ints and ints to Cs correctly, although there is a solution to the equivalent problem with arithmetic operators. I guess the designers of Python considered arithmetic operators more important than relational operators. We will discuss arithmetic operators below. So, we must either restrict ourselves to comparing objects from class C with objects of class C, or write all the relational operators in class C saying how to compare objects from class C with any other types (but only as right operands). No matter what we do, there will be examples that don't compute the intuitively correct answer. Note that chained relational operators are translated in pairwise comparisons joined by and: e.g., a < b < c == d: a < b and b < c and c == d Finally note that we can make a < b do whatever we want. We could print values, change the state of a or b, do whatever we can do with Python code. Here is where beginners might go wild, but more experience programmers will ensure that these methods, which relational operators translate to, are pure accessors and just return a bool value (or one that can be interpreted as a bool value using the __bool__ method) without making any state changes in the arguments. ------------------------------------------------------------------------------ __repr__ and __str__ (and other conversion functions) Python can call two methods that should return string representations of an object. The __str__ method is called when the conversion function str is called: so again, this is like len: Python translates str(x) into type(x).__str__(x). For example, the print function calls str on all the arguments it is printing. In fact, if there is no __str__ method in this class, or it raises the NotImplementedError exception, Python tries calling the __repr__ method next (if we call repr(x) Python returns x.__repr__() for this method, similarly to what it does for len(x)): if that too fails, Python reverts to its standard method for computing a string value objects: a bracket string with the name of the object's class and the location of this object in memory (as a hexidecimal number). So in the class C as it exists, if we write x = C(['ab','cd']) print(x) Python prints something like <__main__.C object at 0x0289B8D0> The requirement for __repr__ is that it returns a string, which if passed as the argument to the eval function, would produce an object with the same state. So for C, we define __repr__ as follows def __repr__(self): return 'C(' + str(self.los) + ')' Now, if we executed the code above, Python prints the following (there is still no definition of __str__, so __repr__ is called). C(['ab', 'cd']) And sure enough, calling eval(str(x)) would produce an object whose state was the same as x's object state. The advice for __str__ is that it return a string that nicely formats interesting aspects (maybe all instance variables, maybe not) of the object. Some objects have more instance variables than those needed to construct the object. The __str__ below just prints the list of string def __str__(self): return str(self.los) Now, if we executed the code above, Python prints the following ['ab', 'cd'] If we decided we wanted to return a string that contained one concatenation of all the strings in the list of strings, we could instead define __str__ as def __str__(self): return ''.join(self.los) Now, if we executed the code above, Python prints the following abcd There are no requirements for __str__ as there are for __repr__ so we can use our judgement of how best to show the string representation of such objects. We will go back to the original definition of __str__ for the rest of this lecture. Finally the other type coversion functions: int float complex oct hex index trunc call the Python methods __int__ __float__ __complex__ __oct__ __hex__ __index__ __trunc__ __coerce__ ------------------------------------------------------------------------------ Unary arithmetic operators: + - ~ (and functions: abs round floor ceil trunc) Continuing with overloading operators, when Python recognizes a unary arithmetic operator (or a binary arithmetic operator, see the next section) it translates it into the appropriate method call for the class/type of its argument: so it translates -x into type(x).__neg__(x). Suppose that we wanted the __neg__ operator for C to return a C whose list of string contents is reversed. Generally, as described above, __neg__ should not mutate its operand but should leave it operand unchanged and return a new object of the type C, whose state is initialized to the appropriate one. Here is the __neg__ method. Note it returns a newly constructed object (of class C) with the appropriate contents, def __neg__(self): return C([i for i in reversed(self.los)]) and an example of it running x = C(['ab','cd']) print(x) print(-x) which produces the following results; note x's state before and after printing -x is the same: there is no mutation. Of course we could write x = -x which also has no mutation, but the name x is rebound to another object produced by calling __neg__, so if we printed it we would see a different result. ['ab', 'cd'] ['cd', 'ab'] ['ab', 'cd'] There are two other unary arithmetic operators we can overload: + and ~ whose methods go by the names __pos_ and __invert__. In addition, while discussing arithmetic, the following unary functions (all defined in the math module) abs, round, floor, ceil, trunc work by calling one of these special methods on its argument (much like len, described above): so abs(x) returns as its result x.__abs__(). ------------------------------------------------------------------------------ Binary Arithmetic Operators: + - * / // % divmod ** << >> & | ^ Now lets move from unary to binary operators, where there are more operators and they are it bit harder to write (and get right). We will start by discussing one method in particular __plus__ and also discuss the need for another related method __rplus__. What we learn applies identically to all the arithmetic operators. Binary arithmetic operators, like relational operators, are written in between their two operands. Python translates the call x + y into type(x).__add__(y). As with the unary arithmetic operators, neither operand should be mutated, and the method should return a new C object with the correct state. Here is an example of the + operator overloaded for C: the resulting object has its list of string the catenated list of strings of its operands: recall catenation for lists also uses the + operator. def __add__(self,right): return C(self.los+right.los) and an example of it running x = C(['ab','cd']) print(x + C(['12','34'])) which produces the result ['ab', 'cd', '12', '34'] Notice the + operator in __add__ is not recursive! Python calls the __add__ method above when adding to C() objects; inside this method the + operator is called on two lists, so it callse list.__add__(self.los,right.los) to compute the results: an __add__ in a different class. Now recall that we allowed objects from class C to compare to int using relational operators: we interpreted the int 132 as ['1','3','2']. Let's use this same interpretation for allowing object from class C to be added to ints. To do so we extend __add__ in a similar way def __add__(self,right): if type(right) is C: return C(self.los + right.los) elif type(right) is int: return C(self.los + [d for d in str(right)]) else: raise TypeError('unaddable types: C() + '+str(type(right))[8:-2]+'()') and an example of it running x = C(['ab','cd']) print(x + 132) which produces the result ['ab', 'cd', '1', '3', '2'] What if we also wanted to allow the expression 132 + x? If we try to execute this code, unsurprisingly Python responds by raising an exception: TypeError: unsupported operand type(s) for +: 'int' and 'C' because Python calls int.__add__(132,x) and the int class has no clue about objects of type C (and we cannot change this class to process C's correctly). Recall that we covered this problem when discussing __lt__: because the class of the left operand is use to call the method (sometimes called "left-operand dispatch", we would have to change the definition of __lt__ in the int class, which we cannot do. Although this problem is not solvable with relational operators, there is a new mechanism that we will learn can solve it for arithmetic operators. For every binary arithmetic operator, Python also allows us to define a "right" version of it: where the method name is prefixed by an r: so __add__ has an equivilant __radd__ method ("right add"). Here is how we could define __radd__ in class C to successfully compute expressions of the form int() + C(); for 123 + x, Python translates the + into type(x).__radd__(x,123) using "right- operand dispatch". This methods determines what to do if the left operand is an int. def __radd__(self,left): if type(left) is int: return C([d for d in str(left)] + self.los) else: raise TypeError('unaddable types: '+str(type(left))[8:-2]+'() + C()') and an example of it running x = C(['ab','cd']) print(132 + x) which produces the result ['1', '3', '2', 'ab', 'cd'] For arithmetic types/operators that are commutative (e.g., a+b == b+a) we can write __radd__ by simply calling __add__ with the arguments reversed: e.g., def __radd__(self,left): self + left # or self.__add__(left) Note this works with *, but doesn't work with operators like - and /, which are not commutative. We now get a point where you can really test your understanding. Here is the rule Python uses to determine how to compute x + y: (1) Try to call type(x).__add__(x,y) (2) If this produces a result, that is the result of the expression (3) If there is no such __add__ method, or calling it raises an exception, (which is handled), then call type(y).__radd__(y,x); if there is no such __radd__ method, or calling it raises an exception propagate the exception (don't handle it) So, an interesting question is, why doesn't the __radd__ method need to also specify the following, like the __add__ method, which has code that specifies what to do for objects of type C in both its left and right operands? if type(left) is C: return C(left.los + self.los) The answer is, by the rules above, before Python calls this method, it would first call the __add__ method on its left operand; if that operand's type/class is C, then it would call C.__add__ which would successfully process two operands of type/class C. This solution leads to the question, why doesn't Python support __rlt__ and other right-dispatching relational operators? I don't know, and I cannot even find a discussion of this topic on the web. We have now explored (in about 100 lines of text) the relationship between the + operator and __add__ and __radd__ methods. Here is the complete list of operators and their related methods: + - * / // % divmod ** << >> & | ^ translate to __add__ __sub__ __mul__ __truediv__ __floordiv__ __mod__ __divmod__ __pow__ __lshift__ __rshift__ __and__ __or__ __xor__; also exist in their right forms. Again, for certain classes it is obvious which operators to overload naturally. One final comment, for arithmetic - operator, if one had defined -/__neg__ one can often implement the __sub__ method as self + -right, which uses the __neg__ and __add__ methods to compute subtraction: again, this can be a bit inefficient, but it is oh so easy to code. ------------------------------------------------------------------------------ Incrementing Arithmetic Delimiters: += -= *= /= //= %= **= <<= >>= &= |= ^= Again in this section we will look at one incrementing arithmetic delimiter, +=. Technically this is not an operator, because we cannot compose bigger expressions with it: += is more like = than +. This same discussion applies similarly to all the other delimiters. For every arithmetic operator method, there is another method prefaced by i that we can write and Python will try to call (e.g. __add__ and __iadd__). Recall that the meaning of x += y is similar to x = x + (y). We parenthesize y in case it contains any operators whose precedence is lower than +. When Python executes x += y, it tries to call the type(x).__iadd__(x,y): if that method is available and doesn't raise an exception, that is the result; if it cannot find that method or it raises an exception, Python executes the code x = x + (y), which can fail if __add__ is not defined for the types of x and y. Note that unlike arithmetic operators, __iadd__ should mutate the object that x refers to, and should return the value self (the object the method is called on). I'm not sure why this is: technically, it should make no difference what value we return, because we can do nothing with the returned value: it is a syntax mistake to write, for example, print(x += 1) is illegal syntax. But it seems like if I return, say None, then x += 1 makes x refer to None (I'm still thinking about this). Here is an example of the __iadd__ method for the C class, which works for incrementing by objects of type/class C and int. def __iadd__(self,right): if type(right) is C: self.los = self.los + right.los elif type(right) is int: self.lost = self.los + [d for d in str(right)] else: raise TypeError('unaddable types: C() + '+str(type(right))[8:-2]+'()') return self So, what is the difference between executing this method and instead using the = delimiter and __add__ method. It is a bit subtle here, but for other classes can be more obvious. Notice in __iadd__ there is no new construction of an object (as there is in __add__). We are changing the state of an existing object, not creating a new object. Sometimes object creation is wasteful in time and space, and if it wastes a lot of time or space, we could write an __iadd__ method to avoid such a waste. Other times the waste is not much, so it is not worth our time as programmers to define the __iadd__ method, and instead we let Python automatically translate such statements into a combination of using = and __add__. Here is the complete list of arithmetic incrementing delimiters and their related methods: += -= *= /= //= %= **= <<= >>= &= |= ^= translate to __iadd__ __isub__ __imul__ __itruediv__ __ifloordiv__ __imod__ __idivmod__ __ipow__ __lishift__ __irshift__ __iand__ __ior__ __ixor__. ------------------------------------------------------------------------------ Other overloaded operators (coming up) In the next lecture we will discuss container operators (including [index]), the call operator (using ()), context manager protocol, and managing simple attributes. Next week we will have a lengthy discuss aobut the intricacies of using the iterator protocol (and the __iter__ and __next__ methods) ------------------------------------------------------------------------------ FYI, here is the entire C class with all the methods described above. class C: def __init__(self,los): self.los = los def __len__(self): print('Calling len') return sum((len(i) for i in self.los)) def __bool__(self): return ''.join(self.los) != 'False' def __lt__(self,right): if type(right) is C: return self.los < right.los elif type(right) is int: return self.los < [d for d in str(right)] else: raise TypeError('unorderable types: C() < '+str(type(right))[8:-2]+'()') def __gt__(self,right): if type(right) is C: return self.los > right.los elif type(right) is int: return self.los > [d for d in str(right)] else: raise TypeError('unorderable types: C() > '+str(type(right))[8:-2]+'()') def __le__(self,right): return (self < right) or self == right def __ge__(self,right): return (self > right) or self == right def __eq__(self,right): return self.los == right.los def __ne__(self,right): return not (self == right) def __repr__(self): return 'C(' + str(self.los) +')' def __str__(self): return str(self.los) def __neg__(self): return C([i for i in reversed(self.los)]) def __add__(self,right): if type(right) is C: return C(self.los + right.los) elif type(right) is int: return C(self.los + [d for d in str(right)]) else: raise TypeError('unaddable types: C() + '+str(type(right))[8:-2]+'()') def __radd__(self,left): if type(left) is int: return C([d for d in str(left)] + self.los) else: raise TypeError('unaddable types: '+str(type(left))[8:-2]+'() + C()') def __iadd__(self,right): if type(right) is C: self.los = self.los + right.los elif type(right) is int: self.los = self.los + [d for d in str(right)] else: raise TypeError('unaddable types: C() + '+str(type(right))[8:-2]+'()') return self