8

I started reading about python's += syntax and stumbled onto the following post/answer: Interactive code about +=

So what I noticed was that there seems to be a difference between frames and objects.

Frames and Objects

In the global frame, they point to the same object even though they're different variables; if the line

l2 += [item]

was instead

l2 = l2 + [item]

then 'l2' becomes a separate object when that line runs. My biggest question is when would you want a variable to point to a separate object? Also, why and when would you want to keep them pointed at the same object?

Any explanation or use cases would be greatly appreciated! Extra thanks if you can mention anything relevant to data science :)

Community
  • 1
  • 1
thleo
  • 742
  • 2
  • 8
  • 21
  • 6
    If we're talking about the call stack, a [frame](https://en.wikipedia.org/wiki/Call_stack#STACK-FRAME) is the thing that contains all of the locally created objects of your most immediate scope. In your screenshot, the whole blue box is one frame, and contains the objects associated to the names `l1`, `l2`, and `item`. You don't have direct control over the frame; you only have control over objects and their names. – Kevin Nov 16 '16 at 20:36

1 Answers1

33

frame and object don't mean what you think they mean.

In programming you have something called a stack. In Python, when you call a function you create something called a stack frame. This frame is (as you see in your example) basically just a table of all of the variables that are local to your function.

Note that defining a function doesn't create a new stack frame, it's the calling a function. For instance something like this:

def say_hello():
    name = input('What is your name?')
    print('Hello, {}'.format(name))

Your global frame is just going to hold one reference: say_hello. You can see that by checking out what's in the local namespace (in Python you pretty much have a 1:1 relationship between namespace, scope, and stack frames):

print(locals())

You'll see something that looks like this:

{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x1019bb320>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': '/private/tmp/fun.py', '__cached__': None, 'say_hello': <function say_hello at 0x101962d90>}

Note the dunder (short for double underscore double underscore) names - those are automagically provided, and for the purposes of our discussion you can ignore them. That leaves us with:

{'say_hello': <function say_hello at 0x101962d90>}

That 0x bit is the memory address where the function itself lives. So here, our global stack/frame contains just that one value. If you call your function and then check locals() again, you'll see that name isn't there. That's because when you call the function you create a new stack frame and the variable is assigned there. You can prove this by adding print(locals()) at the end of your function. Then you'll see something like this:

{'name': 'Arthur, King of the Brits'}

No dunder names here. You'll also note that this doesn't show a memory address. If you want to know where this value lives, there's a function for that.

def say_hello():
    name = input('What is your name?')
    print('hello {}'.format(name))
    print(locals())
    print(id(name))
    return name

print(id(say_hello()))

That's what the example means when it's talking about a frame.

But what about objects? Well, in Python, everything is an object. Just try it:

>>> isinstance(3, object)
True
>>> isinstance(None, object)
True
>>> isinstance('hello', object)
True
>>> isinstance(13.2, object)
True
>>> isinstance(3j, object)
True
>>> def fun():
...  print('hello')
... 
>>> isinstance(fun, object)
True
>>> class Cool: pass
... 
>>> isinstance(Cool, object)
True
>>> isinstance(Cool(), object)
True
>>> isinstance(object, object)
True
>>> isinstance(isinstance, object)
True
>>> isinstance(True, object)
True

They're all objects. But they may be different objects. And how can you tell? With id:

>>> id(3)
4297619904
>>> id(None)
4297303920
>>> id('hello')
4325843048
>>> id('hello')
4325843048
>>> id(13.2)
4322300216
>>> id(3j)
4325518960
>>> id(13.2)
4322300216
>>> id(fun)
4322635152
>>> id(isinstance)
4298988640
>>> id(True)
4297228640
>>> id(False)
4297228608
>>> id(None)
4297303920
>>> id(Cool)
4302561896

Note that you also can compare whether or not two objects are the same object by using is.

>>> True is False
False
>>> True is True
True
>>> 'hello world' is 'hello world'
True
>>> 'hello world' is ('hello ' + 'world')
False
>>> 512 is (500+12)
False
>>> 23 is (20+3)
True

Ehhhhh...? Wait a minute, what happened there? Well, as it turns out, python(that is, CPython) caches small integers. So the object 512 is different from the object that is the result of the object 500 added to the object 12.

One important thing to note is that the assignment operator = always assigns a new name to the same object. For example:

>>> x = 592
>>> y = 592
>>> x is y
False
>>> x == y
True
>>> x = y
>>> x is y
True
>>> x == y
True

And it doesn't matter how many other names you give an object, or even if you pass the object around to different frames, you still have the same object.

But as you're starting to gather, it's important to understand the difference between operations that change an object and operations that produce a new object. Generally speaking you have a few immutable types in Python, and operations on them will produce a new object.

As for your question, when do you want to change objects and when do you want to keep them the same is actually looking at it the wrong way. You want to use a mutable type when you want to change things, and you want to use an immutable type if you don't want things to change.

For instance, say you've got a group, and you want to add members to the group. You might use a mutable type like a list to keep track of the group, and an immutable type like strings to represent the members. Like this:

>>> group = []
>>> id(group)
4325836488
>>> group.append('Sir Lancelot')
>>> group.append('Sir Gallahad')
>>> group.append('Sir Robin')
>>> group.append("Robin's Minstrels")
>>> group.append('King Arthur')
>>> group
['Sir Lancelot', 'Sir Gallahad', 'Sir Robin', "Robin's Minstrels", 'King Arthur']

What happens when a member of the group is eaten?

>>> del group[-2]  # And there was much rejoicing
>>> id(group)
4325836488
>>> group
['Sir Lancelot', 'Sir Gallahad', 'Sir Robin', 'King Arthur']

You'll notice that you still have the same group, just the members have changed.

Community
  • 1
  • 1
Wayne Werner
  • 49,299
  • 29
  • 200
  • 290