meta-class review

Ethan Furman ethan at stoneleaf.us
Tue Oct 5 19:17:04 EDT 2010


On one the many mini-reports we use, we have a bunch of counts that are 
frequently zero; because the other counts can also be low, it becomes 
easy to miss the non-zero counts.  For example:

Code  Description

       Conv Errors              :       6

31,N  DPV Failure              :       4
10:   Invalid Address          :       0
11:   Invalid C/S/Z            :       0
12:   Invalid State            :       0
13:   Invalid City             :       0
17:   Insufficient Information :       0
33:   Non-Deliverable          :       0
98:   Non-USPS zip             :       0

21:   Address Not Found        :       0
22:   Multiple Responses       :       3
23:   Error in Primary         :       0
24:   Error in Secondary       :       0


So I thought I would print '-' instead...

Code  Description

       Conv Errors              :       6

31,N  DPV Failure              :       4
10:   Invalid Address          :       -
11:   Invalid C/S/Z            :       -
12:   Invalid State            :       -
13:   Invalid City             :       -
17:   Insufficient Information :       -
33:   Non-Deliverable          :       -
98:   Non-USPS zip             :       -

21:   Address Not Found        :       -
22:   Multiple Responses       :       3
23:   Error in Primary         :       -
24:   Error in Secondary       :       -


Much easier to pick out the numbers now.  To support this, the code 
changed slightly -- it went from

'%-25s: %7d' % ('DPV Failure', counts['D'])

to

'%-25s: %7s' % ('DPV Failure', counts['D'] if counts['D'] else '-'))

This became a pain after a dozen lines, prompting my previous question 
about the difference between %s and %d when printing integers.  With the 
excellent replies I received I coded a short class:

class DashInt(int):
     def __str__(x):
         if x:
             return str(x)
         return '-'

and my line printing code shrunk back to it's previous size.  Well, it 
wasn't long before I realized that when a DashInt was added to an int, 
an int came back... and so did the '0's.  So I added some more lines to 
the class.

     def __add__(x, other):
         result = super(DashInt, x).__add__(other)
         return result

and then I tried to do a floating type operation, so added yet more lines...

     def __add__(x, other):
         result = super(DashInt, x).__add__(other)
         if result == NotImplemented:
             return NotImplemented
         return result

and so on and so on for the basic math functions that I will be using... 
what a pain!  And then I had a thought... metaclasses!  If DashInt used 
a metaclass that would automatically check the result, and if it was 
base class wrap it up in the new subclass, my DashInt class could go 
back to being five simple lines, plus one more for the metaclass specifier.

So DashInt currently looks like this:

class TempInt(int):
     __metaclass__ = Perpetuate
     def __str__(x):
         if x == 0:
             return '-'
         return int.__str__(x)

and Perpetuate looks like this:

class Perpetuate(type):
     def __init__(yo, *args, **kwargs):
         super(type, yo).__init__(*args)
     def __new__(metacls, cls_name, cls_bases, cls_dict):
         if len(cls_bases) > 1:
             raise TypeError("multiple bases not allowed")
         result_class = type.__new__( \
           metacls, cls_name, cls_bases, cls_dict)
         base_class = cls_bases[0]
         known_methods = set()
         for method in cls_dict.keys():
             if callable(getattr(result_class, method)):
                 known_methods.add(method)

         base_methods = set()
         for method in base_class.__dict__.keys():
             if callable(getattr(base_class, method, None)) and \
                     method not in ('__new__'):
                 base_methods.add(method)

         for method in base_methods:
             if method not in known_methods:
                 setattr(result_class, method, \
                         _wrap(base_class, getattr(base_class, method)))

         return result_class


def _wrap(base, code):
     def wrapper(self, *args, **kwargs):
         result = code(self, *args, **kwargs)
         if type(result) == base:
             return self.__class__(result)
         return result
     wrapper.__name__ = code.__name__
     wrapper.__doc__ = code.__doc__
     return wrapper

It seems to work fine for normal operations.  I had to exclude __new__ 
because it was a classmethod, and I suspect I would have similar issues 
with staticmethods.

Any comments appreciated, especially ideas on how to better handle 
class- and staticmethods

~Ethan~



More information about the Python-list mailing list