"Aliasing" an object's __str__ to a different method

Bengt Richter bokr at oz.net
Sun Jul 24 01:00:35 EDT 2005


On Sat, 23 Jul 2005 10:59:56 -0400, "Jeffrey E. Forcier" <jforcier at strozllc.com> wrote:

>Thanks for all the additional replies thus far!
>
>Apparently the issue, as stated implicitly or explicitly by most of  
>you, is that new-style class instances essentially defer their magic  
>methods to the class's static versions of same. This is good to know :)

Actually, it's not just the "magic" methods. If you have an instance
a of a newstyle class A, any attribute lookup a.attr will undergo the
same search first to see if attr is a descriptor object, and if not,
*then* to look in the instance attribute directory. But the descriptor
search doesn't start in inst.__dict__, it goes through the chain of
classes and base classes provided by type(inst).mro(), which starts in
type(inst). And for our class A instance a, type(a) will be A, so the
search for a.attr starts there. Same applies to a.__str__. This ensures
that all instances of the same class will share the same methods. The way
a method, which is just a class variable with a function as its value, gets
to be a callable bound method, is the same as any attribute lookup looking
for a descriptor with a __get__ method (which a function also has, for this purpose).
If the descriptor doesn't have a __set__ method as well, then an instance attribute
takes priority. If there is a __set__ method, and instance attribute can't shadow
the attribute name, and the descriptor __get__ method takes precedence. Unshadowed,
a method search looks something like

    cbm = ((base for base in type(inst).mro() if 'attr' in base.__dict__)
          .next().__dict__['attr'].__get__(inst, type(inst)))

if this doesn't succeed and meet the __set__ vs shadowing logic, then you get
the instance attribute per se.

Jumping ahead using a MyClass inst as an example:

 >>> cbm = ((base for base in type(inst).mro() if '__str__' in base.__dict__)
 ...    .next().__dict__['__str__'].__get__(inst, type(inst)))
 >>> cbm
 <bound method MyClass.View of I, <__main__.MyClass object at 0x02FB91AC>, am being viewed>

This looks a little strange because the repr of the bound method includes a repr of the thing bound to,
which returns the Edit/View presentation (after the 'of' above).
(A possible reason to consider using just __str__ and letting __repr__ be).

A function's __get__ method will deliver a bound method if the lookup
is via an inst, or an unbound method if the lookup is via the class, in which
case None is passed to __get__ instead of the instance.

The upshot is that you can create descriptors __str__ and __repr__for your class that will
return bound methods using __str__ and __repr__ function attributes of your instance (if they exist)
instead of the normal class attributes, and we can chose from several normal class attributes according
to a general mode flag of the class. E.g., using your original example, and rearranging things a bit,
we can do something like what (I think) you wanted: (The first class below defines descriptors and the
example uses two instances in MyClass)

 >>> class InstanceMethodSetterAndBinder(object):
 ...     def __init__(self, inst_attr_name):
 ...                         # lambda self:'?? no %s format ??'% object.__repr__(self)
 ...         self.inst_attr_name = inst_attr_name
 ...     def __get__(self, inst, cls=None):
 ...         if inst is None: return self
 ...         default = getattr((cls or type(inst)), 'SharedEVMethod', None)
 ...         if default is None:
 ...             def default(self):
 ...                 return '<?? no SharedEVMethod for %s ??>'% object.__repr__(inst)
 ...         return vars(inst).get(self.inst_attr_name, default).__get__(inst, cls)
 ...     def __set__(self, inst, value):
 ...         inst.__dict__[self.inst_attr_name] = value
 ...     def __delete__(self, inst):
 ...         try: del inst.__dict__[self.inst_attr_name]
 ...         except KeyError: pass
 ...
 >>> class MyClass(object):
 ...     __str__ = InstanceMethodSetterAndBinder('__str__')
 ...     __repr__ = InstanceMethodSetterAndBinder('__repr__')
 ...     @staticmethod
 ...     def Edit(self):
 ...         return "I, %s, am being edited" % (object.__repr__(self)) # %s on self recurses!!
 ...     @staticmethod
 ...     def View(self):
 ...         return "I, %s, am being viewed" % (object.__repr__(self)) # %s on self recurses!!
 ...     SharedEVMethod = View
 ...     def setEdit(self, shared=True):
 ...         if not shared:
 ...             self.__str__ = self.__repr__ = self.Edit
 ...         else:
 ...             type(self).SharedEVMethod = self.Edit
 ...     def setView(self, shared=True):
 ...         if not shared:
 ...             self.__str__ = self.__repr__ = self.View
 ...         else:
 ...             type(self).SharedEVMethod = self.View
 ...     def delInstEVM(self):
 ...         del self.__str__
 ...         del self.__repr__
 ...
 >>> inst = MyClass()
 >>> inst2 = MyClass()
 >>> inst3 = MyClass()
 >>> inst
 I, <__main__.MyClass object at 0x02FB91AC>, am being viewed
 >>> inst2
 I, <__main__.MyClass object at 0x02FB91CC>, am being viewed
 >>> inst3
 I, <__main__.MyClass object at 0x02FB91EC>, am being viewed
 >>> inst.setEdit()
 >>> inst
 I, <__main__.MyClass object at 0x02FB91AC>, am being edited
 >>> inst2
 I, <__main__.MyClass object at 0x02FB91CC>, am being edited
 >>> inst3
 I, <__main__.MyClass object at 0x02FB91EC>, am being edited
 >>> inst2.setView(False)
 >>> inst
 I, <__main__.MyClass object at 0x02FB91AC>, am being edited
 >>> inst2
 I, <__main__.MyClass object at 0x02FB91CC>, am being viewed
 >>> inst3
 I, <__main__.MyClass object at 0x02FB91EC>, am being edited
 >>> inst2.setView() # all
 >>> inst2.setEdit(False) # just inst
 >>> inst
 I, <__main__.MyClass object at 0x02FB91AC>, am being viewed
 >>> inst2
 I, <__main__.MyClass object at 0x02FB91CC>, am being edited
 >>> inst3
 I, <__main__.MyClass object at 0x02FB91EC>, am being viewed
 >>> inst2.delInstEVM()
 >>> inst2
 I, <__main__.MyClass object at 0x02FB91CC>, am being viewed

You could use this kind of thing in a base class and specialize
in subclasses to override stuff, and you can do other stuff too.
Your choices are more varied that you probably thought ;-)

>
>Ironically, the reason I'm using new-style classes is that I only  
>read up on them very recently, and was attempting to get myself into  
>the habit of inheriting from 'object' by default. Go figure.
>
>Anyway, to take a step back, the *actual* actual reason for my  
>investigation into this issue is that I'm attempting to create a  
>collection of classes representing HTML page/form elements, many of  
>which are represented differently depending on whether they're in a  
>"view" or "edit" state.
>
>And ideally I wanted to be able to hold a collection of these objects  
>and toggle them all to one state or the other, then bandy them about
     ^^^^^^^^^^^^^^^
does that mean all instances with a single toggling action, or each
individually? You could have both. I.e., a general mode flag and the
ability to assign an arbitrary __str__ and/or __repr__ override for
a particular instance. See example, where all are toggled by default.

>as if they were strings, e.g. mixing them with literal strings via str 
>(obj).
Note that your example set __repr__ also, which is not used for str(x)
if x.__str__ exists.
>
>Clearly there are at least a handful of ways to accomplish this, but  
>the one that came to mind first was, as I said at the beginning, to  
>define both behaviors on each object and then have the ability to  
>point __str__ to one or the other. I suppose now I need to figure out  
>which is more important, that ease-of-use of overriding __str__ or  
>whatever benefits new-style classes give (something I'm still a bit  
>unclear on).
>
Those aren't your only choices ;-)

Regards,
Bengt Richter



More information about the Python-list mailing list