预计阅读本页时间:-
Coding Mix-in Display Classes
As we’ve seen, Python’s default way to print a class instance object isn’t incredibly useful:
>>> class Spam:
... def __init__(self): # No __repr__ or __str__
... self.data1 = "food"
...
>>> X = Spam()
>>> print(X) # Default: class, address
<__main__.Spam object at 0x00864818> # Displays "instance" in Python 2.6
广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元
As you saw in Chapter 29 when studying operator overloading, you can provide a __str__ or __repr__ method to implement a custom string representation of your own. But, rather than coding one of these in each and every class you wish to print, why not code it once in a general-purpose tool class and inherit it in all your classes?
That’s what mix-ins are for. Defining a display method in a mix-in superclass once enables us to reuse it anywhere we want to see a custom display format. We’ve already seen tools that do related work:
- Chapter 27’s AttrDisplay class formatted instance attributes in a generic __str__ method, but it did not climb class trees and was used in single-inheritance mode only.
- Chapter 28’s classtree.py module defined functions for climbing and sketching class trees, but it did not display object attributes along the way and was not architected as an inheritable class.
Here, we’re going to revisit these examples’ techniques and expand upon them to code a set of three mix-in classes that serve as generic display tools for listing instance attributes, inherited attributes, and attributes on all objects in a class tree. We’ll also use our tools in multiple-inheritance mode and deploy coding techniques that make classes better suited to use as generic tools.
Listing instance attributes with __dict__
Let’s get started with the simple case—listing attributes attached to an instance. The following class, coded in the file lister.py, defines a mix-in called ListInstance that overloads the __str__ method for all classes that include it in their header lines. Because this is coded as a class, ListInstance is a generic tool whose formatting logic can be used for instances of any subclass:
# File lister.py
class ListInstance:
"""
Mix-in class that provides a formatted print() or str() of
instances via inheritance of __str__, coded here; displays
instance attrs only; self is the instance of lowest class;
uses __X names to avoid clashing with client's attrs
"""
def __str__(self):
return '<Instance of %s, address %s:\n%s>' % (
self.__class__.__name__, # My class's name
id(self), # My address
self.__attrnames()) # name=value list
def __attrnames(self):
result = ''
for attr in sorted(self.__dict__): # Instance attr dict
result += '\tname %s=%s\n' % (attr, self.__dict__ [attr])
return result
ListInstance uses some previously explored tricks to extract the instance’s class name and attributes:
- Each instance has a built-in __class__ attribute that references the class from which it was created, and each class has a __name__ attribute that references the name in the header, so the expression self.__class__.__name__ fetches the name of an instance’s class.
- This class does most of its work by simply scanning the instance’s attribute dictionary (remember, it’s exported in __dict__) to build up a string showing the names and values of all instance attributes. The dictionary’s keys are sorted to finesse any ordering differences across Python releases.
In these respects, ListInstance is similar to Chapter 27’s attribute display; in fact, it’s largely just a variation on a theme. Our class here uses two additional techniques, though:
- It displays the instance’s memory address by calling the id built-function, which returns any object’s address (by definition, a unique object identifier, which will be useful in later mutations of this code).
- It uses the pseudoprivate naming pattern for its worker method: __attrnames. As we learned earlier in his chapter, Python automatically localizes any such name to its enclosing class by expanding the attribute name to include the class name (in this case, it becomes _ListInstance__attrnames). This holds true for both class attributes (like methods) and instance attributes attached to self. This behavior is useful in a general tool like this, as it ensures that its names don’t clash with any names used in its client subclasses.
Because ListInstance defines a __str__ operator overloading method, instances derived from this class display their attributes automatically when printed, giving a bit more information than a simple address. Here is the class in action, in single-inheritance mode (this code works the same in both Python 3.0 and 2.6):
>>> from lister import ListInstance
>>> class Spam(ListInstance): # Inherit a __str__ method
... def __init__(self):
... self.data1 = 'food'
...
>>> x = Spam()
>>> print(x) # print() and str() run __str__
<Instance of Spam, address 40240880:
name data1=food
>
You can also fetch the listing output as a string without printing it with str, and interactive echoes still use the default format:
>>> str(x)
'<Instance of Spam, address 40240880:\n\tname data1=food\n>'
>>> x # The __repr__ still is a default
<__main__.Spam object at 0x026606F0>
The ListInstance class is useful for any classes you write—even classes that already have one or more superclasses. This is where multiple inheritance comes in handy: by adding ListInstance to the list of superclasses in a class header (i.e., mixing it in), you get its __str__ “for free” while still inheriting from the existing superclass(es). The file testmixin.py demonstrates:
# File testmixin.py
from lister import * # Get lister tool classes
class Super:
def __init__(self): # Superclass __init__
self.data1 = 'spam' # Create instance attrs
def ham(self):
pass
class Sub(Super, ListInstance): # Mix in ham and a __str__
def __init__(self): # listers have access to self
Super.__init__(self)
self.data2 = 'eggs' # More instance attrs
self.data3 = 42
def spam(self): # Define another method here
pass
if __name__ == '__main__':
X = Sub()
print(X) # Run mixed-in __str__
Here, Sub inherits names from both Super and ListInstance; it’s a composite of its own names and names in both its superclasses. When you make a Sub instance and print it, you automatically get the custom representation mixed in from ListInstance (in this case, this script’s output is the same under both Python 3.0 and 2.6, except for object addresses):
C:\misc> C:\python30\python testmixin.py
<Instance of Sub, address 40962576:
name data1=spam
name data2=eggs
name data3=42
>
ListInstance works in any class it’s mixed into because self refers to an instance of the subclass that pulls this class in, whatever that may be. In a sense, mix-in classes are the class equivalent of modules—packages of methods useful in a variety of clients. For example, here is Lister working again in single-inheritance mode on a different class’s instances, with import and attributes set outside the class:
>>> import lister
>>> class C(lister.ListInstance): pass
...
>>> x = C()
>>> x.a = 1; x.b = 2; x.c = 3
>>> print(x)
<Instance of C, address 40961776:
name a=1
name b=2
name c=3
>
Besides the utility they provide, mix-ins optimize code maintenance, like all classes do. For example, if you later decide to extend ListInstance’s __str__ to also print all the class attributes that an instance inherits, you’re safe; because it’s an inherited method, changing __str__ automatically updates the display of each subclass that imports the class and mixes it in. Since it’s now officially “later,” let’s move on to the next section to see what such an extension might look like.
Listing inherited attributes with dir
As it is, our Lister mix-in displays instance attributes only (i.e., names attached to the instance object itself). It’s trivial to extend the class to display all the attributes accessible from an instance, though—both its own and those it inherits from its classes. The trick is to use the dir built-in function instead of scanning the instance’s __dict__ dictionary; the latter holds instance attributes only, but the former also collects all inherited attributes in Python 2.2 and later.
The following mutation codes this scheme; I’ve renamed it to facilitate simple testing, but if this were to replace the original version, all existing clients would pick up the new display automatically:
# File lister.py, continued
class ListInherited:
"""
Use dir() to collect both instance attrs and names
inherited from its classes; Python 3.0 shows more
names than 2.6 because of the implied object superclass
in the new-style class model; getattr() fetches inherited
names not in self.__dict__; use __str__, not __repr__,
or else this loops when printing bound methods!
"""
def __str__(self):
return '<Instance of %s, address %s:\n%s>' % (
self.__class__.__name__, # My class's name
id(self), # My address
self.__attrnames()) # name=value list
def __attrnames(self):
result = ''
for attr in dir(self): # Instance dir()
if attr[:2] == '__' and attr[-2:] == '__': # Skip internals
result += '\tname %s=<>\n' % attr
else:
result += '\tname %s=%s\n' % (attr, getattr(self, attr))
return result
Notice that this code skips __X__ names’ values; most of these are internal names that we don’t generally care about in a generic listing like this. This version also must use the getattr built-in function to fetch attributes by name string instead of using instance attribute dictionary indexing—getattr employs the inheritance search protocol, and some of the names we’re listing here are not stored on the instance itself.
To test the new version, change the testmixin.py file to use this new class instead:
class Sub(Super, ListInherited): # Mix in a __str__
This file’s output varies per release. In Python 2.6, we get the following; notice the name mangling at work in the lister’s method name (I shortened its full value display to fit on this page):
C:\misc> c:\python26\python testmixin.py
<Instance of Sub, address 40073136:
name _ListInherited__attrnames=<bound method Sub.__attrnames of <...more...>>
name __doc__=<>
name __init__=<>
name __module__=<>
name __str__=<>
name data1=spam
name data2=eggs
name data3=42
name ham=<bound method Sub.ham of <__main__.Sub instance at 0x026377B0>>
name spam=<bound method Sub.spam of <__main__.Sub instance at 0x026377B0>>
>
In Python 3.0, more attributes are displayed because all classes are “new-style” and inherit names from the implied object superclass (more on this in Chapter 31). Because so many names are inherited from the default superclass, I’ve omitted many here; run this on your own for the full listing:
C:\misc> c:\python30\python testmixin.py
<Instance of Sub, address 40831792:
name _ListInherited__attrnames=<bound method Sub.__attrnames of <...more...>>
name __class__=<>
name __delattr__=<>
name __dict__=<>
name __doc__=<>
name __eq__=<>
...more names omitted...
name __repr__=<>
name __setattr__=<>
name __sizeof__=<>
name __str__=<>
name __subclasshook__=<>
name __weakref__=<>
name data1=spam
name data2=eggs
name data3=42
name ham=<bound method Sub.ham of <__main__.Sub object at 0x026F0B30>>
name spam=<bound method Sub.spam of <__main__.Sub object at 0x026F0B30>>
>
One caution here—now that we’re displaying inherited methods too, we have to use __str__ instead of __repr__ to overload printing. With __repr__, this code will loop—displaying the value of a method triggers the __repr__ of the method’s class, in order to display the class. That is, if the lister’s __repr__ tries to display a method, displaying the method’s class will trigger the lister’s __repr__ again. Subtle, but true! Change __str__ to __repr__ here to see this for yourself. If you must use __repr__ in such a context, you can avoid the loops by using isinstance to compare the type of attribute values against types.MethodType in the standard library, to know which items to skip.
Listing attributes per object in class trees
Let’s code one last extension. As it is, our lister doesn’t tell us which class an inherited name comes from. As we saw in the classtree.py example near the end of Chapter 28, though, it’s straightforward to climb class inheritance trees in code. The following mix-in class makes use of this same technique to display attributes grouped by the classes they live in—it sketches the full class tree, displaying attributes attached to each object along the way. It does so by traversing the inheritance tree from an instance’s __class__ to its class, and then from the class’s __bases__ to all superclasses recursively, scanning object __dicts__s along the way:
# File lister.py, continued
class ListTree:
"""
Mix-in that returns an __str__ trace of the entire class
tree and all its objects' attrs at and above self;
run by print(), str() returns constructed string;
uses __X attr names to avoid impacting clients;
uses generator expr to recurse to superclasses;
uses str.format() to make substitutions clearer
"""
def __str__(self):
self.__visited = {}
return '<Instance of {0}, address {1}:\n{2}{3}>'.format(
self.__class__.__name__,
id(self),
self.__attrnames(self, 0),
self.__listclass(self.__class__, 4))
def __listclass(self, aClass, indent):
dots = '.' * indent
if aClass in self.__visited:
return '\n{0}<Class {1}:, address {2}: (see above)>\n'.format(
dots,
aClass.__name__,
id(aClass))
else:
self.__visited[aClass] = True
genabove = (self.__listclass(c, indent+4) for c in aClass.__bases__)
return '\n{0}<Class {1}, address {2}:\n{3}{4}{5}>\n'.format(
dots,
aClass.__name__,
id(aClass),
self.__attrnames(aClass, indent),
''.join(genabove),
dots)
def __attrnames(self, obj, indent):
spaces = ' ' * (indent + 4)
result = ''
for attr in sorted(obj.__dict__):
if attr.startswith('__') and attr.endswith('__'):
result += spaces + '{0}=<>\n'.format(attr)
else:
result += spaces + '{0}={1}\n'.format(attr, getattr(obj, attr))
return result
Note the use of a generator expression to direct the recursive calls for superclasses; it’s activated by the nested string join method. Also see how this version uses the Python 3.0 and 2.6 string format method instead of % formatting expressions, to make substitutions clearer; when many substitutions are applied like this, explicit argument numbers may make the code easier to decipher. In short, in this version we exchange the first of the following lines for the second:
return '<Instance of %s, address %s:\n%s%s>' % (...) # Expression
return '<Instance of {0}, address {1}:\n{2}{3}>'.format(...) # Method
Now, change testmixin.py to inherit from this new class again to test:
class Sub(Super, ListTree): # Mix in a __str__
The file’s tree-sketcher output in Python 2.6 is then as follows:
C:\misc> c:\python26\python testmixin.py
<Instance of Sub, address 40728496:
_ListTree__visited={}
data1=spam
data2=eggs
data3=42
....<Class Sub, address 40701168:
__doc__=<>
__init__=<>
__module__=<>
spam=<unbound method Sub.spam>
........<Class Super, address 40701120:
__doc__=<>
__init__=<>
__module__=<>
ham=<unbound method Super.ham>
........>
........<Class ListTree, address 40700688:
_ListTree__attrnames=<unbound method ListTree.__attrnames>
_ListTree__listclass=<unbound method ListTree.__listclass>
__doc__=<>
__module__=<>
__str__=<>
........>
....>
>
Notice in this output how methods are unbound now under 2.6, because we fetch them from classes directly, instead of from instances. Also observe how the lister’s __visited table has its name mangled in the instance’s attribute dictionary; unless we’re very unlucky, this won’t clash with other data there.
Under Python 3.0, we get extra attributes and superclasses again. Notice that unbound methods are simple functions in 3.0, as described in an earlier note in this chapter (and that again, I’ve deleted most built-in attributes in object to save space here; run this on your own for the complete listing):
C:\misc> c:\python30\python testmixin.py
<Instance of Sub, address 40635216:
_ListTree__visited={}
data1=spam
data2=eggs
data3=42
....<Class Sub, address 40914752:
__doc__=<>
__init__=<>
__module__=<>
spam=<function spam at 0x026D53D8>
........<Class Super, address 40829952:
__dict__=<>
__doc__=<>
__init__=<>
__module__=<>
__weakref__=<>
ham=<function ham at 0x026D5228>
............<Class object, address 505114624:
__class__=<>
__delattr__=<>
__doc__=<>
__eq__=<>
...more omitted...
__repr__=<>
__setattr__=<>
__sizeof__=<>
__str__=<>
__subclasshook__=<>
............>
........>
........<Class ListTree, address 40829496:
_ListTree__attrnames=<function __attrnames at 0x026D5660>
_ListTree__listclass=<function __listclass at 0x026D56A8>
__dict__=<>
__doc__=<>
__module__=<>
__str__=<>
__weakref__=<>
............<Class object:, address 505114624: (see above)>
........>
....>
>
This version avoids listing the same class object twice by keeping a table of classes visited so far (this is why an object’s id is included—to serve as a key for a previously displayed item). Like the transitive module reloader of Chapter 24, a dictionary works to avoid repeats and cycles here because class objects may be dictionary keys; a set would provide similar functionality.
This version also takes care to avoid large internal objects by skipping __X__ names again. If you comment out the test for these names, their values will display normally. Here’s an excerpt from the output in 2.6 with this temporary change made (it’s much larger in its entirety, and it gets even worse in 3.0, which is why these names are probably better skipped!):
C:\misc> c:\python26\python testmixin.py
...more omitted...
........<Class ListTree, address 40700688:
_ListTree__attrnames=<unbound method ListTree.__attrnames>
_ListTree__listclass=<unbound method ListTree.__listclass>
__doc__=
Mix-in that returns the __str__ trace of the entire class
tree and all its objects' attrs at and above self;
run by print, str returns constructed string;
uses __X attr names to avoid impacting clients;
uses generator expr to recurse to superclasses;
uses str.format() to make substitutions clearer
__module__=lister
__str__=<unbound method ListTree.__str__>
........>
For more fun, try mixing this class into something more substantial, like the Button class of Python’s tkinter GUI toolkit module. In general, you’ll want to name ListTree first (leftmost) in a class header, so its __str__ is picked up; Button has one, too, and the leftmost superclass is searched first in multiple inheritance. The output of the following is fairly massive (18K characters), so run this code on your own to see the full listing (and if you’re using Python 2.6, recall that you should use Tkinter for the module name instead of tkinter):
>>> from lister import ListTree
>>> from tkinter import Button # Both classes have a __str__
>>> class MyButton(ListTree, Button): pass # ListTree first: use its __str__
...
>>> B = MyButton(text='spam')
>>> open('savetree.txt', 'w').write(str(B)) # Save to a file for later viewing
18247
>>> print(B) # Print the display here
<Instance of MyButton, address 44355632:
_ListTree__visited={}
_name=44355632
_tclCommands=[]
...much more omitted...
>
Of course, there’s much more we could do here (sketching the tree in a GUI might be a natural next step), but we’ll leave further work as a suggested exercise. We’ll also extend this code in the exercises at the end of this part of the book, to list superclass names in parentheses at the start of instance and class displays.
The main point here is that OOP is all about code reuse, and mix-in classes are a powerful example. Like almost everything else in programming, multiple inheritance can be a useful device when applied well. In practice, though, it is an advanced feature and can become complicated if used carelessly or excessively. We’ll revisit this topic as a gotcha at the end of the next chapter. In that chapter, we’ll also meet the new-style class model, which modifies the search order for one special multiple inheritance case.
Note
Supporting slots: Because they scan instance dictionaries, the ListInstance and ListTree classes presented here don’t directly support attributes stored in slots—a newer and relatively rarely used option we’ll meet in the next chapter, where instance attributes are declared in a __slots__ class attribute. For example, if in textmixin.py we assign __slots__=['data1'] in Super and __slots__=['data3'] in Sub, only the data2 attribute is displayed in the instance by these two lister classes; ListTree also displays data1 and data3, but as attributes of the Super and Sub class objects and with a special format for their values (technically, they are class-level descriptors).
To better support slot attributes in these classes, change the __dict__ scanning loops to also iterate through __slots__ lists using code the next chapter will present, and use the getattr built-in function to fetch values instead of __dict__ indexing (ListTree already does). Since instances inherit only the lowest class’s __slots__, you may also need to come up with a policy when __slots__ lists appear in multiple superclasses (ListTree already displays them as class attributes). ListInherited is immune to all this, because dir results combine both __dict__ names and all classes’ __slots__ names.
Alternatively, as a policy we could simply let our code handle slot-based attributes as it currently does, rather than complicating it for a rare, advanced feature. Slots and normal instance attributes are different kinds of names. We’ll investigate slots further in the next chapter; I omitted addressing them in these examples to avoid a forward dependency (not counting this note, of course!)—not exactly a valid design goal, but reasonable for a book.