3
\$\begingroup\$

As a bit of a learning project, I decided to take the concept of proxying objects a bit further and extend it to creating proxy classes which create proxy'd objects. I originally found the idea of proxying objects in python on Activestate. I'm currently using a very odd pattern (creating a class with just staticmethods and abusing __new__ to return my classes, instead of using a function (since that would reinstantiate __special_names__, __imethods__, and __other_magic__ every time it ran.

Is this a good idea, or should I return to using a function that takes a type and returns a Proxy subclass? On a second note, is there any way to make isinstance and issubclass happy without actually subclassing the given type? I originally wanted to use __call__ for the class which would make it at least somewhat readable, since when you call functions, you call their __call__ attribute (afaik), but python seems to think I have to use the __new__ attribute instead to use a class as a function.

The entire purpose of this class Proxy hack is to create Proxy classes for immutable objects - this way if you do a = b = 10 and a += 5, a == b == 15 without having to update b.

Shameless self-plugging: This is actually a core piece of code at pyranoid, which I work on, since it seems interesting.

import functools
import collections

class Proxy(type):
    __special_names__ = {
        '__abs__', '__add__', '__and__', '__call__', '__cmp__', '__coerce__',
        '__contains__', '__delitem__', '__delslice__', '__div__', '__divmod__',
        '__eq__', '__float__', '__floordiv__', '__ge__', '__getitem__', '__getslice__',
        '__gt__', '__hash__', '__hex__', '__int__', '__iter__', '__le__', '__len__',
        '__long__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__',
        '__oct__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdiv__',
        '__rdivmod__', '__reduce__', '__reduce_ex__',  '__reversed__',
        '__rfloorfiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__rpow__',
        '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setitem__',
        '__setslice__', '__sub__', '__truediv__', '__xor__', '__next__'
    }
    __imethods__ = {
        '__iadd__', '__iand__', '__idiv__', '__idivmod__',
        '__ifloordiv__', '__ilshift__', '__imod__', '__imul__',
        '__invert__', '__ior__', '__ipow__', '__irshift__',
        '__isub__', '__itruediv__', '__ixor__'
    }
    __other_magic__ = {
        '__int__',
        '__long__',
        '__float__',
        '__complex__',
        '__oct__',
        '__hex__',
        '__index__',
        '__trunc__',
        '__coerce__',
        '__str__',
        '__repr__',
        '__unicode__',
        '__format__',
        '__hash__',
        '__nonzero__',
        '__dir__',
        '__sizeof__'
    }
    __overridden__ = __special_names__.union(__imethods__).union(__other_magic__)
    @staticmethod
    def __imethod_wrapper____(method):
        '''makes a wrapper around __i<something>__ methods, such as __iadd__ to act on __subject__'''
        @functools.wraps(method)
        def wrapper(self,*args,**kwargs):
            tmp = self.__subject__
            tmp = method(tmp,*args,**kwargs)
            self.__subject__ = tmp
            return self
        return wrapper
    @staticmethod
    def __method_wrapper__(method):
        '''makes a wrapper around methods and cast the result to a proxytype if possible'''
        @functools.wraps(method)
        def wrapper(self,*args,**kwargs):
            res = method(self.__subject__,*args,**kwargs)
            try:
                return Proxy(type(res),'Proxy<{t}>'.format(t=type(res).__name__))(res)
            except TypeError: #if the result's type isn't subclassable, i.e. types.FunctionType would raise a TypeException
                return res
        return wrapper

    @staticmethod
    def usable_base_type(Type):
        try:
            type('',(Type,),{})
            return True
        except TypeError:
            return False
    def __new__(cls,parentType,classname=None): #So that Proxy emulates a function
        '''parentType is the type you wish to proxy, and classname is the name that appears for the class, <class 'classname'>'''
        if not cls.usable_base_type(parentType):
            raise TypeError("type '{Type}' is not an acceptable base type".format(Type=parentType.__name__))
        if classname is None:
            classname = 'Proxy<{name}>'.format(name=parentType.__name__)
        class newType(parentType):
            def __init__(self,*args,**kwargs):
                self.__subject__ = parentType(*args,**kwargs)
            def setvalue(self,value):
                self.__subject__ = value
            def getvalue(self):
                return self.__subject__
            def __getattr__(self,name):
                if name not in cls.__overridden__:
                    return getattr(self.__subject__,name)
        for name,prop in ((k,v) for k,v in parentType.__dict__.items() if k != '__doc__'): #because magic methods are implemented as staticmethods
            if name in cls.__special_names__:
                setattr(newType,name,cls.__method_wrapper__(prop))
        for name in cls.__imethods__: #parentType may not implement all of them
            if hasattr(parentType,name):
                setattr(newType,name,cls.__imethod_wrapper____(parentType.__dict__[name]))
            else:
                non_i_name = name[:2]+name[3:]
                if hasattr(parentType,non_i_name):
                    setattr(newType,name,cls.__imethod_wrapper____(getattr(parentType,non_i_name)))
        for name in cls.__other_magic__:
            if hasattr(parentType,name):
                parent_item = getattr(parentType,name)
                if isinstance(parent_item,collections.Callable):
                    setattr(newType,name,lambda self,*args,**kwargs:parent_item(self.__subject__,*args,**kwargs))
                else:
                    setattr(newType,name,parent_item)
        newType.__name__ = classname
        return newType
\$\endgroup\$
  • \$\begingroup\$ If it were me, I'd make Proxy a function which returns the type. It can still be implemented in terms of the class but you shouldn't make it directly accessible. Keep its definition within the function so people can't go poking around in it. Cleaner (IMHO) if you are making this into a module. \$\endgroup\$ – Jeff Mercado Jun 18 '13 at 1:54
  • \$\begingroup\$ @JeffMercado but then what if I want to subclass Proxy and modify the wrapper functions, etc? \$\endgroup\$ – Snakes and Coffee Jun 18 '13 at 5:03
  • \$\begingroup\$ It sounds like the only reason you made Proxy a class was so you could easily subclass it. In that case perhaps it would be better to implement it as a metaclass because you can also subclass them and it seems to me that's what you're really doing here. \$\endgroup\$ – martineau Jul 1 '13 at 14:29
2
\$\begingroup\$

Some quick thoughts:

  1. I can't tell from reading the code what the purpose is. What is this for? What are the use cases? How am I supposed to use it?

  2. There's no docstring for the Proxy class or for the usable_base_type method.

  3. What is the role of the Proxy class? You never seem to create any instances of it. (The __new__ method returns an instance of the newType that it has just created.) Python is not Java: not everything needs to be a class.

  4. Names starting and ending with two underscores are reserved for future use by the language and the standard library:

    __*__ System-defined names. These names are defined by the interpreter and its implementation (including the standard library). Current system names are discussed in the Special method names section and elsewhere. More will likely be defined in future versions of Python. Any use of __*__ names, in any context, that does not follow explicitly documented use, is subject to breakage without warning.

    So I'd recommend avoiding names of this form in your own code.

  5. What's the difference between __special_names__ and __other_magic__? Why is __slots__ not in either of these sets?

  6. The docstring for __method_wrapper__ says "cast the result", but Python doesn't have casting. What does this method really do?

  7. Why does __imethod_wrapper____ end in four underscores?

  8. Instead of

    __special_names__.union(__imethods__).union(__other_magic__)
    

    you could write

    __special_names__ | __imethods__ | __other_magic__
    
  9. __special_names__ is almost in alphabetical order (which is a useful way to order a list like this, because it helps you spot missing and duplicate items), but __next__ is out of order. Why is that?

  10. What order are the names in __other_magic__ in? Why not alphabetical order like the two other lists? And why is this list formatted with one name per line while the others are paragraph-wrapped?

  11. The method returned by __method_wrapper__ calls Proxy() each time it is called, which will create another newType class. Is this really what you want to happen? Won't this lead to a proliferation of identical classes?

But really this is just fumbling in the dark because I don't understand what problem you are trying to solve. I see that you edited the post to add an explanation (you want to be able to proxy immutable objects), but this still leaves me in the dark. Why do you want to proxy immutable objects?

\$\endgroup\$
  • \$\begingroup\$ I think one use of this could be to simulate references that other language have, as illustrated in the example a = b = 10, a += 5, a == b == 15 in the OP's question. WRT coding style, I think there should be a single space between items in most comma-separated list, a recommended in PEP-8. Also whitespace related is the fact that a number of lines are extremely long and should have been broken-up into multiple lines for better viewing and readability. \$\endgroup\$ – martineau Jun 28 '13 at 3:29
  • \$\begingroup\$ @martineau: That's what the OP wants to achieve (proxying of immutable objects) but I want to know why. \$\endgroup\$ – Gareth Rees Jul 1 '13 at 9:56
  • \$\begingroup\$ Oh, I don't know, most likely it's one of the several things people use references to accomplish. What relevance does knowing why have on answering the OP's question about whether a class is a good way to implement a class factory in Python? \$\endgroup\$ – martineau Jul 1 '13 at 13:56
  • \$\begingroup\$ @martineau: I'd like to know the purpose in order to try to provide a better answer. An implementation is only good (or bad) in so far as it meets (or fails to meet) someone's goals. If I knew more about the OP's goals (or use cases) then I would be in a better position to evaluate the solution presented in the question, or to propose a better solution. \$\endgroup\$ – Gareth Rees Jul 1 '13 at 14:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.