views:

240

answers:

4

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)
+7  A: 

You need to define __div__ if you want c/3 to work. Python won't convert your object to a number first for you.

Ned Batchelder
I don't expect it to convert for me, but I do expect to be able to do it myself if needed without defining each and every operation. I guess that's what you're suggesting really.
bukzor
@Ned: In particular, I'm surprised that I can't catch an attempted call to `__div__` via `__getattribute__`.
bukzor
http://docs.python.org/reference/datamodel.html#new-style-special-lookup says "For new-style classes, implicit invocations of special methods are only guaranteed to work correctly if defined on an object’s type" and later "implicit special method lookup generally also bypasses the `__getattribute__()` method even of the object’s metaclass". So, no, you have to define each and every operation.
Zack
@Zack: that's one of the only helpful things people have said.
bukzor
A: 
class Castable(object):
    def __div__(self, other):
        return 42 / other
Matt Williamson
+5  A: 

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)
bukzor
I actually think this is a pretty neat solution. Given that Python doesn't seem to provide this functionality, this is not many lines of code to add a pretty powerful feature to the language.
jchl
There are a few things you could do to make it neater still. Better to modify `dict` before calling `type.__new__` than to use `setattr` afterwards. Better to give your lambda functions an appropriate `__name__`, and to use a closure rather than the default arguments trick, so that they have the proper prototype. And I'm not sure whether you consider it a feature that it assumes that the type doesn't define `__radd__` if it doesn't define `__add__`, or whether this is a bug.
jchl
@jchl: thanks. I'll do those after work, or you can feel free and try to grab the bounty. I guess the main thing I hate about this solution is the static lists _unary and _binary. It also doesn't support the 3-argument pow() operator.
bukzor
@bukzor: Yeah, I was hoping the `operator` module had lists of the unary and binary operators, but no such luck.
jchl
@jchl: I've refactored the setattr's away, as suggested. I'm not sure how you intended to use a closure, since it still has the same late-binding problems as lambda functions (all ops become `__invert__`).
bukzor
@bukzor: You need to return the function/lambda from within another function. I'll post what I mean; easier than trying to write code in a comment.
jchl
I don't think it's ugly at all. You probably can't get any better with Python.
Philipp
@jchl: Thanks. I was able to improve on that and merge most of the unary/binary code. As a bonus, the 3-argument pow operation now works as well.
bukzor
Doesn't the inline code in the class definition leave `MetaCastable` with two unwanted attributes named `mytype` and `op`? I think you need to either `del` them or move that code to a separate function to avoid that.
jchl
@jchl: fixed, thanks. I haven't found someone more picky than myself before :P
bukzor
+3  A: 
class MetaCastable(type):
    __binary_ops = ( 
            'add', 'sub', 'mul', 'floordiv', 'mod', 'divmod', 'pow', 'lshift', 
            'rshift', 'and', 'xor', 'or', 'div', 'truediv',
    )

    __unary_ops = ( 'neg', 'pos', 'abs', 'invert', )

    def __new__(mcs, name, bases, dict):
        def make_binary_op(op):
            fn = lambda self, other: self.__op__(op, other)
            fn.__name__ = op
            return fn

        for opname in mcs.__binary_ops:
            for op in ( '__%s__', '__r%s__' ):
                op %= opname
                if op in dict:
                    continue
                dict[op] = make_binary_op(op)

        def make_unary_op(op):
            fn = lambda self: self.__op__(op, None)
            fn.__name__ = op
            return fn

        for opname in mcs.__unary_ops:
            op = '__%s__' % opname
            if op in dict:
                continue
            dict[op] = make_unary_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, other):
        if other is None:
            print "%s(%s)" % (op, self)
            self, other = coerce(self, 0.0)
            return getattr(self, op)()
        else:
            print "%s %s %s" % (self, op, other)
            self, other = coerce(self, other)
            return getattr(self, op)(other)

    def __coerce__(self, other):
        print "coercing like %r!" % other
        return (type(other)(self), other)
jchl