Welcome, guest | Sign In | My Account | Store | Cart

Mutating list methods such as 'append' or 'extend' return None instead of the (mutated) list itself. Sometimes, this is not the desired behaviour. The <a href="http://groups.google.de/groups?dq=&hl=de&lr=&ie=UTF-8&oe=UTF-8&threadm=I706b.17723%24hE5.626547%40news1.tin.it&prev=/groups%3Fdq%3D%26num%3D25%26hl%3Dde%26lr%3D%26ie%3DUTF-8%26oe%3DUTF-8%26group%3Dcomp.lang.python%26start%3D75"> Discussion</a> on comp.lang.python resulted in the following solution. The shown code is my own, while two other solutions are presented in the discussion.

Python, 28 lines
def wrapedmeth(classname,meth):
    def _meth(self,*argl,**argd):
        return self

    return _meth

class ReturnMeta(type):
    def __new__(cls,classname,bases,classdict):
        wrap = classdict.get('return_self_super_methods')
        if wrap is not None:
            for method in wrap:
                classdict[method] = wrapedmeth(classname,method)
        return super(ReturnMeta,cls).__new__(cls,classname,bases,classdict)

class mylist(list):
    __metaclass__ = ReturnMeta
    return_self_super_methods = ['append','extend','insert','remove','reverse','sort']

if __name__ == '__main__':
    print 'l = [1,2]'
    print 'mylist: print l.append(3)'
    l = mylist([1,2])
    print l.append(3)
    print 'list: print l.append(3)'
    l = [1,2]
    print l.append(3)

To have a reference to the (mutated) list returned is usefull, if one wants to chain commands such as mylistinstance.append(7).sort(). GvR (and others) took the decision not to allow this because it could be confusing programmers who thought that such methods would return a new object. This leads to problems where the sideeffects are desired. Expressions inside lambda functions and list comprehension comes to mind.

During the news list dicussion, two alternative implementations were presented. The first one is from Jacek Generowicz and doesn't involve meta classes: <pre> class returner:

def __init__(self, object):
    self.object = object

def __getattr__(self, name):
    def proxy(*args, **kwds):
        getattr(self.object, name)(*args, **kwds)
        return self.object
    return proxy

lst = [1,2,3] print lst.append(4) # Here you get None print returner(lst).append(5) # Here you get the modified list. </pre>

The second one is from Alex Martelli and is (as far as I can see) virtually the same than my own solution (Alex was answering Jacek and didn't know the original solution)

<pre> def wrapReturning(func): def returningWrapper(args, *kwds): func(args, *kwds) return args[0] return returningWrapper

class metaReturner(type): ''' simplified metaReturner: deal with single inheritance only '''

def __new__(mcl, className, classBases, classDict):

    # get the "real" base class, then wrap its mutators
    for base in classBases:
        if not isinstance(base, metaReturner):
            for mutator in classDict['__mutators__']:
                classDict[mutator] = wrapReturning(getattr(base,

mutator)) break

    # delegate the rest to built-in 'type'
    return type.__new__(mcl, className, classBases, classDict)

class Returner: __metaclass__ = metaReturner

example usage

class returnerlist(Returner, list): __mutators__ = 'sort reverse append extend insert'.split()

print returnerlist('hello').extend('ciao').sort().reverse() </pre>

I think that the metaclass approach is more flexible than the 'traditional' one, but requires more classmagic and is probably not as understandable.

Both metaclass solutions have the problem, that these methods can not be overwritten. For example, if I would define my own 'append' method in 'mylist' (and had forgotten what the metaclass is doing), it would be silently ignored and this could lead to a very frustating programming experience.


andy mckay 18 years, 2 months ago  # | flag

Typo. There is a NameError on meth, I believe line 13 should read:

classdict[method] = wrapedmeth(classname,method)
Stephan Diehl (author) 18 years, 2 months ago  # | flag

Corrected. Yes, looks like a typo (I'm sure, I did test the code before posting :-) It's corrected now.

Runsun Pan 17 years ago  # | flag

another shortcut (Runsun Pan).

Returning a modified list can be achieved with fewer code using
the "x or y" logic:

aList.method(arg) or aList

>>> L = range(5)
>>> L
[0, 1, 2, 3, 4]
>>> L.insert(3,10)
>>> L
[0, 1, 2, 10, 3, 4]
>>> L.append(-10)
>>> L
[0, 1, 2, 10, 3, 4, -10]
>>> L.extend([3,4,5])
>>> L
[0, 1, 2, 10, 3, 4, -10, 3, 4, 5]
>>> L.sort()
>>> L
[-10, 0, 1, 2, 3, 3, 4, 4, 5, 10]

The above procedures can be achieved in one line of code without
involving any additional function:

>>> m = range(5)
>>> (((m.insert(3,10) or m).append(-10) or m).extend([3,4,5]) or m).sort() or m
[-10, 0, 1, 2, 3, 3, 4, 4, 5, 10]

Certainly, this is a lot more difficult to read. However, in
many cases, especially you perform only one or two operations
(but not a lot series like the example above):

m.sort() or m
(m.sort() or m).append(5) or m

this approach looks more handy.
Created by Stephan Diehl on Sun, 7 Sep 2003 (PSF)
Python recipes (4591)
Stephan Diehl's recipes (5)

Required Modules

  • (none specified)

Other Information and Tasks