0

ie we have the global declaration, but no local.

"Normally" arguments are local, I think, or they certainly behave that way. However if an argument is, say, a list and a method is applied which modifies the list, some surprising (to me) results can ensue.

I have 2 questions: what is the proper way to ensure that a variable is truly local? I wound up using the following, which works, but it can hardly be the proper way of doing it:

 def AexclB(a,b):
    z = a+[]   # yuk
    for k in range(0, len(b)):
        try:    z.remove(b[k])
        except: continue
    return z

Absent the +[], "a" in the calling scope gets modified, which is not desired. (The issue here is using a list method,

The supplementary question is, why is there no "local" declaration?

Finally, in trying to pin this down, I made various mickey mouse functions which all behaved as expected except the last one:

def fun4(a):
   z = a
   z = z.append(["!!"])
   return z

a = ["hello"]
print "a=",a
print  "fun4(a)=",fun4(a)
print "a=",a

which produced the following on the console:

a= ['hello']
fun4(a)= None
a= ['hello', ['!!']]
...
>>>

The 'None' result was not expected (by me).

Python 2.7 btw in case that matters.

PS: I've tried searching here and elsewhere but not succeeded in finding anything corresponding exactly - there's lots about making variables global, sadly.

RFlack
  • 436
  • 1
  • 5
  • 19
  • 2
    `z.append(["!!"])` returns `None` because it is an in-place method, and you're reassigning that to `z` and returning it. – Volatility Dec 21 '13 at 00:08
  • 1
    I think your root problem is that you're assuming that Python variables work like, say, C++ variables. They don't. _Values_ have types, memory locations, etc. Variables are just names for those values. You can assign lots of names—local or global or otherwise—to a value, and the value doesn't care. – abarnert Dec 21 '13 at 00:09
  • 1
    This isn't really about variables. The *variables* `a` and `b` are local to your function. What isn't local is the *object* passed as the `a` argument to your function. Lists and other objects in Python aren't attached to a single variable; as many variables as you want can refer to the same object. Unlike, say, C++, Python will not automatically make a copy every time you pass something around. – user2357112 Dec 21 '13 at 00:09
  • 1
    'The supplementary question is, why is there no "local" declaration?' The supplementary answer is, your variables are already "local" unless you declared them `global` or `nonlocal` (except in cases where you never assigned to them so it doesn't matter), so what would be the point? – abarnert Dec 21 '13 at 00:14
  • related: [In Python, why can a function modify some arguments as perceived by the caller, but not others?](http://stackoverflow.com/q/575196/4279) – jfs Dec 21 '13 at 09:25

5 Answers5

4

It's not that z isn't a local variable in your function. Rather when you have the line z = a, you are making z refer to the same list in memory that a already points to. If you want z to be a copy of a, then you should write z = a[:] or z = list(a).

See this link for some illustrations and a bit more explanation http://henry.precheur.org/python/copy_list

Frank Riccobono
  • 1,043
  • 6
  • 13
  • Nice answer; it's the only one that tries to address the root cause of the OP's confusion (and does it without even _trying_ to explain everything about what variables mean in Python; I couldn't even begin to answer this question without that explanation, but you hopefully pulled it off). – abarnert Dec 21 '13 at 00:13
1

Python will not copy objects unless you explicitly ask it to. Integers and strings are not modifiable, so every operation on them returns a new instance of the type. Lists, dictionaries, and basically every other object in Python are mutable, so operations like list.append happen in-place (and therefore return None).

If you want the variable to be a copy, you must explicitly copy it. In the case of lists, you slice them:

z = a[:]
Blender
  • 289,723
  • 53
  • 439
  • 496
1

What you're missing is that all variable assignment in python is by reference (or by pointer, if you like). Passing arguments to a function literally assigns values from the caller to the arguments of the function, by reference. If you dig into the reference, and change something inside it, the caller will see that change.

If you want to ensure that callers will not have their values changed, you can either try to use immutable values more often (tuple, frozenset, str, int, bool, NoneType), or be certain to take copies of your data before mutating it in place.

In summary, scoping isn't involved in your problem here. Mutability is.

Is that clear now?


Still not sure whats the 'correct' way to force the copy, there are various suggestions here.

It differs by data type, but generally <type>(obj) will do the trick. For example list([1, 2]) and dict({1:2}) both return (shallow!) copies of their argument.

If, however, you have a tree of mutable objects and also you don't know a-priori which level of the tree you might modify, you need the copy module. That said, I've only needed this a handful of times (in 8 years of full-time python), and most of those ended up causing bugs. If you need this, it's a code smell, in my opinion.

The complexity of maintaining copies of mutable objects is the reason why there is a growing trend of using immutable objects by default. In the clojure language, all data types are immutable by default and mutability is treated as a special cases to be minimized.

bukzor
  • 37,539
  • 11
  • 77
  • 111
  • Clear-er. Its the 'be certain to ...' bit that was throwing me... I had twigged why if I didn't use z at all, but operated directly on 'a', the calling 'value' of a gets changed. Hence the ploy with 'z'. Still not sure whats the 'correct' way to force the copy, there are various suggestions here. – RFlack Dec 21 '13 at 00:36
1

There is a great answer than will cover most of your question in here which explains mutable and immutable types and how they are kept in memory and how they are referenced. First section of the answer is for you. (Before How do we get around this? header)

In the following line

z = z.append(["!!"])

Lists are mutable objects, so when you call append, it will update referenced object, it will not create a new one and return it. If a method or function do not retun anything, it means it returns None.

Above link also gives an immutable examle so you can see the real difference.

You can not make a mutable object act like it is immutable. But you can create a new one instead of passing the reference when you create a new object from an existing mutable one.

a = [1,2,3]
b = a[:]

For more options you can check here

Community
  • 1
  • 1
Mp0int
  • 18,172
  • 15
  • 83
  • 114
0

If you need to work on a list or other object in a truly local context you need to explicitly make a copy or a deep copy of it.

from copy import copy
def fn(x):
    y = copy(x)
Steve Barnes
  • 27,618
  • 6
  • 63
  • 73
  • Is this the 'correct' way to force a local copy? A opposed to a[:] or a+[] etc etc ? It seems the most straightforward, other than the import. – RFlack Dec 21 '13 at 00:47
  • I personally like it for clarity and for the fact that if the type of x changes, say from a list to another object, it still works the other methods either rely on you keeping track of what type is incoming try `y = x[:]` with a `dict` or on which type you are needing as in `y = list(x)` throws away the values with a `dict` as `x` – Steve Barnes Dec 21 '13 at 09:16