开发者

Python: coerce new-style class

I want this code to "just work":

def main():
  开发者_Go百科  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)

  • Coercion Rules
  • New-style Special Method Lookup

Bonus points:

    print pow(c, 2, 100)


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


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)


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)


class Castable(object):
    def __div__(self, other):
        return 42 / other


New style classes operate faster and more precise than old style classes. Thus no more expensive __getattr__, __getattribute__ , __coerce__ calls for any cheap reasons and in a questionable order.

The old style __coerce__ also had the problem, that it was called even when you have already overloaded an operator method for some special purpose. And it demands casting to equal common types, and is limited to certain binary ops. Think about all the other methods and properties of an int / float / string - and about pow(). Due to all these limitations coerce is missing in PY3. The question examples aim at rather wide virtualization.

With new style classes its just about a loop to provide many "similar" methods with little code, or route those calls to a virtual handler and then its fast and precisely defined and subclassable in correct and fine grained manner. Thats not a "regression in the Python language".

However I would not employ a meta class as shown in other answers just for such a loop or for providing a simple base class kind of behavior. That would be cracking a nut with a sledgehammer.


Here an example helper for virtualization of a "variant":

def Virtual(*methods):
    """Build a (new style) base or mixin class, which routes method or
    operator calls to one __virtualmeth__ and attribute lookups to
    __virtualget__ and __virtualset__ optionally.

    *methods (strings, classes): Providing method names to be routed
    """
    class VirtualBase(object):  
        def __virtualmeth__(self, methname, *args, **kw):
            raise NotImplementedError
    def _mkmeth(methname, thing):
        if not callable(thing):
            prop = property(lambda self:self.__virtualget__(methname),
                            lambda self, v:self.__virtualset__(methname, v))
            return prop
        def _meth(self, *args, **kw):
            return self.__virtualmeth__(methname, *args, **kw)
        _meth.__name__ = methname
        return _meth
    for m in methods:
        for name, thing in (isinstance(m, str) and
                            {m:lambda:None} or m.__dict__).items():
            if name not in ('__new__', '__init__', '__setattr__', ##'__cmp__',
                            '__getattribute__', '__doc__', ):   ##'__getattr__', 
                setattr(VirtualBase, name, _mkmeth(name, thing))
    return VirtualBase

And here an example use case: An Anaphor! (PY2 and PY3) :

import operator
class Anaphor(Virtual(int, float, str)):   
    """remember a sub-expression comfortably:

    A = Anaphor()      # at least per thread / TLS
    if re.search(...) >> A:
        print(A.groups(), +A)
    if A(x % 7) != 0:
        print(A, 1 + A, A < 3.0, A.real, '%.2f' % A, +A)
    """
    value = 0
    def __virtualmeth__(self, methname, *args, **kw):
        try: r = getattr(self.value, methname)(*args, **kw)
        except AttributeError:
            return getattr(operator, methname)(self.value, *args, **kw)
        if r is NotImplemented: # simple type -> coerce
            try: tcommon = type(self.value + args[0])    # PY2 coerce
            except: return NotImplemented
            return getattr(tcommon(self.value), methname)(*args, **kw)
        return r
    def __call__(self, value):   
        self.value = value
        return value
    __lshift__ = __rrshift__ = __call__     # A << x;  x >> A
    def __pos__(self):                      # real = +A
        return self.value
    def __getattr__(self, name):
        return getattr(self.value, name)
    def __repr__(self):
        return '<Anaphor:%r>' % self.value

Seamlessly it also handles the 3-arg opertor pow() :-) :

>>> A = Anaphor()
>>> x = 1
>>> if x + 11 >> A:
...     print repr(A), A, +A, 'y' * A, 3.0 < A, pow(A, 2, 100)
...     
<Anaphor:12> 12 12 yyyyyyyyyyyy True 44
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜