Operator Overloading (continued) In this lecture we examine how to overload more operators: many fewer than in the first lecture, but some that perform more interesting (and subtle) operations -some that get to the core of Python's operation, which we can change! ------------------------------------------------------------------------------ Container operators We saw that when we call the standard len function on an object, it is translated into a call of the __len__ method on that object: for example, len(x) becomes x.__len__(); Python does the same with reversed function and and the __reversed__ method. Since __reversed__ is really an iterator, we will discuss it in depth next week (see how it, and similar iterators are coded). The following methods all relate to indexing: using the [] delimiters, which we interpret as operators for purposes of this section. What appears inside the brackets is typically either a simple integer or a slice (more on slices below), although these are also the same operators that work on dicts, where the index can be any arbitrary values. Let's look at the laundry list of methods first, and then use each in a new class that we define. Imagine we have defined l to be a list. Note that the index parameter can be an int or a slice for lists (which is what we are focusing on) __getitem__(self,index) : index: l[3] or l[1:-1] __setitem__(self,index,value) : store list: l[3]='a' or l[2:4]=('a','b','c') __delitem__(self,index) : delete: del l[3] or del l[1:-1] __contains__(self,item) : called by the in operator: 'a' in l To illustrate these methods, let's write a class that allows us to specify a list that is indexed staring at 1 (not 0). Really we should think in terms of defining a class for a new type of data (lists, tuples, sets, frozensets, dicts, and defaultdicts are all types of data), but for now let's look at adapting/using standard lists in this new way, to simplify what we are learning (we could do the same with strings, writing Str1). This example also uses delegation, where an operation on a List1 is translated into a "similar" operation on the list it stores/delegates to. We start with class List1: def __init__(self,thelist): self.thelist = list(thelist) def __str__(self): return str(self.thelist) So, we can write x = List1(['a', 'b', 'c', 'd', 'e']) print(x) which prints ['a', 'b', 'c', 'd', 'e'] So x looks just like a list when we create one and print one (although we must use the List1 constructor, not just [...] for these special lists). We now want to implement list like behavior, but with indexes that start at 1 instead of 0. First we look at the __getitem__ method, called by l[...]. We can use either integers or slices inside the brackets, for now let's just look at just integers. Note that in Python lists, integer indexes are either non-negative (0, 1, ...), which specify an index from the beginning (e.g., 0 is the first, 1 is the second...), or negative, which specify an index from the end (e.g., -1 is the last index, -2 second from last). Note the asymmetry we are now fixing: in List1 we want 1 to be first and -1 be last, 2 to be second and -2 to be 2nd from last, until Python lists, where 0 is the first index and -1 the last. So we will start with a class function, _fix_index that will demote positive indexes by 1, but leave 0 and negative indexes as is. So if _fix_index(1) returns 0: index 1 in List1 will translate to index 0 when delegated to index self.thelist. def _fix_index(i): return (i-1 if i >= 1 else i) Because this is a class method, it has no self parameter. We will call this method (see below) by List1._fix_index(int). Methods like these are also called static methods: static means they do not operate on instances of a class: again, no self parameter. Alternatively, we could have defined _fix_index as a global function in this module, and called it just as _fix_index(int) in the class methods, but it is better to define this class/static method in the class itself. Note we use a prefix underscore to indicate no one outside the class should call this function: it is just a helper for the methods in the class. With _fix_index defined, we can write __getitem__ as follows. Notice that it ensures index is an int, otherwise it raises an exception. If index is an int, it delegates to thelist to access its information, but when accessing self.thelist we decrease the index by 1 for positive indexes, but leave zero and negative indexes alone. For illumination/debugging purposes we have put a print in __getitem__ which we will comment/uncomment as needed. For the examples below we will leave it in (so we can see when __getitem__ is called by Python, since we don't explicitly call it.. def __getitem__(self,index): print('__getitem__('+str(index)+')') # for illumination/debugging if type(index) is int: return self.thelist[_fix_index(index)] else: raise TypeError('List1.__getitem__ index('+str(index)+') must be int') We have not completely specified this method (we need to talk about slices and how we can process them) but we are more than half-way there. Running the following script illustrates how __getitem__ is called. x = List1(['a','b','c','d','e']) print(x) print(x[1], x[2], x[-2], x[-1]) Python produces the following output, printing the entire list, the first, second, last, and second to last values. Notice the calls that Python automatically makes to __getitem__ when we use the [] operator. ['a', 'b', 'c', 'd', 'e'] __getitem__(1) __getitem__(2) __getitem__(-2) __getitem__(-1) a b d e To finish writing __getitem__ we must take a short detour to talk about slices. Recall that for Python lists we can write indexes like x[1:4], x[2:-2], x[:-1] and even x[::2]. Each of these slices translates into an actual slice object (yes there is a slice class defined in builtins) that is passed to __getitem__. Each slice object has three atrributes that we can access: start, stop and step. x[1:4] translates to x[slice(1,4,None)] x[2:-2] translates to x[slice(1,-2,None)] x[:-1] translates to x[slice(None,-1,None)] x[::2] translates to x[slice(None,None,2)] The __getitem__ methods for lists know how to process slices. We can delegate these slices to be used on self.thelist, but we need to fix the start and stop indexes (as done above for simpler int indexes, with the same function), but leave the step as is. Also, since slices can specify None, we need to fix those by leaving them unchnaged. So, we update the _fix_index and __getitem__ methods as shown below. Now for slices, we construct a fixed slice from the one passed as an argument (fixed for start and stop in 1-origin lists) and use this slice when delegating to self.theilst. def _fix_index(i): if i == None: return None else: return (i-1 if i >= 1 else i) # for + indexes, 1 smaller: 1 -> 0 # for - indexes, the same: -1 (still last), -2 (still 2nd to last) def __getitem__(self,index): print('__getitem__('+str(index)+')') # for illumination/debugging if type(index) is int: return self.thelist[List1._fix_index(index)] elif type(index) is slice: s = slice(List1._fix_index(index.start), List1._fix_index(index.stop), index.step) return self.thelist[s] else: raise TypeError('List1.__getitem__ index('+str(index)+') must be int/slice') Running the following script illustrates how __getitem__ works with slices; we left the print statement in __getitem__ and the __str__ for slice objects prints as slice(start, stop, step) x = List1(['a','b','c','d','e']) print(x) print(x[1:4], x[2:-2], x[:-1], x[::2]) Python produces the following output, printing the entire list, and then the specified slices of that list (again, where the index of the first item is 1). Notice the calls that Python automatically makes to __getitem__ when we use the [] operator with slices. Remeber that slices include indexes up to but not including the stop index (we could fix this too, if we didn't like, by always inrementing the stop index by 1, therefore using one more index; this would be a problem, though for incrementing -1 to 0 which would be wrong and we'd need a special way to fix that: incrementing -1 to None). ['a', 'b', 'c', 'd', 'e'] __getitem__(slice(1, 4, None)) __getitem__(slice(2, -2, None)) __getitem__(slice(None, -1, None)) __getitem__(slice(None, None, 2)) ['a', 'b', 'c'] ['b', 'c'] ['a', 'b', 'c', 'd'] ['a', 'c', 'e'] Now that we know how to handle indexes that are integers or slices, in our example fixing them and delgating their use to self.thelist, we can easily write the remaining methods. So for example the __setitem__(self,index,value) method is supposed to assign value to object at the specified index(es). Its structure is identical to __getitem__, processing int indexes, slice indexes, or raising TypeError. Here, thought we are assigning to self.thelist, not returning a value. Because there are no return statements in this function, Python will return None when it finishes executing; we could also specify explicitly: return None def __setitem__(self,index,value): print('__getitem__('+str(index)+')') # for illumination/debugging if type(index) is int: self.thelist[List1._fix_index(index)] = value elif type(index) is slice: s = slice(List1._fix_index(index.start), List1._fix_index(index.stop), index.step) self.thelist[s] = value else: raise TypeError('List1.__setitem__ index('+str(index)+') must be int/slice') Running the following script illustrates how __setitem__ works with int and slice indexes; we again left the print statement in __setitem__. x = List1(['a','b','c','d','e']) print(x) x[1] = 1 x[4:5] = (4,5) print(x) Python produces the following output, printing the entire list, and then the updated list. Notice the calls that Python automatically makes to __setitem__ when we use the [] operator with slices. ['a', 'b', 'c', 'd', 'e'] __setitem__(1,1) __setitem__(slice(4, 5, None),(4, 5)) [1, 'b', 'c', 4, 5, 'e'] Next, the __delitem__(self,index) method is supposed to delete/remove values from the specified index(es). Its structure is identical to __getitem__ and __set__item, processing int indexes, slice indexes, or raising TypeError. As with __setitem__ we returning None def __delitem__(self,index): print('__delitem__('+str(index)+')') # for illumination/debugging if type(index) is int: del self.thelist[List1._fix_index(index)] elif type(index) is slice: s = slice(List1._fix_index(index.start), List1._fix_index(index.stop), index.step) del self.thelist[s] else: raise TypeError('List1.__delitem__ index('+str(index)+') must be int/slice') Running the following script illustrates how __delitem__ works with int and slice indexes; we again left the print statement in __delitem__. x = List1(['a','b','c','d','e']) print(x) del x[1] # now ['b','c','d','e'] index 1 deleted print(x) del x[2:4] # now ['b','e'] indexes 2-3 (not 4) deleted print(x) Python produces the following output, printing the entire list, and then the updated list. Notice the calls that Python automatically makes to __setitem__ when we use the [] operator with slices. ['a', 'b', 'c', 'd', 'e'] __delitem__(1) ['b', 'c', 'd', 'e'] __delitem__(slice(2, 4, None)) ['b', 'e'] Before defining the __contains__ method, we will learn that if there is no defined __contains__ method, Python will check "x in l" by first checking if x == l[0], then x == l[1], then x == l[2], ... until it finds x, or indexing throws an exception (Python takes this approach instead of compuing len(l) as an upper bound to index, because the __len__ method might not be defined for this class). So if we ran the following script (without defining the __contains__ method) x = List1(['a','b','c','d','e']) print('d' in x) print('z' in x) Python produces the following output, printing __getitem__(0) __getitem__(1) __getitem__(2) __getitem__(3) __getitem__(4) True __getitem__(0) __getitem__(1) __getitem__(2) __getitem__(3) __getitem__(4) __getitem__(5) __getitem__(6) False Let's look at this carefully. For the first print Python executes 'd' in x; it first calls __getitem__(0) but in the List1 objects there is nothing at index 0; actually if you look at the code a call of __getitem__(0) is translated to [0] -'a'- which looks at the first value and doesn't find 'd'; then Python calls __getitem(1), which is also translated to [0] -'a'- which again looks at the first value and doesn't find 'd'; then Python calls __getitem(2), which is translated to [1] -'b' which looks at the second value and doesn't find 'd'; ...; then Python calls __getitem(4), which is translated to [3] -'d', which looks at the fourth value and does find 'd', so the contains returns True, which is printed. For the second print Python executes 'z' in x; it calls __getitem__ multiple times; __getitem(5) is translated to [4] -'e'- which looks at the fifth/last value and doesn't find 'z'; then Python calls __getitem(6), which raises an exception indicating that there are no more values to examine, so the contains returns False, which is printed. Now, we can define our own contains to just use the in operator for lists. def __contains__(self,item): return item in self.thelist So if we ran the following script (defining the __contains__ method above) x = List1(['a','b','c','d','e']) print('d' in x) print('z' in x) Python now produces the following output, printing just the following, showing that there are no more calls to __getitem__. True False If we want to iterate over a class (for example, in a for loop) we should implement the __iter__ and __next__ methods. We will discuss these in detail in the next few lectures. But like in/__contains__ if those methods aren't defined, Python uses indexing. For example, if Python executes x = List1(['a','b','c','d','e']) for i in x: print(i) It prints the following __getitem__(0) a __getitem__(1) a __getitem__(2) b __getitem__(3) c __getitem__(4) d __getitem__(5) e __getitem__(6) Notice as with the in operator above, it starts calling __getitem__ at index 0, so that value gets produced/printed twice by the iterator. For the in operator this wasn't a problem, but here it produces/prints the first value twice, which we will fix when we define the real __iter__ method. Also like the in operator it call __getitem__ with successively bigger indexes, returning those values until at __getitem__(6) an exception is raised, because there is no index 6 in this List1 object: just indexes 1 through 5. We are now coming to the end of this example. Although this example used integer indexes for Lists, if we wanted to produce a special kind of dictionary we can use any type of index in the __getitem__, __setitem__, and __delitem__ methods. Of course we cannot use slices with dictionaries the way we did above with List1. For classes that will be used more like dicts, there is another method __missing__(self,key) which is called whenever a dict failes to find a key it is looking up. We can define the __missing__ method to tell Python what to do. In a normal dict class Python throws an exception; in the defaultdict class, its __missing__ methods put a special object in the defaultdict (specificied in the construction of the dictionary) with that key, and then returns that object for possible further processin. One last point here. most of these methods return None, because they are meant to be the result of commands an return no values. But, it might be useful for them to return a value. For example, in the Java library for maps (which are Java dicts), mapping keys to values, calling __setitem__ returns the old value associated with the key being set; likewise, __delitem__ returns the value associated with the key being deleted. Using these returned values sometimes makes for more elegant code. Finally, although we changed the indexes to start at 1, we did not change the upper-bound meaning of slices. So a slice 2:4 was translated to 2:5, which correspondes to indexe 2, 3, and 4 (not including 5). I often find it difficult to remember/use the fact that the stop index is not included in the slice, so it might be interesting to change the slices so that the stop index is included, which can be done by simply when the slices are created and used in __getitem__, __setitem__, and __delitem__. ------------------------------------------------------------------------------ Function Call We know how Python calls methods on instance of classes: by the Fundamental Equation of OOP, o.m(...) is translated into type(o).m(o,...). But we can also define how to use an instance as if it were a function itself, allowing us to calling it as o(...). The way we do this is by defining a method named __call__ in the class. The call o(...) is translated into o.__call__(...) The following class a bit ahead of where we are now, but we will discuss it, and classes like it, in detail later in the quarter. Track_Calls is a (decorator) class that we can use to remember how many times a function is called. We illustrate its use with a recursive fibonnaci (fib) function. When a Track_Calls object is created, it remembers the function it is given in its f instance variable, and initializes its call counter instance variable to 0. An object of class Track_Calls can be called directly: its __call__ method increments the call counter and calls and returns the value computed by the remembered function: delegating to it to compute the real values we want. Thus, we can replace a real function call with a function call to an object in which it is embedded; and objects can do more than functions: for example, they can remember information/state, like how many times they are called. class Track_Calls: def __init__(self,f): self.f = f self.calls = 0 def __call__(self,x): # or ,*args,**kargs): # bundle arbitrary arguments self.calls += 1 return self.f(x)# or ,*args,**kargs) # unbundle arbitrary arguments So, if we define the fib function, and then bind to the name fib the object created by Track_Calls, when passed a reference to the fib function. When we we call fib(...), Python calls fib.__calls__(...) which increments a counter and then calls/returns fib (and the recursive calls of fib -remember fib is now bound to this TrackCalls objectt- are all handled by the __call__ method. Ultimately the recursive function call returns an answer and the calls instance variable stores how many calls of fib, though fib.__call__ were made. def fib(n): assert n>=0, 'fib cannot have negative n('+str(n)+')' if n == 0: return 1 elif n == 1: return 1 else: return fib(n-1) + fib(n-2) fib = Track_Calls(fib) Here is code that creates a table of the returned values from fib and the number of calls each required. I directly reset fib.calls to 0, but I could have created a method in Track_Calls to reset this instance variable, or just remembered the number of calls before and after a new call and subtracted. for i in irange(0,31): print('fib(',i,') = ',fib(i),' and was called ',fib.calls,' times',sep='') fib.calls = 0 # reset instance variable to 0 When we discuss decorators later in the course, we will see all sorts of what I would call fascinating uses of classes that remember a function and define function calls for the object that delegate to the function, but do something else too. ------------------------------------------------------------------------------ Context managers We have seen (I think you saw them in ICS-32) context managers, which have the form with A_Class(...) [as name]: block (using name) Now we will learn to write our own. The purpose of context managers is to simplify the use of exception handling. Intead of having to write try/except whenever a class is used, we can put the try/except code in the class itself, defining __enter__ and __exit__ methods, thus reusing this exception code whenever an object from this class is created. Here is a fairly large but useful example of a Logging class. It allows a programmer to execute code and log messages into a file, including whether or not the block terminated/exited normally or by having one of its statements raise an exception. The __enter__ method takes as an argument an instance of A_Class; if the [as name] option is used, then this method must return self to bind to name. The __exit__ method takes three arguments: an exception class, its string value, and a traceback (which can be printed nicely, and is in this example, into the log file). These are filled in if an actual exception is raised in the block, if not they are all None. The __exit__ method does what it wants: if it returns True the exception is considered handled and not propagate; if it returns False Python propagates the exception. Note in __exit__ we just looked to see whether or not there was an exception, but we could have written code like the following to handle different exceptions in different ways. if exc_type in (AssertiontError, KeyError, ... ): handle these errors the same way elif exc_type in (...) etc. import traceback class Logging: def __init__(self, file_name, stop_exception): self.file = open(file_name,'w') self.stop_exception = stop_exception self.count = 0 def log(self,message): self.file.write(message+'\n') self.count += 1 def __enter__(self): self.log('Entered Logging context') return self # so log can be called in block def __exit__(self, exc_type, exc_value, exc_traceback): if exc_type == None: self.log('Exited Logging context with no exception') else: self.log('Exited with exception that is ' + ('stopped here' if self.stop_exception else 'propagated')) traceback.print_tb(exc_traceback,file=self.file) self.log('Logged ' + str(self.count+1)+' messages') self.file.close() return (None if exc_type == None else self.stop_exception) with Logging('log1.txt',stop_exception=True) as now: print('do some operations') now.log('successfully did some operations without an exception') print('do some more operations') now.log('successfully did some more operations without an exception') if prompt.for_bool('pretend final operations raised exception?'): raise AssertionError('raised by user') now.log('successfully did final operations without an exception') print('Continuting after block') Execute this code 3 ways, entering False to the bool prompt in the block (stop_exception doesn't matter), answering True to the bool prompt in the block (with stop_exception=True); answering False to the bool prompt in the block (with stop_exception=False). Note that when passing bool arguments, it is great to use the named-argument form (even if you could write it positionally), so the reader of the program (mostly you!) has an idea what the True/False means. Of course, choose a good paramter name in this case too. ------------------------------------------------------------------------------ Attribute: In this last section we will discuss some of the methods that are at the heart of how Python executes our code. All require carefully use or they will cause big problems (often infinite recursion) that stop the execution of all Python code. All concern getting and setting the value of attributes. Here is the laundry list. __getattr__(self,name) : cannot find name attribute __setattr__(self,name,value) : set name attribute to value __delattr__(self,name) : delete name attribute __getattribute__(self,name) : access name attribute (very tough to use) Whenever we refer to an attribute in some object's namespace (recall a dict stores the namespaces for objects, containing the bindings of their instance variables) Python calls a one of a few double-underscore method: We will look at __getattr__ first (it is the safest) which is called when a attribute cannot be found. Here is a small class that defines this method to return a string that includes the name of the nonexistant attribute, instead of raising an NameError exception. We could also easily return None for such non-existant attributes when they are "gotten". class C: def __getattr__(self,name): # called when name (a str) is not in the namespace return name+'?' c = C() print(c.a_name) c.a_name = 0 print(c.a_name) Running this code prints the results a_name? 0 When the a_name attribute of c is referred to in the first print, it doesn't exist, so Python calls __getattr__ which returns the attribute's name with a ? appended. When the a_name attribute of c is referred to in the second print, it has been bound to a value (see c.a_name = 0), so Python doesn't call the __getattr__ method. IMPORTANT __getattr__ is called only for attributes that don't exist, not all attributes (that is what __getattribute__ is for). Now let's looks a sophisticated use of the __setattr__ method, which is called whenever we are going to set some attibute: e.g., x.a = 0 is setting attribute a of instance object x. Thie class defines the __setattr_ method so that it uses a dict to remember all the values every bound to an attribute (stored in self.history). from collections import defaultdict # for elegance; with __setattr__ we need it class C: def __init__(self): self.history = defaultdict(list) self.s = 0 def bump(self): self.s += 1 # bind s to a value one bigger def __setattr__(self,name,value): # print('__setattr__',name,value) # helps for debugging if 'history' in self.__dict__: # false 1st time only: self.history = self.history[name].append(value) self.__dict__[name] = value def report(self): print('History Report:') for k,v in self.history.items(): print(' ',k,' had the values:', v) Whenever __setattr__ is called, it checks to see if history is an attribute name; if not yet (only the first time, when self.history = defaultdeict(list) is executed) it skips updating the history. Then it sets the attribute directly, by using __dict__ directly, using the string name. If we are not careful with the code we write in __setattr__ and try to set other attributes in this method, we might cause infinite recursion. So we we ran the following script x = C() x.s = 3 x.bump() x.bump() x.y = 11 x.s = 8 x.y += 1 x.report() Calling x.report would show a history of all the values the two instance variables stored. History Report: s had the values: [0, 3, 4, 5, 8] y had the values: [11, 12] Note that x.history['s'][-1] is the current value bound to x.s, and x.history['s'][-2] is the previous one. We might call this an elephant class: it never forgets the binding for a value. The method __delattr__ is not so useful and __getattribute__ is so dangerous to use that we will byplass it now. But here is a short example of a class that keeps a list of names that have been deleted from it (like remembering the old binddings of instance varaibles). class C: def __init__(self): self.deleted = [] def __delattr__(self,name): self.deleted.append(name) del self.__dict__[name] def report(self): print("Deleted attributes:',self.deleted) c = C() c.x = 1 c.y = 2 del c.x del c.y c.report() ------------------------------------------------------------------------------ The double-underscore methods __iter__ and __next__ are so useful (everything about iteration is s useful) we will spend next week examining this protocol and various ways to implement it. ------------------------------------------------------------------------------ FYI, here is the entire L1C class with all the methods described above. class List1: def __init__(self,thelist): self.thelist = list(thelist) def __str__(self): return str(self.thelist) def _fix_index(i): if i == None: return None else: # for positive indexes, 1 smaller: 1 -> 0 # for - indexes, the same: -1 (still last), -2 (still 2nd to last return (i-1 if i >= 1 else i) def __getitem__(self,index): print('__getitem__('+str(index)+')') # for illumination/debugging if type(index) is int: return self.thelist[List1._fix_index(index)] elif type(index) is slice: s = slice(List1._fix_index(index.start), List1._fix_index(index.stop), index.step) return self.thelist[s] else: raise TypeError('List1.__getitem__ index('+str(index)+') must be int/slice') def __setitem__(self,index,value): print('__setitem__('+str(index)+','+str(value)+')') # for illumination/debugging if type(index) is int: self.thelist[List1._fix_index(index)] = value elif type(index) is slice: s = slice(List1._fix_index(index.start), List1._fix_index(index.stop), index.step) self.thelist[s] = value else: raise TypeError('List1.__setitem__ index('+str(index)+') must be int/slice') def __delitem__(self,index): print('__delitem__('+str(index)+')') # for illumination/debugging if type(index) is int: del self.thelist[List1._fix_index(index)] elif type(index) is slice: s = slice(List1._fix_index(index.start), List1._fix_index(index.stop), index.step) del self.thelist[s] else: raise TypeError('List1.__delitem__ index('+str(index)+') must be int/slice') def __len__(self): return len(self.thislist) def __contains__(self,item): return item in self.thelist ------------------------------------------------------------------------------ FYI, here is a list of operators and the double-underscore methods that we can define to overload them. We've covered most but not all. Relational operators: < > <= >= eq ne __lt__ __gt__ __le__ __ge__ __eq__ __ne__ Unary operators/functions: + - abs ~ round floor ceil trunc __pos__ __neg__ __abs__ __invert__ __round__ __floor__ __ceil__ Arithmetic: + - * / // % divmod ** << >> & | ^ __add__ __sub__ __mul__ __div__ __floordiv__ __mod__ __divmod__ __pow__ __lshift__ __rshift__ __and__ __or__ __xor__ Reflected (right) arithmetic __radd__ __rsub__ __rmul__ __rdiv__ __rfloordiv__ __rmod__ __rdivmod__ __rpow__ __rlshift__ __rrshift__ __rand__ __ror__ __rxor__ Incrementing Arithmetic: += -= *= /= //= %= **= <<= >>= &= |= ^= __iadd__ __isub__ __imul__ __idiv__ __ifloordiv__ __imod__ __idivmod__ __ipow__ __lishift__ __irshift__ __iand__ __ior__ __ixor__ Type conversion: int float complex oct hex index trunc coerce __int__ __float__ __complex__ __oct__ __hex__ __index__ __trunc__ __coerce__ Class representation: __str__ __repr__ __unicode__ __format__ __hash__ __nonozero__ __dir__ Attribute: __getattr__ __setattr__ delattr__ __getattribute__ Containers: __len__ __getitem__ __setitem__ __delitem__ __iter__ __reversed__ __contains__ __missing__ Misc: __call__ __copy__ __deepcopy__ getattr/setattr: special classes, wrapped, inheritance soon, and decorators Context managers __enter__ __exit__ Descriptors: __get__ __set__ __delete__ Do you want to build your own context manager: overload the __enter__ and __exit__ methods __iter__ and __next_ for generators