1

I have a rather complex base class for some sqlalchemy models and I want to create a rails style setters but, since i'm fairly new to python, I'm stuck with a problem that I cant seem to bypass. I create the setters on the new method so I can have the triggered both on new instances and on queries but no matter what setter I define and execute it always picks the last setter to execute. An example serves better:

class Test(object):

    columns = ['email', 'username']

    def __new__( cls, *args, **kwargs ):
        for column in cls.columns:
            setattr( cls, "set%s" % column.capitalize(), lambda cls, v: cls.setAttribute( cls, column, v ) )
        return super( Test, cls ).__new__( cls, *args, **kwargs )

    @staticmethod
    def setAttribute(cls, attribute, value):
        print "Setting attribute %s with value %s" % ( attribute, value )
        setattr( cls, attribute, value )

test = Test()
test.setEmail('test@test.com')

As you can see I'm setting the email but, when executed, the code tries to set the username which is the last column. Any idea why is that?

Romeo M.
  • 3,218
  • 8
  • 35
  • 40
  • You need to learn about [closures](http://stackoverflow.com/questions/233673/lexical-closures-in-python) – JBernardo Jun 10 '12 at 17:26
  • My code was perfectly valid. I just forgot to add the column as a parameter. Will take a look at closures now. – Romeo M. Jun 10 '12 at 17:43
  • Not related to the problem, but instead of overriding `__new__`, you should use a metaclass or a class decorator. – ecatmur Jun 10 '12 at 19:37

1 Answers1

2

This happens because your lambda function references column but doesn't pass it in as an argument:

lambda cls, v: cls.setAttribute( cls, column, v )

When this function is executed, it will look for the name column in a containing or global scope, and always find the value 'username' because that is what column was set to last.

Here is a straightforward way to fix this using a default argument value:

    def __new__( cls, *args, **kwargs ):
        for column in cls.columns:
            setattr( cls, "set%s" % column.capitalize(), lambda cls, v, column=column: cls.setAttribute( cls, column, v ) )
        return super( Test, cls ).__new__( cls, *args, **kwargs )

Another alternative would be to use a closure (in a way, the mutable default argument is a type of closure):

    def __new__( cls, *args, **kwargs ):
        def make_setter(column):
            return lambda cls, v: cls.setAttribute( cls, column, v )
        for column in cls.columns:
            setattr( cls, "set%s" % column.capitalize(), make_setter(column))
        return super( Test, cls ).__new__( cls, *args, **kwargs )
Andrew Clark
  • 202,379
  • 35
  • 273
  • 306