Naming and Binding ================== .. toctree:: :maxdepth: 1 Indices and tables ------------------ * :ref:`genindex` * :ref:`modindex` * :ref:`search` 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 :numref:`figure-naming_and_binding0`: .. _figure-naming_and_binding0: .. figure:: naming_and_binding0.png :scale: 100 % :align: center 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: .. _figure-naming_and_binding1: .. figure:: naming_and_binding1.png :scale: 100 % :align: center 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 :numref:`figure-naming_and_binding1` 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: .. _figure-naming_and_binding2: .. figure:: naming_and_binding2.png :scale: 100 % :align: center Object Introspection Example * You can get the unique ID (the identity) of an object that Python creates using the :py:func:`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 :py:func:`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 :py:func:`type()` function: >>> type(msg0) str >>> type(msg1) int >>> type(msg2) str .. tip:: The :py:func:`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 :py:exc:`TypeError` exception: >>> a = 1 >>> b = "y" >>> a + b Traceback (most recent call last): File "", line 1, in 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 :py:func:`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 :py:func:`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 :py:func:`dir` in an ugly unformatted list. The IPython interpreter presents the output from dir() in a formatted list like what is shown above. * The :py:func:`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. .. _figure-naming_and_binding3: .. figure:: naming_and_binding3.png :scale: 100 % :align: center 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. .. admonition:: Try it! :class: TryIt 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)) .. Rename page "variables" Have a section for "naming and binding", "assignment" and "scope" single assignment multiple assignment augmented / in-place assignment .. _CPython: https://en.wikipedia.org/wiki/CPython .. _Jython: https://en.wikipedia.org/wiki/Jython .. _IronPython: https://en.wikipedia.org/wiki/IronPython .. _PyPy: https://en.wikipedia.org/wiki/PyPy .. _suite: https://docs.python.org/3.5/reference/compound_stmts.html#grammar-token-suite .. _dedent: https://docs.python.org/3.5/library/token.html?highlight=dedent#token.DEDENT