1

I have the following code:

def function_reader(path):
    line_no = 0 
    with open(path, "r") as myfile:
        def readline():
            line_no +=1
            return myfile.readline()

Python keeps returning:

UnboundLocalError: local variable 'line_no' referenced before assignment

when executing line_no +=1.

I understand that the problem is that nested function declarations have weird scoping in python (though I do not understand why it was programmed this way). I'm mostly wondering if there is a simple way to help python resolve the reference, since I really like the functionality this would provide.

5
  • It's working in Python 3.4 Commented Jan 16, 2015 at 2:49
  • 2
    @sacma: Did you actually call readline()? Commented Jan 16, 2015 at 2:52
  • 1
    No, this isn’t possible (at all!) in Python 2. In Python 3, it’s easy: nonlocal line_no. (Use Python 3 if you can.) Commented Jan 16, 2015 at 2:53
  • This problem is actually documented in the FAQ, but alas, you need to use the nonlocal keyword that wasn't introduced until Python 3. Commented Jan 16, 2015 at 2:56
  • And what exactly you're trying to do here? I'd use a class here. Commented Jan 16, 2015 at 3:06

3 Answers 3

3

Unfortunately, there is not a way to do this in Python 2.x. Nested functions can only read names in the enclosing function, not reassign them.

One workaround would be to make line_no a list and then alter its single item:

def function_reader(path):
    line_no = [0]
    with open(path, "r") as myfile:
        def readline():
            line_no[0] += 1
            return myfile.readline()

You would then access the line number via line_no[0]. Below is a demonstration:

>>> def outer():
...     data = [0]
...     def inner():
...        data[0] += 1
...     inner()
...     return data[0]
...
>>> outer()
1
>>>

This solution works because we are not reassigning the name line_no, only mutating the object that it references.


Note that in Python 3.x, this problem would be easily solved using the nonlocal statement:

def function_reader(path):
    line_no = 0
    with open(path, "r") as myfile:
        def readline():
            nonlocal line_no
            line_no += 1
            return myfile.readline()
Sign up to request clarification or add additional context in comments.

2 Comments

nonlocal actually equal to global in some cases right? Or they are doing the same thing in some points. The difference is global variables can changeable by outside of function, nonlocal not. Am I right?
Yes. global is for names in the global namespace (outside all functions) where as nonlocal is for names in enclosing functions. Both are scope declarations through.
1

It's hard to say what you're trying to achieve here by using closures. But the problem is that with this approach either you'll end with an ValueError: I/O operation on closed file when you return readline from the outer function or just the first line if you return readline() from the outer function.

If all you wanted to do is call readline() repeatedly or loop over the file and also remember the current line number then better use a class:

class FileReader(object):
    def __init__(self, path):
        self.line_no = 0
        self.file = open(path)

    def __enter__(self):
        return self

    def __iter__(self):
       return self

    def next(self):
        line = next(self.file)
        self.line_no += 1
        return line

    def readline(self):
        return next(self)

    def __exit__(self, *args):
        self.file.close()

Usage:

with FileReader('file.txt') as f:
    print next(f)
    print next(f)
    print f.readline()
    print f.line_no # prints 3
    for _ in xrange(3):
        print f.readline() 
    print f.line_no # prints 6
    for line in f:
        print line
        break
    print f.line_no # prints 7

1 Comment

A class is overkill here since the file itself can iterate linewise. For keeping track of the current line number, all you need is enumerate.
0

The more Pythonic way to get the next line and keep track of the line number is with the enumerate builtin:

with open(path, "r") as my file:
    for no, line in enumerate(myfile, start=1):
        # process line

This will work in all current Python versions.

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.