Inheritance III In this lecture we will look at a few useful examples of inheritance, classifying them into two categories: normal and mix-in inheritance. In normal inheritance, a large base class (typically with lots of data and method attributes) is specialized to perform a bit differently: it typically involves a small derived class (defining few data and method attributes) single-inheriting from a large base class. Mix-in inheritance typically involves a derived class multiply-inheriting -to add many small special behaviors to it- from a variety of small base classes. ------------------------------------------------------------------------------ Inheritance Summary: 1) A derived class should be "like" its base class: all the base class methods should be meaningful for it: either inherited by the derived class -like reset and value_of in Counter/Modular_Counter- or overridden by it -like __str__ and inc; the derived class can also define new methods that are meaningful only for the derived class -like get_modulus. The relationship between the derived class class and its base class is an "IS-A" relationship: we say a Modular_Counter IS- A Counter (it is-a specialized kind of counter); a defaultdict IS-A special kind of dict. We characterize other relationships (non-inheritance) among classes as HAS-A relations: For example, a FA (Finite Automata) class HAS-A attribute that stores the states and transitions; we WOULDN'T derive the FA class from the dict class: a FA is not a special kind of dict, but it has-a dict to represent its information. 2) Inheritance in Python works by augmenting the FEOOP look-up rule for attributes: after checking for an attribute in the namespace (__dict__) of an object, it is sequentially checked for in the namespaces of each class in the __mro__ tuple attribute (stored in the class from which the object was constructed). The first value in __mro__ is that class itself; the other values include any base classes that it was derived from, all the way back up the inheritance hierarchy to the object class at its root (that will alway be the last class in the __mro__). It is constrained by looking at derived classes before their base classes, and when deriving involves multiple base classes, looking at the classes in the order they are specified in the derived class. 3) To call a method in the base class from the same method (the method that overrides it) in a derived class, use BaseClassName.method (e.g., we call Counter.inc(...) in the inc method defined in the Modular_Counter class). This often happens in the __init__ method too (see below). 4) Typically the __init__ in the derived class calls the __init__ method of its base class (see the form above), passing along some of its arguments. This means that the parameter structure for the derived class is often longer than the parameter structure the base class(es) it was derived from. By calling __init__ in the base class, it will setup all the attributes it needs to work correctly, when its methods are called (via inheritance, or directly in overridden methods: see 3 above). 5) A derived class should not directly access the data attributes in any of its base classes; instead (if the attribute needs to be examined or mutated) the base class should provide methods to access/mutate it. E.g., the Counter class provides the get_value method to access its _value attribute, and the reset/inc methods to mutate its _value attribute in constrained ways. So Counter controls _value and Modular_Counter controls _modulus (which can be examined by the get_modulus method, but not mutated/changed). ------------------------------ Normal Inheritance Examples pdefaultdict derived from the base-class dict: The dict class implements a standard Python dictionary. It is a large class that defines many methods. It uses many attributes to store its dict, but we don't care about these (since we will not access by name attributes defined in a base class). In this example we will use it as a base class, deriving from it (using single-inheritance) the defaultdict class. Actually defaultdict is already defined in the collections module, so here we will define the pdefaultdict (pseudo-defaultdict) class in the pcollection (pseudo-collections) module. A defaultdict IS-A special kind of a dict, so it is a good candidate to define via normal inheritance. All operators/methods working on dicts make sense working on defaultdicts; most work correctly purely by inheritance, but we must override the meaning of the __init__ method (adding another parameter to represent a function it can call with no arguments to create a value for keys that are accessed but not present) and override the meanings of the __repr__ and __missing__ methods (which is called by __getitem__ in a dict when a key is not present). A defaultdict has only three behavioral differences from a dict: (1) Its __init__ method can/should be initialized with a special reference to a class/function that is called (with no arguments) to store/return the associated value for any missing key. (2) Its __repr__ method returns a string with 'defaultdict' followed, in parentheses, by the special reference to a class/function passed to __init__ followed by the key:value items in the dictionary. (3) When a key that is missing from the defaultdict is referred to, the key is automatically added to the defaultdict, associated with the value returned by calling (with no arguments) the special reference to a class/function passed to __init__. Note that the method __missing__ is already called in such circumstances (in a normal dict, calling __missing__ raises a KeyError exception), so we will override this method in the derived/defaultdict class to instead perform these actions. Here is a complete definition of pdefaultdict derived from dict. class pdefaultdict(dict): def __init__(self,default_factory=None,initial_dict=[],**kargs): dict.__init__(self,initial_dict,**kargs) # call to initialize base-class self._default_factory = default_factory # used in overridden methods def __repr__(self): return 'pdefaultdict('+str(self._default_factory)+','+dict.__repr__(self)+')' # When accessing d[key] the inherited method __getitem__ is # called; if it finds key is not in the dictionary, it calls # self.__missing__(key), returns the result of executing this # method, which uses default_factory, if present, to create a # value associated with d[key] and return that value def __missing__(self,key): if self._default_factory == None: dict.__missing__(self,key) # same as: raise KeyError(str(key)) result = self._default_factory() # Call default, with no arguments self[key] = result # Store it in the pdefaultdict return result # Return a reference to it x = pdefaultdict(list) x['a'].append('x') print(x) prints pdefaultdict(,{'a': ['x']}) Notes: (1) The standard dict.__init__ method has default-value parameters (a) an iterable of items to pre-load the dictionary and (b) any number of item parameters of the form key=value (collected in **kargs and added to the dictionary after the pre-loaded items). IMPORTANT: We don't have to know how dict.__init__ processes such data. We do need to be aware of what arguments it expects/processes, and supply parameters for these arguments to pdefaultdict.__init__, so its can pass them along to dict.__init__ for real processing (just like init_value in Counter). The pdefaultdict.__init__ method also has an extra default_factory parameter: it uses its second two parameters to call dict.__init__ and then also stores default_factory as an attribute the newly constructed pdefauldict object, where it is used in the overridden methods __repr__ and __missing___. (2) The pdefaultdict.__repr__ method returns the appropriate string, including the name 'pdefaultdict', the special reference to a class/function to call for missing keys, and the current contents of the dictionary . Of special interest is that fact that there is an explicit call to dict.__repr__(self) in this method, which overrides __repr__, to show the dict part of the pdefaultdict. (3) The pdefaultdict.__missing__ method supplies the main difference between dicts and defaultdicts, by overriding the dict.__missing__ method (which in dict always raises KeyError): here, so long as self.default_factory is not None (meaning an argument has been supplied to this parameter in its __init__), it is called to produce an initial value to associate with the missing key. (4) Any other operator/method applied to a pdefaultdict object executes the corresponding method inherited from dict: there are very many of these inherited (and not overridden) methods. ---------- Lists indexes starting at 1: The list class implements a standard Python list. It is a large class that defines many methods. In this example we will use it as a base class, deriving the class list1, which represents a list that we index starting at 1. Notice that it inherits from list (so all the list functions like .append and .sort work for list1) and overrides __getitem__ to call list's __getitem__ with a value one smaller. There are a few weird cases where list1 doesn't quite perform as expected, but for most it does. from goody import irange class list1(list): # Handle positive and negative indexes correctly def __getitem__(self,index): assert index!=0, 'list1.__getitem__: 0 index in origin-1 indexed list1' return list.__getitem__(self, index-1 if index > 0 else index) def __setitem__(self,index,value): assert index!=0, 'list1.__setitem__: 0 index in origin-1 indexed list1' list.__setitem__(self, index-1 if index > 0 else index, value) def __delitem__(self,index): assert index!=0, 'list1.__deltitem__: 0 index in origin-1 indexed list1' list.__delitem__(self, index-1 if index > 0 else index) ... other methods too x = list1([1,2,3,4,5]) print(len(x),bool(x)) for i in irange(1,len(x)): print(i,x[i]) x.append('x') print(x) x.reverse() print(x) print(2 in x) this prints 5 True 1 1 2 2 3 3 4 4 5 5 [1, 2, 3, 4, 5, 'x', 'x', 'x', 'x', 'x'] ['x', 'x', 'x', 'x', 'x', 5, 4, 3, 2, 1] True ---------- OptionMenuValue derived from the base-class OptionMenu (in tkinter) The OptionMenu class is defined in the tkinter module. Constructing an OptionMenu widget in a Frame presents the user with a labeled menu, allowing the user to see, select, and re-select a desired option, and discover what the string value of the current selected option is. The constructor for OptionMenu has an arbitrary number of arguments (see 3). (1) A parent Frame in which the widget is displayed (2) A StringVar into which the text of the selection is put. When initially displayed, an OptionMenu will show this StingVar's value. Clicking it exposes all the other options, which can be selected, replacing the value in the StringVar. So StringVar is like a mutable string: its contents can be changed.* *This means if we execute a = StringVar('x') b = a a.set('y') then both a and b refer to the same StringVar object that stores 'y'. Contrast this to a = 'x' b = a a = 'y' where a refers to the str object 'y' and b refers to the str object 'x'. Draw the pictures. (3) Any number of strings which comprise the options for this option menu. Below is a trivial demo tkinter application that illustrates how an OptionMenu is use, which also uses tkinter's StringVar, Entry, and Button widgets. It displays an OptionMenu (in column 1) and a Button and Entry (top and bottom in column 2). The user can select an option from the OptionMenu. The 'Show Chosen Option' button places the selection into the Entry. Run this program a few times, repeatedly selecting options and pressing the 'Show Chosen Option' button. Whenever this button is pushed, Python executes the option_to_entry function (that is the command associated with the button, wrapped in a parameterless lambda), which erases the information currently in the Entry e and then puts the text from the StringVar o_var into this Entry. Note that if no option is chosen yet, the original text in o_var is put in the Entry. When pressing the button calls the lambda, its body calls option_to_entry with the global names o_var (for the option) and e (for the entry). Using a lambda like this one is common for commands in tkinter. ----- commands for widgets (and the partial evaluation alternative) The command parameter in this button (and other widgets) must refer to a parameterless function object. Here we use a parameterless lambda that calls the option_to_entry function with the appropriate arguments. Another way to accomplish this goal is by using partial evaluation, to reduce the number of arguments in the option_to_entry to 0. After importing partial from functools and defining option_to_entry, we could add one line and rewrite b = button(...) code. bs_command = partial(option_to_entry,o_var,e) b = Button(main,text='Show Chosen Option',command = bs_command) ----- from tkinter import * root = Tk() root.title('Widget Tester') main = Frame(root) main.pack(side=TOP,anchor=W) o_var = StringVar() o_var.set('Choose Option') om = OptionMenu(main, o_var, 'option1','option2','option3') om.grid(row=1,column=1) om.config(width = 10) e = Entry(main) e.grid(row=2,column=2) def option_to_entry(option,entry): entry.delete(0,END) entry.insert(0,option.get()) b = Button(main,text='Show Chosen Option',command=lambda : option_to_entry(o_var,e)) b.grid(row=1,column=2) root.mainloop() This is 18 non-blank lines of code. Notice that to control the OptionMenu we are required to bind two names: one for the OptionMenu itself (om) and one to store the value of the chosen option (o_var). We will now derive a simple variant of the OptionMenu class, named OptionMenuValue, which keeps track of its own selection's value. In addition, we will implement it so that it returns 'None' (rather than the initial value of o_var, which is the title for the OptionMenu) if the user has made no selection yet. Finally, a reset method allows us to reset what appears on the option menu to the string displayed there initially. As with pdefaultdict/dict or list1/list, an OptionMenuValue is a slight variant of an OptionMenu, so it is a good candidate to define via inheritance. All operators/methods working on OptionMenu make sense working on OptionMenuValue: most (e.g. grid and config, called in the code) work purely by inheritance, but we must change the parameters of the __init__ method and add get/reset methods in the OptionMenuValue class. Note that there are hundreds of methods that we can call on most GUI widgets like OptionMenu (for placement, size, etc.) which are all inherited by OptionMenuValue; we don't have to think about them, nor do we have to think about the data attributes defined in OptionMenu, since we do not refer to them directly. Observe how the OptionMenuValue class stores and uses its own StringVar, how get works, and how the option_to_entry function now manipulates the object constructed from the OptionMenuValue class directly. from tkinter import * # OptionMenuValue: with title and option_tuple # get is a pull function to get the option selected class OptionMenuValue(OptionMenu): def __init__(self,parent,title,*option_tuple,**configs): self.result = StringVar() # create a new StringVar to use for option self.result.set(title) # set it to the title self.original_title = title # remember original title in attribute # initialize base class (which does most of the work) with self.result, # passing along the other arguments passed to this __init__ OptionMenu.__init__(self,parent,self.result,*option_tuple,**configs) def reset(self): self.result.set(self.original_title) def get(self): value = self.result.get() return value if value != self.original_title else 'None' root = Tk() root.title('Widget Tester') main = Frame(root) main.pack(side=TOP,anchor=W) om = OptionMenuValue(main, 'Choose Option', 'option1','option2','option3') om.grid(row=1,column=1) om.config(width = 10) e = Entry(main) e.grid(row=2,column=2) def option_to_entry(option,entry): entry.delete(0,END) entry.insert(0,option.get()) b= Button(main,text='Show Chosen Option',command=lambda : option_to_entry(om,e)) b.grid(row=1,column=2) b = Button(main,text='Reset Option',command=om.reset) b.grid(row=1,column=3) root.mainloop() Not counting the class definition, this is 18 lines of code, but two are required to add the new 'Reset Option' button, so the equivalent code has shrunk from 18 to 16 lines. This reduction isn't tremendous, but most of the code included does bookkeeping operations. Mostly what we have accomplished is establishing one object that keeps track of the StringVar functionality inside the OptionMenuValue object itself. If we had 10 OptionMenu GUI objects in our original program, we would need 10 StringVar objects too: now we can use just 10 OptionMenuValue objects (and not have to remember which OptionMenu object goes with which StringVar object). ---------- OptionMenuToEntry derived from the base-class OptionMenuValue We will now derive a simple variant of OptionMenuValue named OptionMenuToEntry, which links to an Entry (see __init__) and updates this Entry automatically when a new selection is made (indicating it is a new -not yet "gotten with get"- selection with a green background). When the 'Show Chosen Option' button is pressed, it shows the option in the Entry (actually, it is already there), but with a white background. Finally, for this class, reset also puts an empty string with a white background into the Entry. As with OptionMenu/OptionMenuValue, an OptionMenuToEntry is a slight variant of an OptionMenuValue, so it is a good candidate to define via inheritance. All operators/methods working on OptionMenuValue make sense working on OptionMenutoEntry: most (e.g. grid, config, get) work purely by inheritance, but we must change the parameters of the __init__ method, add a put method, and override the reset method to do something different. Observe how the OptionMenuToEntry stores and uses its own entry reference, how reset calls the overridden OptionMenuValue.reset before blanking the Entry, how put fills the Entry with a green background, and how the command for the 'Show Chosen Entry' button now calls get/put to accomplish its task: we no longer need the option_to_entry function. Also notice a "command" parameter being passed to OptionMenuValue.__init__, which passes it along to OptionMenu.__init__ via one of the **configs parameters. Whenever the user chooses an option in OptionMenu, Python calls this command, which calls the self.put function always passing it the StringVar() passed to OptionMenu.__init__ (which will be the attribute created to remember this StringVar in OptionMenuValue). So selecting an option activates a call to the "put" method defined in OptionMenutoEntry. Unlike the command function in buttons, which are passed no arguments when called, the command function in OptionMenus will bee passed one argument: the StringVar with the selected string Here is the actual class definition. from tkinter import * # ... define OptionMenuValue class (see above) #OptionMenuToEntry: with title,linked_entry, and option_tuple #get is an inherited pull function; put is a push function, pushing # the selected option into the linked_entry (replacing what is there) # with a green backround; reset now resets this text too class OptionMenuToEntry(OptionMenuValue): def __init__(self,parent,title,linked_entry,*option_tuple,**configs): self.entry = linked_entry OptionMenuValue.__init__(self,parent,title,*option_tuple,command=self.put,**configs) def reset(self): OptionMenuValue.reset(self) self.put('','white') def put(self,option,bg='green'): self.entry.delete(0,len(self.entry.get())) self.entry.insert(0,option) self.entry.config(bg=bg) root = Tk() root.title('Widget Tester') main = Frame(root) main.pack(side=TOP,anchor=W) e = Entry(main) e.grid(row=2,column=2) omte = OptionMenuToEntry(main, 'Choose Option', e, 'option1','option2','option3') omte.grid(row=1,column=1) omte.config(width = 10) b= Button(main,text='Show Chosen Option',command=lambda : omte.put(omte.get(),'white')) b.grid(row=1,column=2) b = Button(main,text='Reset Option',command=omte.reset) b.grid(row=1,column=3) root.mainloop() Not counting the class definitions, this is 15 lines of code, shrunk from 16 lines. So, we have seen how we can start with a powerful class (MenuOption) and easily specialize it, via inheritance (multiple times), to operate a bit differently each time. In this way it is easy to define widgets with exactly the behavior that we need, built on widgets supplied in Tkinter. It is common to use inheritance when defining/refining advanced GUI widgets. ---------- PrivacyError derived from the base-class Exception We have seen that Python defines many exceptions and we have used a variety of them (either raising them or handling them in except clauses of try/except statements). We can easily define our own exception classes using inheritance. In the mix-in inheritance example below, we will refer to PrivacyException, which is defined as follows. class PrivacyError(Exception): pass # inherits everything (including __init__) and overrides nothing So here, the whole purpose is to define a class name, which we can name when we raise an exception, and can name when we handle an exception in a try/except statements. It inherits ALL it (no overriding, no new) methods, including __init__. ------------------------------------------------------------------------------ Mix-in Inheritance Examples Now we will switch to a discussion of mix-in inheritance. Typically a mix-in base class is something small that other classes can be derived from, to inherit their behavior. For example, the Privacy class (itself derived from object) ensures that certain attributes are never rebound: its __setattr__ method raises PrivacyError (defined above) if the attribute is in the set of the 'privates' attribute: if o is constructed from a class that is derived from Privacy, then if we write o.attr = ..., and attr is one of the strings in o.privates, then Python will raise PrivacyError in the inherited __setattr__ method. The reasoning behind how Privacy works (see the Test class that is derived from it) depends on our knowledge of how attributes are located: inside the object or inside the class the object was constructed from. Here is the actual code and an analysis of what is happening. class PrivacyError(Exception): pass # inherit __init__/constructor class Privacy: def __setattr__(self,attr_name,new_value): print('__setattr__:',attr_name,'to be set to',new_value,'; privates = ',self.privates) # for illustration if attr_name in self.privates: raise PrivacyError('Privacy: attempt to set private: '+ attr_name+' to '+str(new_value)) else: self.__dict__[attr_name] = new_value class Test(Privacy): # mix-in a single base class: Privacy privates = {'y'} # y attribute of Test objects cannot be rebound after __init__ def __init__(self,x,y): self.privates = set() # allow setting x y attributes; privates now found in object self.x = x self.y = y del self.privates # now use class attribute privates above for future privacy t = Test(0,1) t.x = 'rebound' t.y = 'rebound' Note the print statement in Privacy.__setattr__. When we run this program it prints __setattr__: privates to be set to set() ; privates = {'y'} __setattr__: x to be set to 0 ; privates = set() __setattr__: y to be set to 1 ; privates = set() __setattr__: x to be set to rebound ; privates = {'y'} __setattr__: y to be set to rebound ; privates = {'y'} Traceback (most recent call last): File "C:\Users\pattis\Desktop\python32\ztest\experiment.py", line 35, in t.y = 'rebound' File "C:\Users\pattis\Desktop\python32\ztest\experiment.py", line 10, in __setattr__ attr_name+' to '+str(new_value)) __main__.PrivacyError: Privacy: attempt to set private: y to rebound Here is an explanation of all the actions (some are a bit subtle) (1) When Python defines the Test class it has two attributes: privates is a name in the class namespace bound to {'y'} and __init__ is bound to its defined method. (2) When Python executes t = Test(0,1), Test.__init__ is called. (2a) When Python executes self.privates = {}, it calls the __setattr__ method inherited from Privacy; at this point it looks up self.privates, doesn't find 'privates' in the object (yet; see the result of this statement) so it tries to look up the attribute in the Test class, where its value is {'y'}; since 'privates' -which is being bound- is not in this set, __setattr__ binds the 'privates' attribute to {} IN THE OBJECT being constructed. (2b) When Python executes self.x = x, it calls the __setattr__ method inherited from Privacy; at this point it looks up self.privates, and finds 'privates' IN THE OBJECT (see 2a), where its value is {}; since 'x' is not in this set, __setattr__ binds the 'x' attribute to 0 in the object being constructed. (2c) When Python executes self.y = y, the same series of events happen (as in 2b), and __setattr__ binds the 'y' attribute to 1 in the object being constructed. (2d) When Python executes del self.privates it removes the 'privates' attribute from the self object. After __init__ finishes, the object created will not have a 'privates' attribute, so in the future when Python looks up self.privates in __setattr__, it will find the 'privates' attribute not in the object but instead in the Test class (bound to {'y'}) from which the object was constructed. (3) When Python executes t.x = 'rebound', it calls the __setattr__ method inherited from Privacy; at this point it looks up self.privates, and finds 'privates' in the Test class (bound to {'y'}); since 'x' is not in this set, __setattr__ binds the 'x' attribute in t's object to 'rebound'. (3) When Python executes t.y = 'rebound', it calls the __setattr__ method inherited from Privacy; at this point it looks up self.privates, and finds 'privates' in the Test class (bound to {'y'}); since 'y' is in this set, __setattr__ raises an exception. Here is a second, simpler definition and use (in Test) of Privacy, but with a bit of a difference in meaning. In the code below, any new attribute is allowed to be added to the namespace of an object (e.g., in the Test.__init__ or elsewhere) but once an attribute is in the namespace of an object, it cannot be rebound if it appears in the 'privates' attribute. Here are the changed classes. class Privacy: def __setattr__(self,attr_name,new_value): print('__setattr__:',attr_name,self.privates) # for illustration if attr_name in self.__dict__ and attr_name in self.privates: raise PrivacyError('Privacy: attempt to set private: '+ attr_name+' to '+str(new_value)) else: # either defining a first time or not private self.__dict__[attr_name] = new_value class Test(Privacy): privates = {'y',} # y attribute of Test objects cannot be rebound def __init__(self,x,y): self.x = x self.y = y The same lines of code t = Test(0,1) t.x = 'rebound' t.y = 'rebound' now produce very similar results (the only difference is that printing self.privates is always {'y'}). But when __init__ sets attribute 'y' Privacy allows it, because 'y' is not currently bound to any value. __setattr__: x {'y'} __setattr__: y {'y'} __setattr__: x {'y'} __setattr__: y {'y'} Traceback (most recent call last): File "C:\Users\Pattis\workspace\inheritance\privacy.py", line 25, in t.y = 'rebound' File "C:\Users\Pattis\workspace\inheritance\privacy.py", line 10, in __setattr__ attr_name+' to '+str(new_value)) __main__.PrivacyError: Privacy: attempt to set private: y to rebound ---------- As a second mix-in example we will define a class that implements an interesting __str__ method. Then, we will write the Test class shown above to mix-in both this class and Privacy. But first, let's use our knowledge of inheritance to better understand what happens when __str__ is called on an object constructed from the Test class, which does not define a __str__ method. Assume as above, t = Test(0,1). Notice the Test class defined above defines no __repr__ or __str__ functions. When we call print(t), Python converts t to a str (the only things it can print) by calling t.__str__(). Now, the t object itself defines no __str__ attribute, so Python tries to look up this attribute in the Test class, but it also doesn't define a __repr__ or __str__ method. So Python next looks in the base class of Test, which is Privacy; but it also doesn't define a __repr__ or __str__ method. So Python next looks in the base class of Privacy, which is object. Now the object class does define only a __repr__ method (which __str__ calls too). It returns a string like '<__main__.Test object at 0x027482F0>' showing just the name of the class of t and its location in memory (all objects come from a class and have a location in memory). So, that is how a class that doesn't define __repr__ or __str__ methods ultimately produces a string representing an object constructed from the class: it calls the one inherited from the object class. The mix-in class Str_All_Attributes defines only a __str__ method, as follows. It returns a string saying what class the object is an instance of, and then lists (on separate lines) each attribute of the object, in alphabetical order, and its value. class Str_All_Attributes: def __str__(self): from goody import type_as_str answer = 'Instance of ' +type_as_str(self)+'\n' for a in sorted(self.__dict__): answer += ' ' + a + ' -> ' + str(self.__dict__[a]) + '\n' return answer Now we can define Test as follows, using both mix-in classes class Test(Privacy,Str_All_Attributes): When we define t = Test(0,1) and call print(t) Python looks up the __str__ attribute using the search order discussed in the previous lecture (__mro__). Python tries to look up the attribute in the t object itself, but it doesn't define a __repr__ or __str__ method. Python next tries to look up this attribute in the Test class, but it also doesn't define a __repr__ or __str__ method. So Python next looks in the first base class of Test, which is Privacy; but it also doesn't define a __repr__ or __str__ method. So Python next looks in the second base class, which is Str_All_Attributes. Now this class does define a __str__ method. It prints the string as Instance of __main__.Test x -> 0 y -> 1 By defining class Test(Privacy,Str_All_Attributes): Any Test objects will check rebinding attempts via the __setattr__ method inherited from Privacy and will print via the __str__ method inherited from Str_All_Attributes. So, it is very simple to derive a class from a variety of mix-in base classes, and have the derived class inherit all the behaviors of the mix-in classes. But note that if TWO MIX-IN BASE CLASSES define the SAME METHOD, if that method is called, it will be called only from the class that is the first one found in the inheritance hierarchy. Therefore, one must be carefully when using mix-in inheritance with classes that define the same methods. Typically, the mix-in classes used in multiple inheritance should not define the same methods. ------------------------------------------------------------------------------ Problems: 1) Define a class name Controlled_Keys_Dict that allows us to specify that some keys are unchangeable: they can be set to associate with a value, but only if they are not already in the dictionary (if they are already in the dictionary they cannot be changed). For d in this class, we can call d.control(...) to specify which keys are controlled (its argument can be any iterable). Thus if we call d.control(d.keys()) it would control all the keys currently in that dictionary. 2) Suppose we define t1 = Test(0,1) t2 = Test(0,1) t1.privacy = {'x'} Can we change t1.x? t1.y? t2.x? t2.y? Explain your reasoning. 3) Write a version of Str_All_Attributes that includes not only the attributes of the object, but all its inherited attributes. See the inheritancetool2 module for some useful code.