Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python closure + global strangeness

Tags:

python

I expected this little snippet to print "Why doesn't this work?" Can someone help me understand why this doesn't work as I expect? I'm using Python 2.6, if this matters.

class WhyDoesntThisWork(object):
  def outer(self):
    acc = ''
    def inner(msg):
      global acc
      acc = acc + msg
    inner("Why doesn't")
    inner(" this work?")
    print acc
WhyDoesntThisWork().outer()
  • If I include the global statement I get a NameError: global name 'acc' is not defined .
  • If I don't include the global statement I get a UnboundLocalError: local variable 'acc' referenced before assignment.
like image 650
sholsapp Avatar asked Jun 22 '12 21:06

sholsapp


1 Answers

I don't know why so many comments above contain the correct answer and no one dared to write an actual answer, so I'll do it hereby.

class ThisWorksNow(object):
  def outer(self):
    acc = []
    def inner(msg):
      acc.append(msg)
    inner("Why doesn't")
    inner(" this work?")
    print "".join(acc)
ThisWorksNow().outer()

What is the difference?

Assigning a name to an object which is in the closure doesn't work in Python 2.x, because the Py3 keyword nonlocal is missing, so we have to find a workaround.

If we have to keep the name-to-object-binding constant, we have to change something else. In this case, it is the object, to which we add the content to be added.

The print line is not very elegant; maybe an object which prints its contents concatenated might be more suitable.

class StringBuilder(list): # class name stolen from Java
    def __str__(self):
        """this makes the object printable in a way which represents the concatenated string"""
        return "".join(self)
    @property
    def string(self):
        """this gives us a property which represents the concatenated string"""
        return "".join(self)
 # use whatever suits you better, one or both

With this, we can do that:

class ThisWorksNow(object):
  def outer(self):
    acc = StringBuilder()
    def inner(msg):
      acc.append(msg)
    inner("Why doesn't")
    inner(" this work?")
    print acc
    print acc.string # depending on what you take above
ThisWorksNow().outer()

Edit (append): Why does global not work?

We could achieve this with global as well, with 2 downsides.

  1. acc would have to be made global on both places we use it

    class WhyDoesntThisWork(object):
      def outer(self):
        global acc
        acc = ''
        def inner(msg):
          global acc
          acc = acc + msg
    

    Hereby we "lift" both acc occurrences to "global" level.

  2. acc could be modified from outside.

    If we do global acc somewhere else, or we use acc on module level, our process can be tampered with. This should be avoided.

like image 120
glglgl Avatar answered Oct 07 '22 00:10

glglgl