1
A SHORT PYTHON PRIMER

Image

In this first chapter, we’ll take a look at some of the Python features we’ll use throughout the book. This is not meant to be an introduction to Python; I’m assuming you have a basic understanding of the language. If you don’t, there are plenty of good books and online tutorials that’ll get you started.

We’ll first explore how Python code can be split into packages and how to import these packages into our programs. We’ll learn how to document Python code and how to consult this documentation using Python. Then, we’ll review tuples, lists, sets, and dictionaries, which are the most popular Python collections.

Python Packages and Modules

Software projects of a reasonable size usually consist of lots of source files, also called modules. A coherent bundle of Python modules is referred to as a package. Let’s start our discussion on Python by taking a look at these two concepts: modules and packages.

Modules

A Python module is a file that contains Python code that’s meant to be imported by other Python modules or scripts. A script, on the other hand, is a Python file that’s meant to be run.

Python modules allow us to share code between files, which spares us from having to write the same code over and over again.

Every Python file has access to a global variable named __name__. This variable can have two possible values:

  • The name of the module, that is, the name of the file without the .py extension
  • The string ’__main__

Python determines the value of __name__ based on whether the file is imported by some other module or run as a script. When the module is imported inside another module or script, __name__ is set to the name of the module. If we run the module as a script, for example,

$ python3 my_module.py

then the value of __name__ is set to ’__main__’. This may seem a bit abstract at the moment, but we’ll explain why we care about the __name__ global variable later in the chapter. As we’ll see, knowing if a given module is being imported or run as a script is an important piece of information we’ll want to consider.

As we write more and more Python modules for our project, it makes sense to separate them into groups according to functionality. These groups of modules are called packages.

Packages

A package is a directory containing Python modules and a special file whose name is required to be __init__.py. Python’s interpreter will understand any folder containing an __init__.py file as a package.

For instance, a folder structure like:

    geom2d
      |- __init__.py
      |- point.py
      |- vector.py

is a Python package called geom2d containing two files, or modules: point.py and vector.py.

The __init__.py file is executed whenever something is imported from the package. This means that the __init__.py file can contain Python code, usually initialization code. Most of the time, however, this __init__.py file remains empty.

Running Files

When Python imports a file, it reads its contents. If this file contains only functions and data, Python loads these definitions, but no code is actually executed. However, if there are top-level instructions or function calls, Python will execute them as part of the import process—something we usually don’t want.

Earlier, we saw how when a file is run (as opposed to imported), Python sets the __name__ global variable to be the string ’__main__’. We can use this fact to execute the main logic only when the file is being run, and not when the file is imported:

if __name__ == '__main__':
    # only executes if file is run, not imported

We’ll refer to this pattern as the “if name is main” pattern, and we’ll use it in the applications we’ll write in this book.

Remember that when a file is imported, Python sets the __name__ variable to the name of that module.

Importing Code

Let’s say you had some Python code you wanted to use in multiple files. One way to do that would be to copy and paste the code every time you wanted to use it. Not only would this be tedious and boring, but imagine what would happen if you changed your mind about how that code works: you’d need to open every single file where you pasted the code and modify it in the same way. As you can imagine, this is not a productive way of writing software.

Fortunately, Python provides a powerful system to share code: importing modules. When module_b imports module_a, module_b gains access to the code written in module_a. This lets us write algorithms in a single place and then share that code across files. Let’s look at an example using two modules we’ll write in the next part of the book.

Say we have two modules: point.py and vector.py. Both modules are inside the package we saw earlier:

    geom2d
      |- __init__.py
      |- point.py
      |- vector.py

The first module, named point.py, defines the geometric primitive Point, and the second one, vector.py, defines the Vector, another geometric primitive. Figure 1-1 illustrates these two modules. Each module is divided into two sections: a section in gray, for the code in the module that has been imported from somewhere else, and a section in white, for the code defined by the module itself.

Image

Figure 1-1: Two Python modules

Now, say we need our point.py module to implement some functionality that uses a Vector (like, for example, displacing a point by a given vector). We can gain access to the Vector code in vector.py using Python’s import command. Figure 1-2 illustrates this process, which brings the Vector code to the “imported” section of the point.py module, making it available inside the entire module.

Image

Figure 1-2: Importing the Vector class from the vector.py

In Figure 1-2, we use the following Python command:

    from vector import Vector

This command brings just the Vector class from vector.py. We’re not importing anything else defined in vector.py.

As you’ll see in the next section, there are a few ways to import from modules.

Different Import Forms

To understand the different ways we can import modules and names inside a module, let’s use two packages from our Mechanics project.

    Mechanics
      |- geom2d
      |    |- __init__.py
      |    |- point.py
      |    |- vector.py
      |
      |- eqs
      |    |- __init__.py
      |    |- matrix.py
      |    |- vector.py

For this example, we’ll use the geom2d and eqs packages, using two files, or modules, inside of each. Each of these modules defines a single class that has the same name as the module, only capitalized. For example, the module in point.py defines the Point class, vector.py defines the Vector class, and matrix.py defines the Matrix class. Figure 1-3 illustrates this package structure.

Image

Figure 1-3: Two packages from our Mechanics project and some of their modules

With this directory set up in our minds, let’s analyze several scenarios.

Importing from a Module in the Same Package

If we are in module point.py from the package geom2d and we want to import the entire vector.py module, we can use the following:

import vector

Now we can use the vector.py module’s contents like so:

v = vector.Vector(1, 2)

It’s important to note that since we imported the entire module and not any of its individual entities, we have to refer to the module-defined entities using the module name. If we want to refer to the module using a different name, we can alias it:

import vector as vec

Then we can use it like so:

v = vec.Vector(1, 2)

We can also import specific names from a module instead of importing the entire module. As you saw earlier, the syntax for this is as follows:

from vector import Vector

With this import, we can instead do the following:

v = Vector(1, 2)

In this case, we can also alias the imported name:

from vector import Vector as Vec

When we alias an imported name, we simply rename it to something else. In this case, we can now write it as follows:

v = Vec(1, 2)
Importing from a Module in a Different Package

If we wanted to import the point.py module from inside the matrix.py module, which is in a different package, we could do the following:

import geom.point

or equivalently

from geom import point

This lets us use the entire point.py module inside matrix.py:

p = point.Point(1, 2)

Once again, we can choose to alias the imported module:

import geom.point as pt

or equivalently

from geom import point as pt

Either way, we can use pt as follows:

p = pt.Point(1, 2)

We can also import names from the module, instead of bringing the entire module, like so:

from geom.point import Point

p = Point(1, 2)

As before, we can use an alias:

from geom.point import Point as Pt

p = Pt(1, 2)
Relative Imports

Finally, we have relative imports. A relative import is one that refers to a module using a route whose start point is the file’s current location.

We use one dot (.) to refer to modules or packages inside the same package and two dots (..) to refer to the parent directory.

Following our previous example, we could import the point.py module from within matrix.py using a relative import:

from ..geom.point import Point

p = Point(1, 2)

In this case, the route ..geom.point means this: from the current directory move to our parent’s directory and look for the point.py module.

Documenting the Code with Docstrings

When we write code that other developers will use, it’s good practice to document it. This documentation should include information about how to use our code, what assumptions the code makes, and what each function does.

Python uses docstrings to document code. These docstrings are defined between triple quotes (""") and appear as the first statement of the function, class, or module they document.

You may have noticed how the code for the Mechanics project you downloaded earlier uses these docstrings. For example, if you open the matrix.py file, the methods of the Matrix class are documented this way:

def set_data(self, data: [float]):
    """
    Sets the given list of 'float' numbers as the values of
    the matrix.

    The matrix is filled with the passed in numbers from left
    to right and from top to bottom.
    The length of the passed in list has to be equal to the
    number of values in the matrix: rows x columns.

    If the size of the list doesn't match the matrix number
    of elements, an error is raised.

    :param data: [float] with the values
    :return: this Matrix
    """
    if len(data) != self.__cols_count * self.__rows_count:
        raise ValueError('Cannot set data: size mismatch')

    for row in range(self.__rows_count):
        offset = self.__cols_count * row
        for col in range(self.__cols_count):
            self.__data[row][col] = data[offset + col]

    return self

If you ever find yourself using this code and can’t figure something out, Python has the help global function; if you give help a module, function, class, or method, it returns that code’s docstring. For example, we could get the documentation for this set_data method inside a Python interpreter console as follows:

>>> from eqs.matrix import Matrix
>>> help(Matrix.set_data)

Help on function set_data in module eqs.matrix:
set_data(self, data: [<class 'float'>])
    Sets the given list of 'float' numbers as the values of
    the matrix.

    The matrix is filled with the passed in numbers from left
    to right and from top to bottom.
    The length of the passed in list has to be equal to the
    number of values in the matrix: rows x columns.

    If the size of the list doesn't match the matrix number
    of elements, an error is raised.

    :param data: [float] with the values
    :return: this Matrix

There are automated tools, like Sphinx (https://www.sphinx-doc.org/), that generate documentation reports in HTML, PDF, or plaintext using the docstrings in a project. You can distribute this documentation along with your code so that other developers have a good place to start learning about the code you write.

We won’t be writing the docstrings in this book as they take up considerable space. But they should all be in the code you downloaded, and you can look at them there.

Collections in Python

Our programs often work with collections of items, sometimes very large ones. We want to store these items in a way that is convenient for our purposes. Sometimes we’ll be interested in knowing whether a collection includes a particular item, and other times we’ll need to know the order of our items; we may also want a fast way of finding a given item, maybe one that fulfills a particular condition.

As you can see, there are many ways to interact with a collection of items. As it turns out, choosing the right way to store data is crucial for our programs to perform well. There are different collection flavors, each good for certain cases; knowing which type of collection to use in each particular situation is an important skill every software developer should master.

Python offers us four main collections: the set, the tuple, the list, and the dictionary. Let’s explain how each of these collections stores elements and how to use them.

Sets

The set is an unordered collection of unique elements. Sets are most useful when we need to quickly determine whether an element exists in a collection.

To create a set in Python, we can use the set function:

>>> s1 = set([1, 2, 3])

We can also use the literal syntax:

>>> s1 = {1, 2, 3}

Notice that when using the literal syntax, we define the set using curly brackets ({}).

We can get the number of elements contained inside a set using the global len function:

>>> len(s1)
3

Checking whether an element exists in the set is a fast operation and can be done using the in operator:

>>> 2 in s1
True

>>> 5 in s1
False

We can add new elements to the set using the add method:

>>> s1.add(4)
# the set is now {1, 2, 3, 4}

If we try to add an element that’s already present, nothing happens because a set doesn’t allow repeated elements:

>>> s1.add(3)
# the set is still {1, 2, 3, 4}

We can remove an element from a set using the remove method:

>>> s1.add(3)
>>> s1.remove(1)
# the set is now {2, 3, 4}

We can operate with sets using the familiar mathematical operations for sets. For example, we can compute the difference between two sets, which is the set containing the elements of the first set that aren’t in the second set:

>>> s1 = set([1, 2, 3])
>>> s2 = set([3, 4])
>>> s1.difference(s2)
{1, 2}

We can also compute the union of two sets, which is the set containing all the elements that appear in both sets:

>>> s1 = set([1, 2, 3])
>>> s2 = set([3, 4])
>>> s1.union(s2)
{1, 2, 3, 4}

We can iterate through sets, but the order of the iteration is not guaranteed:

>>> for element in s1:
...     print(element)
...
3
1
2

Tuples

Tuples are immutable and ordered sequences of elements. Immutable means that, once created, the tuple cannot be changed in any way. Elements in a tuple are referred to with the index they occupy, starting with zero. Counting in Python always starts from zero.

Tuples are a good option when we’re passing a collection of ordered data around our code but don’t want the collection to be mutated in any way. For example, in code like:

>>> names = ('Anne', 'Emma')
>>> some_function(names)

you can be sure the names tuple won’t be changed by some_function in any way. By contrast, if you decided to use a set like:

>>> names = set('Anne', 'Emma')
>>> some_function(names)

nothing would prevent some_function from adding or removing elements from the passed-in names, so you’d need to check the function’s code to understand whether the code alters the elements.

NOTE

In any case, as we’ll see later, functions shouldn’t mutate their parameters, so the functions we’ll write in this book will never modify their input parameters in any way. You might, nevertheless, use functions written by other developers who didn’t follow the same rule, so you want to check whether those functions have these kinds of side effects.

Tuples are defined between parentheses, and the elements inside a tuple are comma-separated. Here’s a tuple, defined using literal syntax, containing my name and age:

>>> me = ('Angel', 31)

If we want to create a tuple with only one element, we need to write a comma after it:

>>> name = ('Angel',)

It can also be created using the tuple function, passing it a list of items:

>>> me = tuple(['Angel', 31])

We can get the number of items in a tuple using the len global function:

>>> len(count)
2

We can also count how many times a given value appears inside a tuple using the tuple’s count method:

>>> me.count('Angel')
1

>>> me.count(50)
0

>>> ('hey', 'hey', 'hey').count('hey')
3

We can get the index of the first occurrence of a given item using the index method:

>>> family = ('Angel', 'Alvaro', 'Mery', 'Paul', 'Isabel', 'Alvaro')
>>> family.index('Alvaro')
1

In this example, we’re looking for the index of the string ’Alvaro’, which appears twice: at indices 1 and 5. The index method yields the first occurrence’s index, which is 1 in this case.

The in operator can be used to check whether an element exists inside a tuple:

>>> 'Isabel' in family
True

>>> 'Elena' in family
False

Tuples can be multiplied by numbers, a peculiar operation that yields a new tuple with the original elements repeated as many times as the multiplier number:

>>> ('ruby', 'ruby') * 4
('ruby', 'ruby', 'ruby', 'ruby', 'ruby', 'ruby', 'ruby', 'ruby')

>>> ('we', 'found', 'love', 'in', 'a', 'hopeless', 'place') * 16
('we', 'found', 'love', 'in', 'a', 'hopeless', 'place', 'we', 'found', ...

We can iterate through tuple values using for loops:

>>> for city in ('San Francisco', 'Barcelona', 'Pamplona'):
...     print(f'{city} is a beautiful city')
...
San Francisco is a beautiful city
Barcelona is a beautiful city
Pamplona is a beautiful city

Using Python’s built-in enumerate function, we can iterate through the items in the tuple with their indices:

>>> cities = ('Pamplona', 'San Francisco', 'Barcelona')
>>> for index, city in enumerate(cities):
...     print(f'{city} is #{index + 1} in my favorite cities list')
...
Pamplona is #1 in my favorite cities list
San Francisco is #2 in my favorite cities list
Barcelona is #3 in my favorite cities list

Lists

The list is an ordered collection of nonunique elements referenced by their index. Lists are well suited for cases where we need to keep elements in order and where we know the index at which they appear.

Lists and tuples are similar, with the tuple’s immutability being the only difference; items in a list move around, and items can be added and removed. If you are sure the items in a large collection won’t be modified, use a tuple instead of a list; tuple manipulations are faster than their list equivalents. Python can do some optimizations if it knows the items in the collection won’t change.

To create a list in Python, we can use the list function:

>>> l1 = list(['a', 'b', 'c'])

Or we can use the literal syntax:

>>> l1 = ['a', 'b', 'c']

Note the usage of the square brackets ([]).

We can check the number of items in a list using the len function:

>>> len(l1)
3

List elements can be accessed by index (the index of the first element is zero):

>>> l1[1]
'b'

We can also replace an existing element in the list:

>>> l1[1] = 'm'
# the list is now ['a', 'm', 'c']

Be careful not to use an index that doesn’t exist in the list; it’ll raise an IndexError:

>>> l1[35] = 'x'
Traceback (most recent call last):
  File "<input>", line 1, in <module>
IndexError: list assignment index out of range

Items can be appended to the end of the list using the append method:

>>> l1.append('d')
# the list is now ['a', 'm', 'c', 'd']

Lists can be iterated, and the order of iteration is guaranteed:

>>> for element in l1:
...     print(element)
...
a
m
c
d

Often enough, we’re interested not only in the element itself but also in its index in the list. In those cases, we can use the enumerate function, which yields a tuple of the index and element:

>>> for index, element in enumerate(l1):
...     print(f'{index} -> {element}')
...
0 -> a
1 -> m
2 -> c
3 -> d

A new list can be created by taking contiguous elements from another list. This process is called slicing. Slicing is a big topic that requires a section of its own.

Slicing Lists

Slicing a list looks a bit like indexing into the list using square brackets, except we use two indices separated by a colon: [<start> : <end>]. Here’s an example:

>>> a = [1, 2, 3, 4]
>>> b = a[1:3]
# list b is [2, 3]

In the previous example, we have a list a with values [1, 2, 3, 4]. We create a new list, b, by slicing the original list and taking the items starting at index 1 (inclusive) and ending at index 3 (noninclusive).

NOTE

Don’t forget that slices in Python always include the element in the start index and exclude the element in the end index.

Figure 1-4 illustrates this process.

Image

Figure 1-4: Slicing a list

Both the start and end indices in the slice operator are optional because they have a default value. By default, the start index is assigned the first index in the list, which is always zero. The end index is assigned the last index in the list plus one, which is equal to len(the_list).

>>> a = [1, 2, 3, 4]

# these two are equivalent:
>>> b_1 = a[0:4]
>>> b_2 = a[:]

In this example, both b_1 and b_2 lists are a copy of the original a list. By copy we really mean they’re different lists; you can safely modify b_1 or b_2, and list a remains unchanged. You can test this by doing the following:

>>> a = [1, 2, 3, 4]
>>> b = a[:]
>>> b[0] = 55

>>> print('list a:', a)
list a: [1, 2, 3, 4]

>>> print('list b:', b)
list b: [55, 2, 3, 4]

Negative indices are another trick you can use. A negative index is an index that is counted starting from the end of the list and moving toward the beginning of the list. Negative indices can be used in slicing operations the same way as positive indices, with a small exception: negative indices start at –1, not at –0. We could, for instance, slice a list to get its two last values as follows:

>>> a = [1, 2, 3, 4]
>>> b = a[-2:]
# list b is [3, 4]

Here we’re creating a new list starting at the second position from the end all the way to the last element of the list. Figure 1-5 illustrates this.

Slicing lists is a versatile operation in Python.

Image

Figure 1-5: Slicing a list using negative indices

Dictionaries

A dictionary is a collection of key-value pairs. Values in a dictionary are mapped to their key; we retrieve elements from a dictionary using their key. Finding a value in a dictionary is fast.

Dictionaries are useful when we want to store elements referenced by some key. For example, if we wanted to store information about our siblings and wanted to be able to retrieve it by the name of the sibling, we could use a dictionary. We’ll take a look at this in the following code.

To create a dictionary in Python, you can either use the dict function,

>>> colors = dict([('stoke', 'red'), ('fill', 'orange')])

or use the literal syntax,

>>> colors = {'stoke': 'red', 'fill': 'orange'}

The dict function expects a list of tuples. These tuples should contain two values: the first one is used as the key, and the second is used as the value. The literal version for creating dictionaries is much less verbose, and in both cases the resulting dictionary is the same.

As with a list, we access values in a dictionary using square brackets. However, this time we use the key of the value between the brackets, as opposed to the index:

>>> colors['stroke']
red

You can use anything that’s immutable as the key in a dictionary. Remember that tuples are immutable, whereas lists are not. Numbers, strings, and booleans are also immutable and thus can be used as dictionary keys.

Let’s create a dictionary where the keys are tuples:

>>> ages = {('Angel', 'Sola'): 31, ('Jen', 'Gil'): 30}

In this example, we map the age to a key composed of a name and a surname in a tuple. If we want to know Jen’s age, we can ask for the value in a dictionary by using its key:

>>> age = ages[('Jen', 'Gil')]
>>> print(f'she is {age} years old')
she is 30 years old

What happens when we look for a key that’s not in the dictionary?

>>> age = ages[('Steve', 'Perry')]
Traceback (most recent call last):
  File "<input>", line 1, in <module>
KeyError: ('Steve', 'Perry')

We get an error. We can check whether a key is in a dictionary before getting its value using the in operator:

>>> ('Steve', 'Perry') in ages
False

We can also get a set-like view of all the keys in the dictionary:

>>> ages.keys()
dict_keys([('Angel', 'Sola'), ('Jen', 'Gil')])

We can do the same for the values:

>>> ages.values()
dict_values([31, 30])

We can use the in operator to check for the existence of a value in both the keys and values stored in Python dictionaries:

>>> ('Jen', 'Gil') in ages.keys()
True

>>> 45 in ages.values()
False

Dictionaries can be iterated in a few ways. Let’s imagine we have the following ages dictionary:

>>> ages = {'Angel': 31, 'Jen': 30}

We can use for loops to iterate through the dictionary keys:

>>> for name in ages.keys():
...     print(f'we have the age for {name}')
...
we have the age for Angel
we have the age for Jen

We can do the same for the values:

>>> for age in ages.values():
...     print(f'someone is {age} years old')
...
someone is 31 years old
someone is 30 years old

And we can do the same for the key-value tuples:

>>> for name, age in ages.items():
...     print(f'{name} is {age} years old')
...
Angel is 31 years old
Jen is 30 years old

That’s about all we need to know about Python’s collections for now. Let’s continue our Python tour by looking at destructuring collections.

Destructuring

Destructuring or unpacking is a technique that allows us to assign values inside a collection to variables. Let’s look at some examples.

Imagine we have a tuple containing information about a person, including her name and favorite beverage:

>>> anne_info = ('Anne', 'grape juice')

Say we want to have those two pieces of information in separate variables. We could separate them out like so:

>>> name = anne_info[0]
>>> beverage = anne_info[1]

This is perfectly fine, but we can do it in a more elegant way using destructuring syntax. To destructure the two strings inside the tuple into two variables, we have to use another tuple with the variable names on the left side of the assignment:

>>> (name, beverage) = anne_info

>>> name
'Anne'

>>> beverage
>>> 'grape juice'

We can also destructure lists. For example, if we had a list containing similar information about another person, like

>>> emma_info = ['Emma', 'hot chocolate']

we could destructure the name and favorite beverage using a list on the left side of the assignment:

>>> [name, beverage] = emma_info

>>> name
'Emma'

>>> beverage
'hot chocolate'

The left-side tuple or list has to match the size of the one on the right side, but there might be cases where we’re not interested in all of the unpacked values. In such cases, you can use an underscore in those positions where you want to ignore the corresponding value. For example,

[a, _, c] = [1, 2, 3]

assigns the value 1 to variable a and assigns 3 to variable c, but it discards the value 2.

This is another technique that helps us write more concise code.

Summary

This chapter has been a tour of some intermediate and advanced Python techniques we’ll use throughout the book. We took a look at how Python programs are made of modules bundled into packages and how to import these modules from other parts of our code.

We also explained the “if name is main” pattern, which is used to avoid executing portions of the code when the file is imported.

Then, we briefly touched on the four basic Python collections: tuples, lists, sets, and dictionaries. We also looked at how to destructure, or unpack, these collections.

Now let’s shift gears and talk about a few programming paradigms.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.236.214.123