3

The following code works in Python 2.7, but raises this exception in Python 3.4:

  File "/home/sean/dev/ving/meridian/venv/src/django-testmigrate/django_testmigrate/base.py", line 70, in __getattr__
    if e:
UnboundLocalError: local variable 'e' referenced before assignment

e is assigned at the top of the same function though. I assume there's some new scoping rules in Python 3, but I can't find any reference to them.

Here's the code:

def __getattr__(self, name):
    e = None

    if not self._store:
        raise AttributeError("'%s' object has no attribute '%s'" % (self.__class__.__name__, name))

    for state, scope in reversed(list(self._store.items())):
        try:
            val = getattr(scope, name)
        except AttributeError as e:
            continue
        else:
            e = None

            # get fresh instance
            if state != self._current_state and isinstance(val, models.Model):
                model_meta = val.__class__._meta
                model_class = self._current_state.get_model(model_meta.app_label, model_meta.model_name)
                val = model_class.objects.get(pk=val.pk)

            # add this value to the current scope
            setattr(self, name, val)
            break

    if e: # error raised here
        raise AttributeError("'%s' object has no attribute '%s'" % (self.__class__.__name__, name))

    return val

Update:

I got it to work by modifying my code as follows:

except AttributeError as ex:
    # needed for Python 3 compatibility
    e = ex
    continue

Somehow except...as was actually removing the variable e from the local scope. Seems like it might be a bug in Python to me.

Seán Hayes
  • 4,060
  • 4
  • 33
  • 48
  • 1
    Do you perhaps have an indentation error somewhere? – BrenBarn Jan 23 '15 at 21:07
  • Well, I'll be ... There is an error here... @BrenBarn Here's a pastebin of code where I reproduced OP's problem: http://pastebin.com/pHHFyv0V It works on python2.7 but not 3.4 – mgilson Jan 23 '15 at 21:12
  • No indentation errors. I just updated the description with new info. – Seán Hayes Jan 23 '15 at 21:20
  • 1
    Why would you even write code like that? Just raise the new `AttributeError` directly in the `except` clause... – kindall Jan 23 '15 at 21:25
  • @kindall -- Well, it's a little different... This seems to be an attempt to defer the AttributeError until later. e.g. set as many of these attributes as possible and raise if any of them were unable to be set. I agree that this is a bit of a strange thing to do, but I suppose there could be valid reasons? – mgilson Jan 23 '15 at 21:31
  • @kindall I'm searching though a collection of objects for the variable and only want to raise an exception if it wasn't found anywhere. It's for a feature on this project: https://github.com/greyside/django-testmigrate. – Seán Hayes Jan 23 '15 at 21:33

1 Answers1

5

It appears that this is changed behavior in python3.x. Specifically:

When an exception has been assigned using as target, it is cleared at the end of the except clause.

If you read a little further, the rational for this change is outlined (basically it prevents reference cycles in the current stack frame that causes objects to live longer than they normally would otherwise).

The workaround is also described:

This means the exception must be assigned to a different name to be able to refer to it after the except clause.

Which is basically what you already discovered.


Note that we can see this if we look at the op-codes of the disassembled source. Here's a simple program to demonstrate:

def foo():
  e = None
  for _ in 'foobar':
    try:
      raise AttributeError
    except AttributeError as e:
      pass
    else:
      e = None
  if e:
    raise AttributeError

import dis
dis.dis(foo)

foo()

If you run this on python2.x and python3.x you'll notice a few differences. Ignoring that 3.x raises UnboundLocalError, and only looking at the disassembled source (for the py3.x run), you can see:

...
  7          50 POP_BLOCK
             51 POP_EXCEPT
             52 LOAD_CONST               0 (None)
        >>   55 LOAD_CONST               0 (None)
             58 STORE_FAST               0 (e)
             61 DELETE_FAST              0 (e)
             64 END_FINALLY
             65 JUMP_ABSOLUTE           13
        >>   68 END_FINALLY
...

Specifically note the DELETE_FAST op code. This is NOT present if you run with python2.x.

mgilson
  • 300,191
  • 65
  • 633
  • 696