0

in order to automatically generate parameterized tests, I am trying to add methods to a class in by freezing some parameters of an existing method. Here is the piece of Python 3 code

class A:
    def f(self, n):
        print(n)

params = range(10)

for i in params:
    name = 'f{0}'.format(i)
    method = lambda self: A.f(self, i)
    setattr(A, name, method)

However, the following lines then produce rather disappointing output

a = A()
a.f0()

prints "9" (instead of "0"). I must be doing something wrong, but I can't see what. Can you help ?

Thanks a lot


Edit: this question is indeed a duplicate. I would like to acknowledge the quality of all comments, which go much deeper than the raw answer.

2
  • 1
    for such things in tests try mock Commented Feb 7, 2014 at 16:33
  • @warwaruk: after looking at the quick start guide, I am not sure how mock can help... but I am quite happy to learn about this package. Might prove useful at some point. Commented Feb 7, 2014 at 17:21

2 Answers 2

2

Try

method = lambda self, i=i: A.f(self, i)

because otherwise when you call the method i's value may have changed

Sign up to request clarification or add additional context in comments.

10 Comments

This is related to variable scopes. lambda is a simple function with body of A.f(self, i). When you call a method with this body, i's value is requested. It is taken from outer scope. Search SO and google for closures
@Sebastien It's due to the fact that, without the =i part, the value of i will not be determined until the lambda function is executed (in which case it goes with the most recent value assigned to i, which after your loop will be 9). This is true for all Python functions, in fact, and is the reason why you can have issues when referring to nonlocal variables (but is also the reason you can declare a function before declaration of the nonlocal value it uses). There's quite a bit more to scope in Python than that, but that's how it basically works.
@Sebastien Python variables are actually very similar to Java variables (there are some oddities around primitives not being objects, but nothing that affects common operations). It's languages like C, C++ which differ significantly from Python and Java. By the way, Java sidesteps the moral equivalent of this problem (with an inner class instead of a lambda/closure) by requiring i to be final, i.e. non-modifiable.
@Sebastien Scope isn't really the issue here, time of evaluation is. In fact, without this behavior, it would be a lot harder to write closures in Python. (Note that Python [3] actually defaults to final-like behavior for all nonlocal variables; you can modify attributes of the variables, just as you can with non-final members of final objects in Java, but you cannot assign a different object to the nonlocal variable without first declaring the variable as nonlocal inside the closure.)
There's also global, which acts similarly to nonlocal but for global/module-level variables. It also has the difference that, while the statement nonlocal x requires x to exist in lexical scope when the function is created, the statement global x does not (but if you don't create the variable at execution time of the module, all uses of that variable in functions will require the usage of global x even if you don't assign anything to it in those others; while this pattern does have some uses [I remember using it with ply/llvmpy], it's not something you'll want to do regularly).
|
1

The best way to "freeze" parameters in Python is to use functools.partial. It's roughly equivalent to warwaruk's lambda version, but if you have a function with lots of arguments yet only want to freeze one or two of them (or if you only know certain arguments and don't care about the rest) using partial is more elegant as you only specify the arguments you want to freeze rather than having to repeat the whole function signature in the lambda.

An example for your program:

class A:
    def f(self, n):
        print(n)

from functools import partial

for i in range(10): # params
    setattr(A, 'f{0}'.format(i), partial(A.f, n=i))

Depending on which version of Python 3 you're using, you may not need to include the 0 in the string format placeholder; starting with 3.1, iirc, it should be automatically substituted.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.