pythonoopclasscoerce

Python: coerce new-style class


I want this code to "just work":

def main():
    c = Castable()
    print c/3
    print 2-c
    print c%7
    print c**2
    print "%s" % c
    print "%i" % c
    print "%f" % c

Of course, the easy way out is to write int(c)/3, but I'd like to enable a simpler perl-ish syntax for a configuration mini-language.

It's notable that if I use an "old-style" class (don't inherit from object) I can do this quite simply by defining a __coerce__ method, but old-style classes are deprecated and will be removed in python3.

When I do the same thing with a new-style class, I get this error:

TypeError: unsupported operand type(s) for /: 'Castable' and 'int'

I believe this is by design, but then how can I simulate the old-style __coerce__ behavior with a new-style class? You can find my current solution below, but it's quite ugly and long-winded.

This is the relevant documentation: (i think)

Bonus points:

    print pow(c, 2, 100)

Solution

  • This works, and is less gross after several improvements (props to @jchl), but still seems like it should be unecessary, especially considering that you get this for free with "old-style" classes.

    I'm still looking for a better answer. If there's no better method, this seems to me like a regression in the Python language.

    def ops_list():
        "calculate the list of overloadable operators"
        #<type 'object'> has functions but no operations
        not_ops = dir(object)
    
        #calculate the list of operation names
        ops = set()
        for mytype in (int, float, str):
            for op in dir(mytype):
                if op.endswith("__") and op not in not_ops:
                    ops.add(op)
        return sorted(ops)
    
    class MetaCastable(type):
        __ops = ops_list()
    
        def __new__(mcs, name, bases, dict):
            #pass any undefined ops to self.__op__
            def add_op(op):
                if op in dict:
                    return
                fn = lambda self, *args: self.__op__(op, args)
                fn.__name__ = op
                dict[op] = fn
    
            for op in mcs.__ops:
                add_op( op )
            return type.__new__(mcs, name, bases, dict)
    
    
    class Castable(object):
        __metaclass__ = MetaCastable
        def __str__(self):
            print "str!"
            return "<Castable>"
        def __int__(self):
            print "int!"
            return 42
        def __float__(self):
            print "float!"
            return 2.718281828459045
    
        def __op__(self, op, args):
            try:
                other = args[0]
            except IndexError:
                other = None
            print "%s %s %s" % (self, op, other)
            self, other = coerce(self, other)
            return getattr(self, op)(*args)
    
        def __coerce__(self, other):
            print "coercing like %r!" % other
            if other is None: other = 0.0
            return (type(other)(self), other)