On 24/11/2013 14:27, Steven D'Aprano wrote:
On Sat, 23 Nov 2013 19:53:32 +0000, Rotwang wrote:

On 22/11/2013 11:26, Steven D'Aprano wrote:
A frequently missed feature is the ability to chain method calls:
[...]
chained([]).append(1).append(2).append(3).reverse().append(4) =>
returns [3, 2, 1, 4]

That's pretty cool. However, I can imagine it would be nice for the
chained object to still be an instance of its original type.

Why?

Well, if one intends to pass such an object to a function that does something like this:

def f(obj):
    if isinstance(obj, class1):
        do_something(obj)
    elif isinstance(obj, class2):
        do_something_else(obj)

then

>>> f(chained(obj).method1().method2())

looks nicer than

>>> f(chained(obj).method1().method2().obj)

and usually still works with the version of chained I posted last night. This isn't a wholly hypothetical example, I have functions in my own software that perform instance checks and that I often want to pass objects that I've just mutated several times.


During the chained call, you're only working with the object's own
methods, so that shouldn't matter. Even if a method calls an external
function with self as an argument, and the external function insists on
the original type, that doesn't matter because the method sees only the
original (wrapped) object, not the chained object itself.

In other words, in the example above, each of the calls to list.append
etc. see only the original list, not the chained object.

The only time you might care about getting the unchained object is at the
end of the chain. This is where Ruby has an advantage, the base class of
everything has a method useful for chaining methods, Object.tap, where in
Python you either keep working with the chained() wrapper object, or you
can extract the original and discard the wrapper.


How about something like this:

def getr(self, name):
      obj = super(type(self), self).__getattribute__(name)

I don't believe you can call super like that. I believe it breaks when
you subclass the subclass.

Yes. I don't know what I was thinking with the various super calls I wrote last night, apart from being buggy they're completely unnecessary. Here's a better version:

class dummy:
    pass

def initr(self, obj):
    object.__setattr__(self, '__obj', obj)
def getr(self, name):
    try:
        return object.__getattribute__(self, name)
    except AttributeError:
        return getattr(self.__obj, name)
def methr(method):
    def cmethod(*args, **kwargs):
        try:
            args = list(args)
            self = args[0]
            args[0] = self.__obj
        except (IndexError, AttributeError):
            self = None
        result = method(*args, **kwargs)
        return self if result is None else result
    try:
        cmethod.__qualname__ = method.__qualname__
    except AttributeError:
        pass
    return cmethod

class chained(type):
    typedict = {}
    def __new__(cls, obj):
        if isinstance(type(obj), chained):
            return obj
        if type(obj) not in cls.typedict:
            dict = {}
            for t in reversed(type(obj).__mro__):
                dict.update({k: methr(v) for k, v in t.__dict__.items()
                            if callable(v) and k != '__new__'})
            dict.update({'__init__': initr, '__getattribute__': getr})
            cls.typedict[type(obj)] = type.__new__(cls, 'chained%s'
                % type(obj).__name__, (dummy, type(obj)), dict)
        return cls.typedict[type(obj)](obj)
--
https://mail.python.org/mailman/listinfo/python-list

Reply via email to