Is __mul__ sufficient for operator '*'?

Mick Krippendorf mad.mick at gmx.de
Sun Oct 25 20:02:43 EDT 2009


Muhammad Alkarouri schrieb:
> I was having a go at a simple implementation of Maybe in Python when I
> stumbled on a case where x.__mul__(y) is defined while x*y is not.
> 
> class Maybe(object):
>     def __init__(self, obj):
>         self.o = obj
>     def __repr__(self):
>         return 'Maybe(%s)' % object.__getattribute__(self, "o")
>     def __getattribute__(self, name):
>         try:
>             o = object.__getattribute__(self, "o")
>             r = getattr(o,name)
>             if callable(r):
>                 f = lambda *x:Maybe(r(*x))
>                 return f
>             else:
>                 return Maybe(r)
>         except:
>             return Maybe(None)
> 
>>>> x=Maybe(9)
>>>> x.__mul__(7)
> Maybe(63)
>>>> x*7
> 
> Traceback (most recent call last):
>   File "<pyshell#83>", line 1, in <module>
>     x*7
> TypeError: unsupported operand type(s) for *: 'Maybe' and 'int'

Here's how I'd do it. It will not win a beauty contest any time soon,
but at least it's a workaround:

----8<--------8<--------8<--------8<--------8<--------8<--------8<----

def meta(lift, lifted, not_lifted=[]):
    not_lifted = list(not_lifted) + object.__dict__.keys()
    class MetaMaybe(type):
        def __new__(meta, mcls, bases, dct):
            dct.update(
                (name, lift(name))
                    for name in set(lifted) - set(not_lifted)
            )
            return type(mcls, bases, dct)
    return MetaMaybe

class Nothing(object):
    __metaclass__ = meta(lambda name: lambda self, *a, **k: self, (
        "__add__", "__sub__", "__mul__", "__div__", "__truediv__",
        "__floordiv__", "__divmod__", "__radd__", "__rsub__",
        "__rmul__", "__rdiv__", "__rtruediv__", "__rfloordiv__",
        "__rdivmod__", "__rshift__", "__lshift__", "__call__",
        # and so on, for every special method that Nothing knows
    ))
    def __new__(cls, value=None):
        try: # singleton
            return cls.value
        except AttributeError:
            cls.value = super(Nothing, cls).__new__(cls)
            return cls.value
    def __str__(self):
        return "Nothing"
    __repr__ = __str__

Nothing = Nothing()

def just(vcls):
    def lifter(name):
        attr = getattr(vcls, name)
        def lifted(self, *ms):
            try:
                return self.lift(attr)(self, *ms)
            except:
                return Nothing
        return lifted
    class Just(object):
        __metaclass__ = meta(lifter, vcls.__dict__.keys())
        def __new__(cls, value):
            if value in (Nothing, NotImplemented):
                return Nothing
            return super(Just, cls).__new__(cls)
        def __init__(self, value):
            self.value = value
        def __str__(self):
            return "Just(%s)" % self.value
        @classmethod
        def lift(c, f):
            return lambda *ms:c(f(*(m.value for m in ms)))
    return Just

from collections import defaultdict

class TypeDict(defaultdict):
    def __missing__(self, key):
        if self.default_factory is None:
            raise KeyError(key)
        return self.default_factory(key)

class Maybe(object):
    typemap = TypeDict(just)
    def __new__(cls, value):
        return Maybe.typemap[value.__class__](value)

def foo(x, y):
    return x * 2 + y * 3

if __name__ == "__main__":

    print Maybe(Nothing)
    print Maybe(1) / Maybe(0)
    print Maybe(10.) * Maybe(5) / Maybe(2) ** Maybe(3)
    print Maybe(foo)(Maybe(6), Maybe(10))
    print Maybe("hello").upper()
    print Maybe("hello").startswith(Maybe("h"))
    print getattr(Maybe("hello"), "startswith")(Maybe("h"))
    print Maybe(foo)(Maybe("hello! "), Maybe("what? "))
    print Maybe(foo)(Maybe("hello! "), Nothing)

----8<--------8<--------8<--------8<--------8<--------8<--------8<----

I haven't tested it very thoroughly, so it's quite possible there are
lots of bugs in it, but it is only intended as a demo.

As Gabriel Genellina pointed out, the search for special methods is done
in the type, so we have to put our own versions there, during type
creation in the metaclass' __new__ method. The above code does this for
all methods of the wrapped object's type, not just special ones, and
"lifts" them to expect Maybe objects instead of "normal" objects. They
also wrap their return values into Maybe objects.

Maybe is an algebraic data type. The call Maybe(some_value) returns
either Nothing, if some_value happens to be Nothing, or else an object
of type Just that wraps some_value. More precisely, there is not one
type Just, but as many as types of Just-wrapped objects (Just<int>,
Just<string>, ...). Just is therefore a kind of parameterized type. It's
similar to inheriting from a C++ template parameter type:

template <class T>
class MyType : public T {...}

but not quite, since in C++ the inherited member functions' signatures
are unchanged, whereas in the above code "inherited" methods are changed
to expect and return Maybe objects.

Some things don't work, though, e.g. slicing. But this could be
implemented via specialized lifting functions for the __XXXitem__
methods. One thing that does work though, is that ordinary functions can
be wrapped as Maybe objects which then "do the same thing" on other
Maybe objects that the normal functions do on normal objects, like in
the foo-example. So it's quite close to a monadic version, in that it
"lifts" objects and functions from one type space into another one, the
Maybe space. But compared to a real Monads it's much more pythonic, IMO.


HTH,
Mick.



More information about the Python-list mailing list