4

I came across an interesting issue while trying to achieve dynamic sort. Given the following code:

>>> l = []
>>> for i in range(2):
>>>     def f():
>>>         return f.v
>>>     f.v = i
>>>     l.append(f)

You have to be careful about how to use the functions in l:

>>> l[0]()
1
>>> l[1]()
1
>>> [h() for h in l]
[1, 1]
>>> [f() for f in l]
[0, 1]
>>> f = l[0]
>>> f()
0
>>> k = l[1]
>>> k()
0
>>> f = l[1]
>>> k()
1
>>> del f
>>> k()
NameError: global name 'f' is not defined

The behavior of the function depends on what f currently is.

What should I do to avoid this issue? How can I set a function attribute that does not depends on the function's name?

Update

Reading your comments and answers, here is my actual problem.

I have some data that I want to sort according to user input (so I don't know sorting criteria in advance). User can choose on which part of the data to apply successive sorts, and these sorts can be ascending or descending.

So my first try was to loop over the user inputs, define a function for each criterion, store this function in a list and then use this list for sorted's key like this: key=lambda x: [f(x) for f in functions]. To avoid multiplying conditions into functions themselves, I was computing some needed values before the function definition and binding them to the function (different functions with different pre-computed values).

While debugging, I understood that function attribute was not the solution here, so I indeed wrote a class with a __call__ method.

1
  • First we should address the issue of why do this in the first place? Commented Feb 10, 2016 at 14:56

4 Answers 4

5

The issue is due to the fact that return f.v loads the global f, and not the one you intend.1 You can see this by disassembling the code:

>>> dis.dis(l[0])
  3           0 LOAD_GLOBAL              0 (f)
              3 LOAD_ATTR                1 (v)
              6 RETURN_VALUE

After the loop that populates l, f is a reference to the last closure created, as you can see here:

>>> l
[<function f at 0x02594170>, <function f at 0x02594130>]
>>> f
<function f at 0x02594130>

Thus, when you call l[0](), it still loads the f that points to the last function created, and it returns 1. When you redefined f by doing f = l[0], then the global f now points to the first function.

What you seem to want is a function that has a state, which really is a class. You could therefore do something like this:

class MyFunction:
  def __init__(self, v):
    self.v = v
  def __call__(self):
    return self.v

l = [MyFunction(i) for i in range(2)]

l[0]() # 0
l[1]() # 1

Though it may be a good idea to explain your actual problem first, as there might be a better solution.

1: Why doesn't it load the global f and not the current instance, you may ask?

Recall that when you create a class, you need to pass a self argument, like so:

# ...
def my_method(self):
  return self.value

self is actually a reference to the current instance of your object. That's how Python knows where to load the attribute value. It knows it has to look into the instance referenced by self. So when you do:

a.value = 1
a.my_method()

self is now a reference to a.

So when you do:

def f():
  return f.v

There's no way for Python to know what f actually is. It's not a parameter, so it has to load it from elsewhere. In your case, it's loaded from the global variables.
Thus, when you do f.v = i, while you do set an attribute v for the instance of f, there's no way to know which instance you are referring to in the body of your function.

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

Comments

1

Note that what you are doing here:

def f():
    return f.v

is not making a function which returns whatever its own v attribute is. It's returning whatever the f object's v attribute is. So it necessarily depends on the value of f. It's not that your v attribute "depends on the function's name". It really has nothing at all to do with the function's name.

Later, when you do

>>> f = l[0]
>>> k = l[1]
>>> k()
0

What you have done is bound k to the function at l[1]. When you call it, you of course get f.v, because that's what the function does.

But notice:

>>> k.v
1
>>> [h.v for h in l]
[0, 1]

So, a function is an object, and just like most objects, it can have attributes assigned to it (which you can access using dot notation, or the getattr() function, or inspecting the object's dictionary, etc.). But a function is not designed to access its own attributes from within its own code. For that, you want to use a class (as demonstrated by @VincentSavard).

In your particular case, the effect you seem to be after doesn't really need an "attribute" per se; you are apparently looking for a closure. You can implement a closure using a class, but a lighter-weight way is a nested function (one form of which is demonstrated by @TomKarzes; you could also use a named inner function instead of lambda).

1 Comment

Thanks for pointing out [h.v for h in l]. What I will remember is "a function is not designed to access its own attributes".
0

Try this:

l = []
for i in range(2):
    def f(n):
        return lambda: n
    l.append(f(i))

This doesn't use attributes, but creates a closure for each value of i. The value of n is then locked once f returns. Here's some sample output:

>>> [f() for f in l]
[0, 1]

Comments

0

As others said, return f.v looks for f name in the current scope which is equal to the last defined function.

To work around this you can simulate functions:

>>> class Function(object):
...     def __init__(self, return_value):
...         self.return_value = return_value
...     def __call__(self):
...         return self.return_value
...     

>>> l = []

>>> for i in range(2):
...     l.append(Function(i))
...     

>>> l[0]()
>>> 0

>>> l[1]()
>>> 1

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.