78

I have some code like:

def example():
    # other logic omitted

    stored_blocks = {}
    def replace_blocks(m):
        block = m.group(0)
        block_hash = sha1(block)
        stored_blocks[block_hash] = block
        return '{{{%s}}}' % block_hash

    num_converted = 0
    def convert_variables(m):
        name = m.group(1)
        num_converted += 1
        return '<%%= %s %%>' % name

    fixed = MATCH_DECLARE_NEW.sub('', template)
    fixed = MATCH_PYTHON_BLOCK.sub(replace_blocks, fixed)
    fixed = MATCH_FORMAT.sub(convert_variables, fixed)

    # more logic...

Adding elements to stored_blocks works fine, but I cannot increase num_converted in the second nested function. I get an exception that says UnboundLocalError: local variable 'num_converted' referenced before assignment.

I know that in 3.x, I could try nonlocal num_converted, but how can I solve the problem in 2.x? I don't want to use a global variable for this.

Karl Knechtel
  • 62,466
  • 11
  • 102
  • 153
ThiefMaster
  • 310,957
  • 84
  • 592
  • 636
  • 4
    Contrary to somewhat popular belief (judging by this kind of questions) `def` is not the only keyword that defines a namespace: there is also `class`. – Jochen Ritzel Jan 31 '11 at 14:22

6 Answers6

88

Problem: This is because Python's scoping rules are demented. The presence of the += assignment operator marks the target, num_converted, as local to the enclosing function's scope, and there is no sound way in Python 2.x to access just one scoping level out from there. Only the global keyword can lift variable references out of the current scope, and it takes you straight to the top.

Fix: Turn num_converted into a single-element array.

num_converted = [0]
def convert_variables(m):
    name = m.group(1)
    num_converted[0] += 1
    return '<%%= %s %%>' % name
th3an0maly
  • 3,360
  • 8
  • 33
  • 54
Marcelo Cantos
  • 181,030
  • 38
  • 327
  • 365
  • 7
    Can you explain why that is necessary? I would have expected to OPs code to work. – Björn Pollex Jan 31 '11 at 13:47
  • 36
    Because Python's scoping rules are demented. The presence of the `+=` assignment operator marks the target, `num_converted`, as local to the enclosing function's scope, and there is no sound way in Python 2.x to access just one scoping level out from there. Only the `global` keyword can lift variable references out of the current scope, and it takes you straight to the top. – Marcelo Cantos Jan 31 '11 at 13:51
  • 6
    This is not clever, this is actually pretty bad code. There are classes (see comment under the question). This version uses a global variable which you should always avoid. Not using `global` doesn't mean you don't have a global variable. – schlamar Jul 19 '12 at 12:16
  • 15
    @schlamar: The variable in question isn't global in any sense of the word. The OP's opening sentence stipulates that the entire block of code they presented is inside a function. – Marcelo Cantos Jul 04 '13 at 03:32
  • @MarceloCantos Oh, you are right :) However, a class would still be more appropriate in this case. Or an explicit namespace object like answered [here](http://stackoverflow.com/a/3190786/851737), so it is clear what's going on. – schlamar Jul 04 '13 at 09:18
  • 2
    @schlamar: That's a lot of scaffolding for one variable. If you need more clarity than my answer provides (really?), I'd favour `scope = {'num_converted':0} … scope['num_converted'] += 1`. – Marcelo Cantos Jul 04 '13 at 09:28
29

(see below for the edited answer)

You can use something like:

def convert_variables(m):
    name = m.group(1)
    convert_variables.num_converted += 1
    return '<%%= %s %%>' % name

convert_variables.num_converted = 0

This way, num_converted works as a C-like "static" variable of the convert_variable method


(edited)

def convert_variables(m):
    name = m.group(1)
    convert_variables.num_converted = convert_variables.__dict__.get("num_converted", 0) + 1
    return '<%%= %s %%>' % name

This way, you don't need to initialize the counter in the main procedure.

PabloG
  • 25,761
  • 10
  • 46
  • 59
  • 3
    Right. And note that you _must_ create the attribute `convert_variables.num_converted` _after_ defining the function, though it looks strange to do so. – Marc van Leeuwen Sep 21 '13 at 17:51
  • @PabloG most satisfying answer to this question, apart from nonlocal in 3.x; using mutable type [] is cheap workaround. – user2290820 Oct 17 '13 at 21:15
9

Using the global keyword is fine. If you write:

num_converted = 0
def convert_variables(m):
    global num_converted
    name = m.group(1)
    num_converted += 1
    return '<%%= %s %%>' % name

... num_converted doesn't become a "global variable" (i.e. it doesn't become visible in any other unexpected places), it just means it can be modified inside convert_variables. That seems to be exactly what you want.

To put it another way, num_converted is already a global variable. All the global num_converted syntax does is tell Python "inside this function, don't create a local num_converted variable, instead, use the existing global one.

Emile
  • 2,946
  • 2
  • 19
  • 22
  • 4
    `global` in 2.x works pretty much how `nonlocal` does in 3.x. – Daniel Roseman Jan 31 '11 at 13:49
  • 2
    "To put it another way, num_converted is already a global variable" - my code is running inside a function.. so it's currently not global. – ThiefMaster Jan 31 '11 at 13:53
  • 2
    Ah, I haden't paid attention to the "inside a function" part, sorry - in that case, Marcelo's length-one-list may be a better (but ugly) solution. – Emile Jan 31 '11 at 13:55
7

What about using a class instance to hold the state? You instantiate a class and pass instance methods to subs and those functions would have a reference to self...

Seb
  • 17,141
  • 7
  • 38
  • 27
  • 7
    Sounds a bit overkillish and like a solution coming from a Java programmer. ;p – ThiefMaster Jan 31 '11 at 13:55
  • 1
    @ThiefMaster Why is this overkill? If you want access to the parent scope you **should** use a class in Python. – schlamar Jul 19 '12 at 12:20
  • 2
    @schlamar because in every other sane language with first class functions support (JS, functional languages) closures just work. – Dzugaru Jun 25 '17 at 13:15
6

I have couple of remarks.

First, one application for such nested functions comes up when dealing with raw callbacks, as are used in libraries like xml.parsers.expat. (That the library authors chose this approach may be objectionable, but ... there are reasons to use it nonetheless.)

Second: within a class, there are much nicer alternatives to the array (num_converted[0]). I suppose this is what Sebastjan was talking about.

class MainClass:
    _num_converted = 0
    def outer_method( self ):
        def convert_variables(m):
            name = m.group(1)
            self._num_converted += 1
            return '<%%= %s %%>' % name

It's still odd enough to merit a comment in the code... But the variable is at least local to the class.

Steve White
  • 373
  • 5
  • 9
  • Hey, welcome to Stack Overflow - but posting a "remark" as an answer is not really something you can do here. We have comments for this (however, you need some rep to post them - but don't post answers just because you do not have enough rep for a comment yet) – ThiefMaster Mar 27 '13 at 13:12
  • 6
    Hi, You're also welcome! I don't understand your remark, or several of the terms you use. And I'm a busy man, just trying to be helpful! – Steve White Mar 27 '13 at 13:29
  • No problem - have a look at http://stackoverflow.com/about though. While being helpful is always appreciated a comment that is posted will eventually be deleted no matter how good it is. – ThiefMaster Mar 27 '13 at 13:34
0

Modified from: https://stackoverflow.com/a/40690954/819544

You can leverage the inspect module to access the calling scope's globals dict and write into that. That means this trick can even be leveraged to access the calling scope from a nested function defined in an imported submodule.

import inspect 

def get_globals(scope_level=0):
    return dict(inspect.getmembers(inspect.stack()[scope_level][0]))["f_globals"]

num_converted = 0
def foobar():
    get_globals(0)['num_converted'] += 1

foobar()
print(num_converted) 
# 1

Play with the scope_level argument as needed. Setting scope_level=1 works for a function defined in a submodule, scope_level=2 for the inner function defined in a decorator in a submodule, etc.

NB: Just because you can do this, doesn't mean you should.

David Marx
  • 8,172
  • 3
  • 45
  • 66