Why do immutable objects throw UnboundLocalError but mutable ones do not? - python

When trying out code that assigns a GUID to class instances, I wrote something similar to the following:
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
x_id = 0 # immutable
x_id_list = [0] # mutable
def fx(x):
global x_id # Disable to get "UnboundLocalError: local variable 'x_id' referenced before assignment"
if x is None:
x_id += 2
x_id_list[0] += 2
else:
x_id += 1
x_id_list[0] += 1
return (x_id - 1)
return x_id
expected = [x for x in xrange(10)]
actual = [fx(x) for x in expected]
assert(expected == actual), "expected = {}, actual = {}".format(expected, actual)
print x_id_list
print x_id
Notice that only the immutable x_id throws the UnboundLocalError if its global scope is not defined, but the mutable x_id_list continues to work fine without its global scope needing to be defined.
Why is that?

The issue is not that x_id is immutable (it isn't - the integer value 0 is what's immutable), but that you cannot assign to a variable defined outside a function without explicitly declaring your intent to do so via global. Mutating the list x_id_list refers to does not change the value of the x_id_list variable itself, and therefore is permitted.
If you tried to do x_id_list += [1] you'd run into the same error. It is assigning to a variable, not mutating a value, that is the problem here.
From the docs:
In Python, variables that are only referenced inside a function are implicitly global. If a variable is assigned a value anywhere within the function’s body, it’s assumed to be a local unless explicitly declared as global.
Though a bit surprising at first, a moment’s consideration explains this. On one hand, requiring global for assigned variables provides a bar against unintended side-effects. On the other hand, if global was required for all global references, you’d be using global all the time. You’d have to declare as global every reference to a built-in function or to a component of an imported module. This clutter would defeat the usefulness of the global declaration for identifying side-effects.
This answer also goes into some more detail.

That's because global scope variables are read-only in functions.
Since you're modifying the variable x_id, you need to reference it before using it, or Python will try to create a new variable that is local to the function.
Having used the global expression at the beginning of your function, you've told Python that you need to reference and modify that variable in your function. Since you are modifying the variable x_id, this happens. Since x_id_list is mutable, you are essentially not modifying it visibly (only internally, unlike x_id, where such a change is external), and therefore, you don't need the global keyword.

Related

Python weird thing with list local and global scope

Why can we modify list with the append method but can't do the same with the list concatenation?
I know about the local and global scope, I'm confused about why we can do it with append method, thanks in advance
some_list=[]
def foo():
some_list.append('apple')
foo()
print(some_list)
#it works
with list concatenation
some_list=[]
def foo():
some_list+=['apple']
foo()
print(some_list)
#UnboundLocalError: local variable 'some_list' referenced before assignment
Augmented operations like += reassign the original variable, even if its not strictly necessary.
Python's operators turn into calls to an object's magic methods: __iadd__ for +=. Immutable objects like int can't change themselves so you can't do an in-place += like you can in C. Instead, python's augmented methods return an object to be reassigned to the variable being manipulated. Mutable objects like lists just return themselves while immutable objects return a different object.
Since the variable is being reassigned, it has to follow the same scoping rules as any other variable. The reassignment causes python to assume that the variable is in the local namespace and you need the global keyword to override that assumption.
Local and global scopes apply to the variable itself, specifically what object it is referencing. The variable just points to an object in memory. You can't change the 'pointer' of the variable in a different local scope, but you can change the object that it points to. Here, you can use append because it changes the list object itself, the one stored in memory. It doesn't change what the variable some_list points to. On the other hand, the second example, you try to reassign some_list to refer to a new list that was created in combination with the old list. Since this can't happen, the interpreter now treats this some_list as a local variable separate from the other, gloabl one

Why is it that a function can modify global object attributes without an explicit global declaration? [duplicate]

This question already has answers here:
Global on mutable vs immutable
(1 answer)
Why is the global keyword not required in this case? [duplicate]
(1 answer)
Closed 8 months ago.
I am struggling to understand why a local function in Python 3 can seemingly modify global object attributes but not variables. If we consider object attributes to be variables attached to objects, this behavior appears very inconsistent and I cannot understand why the language behaves in this way.
We can create some class and use a function to modify an attribute of that class without anything like a global declaration of that attribute.
If we have some code:
class Integer:
def __init__(self, number):
self.value = number
n = Integer(20)
print(n.value) # display value at initialization
def increase_n():
n.value += 1
increase_n()
print(n.value) # display value after calling increase_n
Running this code would result in the following output:
20
21
We cannot do the same with a variable. An explicit global declaration must be used.
If we have some code:
k = 6
print(k) # display value after assignment
def increase_k():
global k
k += 2
increase_k()
print(k) # display value after calling increase_k
Running the above code results in:
6
8
Note that foregoing the global declaration for the second example would result in an error.
I am hoping someone can enlighten me. I am sure there must be some fundamental misunderstanding on my part. Why are variables and attributes treated so differently? Why is this good/bad?
The thing that the global keyword does is allow you to push a local variable (i.e. a name) assignment into the global (outermost) namespace. By default, a variable exists in the namespace of the scope in which you assign a value to it; global overrides that.
You can reference a variable from an outer scope without needing any special keyword; global is only significant for the assignment operation. Once you have a reference to the associated object, if it's mutable, you can freely mutate it, regardless of how you got that reference or where the object was created.
No global declaration is needed to affect the attribute n.value because it lives within the namespace of the object that n references, not the global namespace. The fact that n itself is a variable in the global namespace is immaterial; you could do:
def increase_n():
local_n = n # local_n is local, n is still global
local_n.value += 1
and it would have the same effect because local_n and n reference the same Integer object.

difference between += and append() in Python [duplicate]

This question already has answers here:
UnboundLocalError trying to use a variable (supposed to be global) that is (re)assigned (even after first use)
(14 answers)
Closed 1 year ago.
When I use the += in my function, I get this error: UnboundLocalError: local variable 'travel_log' referenced before assignment
but it totally works with the append() function.
What is the difference?
travel_log = []
def add_new_country(countries_visited, times_visited, cities_visited):
new_country = {}
new_country["country"] = countries_visited
travel_log += new_country
Because you assigned to travel_log in your add_new_country function (+= is an assignment), it is considered a local variable in that function. Because it is a local variable, and you never assigned it a value, travel_log has no value when you attempt to += to it. Python does not fall back to using the global variable of the same name.
This is somewhat surprising because the += operation on a list is equivalent to calling its extend() method, and the name remains bound to the same list. The error feels more reasonable when the variable is a number or some other immutable object. But it's the same behavior.
If you want to operate on the global variable, say so:
def add_new_country(countries_visited, times_visited, cities_visited):
global travel_log
# etc.
But it's better to use travel_log.append(). Appending isn't an assignment, so it doesn't make travel_log local.
This is because when you make an assignment to a variable in a scope, that variable becomes local to that scope and shadows any similarly named variable in the outer scope. Since the last statement in add_new_country() assigns a new value to travel_log, the compiler recognizes it as a local variable.
In Python, variables that are only referenced inside a function are implicitly global. If a variable is assigned a value anywhere within the function’s body, it’s assumed to be local unless explicitly declared as global.
So in your case, declare travel_log as global scope inside the function
travel_log = []
def add_new_country(countries_visited, times_visited, cities_visited):
# now function will refer the globally declared variable
global travel_log
new_country = {}
new_country["country"] = countries_visited
travel_log += new_country
Checkout this blog for a detailed explanation: https://docs.python.org/3/faq/programming.html#why-am-i-getting-an-unboundlocalerror-when-the-variable-has-a-value

Incrementing an intstance attribute vs incrementing global attributes (scope) [duplicate]

This question already has answers here:
Global dictionaries don't need keyword global to modify them? [duplicate]
(2 answers)
Closed 8 years ago.
class blah(object):
def __init__(self):
self.x=5
blahinstance=blah()
def testclass():
blahinstance.x+=1
print blahinstance.x
testclass() #blah will be incremented
print blahinstance.x #the incremented value holds after function exit
"------------------------------------------------------------------------------------"
x=5
def test():
x+=1
print x
print x
test() #fails because ref before assignemnt
So we have read access and modify access to global variables inside a local scope, but obviously attempts at re-assignment will just create a local variable of the same name as the global variable. In the examples above, what is different about referencing the instance attribute blahinstance.x which is outside of the functions scope? To me these examples are quite similar yet one fails and one does not. We do not have a ref before assignment error with blahinstance.x despite the fact that this object is in the global scope, similar to the second example of x.
To clarify - i totally understand the second example, and global vs local scope. What I don't understand is why the first works because it seems similar to the second. Is it because the instance object and it's attribute are mutable, and we have read/modify access to globals in a local scope?
Bare names are different from attribute references.
There is no name blahinstance.x. There is a name blahinstance, and the object it refers to has an attribute called x. When you do something like blahinstance.x += 2, the only variable you're referencing is blahinstance, and you're not assigning a new value to blahinstance, so all is well. The use of x in blahinstance.x is totally "internal" to blahinstance, and x is not really a variable at all, it's just an attribute name.
The "local variable referenced before assignment" business only comes into play when you assign to a variable --- that is, a bare name --- not an attribute reference, an item reference, or anything else. In this regard blahinstance.x += 2 is no different than somelist[1] += 2; x in the first case is no more a local variable than the index 1 is in the second case.

why is python global scope affected by local scope operations?

I thought that changes to variables passed to a python function remain in the function's local scope and are not passed to global scope. But when I wrote a test script:
#! /usr/bin/python
from numpy import *
def fun(box, var):
box[0]=box[0]*4
var=var*4
return 0
ubox,x = array([1.]), 1.
print ubox,x
fun(ubox,x)
print ubox,x
The output is:
[myplay4]$ ./temp.py
[ 1.] 1.0
[ 4.] 1.0
The integer variable x is not affected by the operation inside the function but the array is. Lists are also affected but this only happens if operating on list/array slices not on individual elements.
Can anyone please explain why the local scope passes to global scope in this case?
The important thing to realize is that when pass an object to a function, the function does not work with an independent copy of that object, it works with the same object. So any changes to the object are visible to the outside.
You say that changes to local variables remain local. That's true, but it only applies to changing variables (i.e. reassigning them). It does not apply to mutating an object that a variable points to.
In your example, you reassign var, so the change is not visible on the outside. However you're mutating box (by reassigning one of its elements). That change is visible on the outside. If you simply reassigned box to refer to a different object (box = something), that change would not be visible on the outside.
In your function
def fun(box, var):
box[0]=box[0]*4
var=var*4
return 0
both box and var are local, and changing them does not change their value in the calling scope. However, this line:
box[0]=box[0]*4
does not change box; it changes the object that box refers to. If that line were written as
box = box[0]*4 + box[1:]
then box in calling scope would indeed remain unchanged.
This has nothing to do with scope at all.
You're passing an object into a function. Inside that function, that object is mutated. The point is that the variable inside the function refers to the same object as in the calling function, so the changes are visible outside.
This does not depend on scope. It depends on how Python copies objects. For this respect, there are three kind of objects: scalars, mutable objects, immutable objects.
Scalars are copied by value, mutable objects are copied by reference and immutable objects are probably copied by reference but since you cannot modify them there is no implication.
Scalars are fore example all numeric types. Immutable are: strings and tuple. Mutable are: lists, dictionaries and other objects.

Categories

Resources