Jay Taylor's notes

back to listing index

Python nested functions variable scoping

[web search]
Original source (stackoverflow.com)
Tags: python scope stackoverflow.com
Clipped on: 2016-08-15

I've read almost all the other questions about the topic, but my code still doesn't work.

I think I'm missing something about python variable scope.

Here is my code:

PRICE_RANGES = {
                64:(25, 0.35),
                32:(13, 0.40),
                16:(7, 0.45),
                8:(4, 0.5)
                }

def get_order_total(quantity):
    global PRICE_RANGES
    _total = 0
    _i = PRICE_RANGES.iterkeys()
    def recurse(_i):
        try:
            key = _i.next()
            if quantity % key != quantity:
                _total += PRICE_RANGES[key][0]
            return recurse(_i) 
        except StopIteration:
            return (key, quantity % key)

    res = recurse(_i)

And I get

"global name '_total' is not defined"

I know the problem is on the _total assignment, but I can't understand why. Shouldn't recurse() have access to the parent function's variables?

Can someone explain to me what I'm missing about python variable scope?

asked Mar 7 '11 at 11:05
Image (Asset 3/12) alt=
Stefan Manastirliu
1,30421518
   upvote
  flag
This is not an answer to your actual question, but just a note that your whole function can be written as return sum(lower for (key, (lower, upper)) in PRICE_RANGES.iterite‌​ms() if quantity % key != quantity) – chthonicdaemon Sep 26 '14 at 12:24
   upvote
  flag
Actually, upon closer inspection, you return the (key, quantity % key), so the part I posted is just the way to calculate _total. Your function always seems to return the last key visited - you may think that the order of your dictionary items will be preserved, but they aren't so the final return value of your function is somewhat random. – chthonicdaemon Sep 26 '14 at 12:30
up vote 29 down vote accepted

When I run your code I get this error:

UnboundLocalError: local variable '_total' referenced before assignment

This problem is caused by this line:

_total += PRICE_RANGES[key][0]

The documentation about Scopes and Namespaces says this:

A special quirk of Python is that – if no global statement is in effect – assignments to names always go into the innermost scope. Assignments do not copy data — they just bind names to objects.

So since the line is effectively saying:

_total = _total + PRICE_RANGES[key][0]

it creates _total in the namespace of recurse(). Since _total is then new and unassigned you can't use it in the addition.

answered Mar 7 '11 at 11:22
Image (Asset 4/12) alt=
Dave Webb
122k34243268

Here's an illustration that gets to the essence of David's answer.

def outer():
    a = 0
    b = 1

    def inner():
        print a
        print b
        #b = 4

    inner()

outer()

With the statement b = 4 commented out, this code outputs 0 1, just what you'd expect.

But if you uncomment that line, on the line print b, you get the error

UnboundLocalError: local variable 'b' referenced before assignment

It seems mysterious that the presence of b = 4 might somehow make b disappear on the lines that precede it. But the text David quotes explains why: during static analysis, the interpreter determines that b is assigned to in inner, and that it is therefore a local variable of inner. The print line attempts to print the b in that inner scope before it has been assigned.

answered Nov 7 '12 at 20:08
Image (Asset 5/12) alt=
moos
983167
8 upvote
  flag
+1 I was confused, but now I see what happens. I'm a c# programmer and every time I begin to like Python something like this comes up and ruins it for me. – nima Dec 19 '12 at 14:14
   upvote
  flag
There's a very good reason why Python works like this. – sudo Jun 27 at 20:22
   upvote
  flag
@sudo can you elaborate on that comment? – radpotato Jul 11 at 11:03
   upvote
  flag
@radpotato Oops, I think I commented on the wrong answer by accident. I don't remember where I meant to put the comment. Anyway, I don't know how compelling this is, but the issue is that b would otherwise be changing scope from "global" to "local" within the function. Not only does Python not seem to support this at a low level, but it would be confusing and unnecessary. – sudo Jul 11 at 22:11

In Python 3, you can use the nonlocal statement to access non-local, non-global scopes.

answered Nov 18 '11 at 6:54
Image (Asset 6/12) alt=
Michael Hoffman
13.6k13062
   upvote
  flag
wow. never knew about that, thanks! – AnojiRox May 26 '13 at 8:00

Rather than declaring a special object or map or array, one can also use a function attribute. This makes the scoping of the variable really clear.

def sumsquares(x,y):
  def addsquare(n):
    sumsquares.total += n*n

  sumsquares.total = 0
  addsquare(x)
  addsquare(y)
  return sumsquares.total

Of course this attribute belongs to the function (defintion), and not to the function call. So one must be mindful of threading and recursion.

answered Sep 26 '14 at 12:09
Image (Asset 7/12) alt=
Hans
18112
1 upvote
  flag
for python 2, provided the disclaimer above is paid attention to, this should be the accepted answer. I found it even allows you to nest a signal handler which can manipulate the outer functions state - I wouldn't suggest that though - just a first step in refactoring some god-awful legacy code which had globals everywhere. Now to refactor again and do it the right way... – ckot May 3 '15 at 22:16

This is a variation of redman's solution, but using a proper namespace instead of an array to encapsulate the variable:

def foo():
    class local:
        counter = 0
    def bar():
        print(local.counter)
        local.counter += 1
    bar()
    bar()
    bar()

foo()
foo()

I'm not sure if using a class object this way is considered an ugly hack or a proper coding technique in the python community, but it works fine in python 2.x and 3.x (tested with 2.7.3 and 3.2.3). I'm also unsure about the run-time efficiency of this solution.

answered Oct 3 '13 at 13:25
Image (Asset 8/12) alt=
CliffordVienna
3,104924
   upvote
  flag
Handy when local method has a lot of local variables to modify but me too I wonder " if using a class object this way is considered an ugly hack or a proper coding technique in the python community " – Mr_and_Mrs_D May 31 at 15:54

You probably have gotten the answer to your question. But i wanted to indicate a way i ussually get around this and that is by using lists. For instance, if i want to do this:

X=0
While X<20:
    Do something. ..
    X+=1

I would instead do this:

X=[0]
While X<20:
   Do something....
   X[0]+=1

This way X is never a local variable

answered Nov 8 '12 at 6:18
Image (Asset 9/12) alt=
redman
13719
2 upvote
  flag
Don't, please don't. That's a horrible and inefficient solution. – Chris Morgan Nov 8 '12 at 6:20
7 upvote
  flag
How is it horrible? What's a bettwr solution? – redman Nov 8 '12 at 6:21
   upvote
  flag
I don't like it too much, but you're the only one to provide a work around. In some way, X becomes an intermediate namespace... – Juh_ Jul 10 '13 at 12:20
2 upvote
  flag
Note that while share the namespace of its container. So it is not a suitable choice for your example. – Juh_ Jul 10 '13 at 12:24

More from a philosophical point of view, one answer might be "if you're having namespace problems, give it a namespace of its very own!"

Providing it in its own class not only allows you to encapsulate the problem but also makes testing easier, eliminates those pesky globals, and reduces the need to shovel variables around between various top-level functions (doubtless there'll be more than just get_order_total).

Preserving the OP's code to focus on the essential change,

class Order(object):
  PRICE_RANGES = {
                  64:(25, 0.35),
                  32:(13, 0.40),
                  16:(7, 0.45),
                  8:(4, 0.5)
                  }


  def __init__(self):
    self._total = None

  def get_order_total(self, quantity):
      self._total = 0
      _i = self.PRICE_RANGES.iterkeys()
      def recurse(_i):
          try:
              key = _i.next()
              if quantity % key != quantity:
                  self._total += self.PRICE_RANGES[key][0]
              return recurse(_i) 
          except StopIteration:
              return (key, quantity % key)

      res = recurse(_i)

#order = Order()
#order.get_order_total(100)

As a PS, one hack which is a variant on the list idea in another answer, but perhaps clearer,

def outer():
  order = {'total': 0}

  def inner():
    order['total'] += 42

  inner()

  return order['total']

print outer()
answered May 22 '14 at 21:34
Image (Asset 10/12) alt=
tantrix
67658
>>> def get_order_total(quantity):
    global PRICE_RANGES

    total = 0
    _i = PRICE_RANGES.iterkeys()
    def recurse(_i):
    print locals()
    print globals()
        try:
            key = _i.next()
            if quantity % key != quantity:
                total += PRICE_RANGES[key][0]
            return recurse(_i)
        except StopIteration:
            return (key, quantity % key)
    print 'main function', locals(), globals()

    res = recurse(_i)


>>> get_order_total(20)
main function {'total': 0, 'recurse': <function recurse at 0xb76baed4>, '_i': <dictionary-keyiterator object at 0xb6473e64>, 'quantity': 20} {'__builtins__': <module '__builtin__' (built-in)>, 'PRICE_RANGES': {64: (25, 0.34999999999999998), 32: (13, 0.40000000000000002), 16: (7, 0.45000000000000001), 8: (4, 0.5)}, '__package__': None, 's': <function s at 0xb646adf4>, 'get_order_total': <function get_order_total at 0xb646ae64>, '__name__': '__main__', '__doc__': None}
{'recurse': <function recurse at 0xb76baed4>, '_i': <dictionary-keyiterator object at 0xb6473e64>, 'quantity': 20}
{'__builtins__': <module '__builtin__' (built-in)>, 'PRICE_RANGES': {64: (25, 0.34999999999999998), 32: (13, 0.40000000000000002), 16: (7, 0.45000000000000001), 8: (4, 0.5)}, '__package__': None, 's': <function s at 0xb646adf4>, 'get_order_total': <function get_order_total at 0xb646ae64>, '__name__': '__main__', '__doc__': None}
{'recurse': <function recurse at 0xb76baed4>, '_i': <dictionary-keyiterator object at 0xb6473e64>, 'quantity': 20}
{'__builtins__': <module '__builtin__' (built-in)>, 'PRICE_RANGES': {64: (25, 0.34999999999999998), 32: (13, 0.40000000000000002), 16: (7, 0.45000000000000001), 8: (4, 0.5)}, '__package__': None, 's': <function s at 0xb646adf4>, 'get_order_total': <function get_order_total at 0xb646ae64>, '__name__': '__main__', '__doc__': None}
{'recurse': <function recurse at 0xb76baed4>, '_i': <dictionary-keyiterator object at 0xb6473e64>, 'quantity': 20}
{'__builtins__': <module '__builtin__' (built-in)>, 'PRICE_RANGES': {64: (25, 0.34999999999999998), 32: (13, 0.40000000000000002), 16: (7, 0.45000000000000001), 8: (4, 0.5)}, '__package__': None, 's': <function s at 0xb646adf4>, 'get_order_total': <function get_order_total at 0xb646ae64>, '__name__': '__main__', '__doc__': None}

Traceback (most recent call last):
  File "<pyshell#32>", line 1, in <module>
    get_order_total(20)
  File "<pyshell#31>", line 18, in get_order_total
    res = recurse(_i)
  File "<pyshell#31>", line 13, in recurse
    return recurse(_i)
  File "<pyshell#31>", line 13, in recurse
    return recurse(_i)
  File "<pyshell#31>", line 12, in recurse
    total += PRICE_RANGES[key][0]
UnboundLocalError: local variable 'total' referenced before assignment
>>> 

as you see, total is in the local scope of the main function, but it's not in the local scope of recurse (obviously) but neither it is in the global scope, 'cause it's defined only in the local scope of get_order_total

answered Mar 7 '11 at 11:21
Image (Asset 11/12) alt=
Ant
2,47811128

While I used to use @redman's list-based approach, it's not optimal in terms of readability.

Here is a modified @Hans' approach, except I use an attribute of the inner function, rather than the outer. This should be more compatible with recursion, and maybe even multithreading:

def outer(recurse=2):
    if 0 == recurse:
        return

    def inner():
        inner.attribute += 1

    inner.attribute = 0
    inner()
    inner()
    outer(recurse-1)
    inner()
    print "inner.attribute =", inner.attribute

outer()
outer()

This prints:

inner.attribute = 3
inner.attribute = 3
inner.attribute = 3
inner.attribute = 3

If I s/inner.attribute/outer.attribute/g, we get:

outer.attribute = 3
outer.attribute = 4
outer.attribute = 3
outer.attribute = 4

So, indeed, it seems better to make them the inner function's attributes.

Also, it seems sensible in terms of readability: because then the variable conceptually relates to the inner function, and this notation reminds the reader that the variable is shared between the scopes of the inner and the outer functions. A slight downside for the readability is that the inner.attribute may only be set syntactically after the def inner(): ....

answered Oct 14 '14 at 4:47
Image (Asset 12/12) alt=
Evgeni Sergeev
4,79053657

Your Answer

asked

5 years ago

viewed

21939 times

active

1 year ago

Get the weekly newsletter! In it, you'll get:

  • The week's top questions and answers
  • Important community announcements
  • Questions that need answers

Hot Network Questions

Technology Life / Arts Culture / Recreation Science Other
  1. Stack Overflow
  2. Server Fault
  3. Super User
  4. Web Applications
  5. Ask Ubuntu
  6. Webmasters
  7. Game Development
  8. TeX - LaTeX
  1. Programmers
  2. Unix & Linux
  3. Ask Different (Apple)
  4. WordPress Development
  5. Geographic Information Systems
  6. Electrical Engineering
  7. Android Enthusiasts
  8. Information Security
  1. Database Administrators
  2. Drupal Answers
  3. SharePoint
  4. User Experience
  5. Mathematica
  6. Salesforce
  7. ExpressionEngine® Answers
  8. more (13)
  1. Photography
  2. Science Fiction & Fantasy
  3. Graphic Design
  4. Movies & TV
  5. Seasoned Advice (cooking)
  6. Home Improvement
  7. Personal Finance & Money
  8. Academia
  9. more (9)
  1. English Language & Usage
  2. Skeptics
  3. Mi Yodeya (Judaism)
  4. Travel
  5. Christianity
  6. Arqade (gaming)
  7. Bicycles
  8. Role-playing Games
  9. more (21)
  1. Mathematics
  2. Cross Validated (stats)
  3. Theoretical Computer Science
  4. Physics
  5. MathOverflow
  6. Chemistry
  7. Biology
  8. more (5)
  1. Stack Apps
  2. Meta Stack Exchange
  3. Area 51
  4. Stack Overflow Careers
site design / logo © 2016 Stack Exchange Inc; user contributions licensed under cc by-sa 3.0 with attribution required
rev 2016.8.15.3893