© Nico Loubser 2021
N. LoubserSoftware Engineering for Absolute Beginnershttps://doi.org/10.1007/978-1-4842-6622-9_4

4. Programming in Python

Nico Loubser1  
(1)
London, UK
 

Solving problems is the main job of the software engineer. Programming is important because a lot of tasks can easily be automated. You can create systems that schedule parcel delivery, that avoid accidents, that recognize your friends, that drive cars. You can create a system that waters your plants and one that fetches your desired news feeds and shows them to you. The list is endless. I remember when I first saw the power of automating mundane tasks by giving them to a computer to do.

I was about 22 years old, working at a bus transport company. My job on weekends at that stage was to take all the passenger lists, every 45 minutes, write down how many people were on each route, and type that into an Excel spreadsheet. Then I would print the sheet and go to this big board with all the routes and busses on those routes and write down the number of passengers per route. The operations team would then make decisions based on that data as to how many busses they needed, how many seats per bus, and so on. This would take me about 35 minutes at a time and I did not like doing it at all. Soon the company hired a developer, installed some computer screens in the operations room, and automated this process. I was incredibly impressed when I saw the board updating every 5 minutes and taking what can only be called “no time at all” to do so.

I saw these changes spread through the company. If I had to work night shifts, we had a large number of manual tasks to do before the next morning. Soon, those tasks disappeared as well. The magic in the system handled it for us. I was very impressed with those automated changes, but at the same time I became aware of the fact that we needed less manpower, and that was the way the world was moving. Fortunately, automation has opened markets and created a lot of new opportunities.

In this chapter, you will look at writing code. Writing code is only part, albeit a big part, of solving a problem. You have already looked at Docker and Git, which form the basis of running code and controlling the codebase. This chapter is just your first step at writing code, and it’s not a very complex introduction. It will take a long time to fully learn your first programming language, but this chapter is a starting point.

What Is Programming?

Writing code on a basic level is nothing more than telling a program what it has to do. The catch is, programs cannot think for themselves, and you have to be very precise in what you tell them to do. Your goal as a programmer is to change the state of a program, or a small portion of a big program, from one state to the other. You must envision the possible states the system can be in, as well as unknown problems that can occur. You will have a start state in your program, and an end state. As an example, say you need to send an email. In order for you to achieve your goal of sending that email, you need to give the software some commands that can manipulate the state of the program from “unsent email” to “sent email.”

Programming can be rather difficult, but it is not the act of writing code that is difficult. It depends on how difficult the problem is you are trying to solve.

Python

We will use Python, an easy-to-learn and very powerful programming language. Your Python source code, which this chapter is all about, is executed by the Python interpreter, which you installed in Chapter 2. So, at this point, you should have a Docker environment capable of running Python applications. You do not need the Docker environment, though. You can just install Python on your local machine, but I strongly recommend that you use Docker to get used to it. You may even learn things about Docker not discussed in this book while playing around with it.

Setup for This Chapter and How to Use It

You will use the Docker environment you created in Chapter 2 to create the example files. You may remember that you set up your docker-compose file to use a shared directory called test. Whatever you put in that directory can be interpreted by Python. Let’s test this quickly.

In your Dockerfile, change the CMD command from
CMD ["python", "/home/test.py"]
to
CMD ["python", "/home/main.py"]
In the /test directory, where test.py is living, add a new file called main.py. Add this line:
print('This is the first line')
Because you made a change to the Dockerfile, you need to rebuild it:
docker-compose build
Now run it:
docker-compose up

Whenever you make a change to main.py, just run docker-compose up to execute it. All of the examples below can be added to main.py for you to test. At your own discretion, you can delete old lines of code in the main.py file, but it is sometimes handy to have if you want to retest an old line of code.

Basics

Before we get into coding, there are some basics you need to be aware of.

In Python, all statements end in a colon. I will cover all the statements throughout this chapter. The colon indicates the start of a code block. A code block is the block of code belonging to the preceding statement, and code blocks can contain more code blocks. A code block, which follows a colon, is indented with four spaces or a tab. The industry standard prefers four spaces and not tabs, purely because not all systems display tabs the same way. In Python 3, you cannot mix tabs and spaces to indent code blocks. Your editor should be set up to provide four spaces when you press tab, and Visual Studio Code does this automatically.

The following piece of code displays the concepts I explained above. It is important to bear in mind that not all editors display spaces with the same width. In general, a Python editor should because it makes it easier to spot indentation errors.
def my_function(): # Start of a code block 1
     if 1 == 2 : # Part of code block 1 and start of code block 2
          print('Part of code block 2') # Part of code block 2
          print('Part of code block 2') # Part of code block 2
     print('Part of code block 1') # Part of code block 1

This was a quick overview, but you will definitely see this whenever there are code blocks and statements, and it will become intuitive to you in no time.

Commenting Your Code

To make your code more understandable, you can add human-readable comments to it. The Python interpreter will ignore these comments and they will have no effect on the execution of the program. Chapter 5 offers a detailed description on how to do this, but for now, you just need to know there are two ways to do add comments. The first way is to add a hash symbol (#) in front of a line, and the Python interpreter will ignore that line. The second way is to encase the description between two sets of triple quotes. This is very handy when you want to explain why you did something in your code. I do so throughout the book to give inline explanations on code, so you will benefit from reading them. Now look at Listing 4-1.
print('The interpreter will interpret me')
# The interpreter will ignore me
"""
The interpreter will ignore this line as well
as this line.
"""
Listing 4-1

Commenting code

Variables

Now let’s move on to variables, which is something you will use constantly. Most people get an intuitive understanding of variables pretty quickly once they start reading about them. Imagine you have one tennis ball. Now imagine you have small box, and you put your tennis ball into it. The box in this instance is the memory location that Python has reserved for your tennis ball. This box is called a variable, and the tennis ball is the value assigned to that variable. You will need to give your box a name. Let’s call it my_ball.

Whenever you want to access your tennis ball, you use the name my_ball, which tells everyone that they must use the box with that name. Whenever you need to pass a tennis ball to someone else, you give them the name of the box, and they will instantly know which box to use. Instead of putting a tennis ball into the box, you can also put a basketball or a rugby ball into the box. You can have many boxes, each with its own name, containing different or similar balls. You can also swap balls and change the ball inside the box. This is true for variables. You have a memory location, which is represented by the box; a value, which is represented by the ball; and then the name, which points to the memory location which stores the value, which is the name which points to the box.

Variables are used by a program to store data. It is temporary storage and the values will not persist past the execution of your program. This data is used by the program to base decisions on, to output it in one way or the other to the user, or the program can save the variables in long-term or short-term storage. An example of a variable in your system can be a password, username, mobile number, or something more complex, even something you created yourself. There are a few types of basic variables that you will use regularly to build your system. It is safe to say that all software, no matter how complex, uses these basic types.

Five of the most basic built-in types are integer, float, string, arrays, and boolean. You can also build your own much more complex data types that will be used as variables, and we will discuss this later in the book. Unlike some programming languages, Python is dynamically typed, meaning you do not need to tell Python that the variable you are creating should be a string or an integer. Python infers the type from the data you give it, meaning Python selects whether it is a string, integer, float, or boolean. So whatever type of data you have assigned to a variable, that will be the variable’s type.

A variable consists of a variable name and a value. You declare a variable by using the variable name, the assignment operator, and the variable type. In the next example, number is the variable name, and the integer value 200 is assigned to it using the assignment operator. This gives us a variable of type integer, containing the value 200. The variable number points to the value 200. You can let more than one variable point to the same value.
number = 200

In Python, and most programming languages, a single = means the value on the right is assigned to the value on the left. ==, a double =, tests whether the values on the left and right are equal to each other. We will also get to this a bit later.

Integers

Integers are numbers without a fraction component. In Python 3, integers can be of unlimited size. Let’s go through the following example step by step.

In main.py, type the following and press Enter:
age = 20
You now have a space in memory in Python holding the value 20. You can reference this value by using the word age. The following line should print 20:
print(age)
Now add the following line just below that:
max_age = 30
You now have two variables, one holding the integer 20 and another the integer 30. The latter can be accessed with the name max_age. Let’s subtract the two values and print the result to the screen. You can either assign the result to another variable and then put that result in the print function or you can place the equation directly into the print function. Enter the following line, and after that, execute the code from the command line. The response on the screen should be 10.
# Use the minus sign to subtract max_age from age
age_difference = max_age - age
print(age_difference)
As a refresher, to execute the code, run this command from the command line :
docker-compose up
You can inspect your variable’s type by using Python’s built-in function called type(). In the example below, variable_type may look like a string when you print it, but it actually is a more complex type and, coincidentally, a type called type. This new complex type holds as a value the type that my_integer is, which is int.
my_integer = 4
variable_type = type(my_integer)
print(variable_type)

The result should be <type 'int'>.

Float

Floating point values are values with fractions, for instance:
distance_in_meters = 8.5
print(distance_in_meters)

They can be used with integers and the result will be a float.

Boolean

Boolean values can either be True or False. In Python, True and False are always capitalized. They can be used in a variety of scenarios. For instance, a program may need to know if you are an admin, so there may be a boolean variable called is_admin that is set to True to signal to the rest of the program you are indeed an admin.
is_admin = True

Strings

Strings are words and sentences, denoted by putting double or single quotes around them.
name = 'John'
print(name)
You can join strings using concatenation with a + concatenation operator. This is something you will encounter a lot.
greeting = 'Hello '
print(greeting + 'John')
Strings that are enclosed in single quotes, which have more single quotes inside them, need to be escaped, and the same goes for double quotes with double quotes within them. To escape the middle ', you need to add a backslash before it. This tells Python that it is an actual ' that should be printed, and not one of the enclosing quotes. For instance, this string assignment will break the system:
# This will cause an error.
message = 'let's go'
print(message)
Escaping fixes it:
# This will print.
message = 'let's go'
print(message)
A non-escaping method to fix it is to put double quotes on the outside, like so:
# This will print
message = "let's go"
print(message)

Last Word on Variables

A variable’s value can change. It is not a fixed value. In other words, you can reassign data to a variable as need be. You can also change a variable’s type. You cannot, however, let two variables of different types work together without casting one of the values to be the same as the other. This is called type casting and it is discussed in the next section.

Constants

A lot of programming languages have a concept called a constant. A constant is a type of variable that can only be assigned once and never be changed, hence it is a constant value. This is a very useful feature to have. There are many use-cases for a variable that cannot have its value changed once assigned, like a file location. Python does not have this functionality. In Python, a constant is identified by declaring a normal variable but writing the variable name in uppercase. This merely signals to other developers that this value should not change.

A normal variable:
file_location = '/bin/bash/'
A constant:
FILE_LOCATION = '/bin/bash/'

Type Casting

Now that you have been introduced to different basic types of variables, it is important to tell you that you will sometimes need to change basic data types. Why? Because data types cannot be intermixed. If you have a string with the value of 10 and an integer with the value of 20, you cannot add the two numbers without changing the string to an integer. In software development, this is called type casting or type juggling.

The type casting functions are in Table 4-1.
Table 4-1

Type Casting Functions

str(x)

If x is not a string, this converts x to a string.

float(x)

If x is not a float, this converts x to a float.

int(x)

If x is not an int, this converts x to an int.

bool(x)

If x is not a boolean, this converts x to a Boolean.

Here is an example where you tell Python to treat an integer as a string when concatenating the integer and the string. Type the following commands:
first = 'There was '
string_part = '1' # 1 is surrounded by quotes, making it a string
int_part = 1 # 1 is standing alone, making it an integer
Python should have no problem with the following command:
print(first + string_part)
The following command should produce a warning, something to the extent of “TypeError: can only concatenate str (not “int”) to str”, so try it out:
print(first + int_part)
You will encounter these instances many times. In this instance, the solution is to cast the variable called int_part to a string, using Python’s built-in function str(). You put the value you need to cast to string between the brackets, like so:
print(first + str(int_part))
To illustrate casting from a float to an integer, run the following code and see if the output is as you expected:
print(int(4.567))
print(float(5))
print(bool(1))
print(bool('false'))

Shorthand Assignment Operators

As you know, = assigns data on the right to the variable on the left.

The following are some shorthand operators. For these shorthand operators to work, variable a must be associated with a value already.
 a = 10

a += 5

This equates to a = a + 5. This will concatenate strings.

a -= 5

This equates to a = a – 5.

a *= 5

This equates to a = a * 5.

a /= 5

This equates to a = a / 5.

Sequences and Maps

Remember the tennis ball analogy I used earlier? I need to change it slightly to explain how sequences and maps work. In this instance, imagine you have a bigger box than previously. Now, instead of only being able to add one ball into the box, you can add more. So, you end up with one box containing multiple balls, plus they don’t have to be the same kind of ball. Not all boxes are the same in this case. Some boxes can take an unlimited amount of balls, and you can remove balls, add balls, or change balls as you please. Some boxes, however, are sealed once you have placed the balls inside, and the balls cannot be removed or edited.

Sequences and maps (the boxes, in the above analogy) are variables, called data structures in this case, containing sets of values. They are referred to in general as arrays, even though there are behavioral differences between real arrays and map, lists, etc. They are normally used to represent data that are related, such as customer details or a shopping list, but your use case will determine what data is inside it. In this section, I will show how these data structures are used and how the data is accessed using slicing and using indexes. A bit further along in the chapter, you will encounter arrays again and see how to access the elements sequentially using loops.

Before we jump in, just a final high-level overview of arrays. Arrays in Python come in the following forms: sequences and maps. They are slightly higher-level terms. On an implementation level, that is on the level you write your code, sequences come as lists, strings, and tuples, and maps are dictionaries. You will work with lists, tuples, and dictionaries, but can refer to them via their higher-level names as well. You can iterate through sequences and maps and do computations and logical checks on the values. Similar to how Python comes with built-in functions, so do lists, strings, sequences, and maps. You will look at how to use built-in functions a bit later in this section.

Lists and Strings

Lists and strings are both sequences. You have already encountered strings, so I only mention it here as it is technically a type of sequence. You can also access a string’s data using indexes and slicing, exactly the same as a list. Both slicing and indexes will be covered in this section. A list is a collection of changeable data. You can add values, remove values, and edit values. Lists are initialized as comma-separated values between square brackets. Or you can use the list’s built-in functions to add and remove values. This is how you declare a list:
my_list = ['apple', 'pear', 'orange', 50, 4]
print(my_list)

Remember that I said lists, tuples, and maps are complex variables? This means they are self-contained objects (you will encounter objects a bit later in this chapter). But it is good to know that they are objects and have their own built-in functionality. To access the functionality of any object, you use the dot operator (.) followed by the function name. In this example, I demonstrate how to do it, using lists as the example object.

Here are more ways to get data into a list:
# Declare lists with a few default values, although it can be left empty.
new_list = ['value1', 'value2', 'value3']
# new_list is now an object
# Use the dot operator, and the built in 'insert' function to add elements to the list.
# The insert function's parameters are as follows insert (position, value)
# position indicates the position at which you want to insert your value
new_list.insert(1, 'value4')
print(new_list)
# You can also just add data to the end of the list using the append method().
my_list.append('another value')
print(my_list)

To add data to the start of a list, you use insert it with a position of 0.

Accessing Data Inside Lists

You need to be able to read and write data in the lists. In general, there are two ways to do this, indexes and slicing. Slicing is based on indexing so we will take a quick look at indexing first.

Indexes
Data inside lists can be accessed for reading and writing using their indexes. An index denotes the location of an item in an array and always starts at 0. You do not need to specify the indexes yourself; it is done automatically when you assign the data to the list.
my_list = ['apple', 'pear', 'orange', 50, 14]
print(my_list[0]) # Lists and tuples are both referenced using square brackets.
print(my_list[2])
The first print function should output the word apple, and the second, the word orange. The index structure will look like this, where the index is to the left of the arrow and the value to the right:
[0 -> 'apple', 1 -> 'pear', 2 -> 'orange', 3 -> 50, 4 -> 14]
Slicing
You can also perform operations called slicing on your lists. It’s a bit more confusing at first, but very powerful once you get the hang of it. Take our previous example of my_list. The anatomy of a slice looks like this:
my_list[start-point : stop-point]
The start point and the stop point are both from the start of the list. Listing 4-2 has some examples.
my_list = ['apple', 'pear', 'orange', 50, 14]
print(my_list[0:1]) # Prints apple
print(my_list[0:2]) # Prints apple, pear
print(my_list[-1:]) # The last item in the array
print(my_list[:-1]) # Everything except the last item
Listing 4-2

Slicing

There is also another parameter that can be added to the slicing operator called step, which indicates at what interval numbers should be returned in the slice:
my_list[start-point : stop-point : step]

Keeping to the my_list list, I’ll illustrate how to skip every second item in the array.

This example states that slicing should start at position 0, go until position 5, and skip every second entry:
print(my_list[0:5:2])
But what if your list will constantly be changing in size? The drawback of the above method is that if you want to operate on a whole list that changes in size, you would manually have to alter the middle argument of the slice, which is now 5. Using the built-in Python function called len(), which returns the length of a list, you can automate the stop-point to always be the length of your list. The lines below show how len() operates on lists:
print('my_list has ' + str(len(my_list)) + ' entries')
print(my_list[0:len(my_list):2])

Tuples

Tuples are of a fixed size and are created using round brackets or without any brackets, like so:
my_tuple = ('eat', 'sleep', 'repeat')
my_tuple = 'eat', 'sleep', 'repeat'

Tuples are sliced exactly the same way as lists. Refer to the code in Listing 4-2. Fixed size (immutable) means that once it has been created, you cannot add or remove elements from it, nor can you edit values. This means that you use a tuple when you know exactly how many elements you will have. This makes tuples a bit faster than lists, and also safer, since you cannot accidentally delete data. Although there are many similarities between lists and tuples, you must remember you cannot add or remove elements from tuples. Tuples can be sliced in the same way as lists.

When to Use a List and When to Use a Tuple

You should use a list when you have a variable amount of data to populate it with. For instance, if your code searches your newsfeeds daily for keywords like invest and adds that news article into your data structure before displaying it, then a list is appropriate because you will get a different amount of news articles every day. If you are looking to store the daily hourly temperature in your structure, then a tuple is the way to go because every day only has 24 hours.

Dictionaries

A dictionary, also called a map or key/value pair, is a data structure where each indexed value has a corresponding key. Data can be accessed using this key. The index should be a meaningful word, describing what the value relates to.

Anatomy of a dictionary:
details = {
  key_unique_name : value,
  key_unique_name : value
}
# This would translate into an actual example looking like this
user_details = {
  "name": "Nico",
  "surname": "Loubser",
  "age": 68,
  "house_number": 68,
  "street_name": "Mitchell street"
}
Below is what the list equivalent looks like. Compared to lists and tuples, the keys in both lists and tuples are not really absent, they’re just implicitly assigned and start at 0. Compared to the dictionary implementation, you can see how the dictionary is a lot more readable. In the list example, which follows, without a readable key, we have no idea what 68 is, whereas in the dictionary version it is easy to understand.
details_list = ["Nico", "Loubser", 68, 68, "Mitchell street"]
Getting data from a dictionary is very easy. As with lists and tuples, you access data using the indexes. You cannot slice a dictionary because it is not a sequence and slicing won’t make sense.
print(user_details['name'])
print(user_details.get('name'))

We will go through list, tuples, and maps in the “Loops” section of this chapter.

Decision-Making Operators and Structures

All decisions made in programming are based on a true or false outcome. This can become very complex as there may be a multitude of factors to base a single decision on. But even the most complex algorithm will boil down to a true or false.

Comparison operators, combined with logical operators, form the basis of decision-making algorithms. But don’t be fooled. You can use these humble components to build systems that are complex. Yes, you can even build AI systems using these components.

Operators

When combining operators (or, in fact, when only using a single one) we form what is called an expression. You will need a lot of expressions to achieve your program’s goal. Python provides you with a set of comparison operators as well as logical operators to help you build your expressions, and you will deal with them in this section. While building your expression, you will use the comparison operators to evaluate different values and logical operators to include (and exclude) certain outcomes.

Comparison Operators

This section is important to understand before you get to decision making structures.

When we want to make decisions based on data, we need some regular mathematical operations to do so. The operators in Table 4-2 are of interest at a beginner level. The results of these operators will always be True or False. A comparison operator takes a value to its left and compares it to a value on its right. The outcome of that evaluation will be positive or negative, in other words, True or False.
Table 4-2

Comparison Operators

==

Equals

!=

Not equal to

<>

Not equal to

>

Left is bigger than the right-hand side

<

Right is bigger than the left-hand side

<=

Right is bigger than or equal to the left-hand side

>=

Left is bigger than or equal to the right-hand side

Examine the following code and run it in your environment. You will see how the outcomes are boolean values.
# Initialise 2 variables
age = 100
limit_age = 120
# Do logical comparisons.
print(age == limit_age)
print(age > limit_age)
print(age < limit_age)

Logical Operators

This section is important to understand before you get to the decision-making structures. When we combine comparisons operators, we need to specify how those comparisons are related to each other. For instance, you can have a variable called car and variable called year_model.

Here’s a demonstration of the and, or, and not logical operators:
Assume car = 'Ford' and year_model=1996.
and
Let’s start with and :
if car == 'Ford' and year_model != 1998:

This will result to True if both comparisons are True. Should the very first comparison, car == 'Ford' in this case, result in False, then Python will not evaluate the rest of the logical operation.

Boolean and logic works as follows:
True and True == True
False and True == False
True and False == False
False and False == False
or
Here is the or version:
if car == 'Ford' or year_model != 1996:

This will result to True if one of the comparisons is True. The whole logical operation will be evaluated.

Boolean or logic works as follows:
True or True == True
False or True == True
True or False == True
False or False == False
not
Finally, not :
if not(car == 'Ford'):
This will negate the comparison, so if name == 'Ford', this will result in False.
not True == False
not False == True
Truth Tables

These expressions can get very complex and need a lot of practice, but they form the basis of the logical evaluations in your code. One of the issues is that you may have a simple expression, as in Listing 4-3, but there are three variables, each of which’s comparison may be True or False, so how do you accurately determine what values will pass the expression successfully? There is something called truth tables , which can help you deduce what the outcome of your expression will be, especially in the beginning as you are just starting out. Truth tables hold all the possible values of your expression and calculate their outcomes. I will give a quick overview here.

Suppose your expression looks like Listing 4-3.
(amount == 10 and tax == 14) or discount != 100
Listing 4-3

Logical expression

This means, if your amount is 10 and your tax is 14 or the discount is not 100%, then this whole expression will evaluate to True. If the amount is indeed 10, the tax is 14, and the discount is not 100%, then this can be rewritten as (True and True) or True.

However, if the discount is 100%, this can be rewritten as
(True and True) or False
You can construct a truth table to further see which values will make the expression pass or fail. See Table 4-3.
Table 4-3

Truth Table

amount == 10

AND

tax == 14

1st result of the ‘and’

OR

discount!= 100

Result of 1st result or discount

True

And

True

True

Or

True

True

True

And

True

True

Or

False

True

True

And

False

False

Or

False

False

False

And

False

False

Or

False

False

False

And

False

False

Or

True

True

False

And

True

False

Or

True

True

False

And

True

False

Or

False

False

True

And

False

False

Or

True

True

Table 4-3 has seven columns. The first and the third columns are all possible results for the first set of comparisons. Column two holds the logical operator for reference and column four holds the results. So the first four columns hold all the possible potential outcomes for (amount == 10 and tax == 14).

Column five just holds the or operator for reference. The or operator will now work on the data of column four and column six. Column six forms part of all possible results that the individual logical expressions, which make the whole expression, can have. The result of that last or is in column seven. Now, reading the table row by row from left to right, you can see what the evaluations should be for your expression to succeed. The successes are marked in blue in the last column.

Identity Operators

Remember in the beginning I said that more than one variable can point to the same value? With the identity operators, we can check whether that is the case. This is not the same as checking whether two variables have the same value using ==. Two variables can have the same value, where the values are separate and occupy two different memory locations in Python. Identity operators do not care about the equality of two values. They care about whether both variables are pointing toward one single value in Python’s memory. You can manually inspect variables to see if they are pointing toward the same memory location using the built-in Python function id (variable). This will print/return the id of the memory location where the variable is pointing.

is
The identity operator is compares data on its left-hand side with data on its right-hand side to see if they point to the same object, as demonstrated in Listing 4-4.
set_one = [1, 2, 3, 4, 5]
set_two = set_one
if set_one is set_two:
    print('Both variables are pointing to the same list object')
Listing 4-4

Using is

is not
The identity operator is not compares data on its left-hand side with data on its right-hand side to see if they do not point to the same object, as demonstrated in Listing 4-5.
set_one = [1, 2, 3, 4, 5]
set_two = set_one
if set_one is not set_two:
    print('Both variables are pointing to the same list object')
else:
    print('Both variables are not pointing to the same list object')
Listing 4-5

Using is not

Membership Operators

Membership operators check whether the value on the left is in the array on the right.

in
The membership operator in checks whether the data on its left-hand side is in the array on its right-hand side. See Listing 4-6.
fruit = ['apple', 'pear', 'orange', 'kiwi']
if 'apple' in fruit:
    print('Apple is in the list')
Listing 4-6

Using in

You can also use the in keyword in loops, which I will demonstrate later.

not in
The membership operator not in checks whether the data on its left-hand side is not in the array on its right-hand side. See Listing 4-7.
fruit = ['apple', 'pear', 'orange', 'kiwi']
if 'watermelon' not in fruit:
    print('watermelon is not in the list')
Listing 4-7

Using not in

Precedence of Operators in Expressions

Operator precedence is a very important aspect of writing accurate algorithms. Precedence of operators, from a high level, is shown in Table 4-4. Note that binary operations are left out of this list. Going through this list, it becomes quite evident that ignoring precedence in your expressions can have dire consequences.
Table 4-4

Operator Precedence

()

Code enclosed in brackets are evaluated first.

**

Exponents are evaluated next.

*, /, //, %

Multiplication and division are evaluated next.

*, -

Addition and subtraction are evaluated next.

==, !=, >, >=, <, <=, is, is not, in, not in

Comparison, identity, and membership are evaluated next.

not

Logical operator not is evaluated next.

and

Logical operator and is evaluated next.

or

Logical operator or is evaluated next.

The following example demonstrates operator precedence:
1 + 2 * 10 == 30
What is the result? Would this equation as a whole return True or False? Most people familiar with mathematics will correctly point out that 1 + 2 * 10 == 30 will never yield True. Because multiplication takes precedence over addition, the value is 21, not 30. You can solve this by adding brackets around the 1 + 2. In the following example, the equation in the brackets takes precedence over the multiplication, and now (1+2)*10 is actually equal to 30:
(1 + 2) * 10 == 30

Proving that the logical operator and takes precedence over or is a bit more tricky and not as obvious. The following equation should do fine. When you execute it, you will see the result is True.

Type the following command:
print(True or False and False)
Because and is evaluated before or, you have the following evaluation pattern, where the bold portion is executed first:
True or False and False
The bold portion (False and False) is equal to False, so you can rewrite this as True or False.
True or False == True

Hence the end result is True.

If or was executed first, the following would happen:
True or False and False
You know that the bold portion, True or False, will yield True, so you just rewrite it in your algorithm as True for clarity. True and False will yield False.
True and False == False
With or taking precedence, you get the result as False. But since running the equation returns True, you can safely conclude that and takes precedence over or. If you really need to let the or portion execute first, just wrap it in brackets:
print((True or False) and False)

Bitwise Operators

I won’t go into bitwise operators. Even though it is a very important concept, it is beyond the scope of this book to teach you binary calculations. I advise you to look into it once you understand the basics of programming.

Scope and Structure of Python Code

Scope is a rather simple but very important aspect in programming. Scope refers to the visibility of a variable under certain conditions. These conditions can be local, enclosed, global, and built-in. I will discuss scope here, but you really start handling it once you see more code.

Local Scope

Local scope refers to any variable that has been declared within a function or a class. We will deal with classes in the next section. These variables cannot be accessed directly, without referencing the class or function, by any code running that class or function.

Enclosing Scope

This scope specifically refers to nested functions, which are functions within functions. If you have a function within a function, then the inner function’s scope includes that of the outer function’s scope.

Global Scope

Global scope occurs when a variable is declared at the top level of a file. This means the variable is available to all scripts that import that file. Global scope variables are best avoided when possible. Due to its nature where it can be edited from anywhere, it is not good to rely on the accuracy of that variable.

Built-in Scope

These are built-in values provided by Python, which are loaded into the system whenever the system executes. This includes functions, among other things.

Control Statements

There are dedicated control structures that use comparison and logical operators to influence the state of your program. You will learn about them in this section.

If Statements

You use if statements when you need to execute a specific codeblock based on certain conditions. An if statement takes a logical expression and evaluates it, using equality operators, to obtain a value of True or False. It can also take a single Boolean value which will equate to True or False. Using the if statement, we link blocks of code to certain conditions, and we execute that code if those conditions are met.

An if statement starts with the word if, followed by an expression consisting of comparison operators and logical operators, and ends with a colon.
if temperature < 15 and rain == True :
    return 'The weather will not be great today'
Some important things to remember:
  • All code nested with four spaces belongs to this if statement.

  • Remove the spaces and you are out of the if statement’s code block.

So, an if statement is followed by an indented code block of at least one line of code. All the code belonging to this if statement should be at that level of indentation. The if statement can also contain two other parts. One is elif, and the other is else. Elif is a contraction of “else if” and you use it if you need more control over what block of code should be executed. In your program, you may want to execute a different block of code for every day of the week. Using elif in this case is more optimized than running seven different if statements, plus it tells us that the whole code block is related, which increases readability.
if day == 'monday':
    some code
elif day == 'tuesday':
    come code
elif day == 'wednesday'
    some code
    # and so on.....
There will also be instances where your if statement does not evaluate to True, and if it has elif’s, that they did not evaluate to True either. Even if none of your code blocks linked to your conditional statements have executed, you may still need it to execute something. For that you use the else statement.
if 1 == 2:
    print('1 is equal to 2')
else:
    print('1 will never be equal to 2')
Let’s look at another example. In Listing 4-8, you can see how the if statement first evaluates whether money_in_wallet is less than the price, then it evaluates whether the amounts are equal to each other, and then if money_in_wallet is more than the price. It bases its decision of what to print on the outcome of these evaluations.
price = 30
money_in_wallet = 25
change_left = 0
if money_in_wallet < price:
    print('You do not have enough money')
elif price == money_in_wallet:
    print('You have the exact amount')
elif money_in_wallet > price:
    change_left = money_in_wallet - price
    print('You have ' + str(change_left) + ' left')
Listing 4-8

if/elif example

Play around with the code in Listing 4-8, changing the money_in_wallet value to 30, and then to an amount higher than 30.

If statements can also be a singular Boolean value:
if True :
    print('Hello')

In short, if and elif evaluate different expressions (or mathematical equations), and else executes if all of the preceding if and elif statements linked to that else statement failed.

Loops

Loops are structures that allow us to do a task repeatedly. This repetition of tasks is based on certain conditions. Loops are often used to iterate over lists, tuples, and dictionaries. There are two loops in Python, the while loop and the for loop. Some languages have even more loops.

While Loops
While loops are executed as long as the expression in them is true. This makes while loops best suited for when you are not sure how many times the loop must execute, and you have a logical algorithm that determines the amount of times it can execute.
while expression : # statement declaration and expression ended with a colon
     run this code # All subsequent code linked to the while statement is indented.
     run this code

The while loop will execute as long as the expression is equal to True. There is a second way to exit a loop called a break, which we will look at later in this chapter.

Let’s look at two actual examples. The first example prints numbers from 0 to 9. If you do not add a mechanism to exit the loop, the loop will keep on running. On the first line in Listing 4-9a, you set a variable called number to 0. On the second line you start your loop. You can consider the variable number as a control variable, as it controls how many iterations the loop will execute. On the second line is the condition, where the loop will execute while number is smaller than 10, and then on the fourth line you update the variable to ensure that your loop stops execution.
number = 0
while number < 10:
     print(number)
     number += 1
Listing 4-9a

Simple while loop example

It is very important to have a control variable that will stop the execution of your while loop. If you do not have this, your loop will keep on executing and become what is called an infinite loop. The biggest drawback of an infinite loop is that the rest of your code does not get executed, rendering your software useless.

Listing 4-9b shows a more complex example. Here number is still your control variable, but the condition governing the amount of times the loop will execute is more complex. You are not just evaluating whether number is smaller than 10; you also consider whether number multiplied by 10 is smaller than 50.
number = 0
while number < 10 or number * 10 < 50:
     print(number)
     number += 1
Listing 4-9b

Complex while loop example

For Loops

For loops are used to iterate over lists, tuples, dictionaries, and also strings, making a for loop best suited for when you have a fixed set of data to iterate through. The syntaxes to iterate over a list, tuple, dictionary, and string are mostly the same in a lot of respects, but there are different variations in syntax or ways to apply them, which we will cover here.

A for loop’s anatomy is very similar to that of a while loop:
for expression:
    code
The use case for a for loop is different than a while loop so its expression will look different. In general, a for loop operates on a structure that is iterable. Iterable means you can iterate, or step through its data, like a sequence or a map. You will recognize the in keyword in the example below. It is the keyword you use to test for value membership in arrays and was mentioned in the section about operators.
students = ('Justin', 'Ron', 'Andy', 'Jonathan')
for name in students:
    print(name)
Here is a slightly more complex example:
for name in students:
    if name == 'Ron':
        print('Found Ron')
Dictionaries are iterated over differently from lists, tuples, and strings, due to the fact that they possess a key. Since a map is also an object, it has a function called items(). You use this function to extract the items from the map. You can also specify a key in your for loop if you want to see the key.
map_of_values = {'name': 'Andy', 'surname': 'Bieber', 'marital_status': 'married', 'country': 'Great Britain'}
for key, value in map_of_values.items():
    print(key + ' ' + value)
List Comprehension (Shorthand Loops)
Python also has something called list comprehension . This is a list that creates itself based on an internal loop. This loop returns a list. Consider the code in Listing 4-10.
# The list we will be iterating over.
numbers = [1, 2, 3, 4, 5]
# Function that will be called in the loop
def multiply(amount):
    return amount * 10
# The loop is wrapped in square brackets, and that list is returned to the variable
# called 'results'.
results = [multiply(value) for value in numbers]
# This will contain a new list, where each value in the old list will be multiplied by 10.
print(results)
Listing 4-10

List comprehension

Continue and Break

Loops do have some more tricks up their sleeves. Three important things to look at is how you can nest loops and how to use continue and break clauses inside your loops. Nesting loop are not always ideal, but sometimes they are needed to achieve your goal. Let’s create a complex list. This list will have complex values as items, as opposed to just having integers or strings. This example has dictionaries as items. Note that continue tells the system to stop executing subsequent code and go back to the start of the loop and break tells the system to stop executing subsequent code and to leave the loop. This is demonstrated in Listing 4-11.
people = [ {'name': 'ron', 'position':'middle'}, {'name':'nico', 'position':'bottom'}, {'name':'andy', 'position':'top'} ]
# One example using continue.
for person in people:
     for details_key, details_value in person.items():
         if person['position'] == 'bottom':
             continue
         print details_value
# One example using break.
for person in people:
    for details_key, details_value in person.items():
        if person['position'] == 'bottom':
            break
        print details_value
Listing 4-11

Continue and Break

The sets of loops are very similar. Each set has two loops. The first loop (the outer loop) iterates over the list called people and sends back the values in the list one by one. The values it sends back are dictionaries. Then, in order for you to iterate the dictionary, you iterate over the values that the list sends back in your second loop. This line person['position'] == 'bottom' just means that the variable person is a dictionary (since you received person from the list, and the list contains dictionaries). If the value at the key called position is equal to ‘bottom, then run the continue clause. This ignores whatever is next in the loop and goes back to the initial loop and carries on iterating. The second example uses break. This is exactly the opposite, and it breaks out of the control structure, effectively stopping the loop from iterating, and continuing outside the scope of the loop.

Functions

Functions are small reusable blocks of code, which once defined can be accessed by using their name. They consist of a name, a parameter block, which is always between round brackets (), and a function body. All the concepts you have encountered in this chapter can be applied inside a function. You can, and will, write your own functions, but Python comes with built-in functions as well. You encountered a built-in Python function in Chapter 3 called print. Here is an example of the print function again:
print('Hello world')

I will not dig into all of the functions that Python provides, but I will introduce more functions as we proceed through the chapters of this book. As a rule of thumb, if you want Python to achieve something small, see whether Python has a function for it first.

Custom Functions

Custom functions are something you will write constantly. We write functions for a few reasons.
  • It helps us reuse code. We can call the function numerous times.

  • It encapsulates tasks, making our code easier to understand.

Functions should be designed to do one of three things:
  • Print something to the screen (not always recommended).

  • Return data so that the code calling the function can use it.

  • In the case of classes, an object can update the internal state of the class. These functions can still return data as well.

Principles of Function Design

When designing a function, it is very important that the function achieves one goal, and one goal only. For instance, if you write a function that calculates VAT, then that function should only calculate VAT. If you first need to sum a bunch of values and then calculate VAT, you should have two functions: one to sum the values and one to calculate VAT on that summed value. One function to sum the values and then calculate the VAT amount sounds innocent enough, but detracts from the understandability of the code and opens you up to bad coding practices.

The Anatomy of a Function

A function’s identifier is the word def, followed by the function name, parameter list (which is between the round brackets), the function return type (which is optional), and the function body. When you need to get data into your function to do operations on, you pass it in via the parameter list.
  • The keyword def is the statement declaring a function that follows it.

  • The function name is used to access the function.

  • The parameter list, between (), takes external data and hands it to the function.

  • The optional return type tells the function that it may only return data of that type.

The parameters are also called arguments, and the order in which they are entered into the parameter list, matters. The order in which they are specified when the function is designed, is the order in which you should provide them to the function when the function is used. The function name, and arguments, should be meaningful names reflecting their purpose. The parameters, or arguments, are values that you receive from the program, and many times this is data provided by user input. This data will be passed into your function to do calculations on. The result of the calculations will be passed back to the system, helping you achieve the goal of manipulating the program’s state.
Def function-name(argument1, argument2) -> function_type_to_return :
      Code you have written.
      Code you have written.
      Code you have written.
      Return or print

Should Functions Return or Print?

This question is a design issue. It depends on what the function needs to do. In general, I prefer letting my functions return data instead of printing it, as displaying data should not necessarily be the job of that function, plus the function calling your function may need to do some more calculations on the returned data, something it cannot do if you print data to screen. There is another option, though, which you will come across when you look at classes.

Example Functions

Listing 4-12 shows a simple function that just prints a name and surname. This is just to illustrate how parameters are passed into a function.
def name_and_surname(name, surname):
      return 'Your name is ' + name + ' and surname is ' + surname
print(name_and_surname('Nico', 'Loubser'))
Listing 4-12

A simple function

Listing 4-13 shows a slightly more complex example that does some calculations.
# Import the datetime library
import datetime
"""
Here the function name, calculate_age, reflects what it will be used for.
The one argument it accepts, year_born, is named after the value you want.
It should always return an integer due to the return type being specified
"""
def calculate_age(year_born) -> int:
      current_year = datetime.datetime.now().year
      # This function returns a value. Y
      return (current_year - year_born)
# Here we are calling the function. It returns the age and stores it in the variable
# called 'age'
age = calculate_age(2000)
# Print age to screen
print(age)
Listing 4-13

A more complex function

Let’s have a little test. You can either try to create it yourself, taking into consideration that you may not have encountered all of the aspects inside the solution, or you can read the question and see how the answer relates to the question. You have a person (and you will provide their name as a function argument) who has x amount of money (which you will provide in the function’s second argument) in their wallet. They want to buy a cup of coffee that costs $4. Write a function that accepts their name and available money as arguments. The system should determine whether they have enough money or to buy coffee or not. The function should return the string “Janet (or whatever name you gave her) has enough money” if she has more than $4, or “Janet does not have enough money” if she has less than $4.

Here is a possible solution:
def enough_funds(name, funds):
      if (funds >= 4):
           return name + ' has enough money'
      else:
           return name + ' does not have enough money'
# To run this function
print(enough_funds('Jane', 3))
print(enough_funds('Jane', 10))

Here the custom function called enough_funds returns the result to a built-in Python function called print(), which you have encountered already. enough_funds does the calculations, and print() returns the answer to your screen.

Parameter Type-Hinting

When defining parameters in your function, you can specify the type of parameter you want. This is called parameter type-hinting . For instance, if you want age to come through as an integer, and not ever as a string, you define your function as follows:
def set_age(age: int)

Default Parameter Values

If you do not provide values for your parameters, your program will not execute and it will complain that some arguments are missing. You can provide default values in situations where you are unsure whether you may receive all parameter arguments or not. Although this is not a good method to ensure your code does not break when data is missing, it is a great way to ensure you have one or two default values that do not need to be provided if they are not available.

The function in Listing 4-14 will either print directly to screen or return the value to the code calling this function. You use a parameter with a default value called return error, which is set to True, in the parameter list to indicate that it should always return its data, unless you provide a different value for the return error parameter in the argument list.
def log_error(error, return_data=True):
      if return_data:
            return error
      else:
            print(error)
# Equivalent calls, prints directly to screen
log_error('error message 1')
log_error('error message 2', True)
"""
Returns its variable to the code calling the function, where you can do more operations on it or in this case, just print it
"""
error = log_error('error message 3', False)
print(error)
Listing 4-14

A function with a default parameter value

Variable Parameters

If you do not know how many parameters you will send to your function, you can use variable parameters. This is handy if your function is expecting x amount of similar data, but you do not know how many values there will be. To illustrate, look at the following function in Listing 4-15. The variables are added as individual variables, but inside the function they are treated as a list. The variable parameter is prepended with an asterisk, *.
def sum (*numbers):
      total = 0 # initialise total to 0
      for number in numbers:
            total += number
      return (total)
print(sum(10,56))
print(sum(1,2,3,4,5,6,7,8,9))
Listing 4-15

Variable parameter example

Named Keyword Arguments

At the start, I mentioned that the order of the arguments matter when they are passed through to the function. This can be circumvented by using the name of the argument in the function call. In this case, Python does not care about the position of the argument, but at the name. See Listing 4-16.
def details(name, surname, mobile_number):
     print(name + ' ' + surname + ' mobile :' + mobile_number)
# Run these two functions and see how the first one basically breaks the output because
# the order is wrong
details('Johnson', '0123456789', 'Don')
details(surname='Johnson', mobile_number="0123456789", name="Don")
Listing 4-16

Named keyword arguments

Classes and Objects

Python is an object-oriented programming language. You have read the terms “object” and “class” a few times in this chapter and have encounter them briefly with sequences and maps. A class is the code you wrote, and an object is that class that has been loaded into memory as a useable object. A class encloses, or encapsulates, behavior and data. Let’s take a real-world example of a dog. As a class, a dog encapsulates the following behavior and data (attributes):

Behavior
  • Barking

  • Running

Attributes
  • Name

  • Breed

  • Color

Your dog class can be written as shown in Listing 4-17. We will go through more examples after this example.
class Dog:
    def __init__(self, name, breed, color):
        self.name = name
        self.breed = breed
        self.color = color
    def bark(self):
        print('woof')
    def run(self):
        print('Running')
# End of the class
Listing 4-17

The Dog class

Creating a new object is called instantiation and looks like this:
variable = Class()
So, in your case, your dog object is instantiated like this:
dog = Dog('Fluffy', 'Poodle', 'white')
You can now run the encapsulated functions on the dog object using the dot operator:
dog.bark()
When we instantiate a class, we create an instance of that class via the constructor. It is optional to include your own constructor, and it is dependent on the class you created. The constructor is defined inside the class using the __init__ function. All classes that have a constructor use this method.
def __init__():
This function can be used to declare and initialize all of the supporting classes and variables that it needs to create this class. You can see a constructor as taking materials and constructing your class into an object using those materials. Say you have a class called Rectangle . You create its constructor as follows:
def __init(self, width, height):
    self.width = width
    self.height = height

Just to rehash, classes are used to create objects. An object is in instance of a class, and the variables passed into the instance are called instance variables. A class can be seen as the code that was written down when you created the class. An object is a class after it has been created and loaded into memory; this is also called instantiation. An object is referred to as an instance of a class. An object, just like an integer, string, and so on, is assigned to a variable. So, in short, you write a class and then instantiate the class into an object.

The Anatomy of a Class

Let’s go through a class definition.
"""
class is the identifier for a class declaration. We use the 'self' keyword in order to
have access to the methods and properties inside a class. You do not need to pass the keyword 'self' in when you call the class or its functions.
Constructor. A very important aspect, a constructor is created using __init__().
The __init__ function runs on every instantiation of the class into an object, and is a great place to get data and dependencies into your object.
"""
class MyClassName:
 # Optional constructor. Gets called automatically, and always takes 'self' as a
 # parameter. The 'self' keyword allows us to access the properties and
 # functions inside the class
 def __init__(self, value):
      # property is an instance variable of MyClassName
      self.property = value
 # Any amount of functions can follow
 def function1(self):
      actions
      actions
 def functionN(self, param):
      actions

Instantiating the Class

The round brackets in an instantiation refer to the constructor. If the constructor has parameters, then you pass it in here. You do not pass self because it is already implied.
a_class = MyClassName(value)
a_class is now a variable of type MyClassName and has access to the functionality inside it. To access that functionality, you use the dot notation:
# Accessing a function
a_class.function1()
# Accessing a property
a_class.property
Let’s look at an actual example of a class in Listing 4-18. In it, you create a class that calculates the amount of money left in a bank account.
class Funds:
    # These two variables are in the class scope. Initialised to 0
    total_expenses = 0
    total = 0
    # Constructor. We populate it with the total amount available when we
    # construct the object
    def __init__(self, total):
         self.total = total
    # Stores the money we have spend
    def set_expense(self, expense):
          self.total_expenses += expense
    # Calculates the amount of money we have left
    def get_funds_left(self):
         return self.total - self.total_expenses
# Code implementation part. Note that you do not have to put 'self' when calling the
# functions
funds = Funds(200)
funds.set_expense(10)
funds.set_expense(15)
# Prints the amount left to screen
print(funds.get_funds_left())
Listing 4-18

The Funds class

You can make this class a bit better, though, so see Listing 4-19. It is not very reliable that you can just deduct money and take your total expenses into the negative. You will also add code to handle the errors a bit better. Keep an eye on the indentation as well; I have exaggerated the indentation to make this a bit easier to follow.
class Funds:
    total_expenses = 0
    total = 0
    # Added a variable to hold any error messages we may encounter
    error = '';
    # Constructor. We populate it with the total amount available
    def __init__(self, total):
        self.total = total
    # Sets the money we have spend
    def set_expense(self, expense):
        # Checks whether we have enough money left to give out
        if self.get_funds_left() > expense:
            self.total_expenses += expense
            return True  # Exit the function
        # If we don't, set the error message and return false. Returning false here
        # allows us to detect a failed expense deduction
        self.error = 'Out of funds'
        return False
    # Calculates the amount of money left
    def get_funds_left(self):
        return self.total - self.total_expenses
    # Returns the error
    def get_error(self):
        return self.error
# Code implementation part
funds = Funds(20)
# Check whether the set_expense function returns True or False. Using the 'not'
# keyword indicates false. Then we print the error.
if not funds.set_expense(10):
    print(funds.get_error())
if not funds.set_expense(2):
    print(funds.get_error())
if not funds.set_expense(15):
    print(funds.get_error())
# Prints the amount left to screen
print('You have £' + str(funds.get_funds_left()) + ' left.')
Listing 4-19

The improved Funds class

Inheritance

We will look at inheritance in this chapter. Classes can be structured hierarchically, in an is-a relationship. This is often called a parent-child relationship. The child will inherit behavior from the parent. Let’s say your parent class has a function called email(). The child can use that email() function as it is, or override the parent’s function with its own function. This is super easy; just redeclare the exact same function in the child class and give it another body.

Let’s say you have a bicycle shop and need to create software to order bike parts. One way to model the classes for bicycles is shown in Listing 4-20. You have a top-level class called Cycle. Below is what the Cycle class may look like. This is almost a template with the functionality of what the children, the classes that inherits from it, should look like.
class Cycles:
    def set_as_assembled(self, is_assembled):
            self.assembled = is_assembled
    def get_wheel_count(self):
        return self.wheel_count
    def who_am_i(self):
        return  'I am the original function'
# Note the change in how we define our class. We added round brackets and placed
# the parent it inherits from name in between it.
# Monocycle IS-A Cycles and inherits from Cycles
class Monocycle(Cycles):
    # Override the parent function with a similar function that behaves differently
    def who_am_i(self):
        return 'I am overriding the parent function'
    wheel_count = 1
# A second child class. Bicycle IS-A Cycles
class Bicycle(Cycles):
    wheel_count = 2
# A third child class. Tricycle IS-A Cycles
class Tricycle(Cycles):
    wheel_count = 3
monocycles = Monocycle()
print(monocycles.get_wheel_count())
cycles = Bicycle()
print(cycles.get_wheel_count())
# In this section we show how the function overriding works
print(monocycles.who_am_i())
print(cycles.who_am_i())
Listing 4-20

Inheritance

In this block of code, you have three classes of type cycle, and all three have an is-a relationship with the parent class called Cycle. Notice how neither Monocycle, Bicycle, nor Tricycle objects implement a get_wheel_count() function, yet they can all use that function. They all inherit it from their parent class called Cycle. Also of interest is that the function called who_am_i is overridden in the Monocycle class, but not in the Bicycle class. In the last two lines of Listing 4-20, you can see how Monocycle uses the overridden code and Bicycle the original code.

Polymorphism

Polymorphism is the ability of an object to take on many different forms. It can be a complicated subject but is often explained in the sense of animals. Listing 4-21 shows a quick example. You have a parent class called Pet, and Pet has two children, Cat and Dog. The function getSound takes a parameter of type Pet, of which you have two, a cat and a dog. Polymorphism resolves the type for you.
# Parent object
class Pet:
    def sound(self):
        # Pass is called a null statement in Python, and nothing happens when Python encounters it
        pass
# IS-A pet
class Cat(Pet):
    def sound(self):
        return 'Meow'
# IS-A pet
class Dog(Pet):
    def sound(self):
        return 'Woof'
# Function getSound receives a Pet as a parameter,
# but we are not specifying what kind of pet. Polymorphism is used to resolve this
def getSound(pet: Pet):
    return pet.sound()
print(getSound(Dog()))
print(getSound(Cat()))
Listing 4-21

Polymorphism

In Listing 4-21, polymorphism is achieved by the is-a relationship as well, where the parent can take many forms via its children. This allows us to use the same functions on the children but let those functions yield different outputs.

Composition

Composition is an easy-to-understand design concept, and it’s not related to Python functionality per se. It is a design aspect. This will be covered in a later chapter.

Magic Methods

Magic methods are methods that you declare inside a class, and write code for, but you never call explicitly yourself. You can find them in other programming languages like PHP as well. They act like a sort of a catch-net for certain scenarios, so when Python encounters the scenarios linked to the class they were declared in, it knows what magic method to execute. There are quite a few of them. You can see three interesting ones in Listing 4-22. An explanation of what the different magic methods are doing is provided above the function.
"""
Class Member consists of a constructor that sets a name. We will use this to demonstrate the magic method for operator overloading
"""
class Member:
    def __init__(self, name):
        self.name = name
"""
Class Group contains three magic method. You have already encountered __init__. This is the constructor, and is called whenever you instantiate an object.
"""
class Group:
    def __init__(self):
        self.members = []
    """
__add__ is a magic method that does operator overloading. Overloading means we add new behaviour to an already existing operator or function, and call that new behaviour under specific conditions. In this case it overloads +, and inside the body of the __add__ function we add the logic of what the overloaded + must do. We need to specify in the parameter list, as the second parameter, what is to be expected on the right hand side of the + sign. In this case it is something of type Member. All we want to do when we add something of type Member to type Group is to take Member's name parameter and add it to Group's list of names.  Whenever Python sees a Group object followed by a plus, it will run the __add__ function.
    """
    def __add__(self, x: Member):
        self.members.append(x.name)
    """
__str__ is a magic method that returns a string whenever you treat your object as a string. Whenever I say for instance do this. print(MyObject), then MyObject will run the __str__ function.
    """
    def __str__(self):
        return ','.join(self.members)
group = Group()
member1 = Member('nico')
member2 = Member('john')
group + member1
group + member2
print(group)
The output of this function code is: nico,john
Whenever a Group object encounters a + sign, for instance : 'group + member1', it will run the __add__ function which we added outselves. This takes the member object to the right of the + sign and add it to an array maintained inside the Group object.
Listing 4-22

Magic methods

Exceptions

Exceptions occur when your system encounters an error or goes into a state from which it cannot recover. You will often encounter exceptions. Exceptions can be system generated, but you can also generate them yourself, as well as write them yourself. Exceptions are cases where your software encountered a problem that was not anticipated and should not be handled in a normal way. Attempting to divide a number by 0 is a very common case that throws an exception.

Let’s take the example of a system that orders wheels for bicycles. If you order wheels, and there are wheels in stock, your software should place the order and return with a value of True. If there aren’t any wheels in stock, the software should not place the order and should return with a value of False. Sometimes, a software developer will let the system react with an exception, but this is wrong in my opinion. Wheels being out of stock is a predictable situation and can be handled normally. But let’s say, for instance, your system cannot communicate with the system that places the order for the wheels. This would be a great place to put an exception. Not being able to place an order because a sub-system was not found is an “exceptional” case.

Think carefully about the following when using exceptions:
  • Is it a unique enough case that I should create my own exception? Can it be dealt with better in another way?

  • When an exception occurs, must the system be allowed to go on, or should it completely abandon execution? Some errors may leave the system in a unusable state, such as not being to find the wheel ordering system, and the system cannot continue, it can only retry or stop.

  • Log your exceptions to a logfile, or create a way to notify someone. Exceptions mean something went wrong and people may want to know about it.

The Anatomy of an Exception

Exceptions are handled by try/except blocks. The try/except structures are wrapped around code blocks that may potentially throw an exception. As with all Python code, the code blocks are indented one level deeper than the try/except code around it. At a basic level it says try this code, but if it throws an exception, then do something else. A very basic construct will look like this:
try:
    Some code that throws an exception
except:
    Do a recovery action here. All exceptions will be handled here.
finally: #optional
    This action will always be executed.
A more complex example will look as follows. This example specifies which specific exceptions are to be caught:
try:
    Some code that may throw an exception
except specific_exception as e:
    Handles specific_exception
except BicyclePartSystemNotFoundException as e:
    Handles BicyclePartSytemNotFound exception
except:
    Handles all other exceptions
finally: # Optional
    This action will always be executed.

Raising an Exception

You can raise Python’s built-in exceptions in your code. There are quite a number of exceptions, which I will not list here. Raising an exception is as simple as locating the area of code where the exception should be raised and using the raise keyword , as follows:
raise Exception('custom optional error message')
# It is preferable to always have a meaningful error message, as it will help you
# debug your problem better.

Catching an Exception

You can catch an exception anywhere in your code. It does not need to be where the exception gets raised. At the point of catching it, you should define clearly what should happen to the execution flow of the program. Let’s take the division-by-zero error. Should your system be able to continue if it tries to do division, but encounters a 0 and cannot do the division? This is one of those questions you have to ask yourself when you create your software. You will encounter questions like this a lot. But you have options. You can, for instance, log the error to a file or email it to yourself. You may even take the option of a default value denominator, as shown in Listing 4-23. In Listing 4-23, you take an amount of money and calculate how much each friend gets. If friends are 0, then the owner keeps all the money.
wallet = 100
friends = 0
try:
    per_friend = wallet / friends
except ZeroDivisionError as e:
    # if friends are 0
    per_friend = wallet / 1
print(per_friend)
Listing 4-23

Catching an exception

Writing an Exception

Sometimes you may want to write your own exceptions. It is very handy to have custom exceptions because it helps separate different error states and helps you handle errors on a much more specific level. Let’s have a look at writing, raising, and catching your own exceptions in Listing 4-24a.
"""
Here is a very simple exception. Note the class declaration. Its parent class is the
built in class called Exception. Extending on the parent class 'Exception' helps us create a class with an IS-A relationship with exception. Pass is a null statement, and nothing happens when Python encoutners it.
In this instance, we also pass the MyError exception a null statement. We do however still get the benefit of a custom exception, however we are not overriding any built in Exception functionality.
"""
class MyError(Exception):
    pass
# Test the exception
name = 'pierre'
try:
    if name != 'john':
       raise MyError('Name is not equal to John')
except MyError as e:
    print(repr(e))
Listing 4-24a

Writing an exception

Imports

In a larger system, your classes will all be in their own separate files and directories, where they can be imported from. They are called packages. A package is a collection of related scripts that is grouped together in a directory. For a package to be importable, it needs a file called __init__.py in that directory.

Why do we need importing? A Python script can only see itself, and because of this we need to import files to allow it to access other functionality. This also keeps your codebase clean and allows you to separate your files into directories where it makes sense for them to live. After we have moved our files around, we need to make them available to our main.py script. Most systems will have one central point of execution, where all the execution calls are handled and the correct objects are built. In our case, it is main.py.

You will explore imports by using a custom exception file. In the directory where main.py is, create a new directory called errors. Create an error.py file inside this directory containing the code found in Listing 4-23. Inside the errors subdirectory, which should now contain a file called errors.py, create a new file called __init__.py and leave it empty. There should be two underscores on each side of the word init. The __init__.py file tells Python that the errors directory is a package and can be imported from. Finally, you need to add an import statement to your main.py script , which will take this form: from package.filename import class. See Listing 4-24b.
"""
This is the same code as in Listing 4–24.a, Wit the exception that following line now lives in /errors/errors.py.
class MyError(Exception):
    pass
"""
from errors.errors import MyError
# Test the exception
name = 'pierre'
try:
    if name != 'john':
        raise MyError('Name is not equal to John')
except MyError as e:
   print(repr(e))
Listing 4-24b

Imports

Your code should be able to find the correct package to import from and execute the code without a problem. That was quite easy. In big systems and especially old systems, there can be hundreds or even thousands of files in different folders. Keeping them separated in logical divisions is very important. I won’t be going in depth with all the intricacies of importing a file in Python, but I wanted to show a more interesting way where you created a package.

If you run the code now, it will import the Exception class from a different directory.

Static Access to Classes

You can access functions and elements on a class level. That means on a level where the class has not yet been instantiated into an object. This is valuable for a lot of reasons. For instance, you may need some functionality that does not require you to construct a whole object. Take the following example. When you use a class statically, there is no object to which the keyword self can refer. For these reasons, when you want to create a static function, you need to design for it. See Listing 4-25.
class VAT:
    vat_rate = 15
    # This function can be used statically. No 'self' is being passed through.
    def static_get_vat_price(price):
        return price * (1.0 + VAT.vat_rate/100)
    # This function cannot be used statically, as it is reliant on self, and
    # self only
    # exist when the object gets created
    def get_vat_price(self, price):
        return price * (1.0 + VAT.vat_rate/100)
# These two examples will work. No object instantiation has happened
print(VAT.vat_rate) # Prints the vat rate
print(VAT.static_get_vat_price(50)) # Calculates the amount after after VAT
# This wont work without object instantiation and will throw an error, the parameter self does not exist
print(VAT.get_vat_price(50))
Listing 4-25

Static access

So basically, whenever you need to use the self keyword, or are completely reliant on a constructor, you cannot access functions statically. But variables are a different story, as long as they do not need to get assigned via the self keyword.

Cheat Sheet

Scope

Always nested with four spaces, or one tab, but not both. The deeper the scope, the deeper the nesting.
if True:
     if True:
         if True:
             print('all true')
print('In the same scope as the first if')

Variables

Variables are to the left of the assignment operator.
  • String = ‘string’ or “string”

  • Integer = Numbers like 18

  • Float = Numbers with decimal points like 24.50

  • Boolean = True or False, which are always capitalized.

  • Object = Your own custom object

Arrays

Lists

  • Can be resized

  • Number-based indexing starting at 0

my_list = [1,4, 'my value']

Tuples

  • Cannot be resized

  • Number-based indexing starting at 0

my_tuple = (2,4, 'my tuple')

Dictionaries

  • Can be resized.

  • Indexed by an explicit key. Key and value separated by a colon.

my_dictionary = { 'index1':12, 'index2': 'value 2}

Control statements

if

if expression:
      Something
elif another expression:
      Do something else
else:
      Do something else

while

Runs while a condition is equal to True:
While mathematical expression is true:
      Do something
      Do something to exit the loop
      (Either alter the expression, or use break)

for

In general, used to iterate over sets of data, like lists, tuples, and dictionaries.
for value in set_of_data:
      print(value)

Functions

def function_name(parameter1, parameter2):
      function body
      function body
      return out of function
Calling the function:
  • function_name(variable1, variable2)

  • function_name(parameter2=variable2, parameter1=variable1)

Type hinting:
  • function_name(variable: int, variable2: str):

  • function_name(variable: int, variable2: str)->str:

Classes

class MyClass:
      properties
      # Optional constructor
      dev __init__(self):
            constructor body
      # Class functions Self gives us access to the class
      def class_function(self):
            function body
      # Can be called statically
      def static_function():
            function body
Instantiating a class:
  • object = ClassName()

Calling a function on an object:
  • object.function()

Calling a function on a class:
  • Class.static_function()

Exceptions

Catching an Exception

try:
      Code
except:
      Handle exception
finally: #Optional
      This code will run regardless

Raising an Exception

Use the raise keyword plus the name of the exception:
raise exceptionName
raise exceptionName('optional error message')

Creating an Exception

# Declare a function using the class Exception as parent
class myException(Exception)

Import

from package.to.filename import class

Reference

A good place to read about Python development: https://docs.python.org/3/tutorial/index.html.

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

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