Names in Python Part 1.

Namespaces have names.
Names refer an object.
Every object is referred by one or more name in one or more namespaces. Names cannot exists alone without referring an object.

By using sys.getrefcount(), we can get how many time an object is being referred by names in different namespaces. For example:

In [2]:
import sys
sys.getrefcount(None)
Out[2]:
26851

By this Jupyter interpretor, None is referred 26851 times in different namespaces with different names. To get all the namespaces used by the interpreter, we can use dir() function.

In [3]:
dir()
Out[3]:
['In',
 'Out',
 '_',
 '_2',
 '__',
 '___',
 '__builtin__',
 '__builtins__',
 '__doc__',
 '__loader__',
 '__name__',
 '__package__',
 '__spec__',
 '_dh',
 '_i',
 '_i1',
 '_i2',
 '_i3',
 '_ih',
 '_ii',
 '_iii',
 '_oh',
 'exit',
 'get_ipython',
 'quit',
 'sys']

Let's filterout the output to remove cluter.

In [38]:
def _dirn(_CLUTTER=dir()):
    print('{:8} {:4} @ {}'.format('Name','Value', 'Id'))
    print('\n'.join([
        f'{k:8} {v:4} @ {id(v)}'
        for (k,v) in globals().items()
        if k not in _CLUTTER and not k.startswith('_')
    ]))

The above helper function remove names that have been created by the interpreter before.

In [51]:
_dirn
Out[51]:
<function __main__._dirn(_CLUTTER=['In', 'None_name', 'Out', '_', '_12', '_2', '_20', '_3', '__', '___', '__builtin__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', '_dh', '_dirn', '_i', '_i1', '_i10', '_i11', '_i12', '_i13', '_i14', '_i15', '_i16', '_i17', '_i18', '_i19', '_i2', '_i20', '_i21', '_i22', '_i23', '_i24', '_i25', '_i26', '_i27', '_i28', '_i29', '_i3', '_i30', '_i31', '_i32', '_i33', '_i34', '_i35', '_i36', '_i37', '_i38', '_i4', '_i5', '_i6', '_i7', '_i8', '_i9', '_ih', '_ii', '_iii', '_oh', 'exit', 'get_ipython', 'number1', 'quit', 'sys'])>

After filtering out, we do not currently have any namespace.

In [52]:
_dirn()
Name     Value @ Id

Let us create a name without assigning any object to it. What happens?

In [53]:
number2
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-53-0f05febabc77> in <module>
----> 1 number2

NameError: name 'number2' is not defined

Python cannot find the name number2 in any namespace. Let us assign an number object to it and see.

In [54]:
number2 = 900
In [55]:
_dirn()
Name     Value @ Id
number2   900 @ 140709459354544

When number2 is assigned value 900 with "=" operator, it is merely name binding operation of the number object 900. If the number object 900 does not already exist, then the object is created at this step. Let us try this:

In [57]:
number3 = 900
In [58]:
_dirn()
Name     Value @ Id
number2   900 @ 140709459354544
number3   900 @ 140709459355120

Above, we can see that the names number2 and number3 are assigned different ids which indicates that the number objects 900 are different. To further prove, check this out:

In [59]:
id(900)
Out[59]:
140709459354832
In [60]:
id(900)
Out[60]:
140709459355216

Above, the 900s are different number objects. However, when the value of one name is assigned to another name, no new object is created and ids are same.

In [61]:
number4 = number2
In [62]:
_dirn()
Name     Value @ Id
number2   900 @ 140709459354544
number3   900 @ 140709459355120
number4   900 @ 140709459354544

Above, the id for number2 and number4 are same which indicates that the same number object 900 is being referenced by both number2 and number4. This is the reason we are not able to use a copy of, for example, a list object input to a function instead of manipulating the original list object inside the function. Check this out:

In [66]:
#don't worry about the absurdness of the function and the logic, the purpose is just to show that you cannot copy 
#an object with simple operation
def remove_zeros(listinput):
    listoutput = listinput
    for i in listoutput:
        if i == 0:
            listoutput.remove(i)
    return listinput, listoutput
In [67]:
remove_zeros([0,1,0,1,0,1,0])
Out[67]:
([1, 1, 1], [1, 1, 1])

We can see above that the removal of zeros happened in the same object.

On the other hand, what happens to an object that is assigned to a name when the name refers some other object. Let us assign different value to number2

In [68]:
number2 = 400
In [70]:
number2
Out[70]:
400
In [71]:
number4
Out[71]:
900

Above, the object 400 is referred by the name number2 but the name number4 still refers the same object. This is kind of putting multiple name tags on the object. When you remove a name tag and put it on another object, then that name refers to that new object. Let us check the id of the objects 400 and 900.

In [72]:
id(number2)
Out[72]:
140709459355856
In [73]:
id(number4)
Out[73]:
140709459354544

While id of number4 remains same, id of number2 has been changed. If we change the value of number4 too, the integer object 900 left without any name refernece.

In [74]:
number4 = 500
In [76]:
id(number4)
Out[76]:
140709459356496

From this point onwards, we wont be able to access the integer object 900 with id 140709459354544 anymore. Let us continue with namespaces in the next part.

Reference: Stuart Williams - Python Epiphanies - PyCon 2018 https://www.youtube.com/watch?v=-kqZtZj4Ky0&t=498s