Groups | Search | Server Info | Keyboard shortcuts | Login | Register [http] [https] [nntp] [nntps]
Groups > comp.lang.python > #60213 > unrolled thread
| Started by | Steven D'Aprano <steve+comp.lang.python@pearwood.info> |
|---|---|
| First post | 2013-11-22 11:26 +0000 |
| Last post | 2013-11-24 15:01 +0000 |
| Articles | 17 — 8 participants |
Back to article view | Back to comp.lang.python
Method chaining Steven D'Aprano <steve+comp.lang.python@pearwood.info> - 2013-11-22 11:26 +0000
Re: Method chaining Chris Angelico <rosuav@gmail.com> - 2013-11-22 22:44 +1100
Re: Method chaining Peter Otten <__peter__@web.de> - 2013-11-22 13:08 +0100
Re: Method chaining Steven D'Aprano <steve+comp.lang.python@pearwood.info> - 2013-11-22 14:26 +0000
Re: Method chaining Peter Otten <__peter__@web.de> - 2013-11-22 16:20 +0100
Re: Method chaining Steven D'Aprano <steve+comp.lang.python@pearwood.info> - 2013-11-22 16:26 +0000
Re: Method chaining Wolfgang Maier <wolfgang.maier@biologie.uni-freiburg.de> - 2013-11-22 16:52 +0000
Re: Method chaining Wolfgang Maier <wolfgang.maier@biologie.uni-freiburg.de> - 2013-11-22 17:55 +0000
Re: Method chaining Antoon Pardon <antoon.pardon@rece.vub.ac.be> - 2013-11-22 16:38 +0100
Re: Method chaining Laszlo Nagy <gandalf@shopzeus.com> - 2013-11-23 17:08 +0100
Re: Method chaining Terry Reedy <tjreedy@udel.edu> - 2013-11-22 07:34 -0500
Re: Method chaining Steven D'Aprano <steve+comp.lang.python@pearwood.info> - 2013-11-22 14:30 +0000
Re: Method chaining Rotwang <sg552@hotmail.co.uk> - 2013-11-23 19:53 +0000
Re: Method chaining Rotwang <sg552@hotmail.co.uk> - 2013-11-24 00:28 +0000
Re: Method chaining Rotwang <sg552@hotmail.co.uk> - 2013-11-24 00:43 +0000
Re: Method chaining Steven D'Aprano <steve+comp.lang.python@pearwood.info> - 2013-11-24 14:27 +0000
Re: Method chaining Rotwang <sg552@hotmail.co.uk> - 2013-11-24 15:01 +0000
| From | Steven D'Aprano <steve+comp.lang.python@pearwood.info> |
|---|---|
| Date | 2013-11-22 11:26 +0000 |
| Subject | Method chaining |
| Message-ID | <528f3f4e$0$29992$c3e8da3$5496439d@news.astraweb.com> |
A frequently missed feature is the ability to chain method calls:
x = []
x.append(1).append(2).append(3).reverse().append(4)
=> x now equals [3, 2, 1, 4]
This doesn't work with lists, as the methods return None rather than
self. The class needs to be designed with method chaining in mind before
it will work, and most Python classes follow the lead of built-ins like
list and have mutator methods return None rather than self.
Here's a proof-of-concept recipe to adapt any object so that it can be
used for chaining method calls:
class chained:
def __init__(self, obj):
self.obj = obj
def __repr__(self):
return repr(self.obj)
def __getattr__(self, name):
obj = getattr(self.obj, name)
if callable(obj):
def selfie(*args, **kw):
# Call the method just for side-effects, return self.
_ = obj(*args, **kw)
return self
return selfie
else:
return obj
chained([]).append(1).append(2).append(3).reverse().append(4)
=> returns [3, 2, 1, 4]
Tested, and works, in CPython 2.4 through 2.7, 3.2 and 3.3, Jython 2.5,
and IronPython 2.6.
See here for further discussion of the limitations:
http://code.activestate.com/recipes/578770-method-chaining/
--
Steven
[toc] | [next] | [standalone]
| From | Chris Angelico <rosuav@gmail.com> |
|---|---|
| Date | 2013-11-22 22:44 +1100 |
| Message-ID | <mailman.3033.1385120652.18130.python-list@python.org> |
| In reply to | #60213 |
On Fri, Nov 22, 2013 at 10:26 PM, Steven D'Aprano
<steve+comp.lang.python@pearwood.info> wrote:
> if callable(obj):
> def selfie(*args, **kw):
> # Call the method just for side-effects, return self.
> _ = obj(*args, **kw)
> return self
> return selfie
> else:
> return obj
Nice piece of magic. One limitation not mentioned is that this
completely destroys the chance to have a method return anything _other
than_ self. Since this is intended for Python's convention of
"mutators return None", I'd be inclined to check for a None return,
though that might still have some false positives.
def selfie(*args, **kw):
# Call the method for side-effects, return self if it
returns None.
_ = obj(*args, **kw)
if _ is None: return self
return _
return selfie
Either that, or manually identify a set of methods to wrap, which
could possibly be done fairly cleanly with a list of names passed to
__init__. That'd be more work, though.
ChrisA
[toc] | [prev] | [next] | [standalone]
| From | Peter Otten <__peter__@web.de> |
|---|---|
| Date | 2013-11-22 13:08 +0100 |
| Message-ID | <mailman.3035.1385122060.18130.python-list@python.org> |
| In reply to | #60213 |
Steven D'Aprano wrote:
> A frequently missed feature is the ability to chain method calls:
>
> x = []
> x.append(1).append(2).append(3).reverse().append(4)
> => x now equals [3, 2, 1, 4]
>
>
> This doesn't work with lists, as the methods return None rather than
> self. The class needs to be designed with method chaining in mind before
> it will work, and most Python classes follow the lead of built-ins like
> list and have mutator methods return None rather than self.
>
> Here's a proof-of-concept recipe to adapt any object so that it can be
> used for chaining method calls:
>
>
> class chained:
> def __init__(self, obj):
> self.obj = obj
> def __repr__(self):
> return repr(self.obj)
> def __getattr__(self, name):
> obj = getattr(self.obj, name)
> if callable(obj):
> def selfie(*args, **kw):
> # Call the method just for side-effects, return self.
> _ = obj(*args, **kw)
> return self
> return selfie
> else:
> return obj
>
>
> chained([]).append(1).append(2).append(3).reverse().append(4)
> => returns [3, 2, 1, 4]
>
>
> Tested, and works, in CPython 2.4 through 2.7, 3.2 and 3.3, Jython 2.5,
> and IronPython 2.6.
>
> See here for further discussion of the limitations:
>
> http://code.activestate.com/recipes/578770-method-chaining/
Here's my take:
class Chained(object):
def __init__(self, value, method=None):
self.value = value
self.method = method
def __call__(self, *args, **kw):
result = self.method(*args, **kw)
if result is None:
result = self.value
return Chained(result)
def __getattr__(self, name):
return Chained(self.value, getattr(self.value, name))
if __name__ == "__main__":
print(Chained([]).append(1).append(2).append(3).reverse().append(4).value)
print(Chained([]).append(1).extend([2,1,1]).count(1).value)
These things are nice to write as long as you omit the gory details, but
personally I don't want to see the style it favours in my or other people's
code.
[toc] | [prev] | [next] | [standalone]
| From | Steven D'Aprano <steve+comp.lang.python@pearwood.info> |
|---|---|
| Date | 2013-11-22 14:26 +0000 |
| Message-ID | <528f6989$0$29992$c3e8da3$5496439d@news.astraweb.com> |
| In reply to | #60216 |
On Fri, 22 Nov 2013 13:08:03 +0100, Peter Otten wrote:
> These things are nice to write as long as you omit the gory details, but
> personally I don't want to see the style it favours in my or other
> people's code.
There's not really a lot of difference between:
obj = MyClass()
obj.spam()
obj.eggs()
obj.cheese()
and
obj = MyClass().spam().eggs().cheese()
except the first takes up a lot more vertical space. Chained method calls
is idiomatic in some languages. If there is a problem with it, it is that
it doesn't make it clear that each method call is being used only for its
side-effects, rather than it being a series of distinct objects. But in
my opinion that flaw is a very minor one.
The nice thing about using an explicit method chaining call rather than
building your class to support it by default is that the initial call to
the adaptor signals that everything that follows is called only for the
side-effects.
obj = chained(MyClass()).spam().eggs().cheese()
--
Steven
[toc] | [prev] | [next] | [standalone]
| From | Peter Otten <__peter__@web.de> |
|---|---|
| Date | 2013-11-22 16:20 +0100 |
| Message-ID | <mailman.3044.1385133587.18130.python-list@python.org> |
| In reply to | #60228 |
Steven D'Aprano wrote:
> On Fri, 22 Nov 2013 13:08:03 +0100, Peter Otten wrote:
>
>> These things are nice to write as long as you omit the gory details, but
>> personally I don't want to see the style it favours in my or other
>> people's code.
>
> There's not really a lot of difference
That cuts both ways ;)
> between:
>
> obj = MyClass()
> obj.spam()
> obj.eggs()
> obj.cheese()
>
> and
>
> obj = MyClass().spam().eggs().cheese()
>
>
> except the first takes up a lot more vertical space.
I've not yet run short of vertical space ;)
> Chained method calls is idiomatic in some languages.
Languages with mutable objects?
> If there is a problem with it, it is that
> it doesn't make it clear that each method call is being used only for its
> side-effects, rather than it being a series of distinct objects. But in
> my opinion that flaw is a very minor one.
>
> The nice thing about using an explicit method chaining call rather than
> building your class to support it by default is that the initial call to
> the adaptor signals that everything that follows is called only for the
> side-effects.
>
> obj = chained(MyClass()).spam().eggs().cheese()
obj = MyClass(); obj.spam(); obj.eggs(); obj.cheese()
OK, that one is disgusting...
Anyway, I'd like to see a sequence of method names taken from actual code
that profits from this chaining pattern.
[toc] | [prev] | [next] | [standalone]
| From | Steven D'Aprano <steve+comp.lang.python@pearwood.info> |
|---|---|
| Date | 2013-11-22 16:26 +0000 |
| Message-ID | <528f859e$0$29992$c3e8da3$5496439d@news.astraweb.com> |
| In reply to | #60235 |
On Fri, 22 Nov 2013 16:20:03 +0100, Peter Otten wrote:
> Steven D'Aprano wrote:
>
>> On Fri, 22 Nov 2013 13:08:03 +0100, Peter Otten wrote:
>>
>>> These things are nice to write as long as you omit the gory details,
>>> but personally I don't want to see the style it favours in my or other
>>> people's code.
>>
>> There's not really a lot of difference
>
> That cuts both ways ;)
Actually, I was wrong. See below.
>> between:
>>
>> obj = MyClass()
>> obj.spam()
>> obj.eggs()
>> obj.cheese()
>>
>> and
>>
>> obj = MyClass().spam().eggs().cheese()
>>
>>
>> except the first takes up a lot more vertical space.
>
> I've not yet run short of vertical space ;)
However, here is a real difference:
# With chaining
thing = func(MyClass().spam().eggs().cheese(),
MyClass().aardvark(),
OtherClass().fe().fi().fo().fum(),
)
do_stuff_with(thing)
versus:
# Without chaining
temp1 = MyClass()
temp1.spam()
temp1.eggs()
temp1.cheese()
temp2 = MyClass()
temp2.aardvark()
temp3 = OtherClass()
temp3.fe()
temp3.fi()
temp3.fo()
temp3.fum()
thing = func(temp1, temp2, temp3)
do_stuff_with(thing)
In this case the chained version doesn't obscure the intention of the
code anywhere near as much as the unchained version and its plethora of
temporary variables.
>> Chained method calls is idiomatic in some languages.
>
> Languages with mutable objects?
Yes. It's a "Design Pattern" applicable to any language with mutator
methods. Here are three examples in C#, Java and Ruby:
http://mrbool.com/fluent-interface-and-method-chaining-a-good-programming-approach/26365
http://www.infoq.com/articles/internal-dsls-java
http://blog.jayfields.com/2008/03/ruby-replace-temp-with-chain.html
although in fairness I wouldn't call it idiomatic in C# or Java.
Ruby 1.9 even added a new method to Object, tap, specifically to allow
chaining of methods, which itself was copied from Ruby-On-Rails'
"returning" helper. So I think it's fair to say that method chaining
for mutation is idiomatic in Ruby.
http://www.seejohncode.com/2012/01/02/ruby-tap-that/
http://blog.moertel.com/posts/2007-02-07-ruby-1-9-gets-handy-new-method-object-tap.html
This idea goes back to Smalltalk, and is essentially just a
pipeline. Hardly something weird.
http://en.wikipedia.org/wiki/Method_chaining
(Technically, if the methods return self rather than None, it's method
cascading. Chaining is any sequence of method calls, whether they return
self or something else.)
Dart includes syntax for method cascades, which I'm told looks like
this:
x = SomeClass()
..spam()
..eggs()
..cheese()
The technique also comes with the blessing of Martin Fowler, where it
is an important part of fluent interfaces:
http://martinfowler.com/bliki/FluentInterface.html
Quote:
"The common convention in the curly brace world is that modifier
methods are void, which I like because it follows the principle
of CommandQuerySeparation. This convention does get in the way of
a fluent interface, so I'm inclined to suspend the convention for
this case."
And last but not least, if you want to go all the way down to the lambda
calculus and combinator theory, my "selfie" adapter function is just a
form of the K-combinator (a.k.a. the Kestrel). So there's a deep and
powerful mathematical pedigree to the idea, if that matters.
--
Steven
[toc] | [prev] | [next] | [standalone]
| From | Wolfgang Maier <wolfgang.maier@biologie.uni-freiburg.de> |
|---|---|
| Date | 2013-11-22 16:52 +0000 |
| Message-ID | <mailman.3048.1385139196.18130.python-list@python.org> |
| In reply to | #60239 |
Steven D'Aprano <steve+comp.lang.python <at> pearwood.info> writes: > # With chaining > thing = func(MyClass().spam().eggs().cheese(), > MyClass().aardvark(), > OtherClass().fe().fi().fo().fum(), > ) > do_stuff_with(thing) > > versus: > > # Without chaining > temp1 = MyClass() > temp1.spam() > temp1.eggs() > temp1.cheese() > temp2 = MyClass() > temp2.aardvark() > temp3 = OtherClass() > temp3.fe() > temp3.fi() > temp3.fo() > temp3.fum() > thing = func(temp1, temp2, temp3) > do_stuff_with(thing) > Another use case might be in comprehensions and generator expressions ?? thing = [MyClass().spam().eggs(x).cheese() for x in sequence] where you can't use all those temporary assignments. Best, Wolfgang
[toc] | [prev] | [next] | [standalone]
| From | Wolfgang Maier <wolfgang.maier@biologie.uni-freiburg.de> |
|---|---|
| Date | 2013-11-22 17:55 +0000 |
| Message-ID | <mailman.3051.1385142973.18130.python-list@python.org> |
| In reply to | #60239 |
Wolfgang Maier <wolfgang.maier <at> biologie.uni-freiburg.de> writes:
>
> Steven D'Aprano <steve+comp.lang.python <at> pearwood.info> writes:
>
> > # With chaining
> > thing = func(MyClass().spam().eggs().cheese(),
> > MyClass().aardvark(),
> > OtherClass().fe().fi().fo().fum(),
> > )
> > do_stuff_with(thing)
> >
> > versus:
> >
> > # Without chaining
> > temp1 = MyClass()
> > temp1.spam()
> > temp1.eggs()
> > temp1.cheese()
> > temp2 = MyClass()
> > temp2.aardvark()
> > temp3 = OtherClass()
> > temp3.fe()
> > temp3.fi()
> > temp3.fo()
> > temp3.fum()
> > thing = func(temp1, temp2, temp3)
> > do_stuff_with(thing)
> >
>
> Another use case might be in comprehensions and generator expressions ??
>
> thing = [MyClass().spam().eggs(x).cheese() for x in sequence]
>
> where you can't use all those temporary assignments.
>
> Best,
> Wolfgang
>
>
Thinking about this, you could define __call__ for your chained class to
return the wrapped object:
class chained:
def __init__(self, obj):
self.obj = obj
def __repr__(self):
return repr(self.obj)
def __getattr__(self, name):
obj = getattr(self.obj, name)
if callable(obj):
def selfie(*args, **kw):
# Call the method just for side-effects, return self.
_ = obj(*args, **kw)
return self
return selfie
else:
return obj
def __call__(self):
return self.obj
This would encourage its localized use on the fly as in:
thing = [MyClass().spam().eggs(x).cheese()() for x in sequence]
where the intention probably would be to generate a list of lists (or other
mutable objects), not of objects of class chained. Localizing the use of
chained also seems like a good idea to me as I do share Peter's worries
about coding style, while I also agree with you that such a class comes in
handy from time to time.
[toc] | [prev] | [next] | [standalone]
| From | Antoon Pardon <antoon.pardon@rece.vub.ac.be> |
|---|---|
| Date | 2013-11-22 16:38 +0100 |
| Message-ID | <mailman.3045.1385134715.18130.python-list@python.org> |
| In reply to | #60228 |
Op 22-11-13 16:20, Peter Otten schreef: > Steven D'Aprano wrote: > >> On Fri, 22 Nov 2013 13:08:03 +0100, Peter Otten wrote: >> >>> These things are nice to write as long as you omit the gory details, but >>> personally I don't want to see the style it favours in my or other >>> people's code. >> >> There's not really a lot of difference > > That cuts both ways ;) > >> between: >> >> obj = MyClass() >> obj.spam() >> obj.eggs() >> obj.cheese() >> >> and >> >> obj = MyClass().spam().eggs().cheese() >> >> >> except the first takes up a lot more vertical space. > > I've not yet run short of vertical space ;) Really? Then you must write only very short programs. Me I continuously run out of vertical space. That is why I need to use such tools as scroll bars. -- Antoon Pardon.
[toc] | [prev] | [next] | [standalone]
| From | Laszlo Nagy <gandalf@shopzeus.com> |
|---|---|
| Date | 2013-11-23 17:08 +0100 |
| Message-ID | <mailman.3091.1385222894.18130.python-list@python.org> |
| In reply to | #60228 |
> > OK, that one is disgusting... > > Anyway, I'd like to see a sequence of method names taken from actual code > that profits from this chaining pattern. > Actually, wx.lib.agw uses this a lot. Especially for AuiPaneInfo: http://www.wxpython.org/docs/api/wx.aui.AuiPaneInfo-class.html All right, this is a C++ proxy. But still, it is actively used by many. -- This message has been scanned for viruses and dangerous content by MailScanner, and is believed to be clean.
[toc] | [prev] | [next] | [standalone]
| From | Terry Reedy <tjreedy@udel.edu> |
|---|---|
| Date | 2013-11-22 07:34 -0500 |
| Message-ID | <mailman.3036.1385123714.18130.python-list@python.org> |
| In reply to | #60213 |
On 11/22/2013 6:26 AM, Steven D'Aprano wrote: > A frequently missed feature is the ability to chain method calls: > > x = [] > x.append(1).append(2).append(3).reverse().append(4) > => x now equals [3, 2, 1, 4] > > > This doesn't work with lists, as the methods return None True for the 7 pure mutation methods but not for .copy, .count, .index, and .pop. The last both mutates and returns. -- Terry Jan Reedy
[toc] | [prev] | [next] | [standalone]
| From | Steven D'Aprano <steve+comp.lang.python@pearwood.info> |
|---|---|
| Date | 2013-11-22 14:30 +0000 |
| Message-ID | <528f6a7f$0$29992$c3e8da3$5496439d@news.astraweb.com> |
| In reply to | #60217 |
On Fri, 22 Nov 2013 07:34:53 -0500, Terry Reedy wrote: > On 11/22/2013 6:26 AM, Steven D'Aprano wrote: >> A frequently missed feature is the ability to chain method calls: >> >> x = [] >> x.append(1).append(2).append(3).reverse().append(4) => x now equals [3, >> 2, 1, 4] >> >> >> This doesn't work with lists, as the methods return None > > True for the 7 pure mutation methods but not for .copy, .count, .index, > and .pop. The last both mutates and returns. Yes, that is correct. In this case, the assumption behind the chained adapter is that we don't care about the results of calling those methods, we only care about the mutation they cause. If that's not the case, then chained() isn't for us. -- Steven
[toc] | [prev] | [next] | [standalone]
| From | Rotwang <sg552@hotmail.co.uk> |
|---|---|
| Date | 2013-11-23 19:53 +0000 |
| Message-ID | <l6r13t$ioa$1@dont-email.me> |
| In reply to | #60213 |
On 22/11/2013 11:26, Steven D'Aprano wrote:
> A frequently missed feature is the ability to chain method calls:
>
> x = []
> x.append(1).append(2).append(3).reverse().append(4)
> => x now equals [3, 2, 1, 4]
>
>
> This doesn't work with lists, as the methods return None rather than
> self. The class needs to be designed with method chaining in mind before
> it will work, and most Python classes follow the lead of built-ins like
> list and have mutator methods return None rather than self.
>
> Here's a proof-of-concept recipe to adapt any object so that it can be
> used for chaining method calls:
>
>
> class chained:
> def __init__(self, obj):
> self.obj = obj
> def __repr__(self):
> return repr(self.obj)
> def __getattr__(self, name):
> obj = getattr(self.obj, name)
> if callable(obj):
> def selfie(*args, **kw):
> # Call the method just for side-effects, return self.
> _ = obj(*args, **kw)
> return self
> return selfie
> else:
> return obj
>
>
> 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. How about
something like this:
def getr(self, name):
obj = super(type(self), self).__getattribute__(name)
if callable(obj):
def selfie(*args, **kwargs):
result = obj(*args, **kwargs)
return self if result is None else result
return selfie
return obj
class chained(type):
typedict = {}
def __new__(cls, obj):
if type(obj) not in cls.typedict:
cls.typedict[type(obj)] = type.__new__(
cls, 'chained%s' % type(obj).__name__,
(type(obj),), {'__getattribute__': getr})
return cls.typedict[type(obj)](obj)
# In the interactive interpreter:
>>> d = chained({}).update({1: 2}).update({3: 4})
>>> d
{1: 2, 3: 4}
>>> type(d)
<class '__main__.chaineddict'>
>>> isinstance(d, dict)
True
The above code isn't very good - it will only work on types whose
constructor will copy an instance, and it discards the original. And its
dir() is useless. Can anyone suggest something better?
[toc] | [prev] | [next] | [standalone]
| From | Rotwang <sg552@hotmail.co.uk> |
|---|---|
| Date | 2013-11-24 00:28 +0000 |
| Message-ID | <l6rh76$hj9$1@dont-email.me> |
| In reply to | #60318 |
On 23/11/2013 19:53, Rotwang wrote:
> [...]
>
> 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. How about
> something like this:
>
> [crap code]
>
> The above code isn't very good - it will only work on types whose
> constructor will copy an instance, and it discards the original. And its
> dir() is useless. Can anyone suggest something better?
Here's another attempt:
class dummy:
pass
def initr(self, obj):
super(type(self), self).__setattr__('__obj', obj)
def getr(self, name):
try:
return super(type(self), self).__getattribute__(name)
except AttributeError:
return getattr(self.__obj, name)
def methr(method):
def selfie(self, *args, **kwargs):
result = method(self.__obj, *args, **kwargs)
return self if result is None else result
return selfie
class chained(type):
typedict = {}
def __new__(cls, 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)
This solves some of the problems in my earlier effort. It keeps a copy
of the original object, while leaving its interface pretty much
unchanged; e.g. repr does what it's supposed to, and getting or setting
an attribute of the chained object gets or sets the corresponding
attribute of the original. It won't work on classes with properties,
though, nor on classes with callable attributes that aren't methods (for
example, a class with an attribute which is another class).
[toc] | [prev] | [next] | [standalone]
| From | Rotwang <sg552@hotmail.co.uk> |
|---|---|
| Date | 2013-11-24 00:43 +0000 |
| Message-ID | <l6ri47$ljb$1@dont-email.me> |
| In reply to | #60342 |
On 24/11/2013 00:28, Rotwang wrote: > [...] > > This solves some of the problems in my earlier effort. It keeps a copy > of the original object, Sorry, I meant that it keeps a reference to the original object.
[toc] | [prev] | [next] | [standalone]
| From | Steven D'Aprano <steve+comp.lang.python@pearwood.info> |
|---|---|
| Date | 2013-11-24 14:27 +0000 |
| Message-ID | <52920cde$0$29993$c3e8da3$5496439d@news.astraweb.com> |
| In reply to | #60318 |
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? 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. -- Steven
[toc] | [prev] | [next] | [standalone]
| From | Rotwang <sg552@hotmail.co.uk> |
|---|---|
| Date | 2013-11-24 15:01 +0000 |
| Message-ID | <l6t4d3$fvs$1@dont-email.me> |
| In reply to | #60375 |
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)
[toc] | [prev] | [standalone]
Back to top | Article view | comp.lang.python
csiph-web