Naming and Binding

Indices and tables

In Python everything is an object. Everything. Strings, integers, functions, classes, return values. Everything.

And every object has 3 things:

  • An identity
  • A type
  • A value

You can never change an objects identity or it’s type. Some objects allow you to change their value (called mutable objects) and other objects don’t (called immutable objects).

An “identifier” or “name” in Python, refers to, or points to, an object. Another term used is “object reference” (which is itself…you guessed it, an object).

But, don’t think of this with the disdain (or love affair) that people have when they think of pointers in C or C++, because in practice, you rarely have to worry too deeply about the details. Most of the time Python magically handles everything for you in a way that makes it look like regular variables from other languages.

So, why talk about this at all?

Most importantly, it’s because sometimes knowing how the engine is built can help you diagnose what is going wrong when it doesn’t work. But in addition to that, Python is a dynamic language, meaning, you don’t have to declare the type of a variable before you use it. The ability to use object references underpins how it is able to pull this off and understanding it now can help you understand other concepts later on.

Lets get started!

Consider the act of assigning a value to a variable:

>>> msg0 = "Hello World"

Under the hood, you are doing something like this pseudo code:

object_reference = id_of[instance_of_string(value=”Hello World”)]

In Python, this is creating 2 items in memory; an object reference, and an object that holds the value. Once those are created Python places a reference to the object in the object-reference, as shown in Fig. 18:

../../_images/naming_and_binding0.png

Fig. 18 Object-Reference and Value-Object Creation

Python will most of the time make it look like you created an ordinary variable by assigning values to names with the = operator. However, the = operator is not the same as variable assignment. Instead, it is performing name binding.

You are free to re-bind names at will. For example, you could execute the following:

>>> msg0 = "It's lunch!"

And you would wind up with the following in memory:

../../_images/naming_and_binding1.png

Fig. 19 Object-Reference Re-Binding

In this case, we say the = operator re-bound the msg0 object-reference to refer to the new string. If msg0 hadn’t already existed, it would have been created.

Notice how the "Hello World" string instance is still in memory. It’s not a memory leak and you don’t have to worry about destroying it. When an object has no more object-references pointing to it, Python’s garbage collector will get around to cleaning it up for you.

Now, think back for a moment to the concept of mutable and immutable objects mentioned earlier. If you try and change the value of an immutable object, Python will make it look like you were successful by creating a new object with the new value and then update the existing object-reference with a reference to the new object. This is shown in Fig. 19 as the string class in Python is one such immutable type, so, assigning a new value is actually creating a new instance of the string class in memory.

Python gives us a few tools for introspection of these objects. But first, lets create a few more object-references (variables if you will) so we have something interesting to play with:

>>> msg1 = 1748
>>> msg2 = msg0

Which creates the following situation in memory:

../../_images/naming_and_binding2.png

Fig. 20 Object Introspection Example

  • You can get the unique ID (the identity) of an object that Python creates using the id() function.

    >>> id(msg0)
    140228449100464
    >>> id(msg1)
    140228467280240
    >>> id(msg2)
    140228449100464
    

    As you can see, msg0 and msg1 point to objects with different ids, but msg2 points to the same object as msg0.

    Note

    When using CPython, the id is the address of the object in memory, and the address in memory of the above msg objects will be different from computer to computer.

  • You can see if two object-references point to the same object using the is() function:

    >>> msg0 is msg1
    False
    >>> msg0 is msg2
    True
    

    Tip

    Checking if 2 object-references point to the same object is very fast in Python.

  • You can check if two objects have the same value using the equality operator ==:

    >>> msg0 == msg1
    False
    >>> msg0 == msg2
    True
    

    Warning

    For the msgN object-references above, the result of is and == were the same. As a result, it may be tempting to think is is equivalent to =. But this is wrong; one compares the object-reference point to the same object (is), the other compares the object-values are the same (=). Using is can actually lead to a logic error when using CPython due to an implementation quirk meant to speed up CPython. Consider the following code:

    >>> a = 1483
    >>> b = 1483
    >>> a == b
    True
    >>> a is b
    False
    

    This makes sense, the 2 objects have the same object-value, but, are independent objects in memory. Now, consider what happens if you use a number that is a small int:

    >>> a = 250
    >>> b = 250
    >>> a == b
    True
    >>> a is b
    True
    

    The result of a is b is accurate but misleading. In CPython, to improve performance, the frequently used small integers (-5 to 256) are created on startup and stored in memory. Whenever they are needed, they don’t need to be created (thus improving performance) and the new object-references are set to refer to them. Only for those small integers and only in CPython is is equivalent to =.

  • You can see what the type of the object is that is being pointed to using the type() function:

    >>> type(msg0)
    str
    >>> type(msg1)
    int
    >>> type(msg2)
    str
    

    Tip

    The isinstance() can check if an object is a instance of a particular class, or sub-class thereof.

You probably notice that we never declared types for any of the object-references (variables) we created. That’s because, Python is a dynamic language, meaning it determines the type of an object, pointed to by an object-reference, during run-time. This is commonly referred to as “duck typing”, along side the oft quoted saying:

“If it looks like a duck and quacks like a duck, it’s probably a duck”

In other words, if it supports the operation you are trying to do on it, it is probably the right type of object. For example:

>>> # Make 'a' and 'b' integers and add them
>>> a = 1
>>> b = 2
>>> a + b
3
>>> # Make 'a' and 'b' strings and add them
>>> a = "x"
>>> b = "y"
>>> a + b
'xy'

Python looked at the types, figured out it could do the addition in both situations (although it means something different for integers than it does for strings), and carried it out. What a world!

This can be scary for some people. But, if you try to do something with variables that isn’t supported, Python will let you know about it using a TypeError exception:

>>> a = 1
>>> b = "y"
>>> a + b
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'str'

Python has some additional introspection facilities that can tell you about the methods supported by the variable.

  • The dir() function, called without arguments, will list all the attributes (names) defined in the current namespace:

    >>> dir()
    ['__builtins__', '__cached__', '__doc__', '__loader__', ... ]
    

    Tip

    The name, __builtin__, shown above holds all the functions and constants (names) that come pre-loaded and available in the interpreter for you. Try dir(__builtin__) to see them all.

  • The dir() function, called with an object or type-name as an argument, will return a list of all the attributes that make up an object which can be useful if you forget the name of one and want to look it up:

    >>> a = 1
    >>> dir(a)
    ['__abs__',
     '__add__',
     '__and__',
     '__bool__',
     '__ceil__',
     '__class__',
     '__delattr__',
     '__dir__',
     '__divmod__',
     '__doc__',
     '__eq__',
     '__float__',
     '__floor__',
     '__floordiv__',
     ...
     'imag',
     'numerator',
     'real',
     'to_bytes']
    

    Tip

    The Python3 interpreter outputs the result of dir() in an ugly unformatted list. The IPython interpreter presents the output from dir() in a formatted list like what is shown above.

  • The help() function outputs type documentation. You can feed it the type name, or, an instance of the type:

    >>> a = 1
    >>> help(a)
    class int(object)
     |  int(x=0) -> integer
     |  int(x, base=10) -> integer
     |
     |  Convert a number or string to an integer, or return 0 if no arguments
     |  are given.  If x is a number, return x.__int__().  For floating point
     |  numbers, this truncates towards zero.
     |
     |  If x is not a number or if base is given, then x must be a string,
     |  bytes, or bytearray instance representing an integer literal in the
     |  given base.  The literal can be preceded by '+' or '-' and be surrounded
     |  by whitespace.  The base defaults to 10.  Valid bases are 0 and 2-36.
     |  Base 0 means to interpret the base from the string as an integer literal.
     |  >>> int('0b100', base=0)
     |  4
     |
     |  Methods defined here:
     |
     |  __abs__(self, /)
     |      abs(self)
     |
     |  __add__(self, value, /)
     |      Return self+value.
     |
    

    Tip

    Similar to above, try help(__builtin__).

If you’ve been following along, you will probably come to the realization, that even the attributes on an object, must themselves, be object-references that point to other object instances.

../../_images/naming_and_binding3.png

Fig. 21 Attributes are Objects

To reiterate, Python typically hides all this for you so that you can usually just assign values to variables and go about your business. But now you know about the engine in case something doesn’t work the way you think it should.

Try it!

Enter the following in the python/ipython/jupyter-notebook and examine the output. In jupyter-notebook, ensure each line is in a separate cell.

>>> msg0 = "It's lunch!"
>>> msg1 = 1748
>>> msg2 = msg0
>>> id(msg0)
>>> id(msg1)
>>> id(msg2)
>>> msg0 is msg1
>>> msg0 is msg2
>>> msg0 == msg1
>>> msg1 == msg2
>>> type(msg0)
>>> type(msg1)
>>> type(msg2)
>>> dir(msg0)
>>> dir(msg1)
>>> help(type(msg0))
>>> help(type(msg1))