0

I'm looking into generating a Python class dynamically from data. (The purpose is to let users specify some software tests in a simple file, without knowing any Python). I have run into an effect I did not expect; as a toy example to quickly check that I can create methods according to a naming scheme I did the following:

import unittest

attrdict = {}
for i in range(3):
    attrdict["test%s"%i]= types.MethodType(lambda self: i)
attrdict["runTest"]=lambda self: [eval("self.test%s()"%i) for i in range(3)]
dynTC = type('dynTC', (unittest.TestCase,), attrdict )

now when I execute

dynTC().runTest()

... I would expect

[0,1,2] 

as output, but the actual result is

[2,2,2]

I would expect the lambda definitions to bind a deep copy of the loop index, since it is just a number rather than a more complex structure, but clearly there's stuff going on here that I don't understand.

I have a feeling this may be a common 'gotcha' for new Python programmers, but all the terms I can think of to describe the problem are so generic that my searches only return a deluge of unrelated answers.

Could you please explain to me what is happening here instead of what I expected, and preferably also what I should have done to create the desired effect of several /different/ methods.

mirari
  • 349
  • 1
  • 3
  • 8
  • The call to types.MethodType( ... ) was actually not part of the initial test and as it stands that call lacks parameters. Sorry! I pasted a slightly different stage of the workflow than I intended. Since it has already been quoted in the answer and did not hurt the main point of the question I'll just leave it as it is. – mirari Jun 28 '13 at 08:37

1 Answers1

1

The problem is with this line...

attrdict["test%s"%i]= types.MethodType(lambda self: i)

When you define a lambda that references a variable which isn't one of its arguments, the variable will be resolved from the scope in which the lambda was defined at the point when it's actually called, so you'll always get whatever the current value of i is, rather than the value of i at the point when you defined the lambda.

In your case, the value of i will end up as 2 after the for i in range(3) loop completes, so you need to create a closure to bind i to a specific value when creating the lambda, by changing the line to...

attrdict["test%s"%i]= types.MethodType(lambda self, i=i: i)
Aya
  • 39,884
  • 6
  • 55
  • 55
  • 1
    "the variable will be resolved from the calling scope when the lambda is called" Not true. The scope where it is called might not have a variable `i` or might have a different one. Instead, it will always refer to the variable from the original scope. – newacct Jun 28 '13 at 01:30
  • That gives me pretty good idea of what's going on and the terminology to search effectively for more. Thank you! – mirari Jun 28 '13 at 08:31
  • @newacct Good point. Updated answer. – Aya Jun 28 '13 at 12:38