# Python

# Basics

# PyCharm Tips

TIP

Ctrl d - duplicate line

TIP

Windows alt c - Show last changes

TIP

right button -> remanufacture - To replace all the equal variables at the same time

TIP

TODO LIST - # todo finish this later

TIP

windows + alt + t - List actions for the variable like while, return, if, etc....

ctrl shift p - Python environment

#%% - run jupiter on Visual Studio code

# Python Essential Training

Bill Weinman

# 2️⃣ Language Overview

# Python Anatomy

# SheBang Line

its called a shebang line or sometimes hashbang. The shebang line is a common pattern for Unix based systems. It allows a script to be invoked from the command line. There are two parts to the shebang line. The hash mark and the exclamation sign must be the first two characters on the first line of the file. These marks together form the shebang. An exclamation mark is sometimes called a bang, so hash plus bang is where the word shebang comes from. There must be no lines before this line, and no spaces before these first two characters. Essentially, these two characters must be the first two bytes in the file. After the shebang, is the path to the executable that will run your script, along with any other optional arguments. In this case, we're using the Unix env command to find the path to the Python interpreter. This is a common usage.

#!/usr/bin/env python3
1

Following the comments, the first line in the script is an import statement. This tells the Python interpreter to import a module from the library. The module may contain any combination of classes, functions, or other Python objects. In this case, its importing the platform module, which is used to display the Python version when we run the script. There are standard library modules or you may define your own. A Python script may have multiple import statements or you may specify multiple modules in one import statement. After the import statements, you find the rest of the script.

#!/usr/bin/env python3

import platform

print("This is python version {}".format(platform.python_version()))
1
2
3
4
5
This is python version 3.7.4
1

This is functionally the same as the other script. It calls this print function with the format and the Python version. You notice that the print function is actually called from inside a message function, and message is called from main, and then we call main down here at the bottom in this conditional statement. This is actually a very common pattern in Python. By having this conditional statement at the bottom that calls main, it actually forces the interpreter to read the entire script before it executes any of the code. This allows a more procedural style of programming, and its because Python requires that a function is defined before its called.

#!/usr/bin/env python3

import platform

def main():
    message()

def message():
    print("This is python version {}".format(platform.python_version()))

if __name__=='__main__': main()
1
2
3
4
5
6
7
8
9
10
11
This is python version 3.7.4
1

# Statements and Expressions

In a scripting language like Python, these definitions require further explanation. In Python, an expression is any combination of literals, identifiers, and operators. Generally, this means anything that returns a value is an expression.

This is an assignment. It assigns a value to a variable.

x = y

This is an operation. You notice it has an operator, the multiplication symbol, and so it returns the result of that operation, the result of that multiplication.

x * y

This is an aggregate value. In this case, its a tuple.

(x, y)

This is a simple value.

x

This is a built-in constant value.

True

And this is a function call. All function calls return a value in Python, even if the value is none, which is the absence of value.

f()


A statement is a line of code. It may be an expression, or it may be something like the import statement or a break or a continue.

In Python, a statement is a line of code. It does not require a semicolon at the end of the line. And so this import statement is a statement. This assignment, even though its also an expression, is a statement because its a line of code by itself. And then this print function, of course, is also a statement, and its also an expression because, even though we're not using the returned value from the function call, it still does return a value.

import platform

version = platform.python_version()

print('This is python version {}'.format(version))
1
2
3
4
5
This is python version 3.7.4
1

Python does not require a semicolon at the end of the line, although you may put more than one statement on a line. If I put a semicolon here and put that other print statement on the same line, I'll run it, you see we get the same result. This is possible, and its extremely rare. You almost never see this in Python. You'll far more often see one statement per line, like this.

import platform

version = platform.python_version()

print('This is python version {}'.format(version)) ; print('hello')
1
2
3
4
5
This is python version 3.7.4
hello
1
2

So statements and expressions are simple concepts in Python, and understanding their distinctions will help you write code that is clear and easy to understand.

# Whitespace and comments

Unlike most other modern scripting languages, whitespace is significant in Python.

In Python a block is delimited by indentation. There's no brackets or parentheses to match up just indents. And so these two functions, main and message, they have blocks which are associated with them and that's the code that comprises the body of the function.

import platform

def main():
    message()

def message():
    print('This is python version {}'.format(platform.python_version()))

if __name__ == '__main__': main()
1
2
3
4
5
6
7
8
9

If I wanted to I could say, if true, and make this part of a separate block, and we'll give it an else, print, not in the block, not in the if, not true, we'll just say not true. If I run this we get the line three, and if I say false and save that and run it now we get the not true. And so these are part of separate blocks that whole if is part of that other block but these are indented in another level and so they're part of a different block.

import platform

def main():
    message()

def message():
    print('This is python version {}'.format(platform.python_version()))
    if False:
        print('line 3')
    else:
        print('not true')

if __name__ == '__main__': main()
1
2
3
4
5
6
7
8
9
10
11
12
13
This is python version 3.7.4
not true
1
2

Comments in Python are introduced by a pound sign.

# this is a comment
# this is another comment
1
2

So in most languages whitespace is mostly ignored in Python its meaningful and you must be conscious of formatting as you write your code and in Python, comments are introduced by the pound sign or the hash sign and they end at the end of the line and again that end of the line is significant in Python.

# Using print()

The Python print function is simple and powerful and its probably a bit different than what you're used to in other languages.

Now if I want to print a variable value in the string, I'll use the format method. Which is actually a method of the string object. And so I put a placeholder in here with a pair of curly braces. And I say dot format. And I'll put a value up here, I'll make a variable. X equals 42. And I put that x in the format method. And when I save and run, you see now we get a 42 down there in our output. The format method is very powerful, and very flexible and we'll cover it in detail later in the course.

#!/usr/bin/env python3

x = 42
print('Hello World. {}'.format(x))
1
2
3
4
Hello World. 42
1

its worth noting that this is not a function or a method of the print function. This is a method of the string object itself. And so I can take this whole string object including the format call, and put it on a separate line and say s equals, and print s. And you see we will get exactly the same result when I save and run this.

#!/usr/bin/env python3

x = 42
s = 'Hello World. {}'.format(x)
print(s)
1
2
3
4
5
Hello World. 42
1

Now beginning with Python 3.6, this is not in all versions of Python three, just beginning with 3.6 and later. And what we have here is 3.6.3 if I remember correctly. We can take all this format stuff out, and not call the method like that. And instead, you use what's called an F string. So I put the letter F in front of the string, and then I simply put the x variable inside of the curly braces. And when we save and run, we see that that works exactly the same. So F stands for format, and the F strings are actually implemented by calling the format method. So it has all the relevant functionality from format.

#!/usr/bin/env python3

x = 42
print(f'Hello World. {x}')
1
2
3
4
Hello World. 42
1

This is how interpolation and formatting of strings works in Python three.

# Blocks and Scope

Python differs from many languages in how blocks are delimited. Python does not use brackets or any special characters for blocks, instead it uses indentation.

#!/usr/bin/env python3

x = 42
y = 73

if x < y:
    print('x < y: x is {} and y is {}'.format(x, y))
print('something else')
1
2
3
4
5
6
7
8
x < y: x is 42 and y is 73
something else
1
2

So blocks in Python are indented. And that indention is how the block is defined. They all have to be indented at the same level. There can be white space in between them. As long as they're all indented at the same level, they'll be considered part of the same block.


Now its worth noting that blocks do not define the scope of variables in Python. And so if I come up here and I make this true again and save and run and you see that that block is now being executed and I'll define another variable in here. I'll say z equals 112. And down here in my print statement outside of the block I'm going to say z is and when I save and run this you'll see that I get my result z is 112 and that's because even though the variable is defined inside of that block, its scope is still the same as the code outside of it. Blocks do not define scope in Python. Functions, objects, and modules do define scope in Python and you'll see examples of this later in this course. As we're talking about blocks its important to understand that the block itself is not what defines the scope in Python.

#!/usr/bin/env python3

x = 42
y = 73

if x < y:
    z = 112
    print('x < y: x is {} and y is {}'.format(x, y))
    print('line 2')
    print('line 3')
    print('line 4')

print('z is {}'.format(z))
1
2
3
4
5
6
7
8
9
10
11
12
13
x < y: x is 42 and y is 73
line 2
line 3
line 4
z is 112
1
2
3
4
5

So blocks in Python are indicated by indentation. And you'll see many more examples of this as you proceed throughout this course.

# Conditionals

Conditional statements in Python work in familiar ways.

#!/usr/bin/env python3

x = 42
y = 73

if x < y:
    print('x < y: x is {} and y is {}'.format(x, y))
1
2
3
4
5
6
7

Now there is a variant here which is not often used in Python but it is available. To put the print statement on the same line as the condition after the colon. In this case you don't get a block, you only get one line of code. In many cases that's fine and of course you save and run this you get exactly the same result.

#!/usr/bin/env python3

x = 42
y = 73

if x < y:print('x < y: x is {} and y is {}'.format(x, y))
1
2
3
4
5
6
x < y: x is 42 and y is 73
1

So el if is like else if and it requires a condition, its just like if only it happens in the case where the preceding if is not evaluated to true.

#!/usr/bin/env python3

x = 42
y = 73

if x < y:
    print('x > y: x is {} and y is {}'.format(x, y))
elif x < y:
    print('x < y: x is {} and y is {}'.format(x, y))
else:
    print('Do something else')
1
2
3
4
5
6
7
8
9
10
11
x > y: x is 42 and y is 73
1

In fact you can have more of them, you can have an el if equals. And if we make these two values equal, then save and run, and now the equals is executed. So you can have as many el ifs as you want to.

#!/usr/bin/env python3

x = 42
y = 42

if x < y:
    print('x > y: x is {} and y is {}'.format(x, y))
elif x < y:
    print('x < y: x is {} and y is {}'.format(x, y))
elif x == y:
    print('x = y: x is {} and y is {}'.format(x, y))
else:
    print('Do something else')
1
2
3
4
5
6
7
8
9
10
11
12
13
x = y: x is 42 and y is 42
1

So Python doesn't have a switch or a case statement and it can easily be argued that it does not need one which is why it doesn't have one because you can do exactly the same thing with a string of el ifs. And this is typically how that's done.

#!/usr/bin/env python3

x = 42
y = 42

if x == 5:
    print('Do 5 stuff')
elif x == 6:
    print('Do 6 stuff')
elif x == 7:
    print('Do 7 stuff')
elif x == 42:
    print('Do 42 stuff')
elif x == 112:
    print('Do 112 stuff')
else:
    print('Do something else')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Do 42 stuff
1

So conditionals in Python work as expected with if else and el if and it doesn't have a switch or a case statement and indeed it can be argued that it doesn't need one.

# Loops

Python provides two basic types of loops.

A while loop tests a conditional expression and the body of the loop is executed while the condition remains true.



A for loop iterates over a sequence and the body of the loop is executed for each element of the sequence and until the sequence is exhausted.



So we've initialized n in the line above and while n remains less than five and of course it starts at zero, which is less than five, so the condition is true. You notice that we increment n each time through the loop, at the end of the loop. Make n plus equals one and for each value of n, so it'll be zero, one, two, three, and four, it will print an element of this list up here and the elements are numbered zero, one, two, three, and four. When n gets to five its no longer less than five, the condition is false, and the loop will finish. So when I save this and run it, you see, we print each of the elements of the words list.

words = ['one', 'two', 'three', 'four', 'five']

n = 0
while(n < 5):
    print(words[n])
    n += 1
1
2
3
4
5
6
one
two
three
four
five
1
2
3
4
5

or we can print a fibonacci series while its less than 1000.

a, b = 0, 1
while b < 1000:
    print(b, end = ' ', flush = True)
    a, b = b, a + b

print() # line ending
1
2
3
4
5
6
1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 
1

A for loop is very different. For many uses this is far more convenient than a while loop. You notice here, on line six, for i in words and so we have our same list of words. For each element of words, in order because its an itterable.


For each element of words the variable i will be assigned that element and the body of the loop is executed, which in this case is simply print i. You see its much easier than doing this with the while loop and when I save this and run it, you see we get exactly the same result.

words = ['one', 'two', 'three', 'four', 'five']

for i in words:
    print(i)
1
2
3
4
one
two
three
four
five
1
2
3
4
5

Or reversing:

words = ['one', 'two', 'three', 'four', 'five']

for i in reversed(words):
    print(i)
1
2
3
4
five
four
three
two
one
1
2
3
4
5

Python provides the two most useful forms of loops, a while loop and a for loop.

# Functions

Functions in Python serve the purpose of both functions and subroutines in other languages.


The function is defined with the D-E-F, def keyword that defines a function, then we have the name of the function, and it will always have parenthesis, even if it does not take any arguments. And any arguments are separated by commas inside of those parentheses. And then that argument becomes a variable within the scope of the function, and that variable can be printed in this case. And so, here's the call to the function, the function call, I have the name of the function, and then the parentheses again, they're not optional, even if there's no argument, and a value. So here, we're passing the function the value 47, and it will print the number 47 when I save and run it. And there we have the number 47 in our command output.

def function(n):
    print(n)

function(47)
1
2
3
4
47
1

If I don't give it a number, then we get an error, because the function requires an argument, but we're not giving it one.


We can however give the argument in the function a default value, say n equals one, and that way if I do not provide a argument, then it'll default to one.

def function(n = 1):
    print(n)

function()
1
2
3
4
1
1

But still, if I do pass it a value, it will use that value instead.

Now, in Python, all functions return a value, and so, if I say X equals function, and then print X, we'll see what this returns. We're not returning it anything explicitly, so by default it returns the default value of none, which is the absence of value. its a special keyword, and a special value in Python that means the absence of a value. And yet, if I give it a return statement.

def function(n = 1):
    print(n)

x = function(42)
print(x)
1
2
3
4
5
42
None
1
2

As a practical example:

def isprime(n):
    if n <= 1:
        return False
    for x in range(2, n):
        if n % x == 0:
            return False
    else:
        return True

n = 5
if isprime(n):
    print(f'{n} is prime')
else:
    print(f'{n} not prime')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
5 is prime
1

If I want a list of prime numbers, then I can define a function to give me a list of primes. You notice that I have to use the parentheses, even though I'm not passing a value to this. its just going to define a list of primes, and I'm going to say for n in range, 100. So, this will give us a range of numbers, from zero to 100, and not including 100, because that's the way the range function works.

If isprime, and give it that n, then we print n. And I'm going to use a special argument to print, end=' ' it'll have it end the print with a space, instead of a new line, because normally print gives us a new line after each printing. This'll give us a space instead. And, because of the way that some operating systems work, I'm going to flush the output buffer, and we'll talk about that later.

And when we're all done, we'll just print a new line by itself. So now we have a function that will list primes, and I'm going to call that here, list primes. And again, I have to use the parentheses, even though I'm not passing it anything, and when I save and run this, you see we get a list of prime numbers up to 100.

def isprime(n):
    if n <= 1:
        return False
    for x in range(2, n):
        if n % x == 0:
            return False
    else:
        return True
def list_primes():
    for n in range(100):
        if isprime(n):
            print(n, end=' ', flush=True)
    print()

list_primes()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 
1

So, functions are a fundamental tool in Python. They're used for creating reusable code, and you see here in this example, I reused isprime for two different purposes. They're used as methods and objects, and they're simply used for breaking down code into smaller, more manageable pieces, as I did with list primes here. We'll cover functions in more detail later in the course.

# Objects

In Python a class is a definition and an object is an instance of a class.


Starting here we have the definition of a class and so it uses the keyword class and the name of the class is duck and the colon introduces the contents of the class which in this case we have two functions. Sometimes functions are called methods and sometimes variables inside of a class are called properties although, Python uses a slightly different definition of property so we'll call them class variables here. And so these two functions, quack and walk are now part of that class and you'll notice that the first argument for a method inside of a class is always self and that's not a keyword, you can actually name it something else if you like. I strongly recommend that you use the word self because everybody else does and then your code becomes readable by everybody else and that is reference to the object when the class is used to create an object. And so you'll use that and we'll show you in a moment how that's used in order to reference the object itself.

 
 
 
 
 
 








class Duck:
    def quack(self):
        print('Quaaack!')

    def walk(self):
        print('Walks like a duck.')

def main():
    donald = Duck()
    donald.quack()
    donald.walk()

if __name__ == '__main__': main()
1
2
3
4
5
6
7
8
9
10
11
12
13

And so down here in main, we've got a variable called Donald and we assign it from the class and that makes Donald an object. And so Donald is now an object of the class duck. And so we use the dot as the de-reference operator to reference members of the object Donald and in this case we're calling this method quack and in this case we're calling the method walk. And so when I save this and run it you see that Donald quacks and Donald walks like a duck.

def main():
    donald = Duck()
    donald.quack()
    donald.walk()

if __name__ == '__main__': main()
1
2
3
4
5
6
Quaaack!
Walks like a duck.
1
2

Now if we want to we can put some variables in here and I can say, sound equals quack, and I can say walking, equals walks like a duck. lets give this a couple more A's and an exclamation point, so it kind of matches, and instead of these now we can say self dot and you see sound and its got a V there, the editor knows about this so its saying its a variable. And, in print we can say, self dot walking, and now when I save it and run it, we get exactly the same result. And I can change it if I want to, I can give it a couple more A's and, say walking like a duck and then when I save and run it we get those new results.

class Duck:
    sound = 'Quaaack!'
    walking = 'Walks like a duck.'

    def quack(self):
        print(self.sound)

    def walk(self):
        print(self.walking)

def main():
    donald = Duck()
    donald.quack()
    donald.walk()

if __name__ == '__main__': main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Quaaack!
Walks like a duck.
1
2

Classes and objects are very easy to work with in Python in fact, object oriented programming is and always has been a focus of the Python language.

# 3️⃣ Types and Values

# Overview

There are just a few fundamental data types in Python.

Python uses a form of dynamic typing sometimes called duck typing where the type of a value is determined by the value itself. In other words, if it walks like a duck, its a duck.

# The string type

In Python 3, all types are classes, even the built-in types. Mostly this fact is transparent, you'll never notice it. In the case of the string type, its very useful to know.

x = 7
print('x is {}'.format(x))
print(type(x))
1
2
3
x is 7
<class 'int'>
1
2

First of all, let me change this 7 to the word seven in a string. You notice that the string is wrapped in single quote marks. And when I save and run this, we have the x is seven and the type's str, which is the internal string type.

But its good to know that the single quotes is a style choice. Double quotes are exactly the same. If I save and run this, you see we have exactly the same result.

There is no difference between single quotes and double quotes in Python.

x = 'seven'
print('x is {}'.format(x))
print(type(x))
1
2
3
x is seven
<class 'str'>
1
2

You can use three single quotes. And if I save and run you'll see there's no difference. What that allows you to do, though, is to put things on several lines. So, using three quotes you can actually make a multi-line string. And if I save and run this, you see that the result here, and I'll open this up, says x is seven with lots of newlines and before and after it. And those single quotes can be double quotes as well. So, you'll often see this. These triple quotes are usually made out of double quotes, I know that seems to be the style that most often people use. So, if I save and run this, you see our result is still the same.

x = """


seven


"""
print('x is {}'.format(x))
print(type(x))
1
2
3
4
5
6
7
8
9
x is 


seven



<class 'str'>
1
2
3
4
5
6
7
8

to be consistent, so pick one and stick with it. Now, as I mentioned, strings are objects, even literal strings. So, here I have a literal string, the seven in quotes, and its an object, so I can run methods on that object. And, say, I'll choose capitalize here. And when I save and run this, the string in our results now has a capital S.

x = 'seven'.capitalize()
print('x is {}'.format(x))
print(type(x))
1
2
3
x is Seven
<class 'str'>
1
2

And so, instead of capitalize, if I say upper, it'll now make it all uppercase, or lower. Of course, the string was lowercase to start with.

x = 'seven'.upper()
print('x is {}'.format(x))
print(type(x))
1
2
3
x is SEVEN
<class 'str'>
1
2

In fact, I can even put some placemarkers in it and use format. And when I run this it'll say seven 8 9 like that.

x = 'seven {} {}'.format(8, 9)
print('x is {}'.format(x))
print(type(x))
1
2
3
x is seven 8 9
<class 'str'>
1
2

Now, the format method has a number of options, and again, we'll get into more detail with this later on. But its good to understand what it does. These are positional arguments here. And so they're in exactly the same position. If I have two of these, and I have two arguments, they will replace them in order. So that'll be seven, that'll be 8, and that'll be 9. I can specify positional arguments by putting a zero in this one and a one in this one. And now they'll swap around in the result. You see now it says seven 9 8.

x = 'seven {1} {0}'.format(8, 9)
print('x is {}'.format(x))
print(type(x))
1
2
3
x is seven 9 8
<class 'str'>
1
2

Or instead, I can make this all an fstring. And lets create a couple of variables, a equals eight and b equals nine. And I can put the a and b inside of these. And save and run it and we have that same result. Now, what I've done here is instead of putting the .format class after the string, I simply used this f before the string and that makes this what's called an fstring. And fstrings are available in Python 3.6 and after.

a = 8
b = 9
x = f'seven {a} {b}'
print('x is {}'.format(x))
print(type(x))
1
2
3
4
5
x is seven 8 9
<class 'str'>
1
2

I can put a colon after this and say I'll left adjust this one nine and I'll right adjust this one nine. And when I save and run this, you notice that now there's nine spaces to the right of this one and nine spaces to the left of that one.

x = 'seven {1:<9} {0:>9}'.format(8, 9)
print('x is {}'.format(x))
print(type(x))
1
2
3
x is seven 9                 8
<class 'str'>
1
2

So, I can put these in quotes, and I'll use double quotes here, 'cause you can use double quotes inside a single quoted string and you can use single quotes inside a double quoted string without escaping them. Now, if I save and run this you see that we have the spaces.

So, what we've done is we've left aligned this one in nine spaces and we've right aligned this one in nine spaces.

x = 'seven "{1:<9}" "{0:>9}"'.format(8, 9)
print('x is {}'.format(x))
print(type(x))
1
2
3
x is seven "9        " "        8"
<class 'str'>
1
2

If I want leading zeroes, I can put a zero before this and it'll fill with zeroes. In fact, if I put a zero before this one, it'll still fill with zeroes to the other side. And so here I've got 9 with the zeroes filled after it and 8 with the zeroes filled before it. And you'll notice that its nine spaces total. I put that nine there. its not nine spaces after and nine spaces before. So, there's eight spaces after and eight spaces before 'cause its nine spaces for the entire number.

x = 'seven "{1:<09}" "{0:>09}"'.format(8, 9)
print('x is {}'.format(x))
print(type(x))
1
2
3
x is seven "900000000" "000000008"
<class 'str'>
1
2

Now, the fstrings, they use the format method on the string class, so they work exactly the same here. Instead of positional argument here, you have the name of the variable or the value itself. Of course, I can put in a value here. And when I save it and run it, it'll work just the same. But usually you'll use it with a variable of some sort. And so I can still put in the colon and I can left align this nine spaces and right align this one nine spaces, save and run. And you see that we get exactly the same results. I can put in a leading zero here and put in a leading zero there, and we have exactly the same results.

So, fstrings work pretty much exactly like strings using the format class.

a = 8
b = 9
x = f'seven {a:<09} {b:>08}'
print('x is {}'.format(x))
print(type(x))
1
2
3
4
5
x is seven 800000000 00000009
<class 'str'>
1
2

So, there are many more string functions and many more formatting options, and we'll cover all of that in a later chapter.

# Numeric types

Python 3 has two basic numeric types, integer and floating point. There are other types derived from these including the built in complex type. In fact, because everything is an object in Python, you can derive your own types. Here we're going to talk about integer and float.

x = 7 * 3
print('x is {}'.format(x))
print(type(x))
1
2
3
x is 21
1

On the other hand if I say seven point zero. And save and run it, we now have class float, which is a floating point number. Or, if I say seven times three point one four one five nine, I save this and run it, we now have a floating point value because we used a floating point number in our calculation.

x = 7.0
print('x is {}'.format(x))
print(type(x))
1
2
3
x is 7.0
<class 'float'>
1
2

On the other hand, if I divide two integers, say seven divided by three, when I save and run this, I have a floating point number even though I divided two integers. This is a behavior that's new in Python 3. In Python 2 it would've done an integer division and given me the result without the remainder. I can still get that result by using a double divide sign. If I save and run this, I now get two, because you can fit two three's and seven with a remainder of one.

x = 7 // 3
print('x is {}'.format(x))
print(type(x))
1
2
3
x is 2
<class 'int'>
1
2

Getting the remainder

x = 7 % 3
print('x is {}'.format(x))
print(type(x))
1
2
3
x is 1
<class 'int'>
1
2

Now here's an interesting problem. If I say point one, plus point one, plus point one, minus point three, you would expect my result would be zero. When I save and run this you notice that my result is five point five five etc, e minus 17. That's all 17 places to the right of the decimal point. We're rather moving the decimal point 17 places to the left. So its a very very minuscule number, its almost zero but its not zero.


This is especially a problem when dealing with money. You remember when we do arithmetic in school, if we add these three point one's and we subtract point three, we're going to get a zero and its not going to have a bunch of numbers 17 places to the right of the decimal point.


This is the difference between accuracy and precision. Because of the way that the computers do floating point, they're sacrificing accuracy for precision and so it may do this arithmetic correctly to 17 decimal places, which is the precision of the floating point processor inside the computer. But it is not accurate. Accuracy is the true value of a calculation.

x = .1 + .1 + .1 - .3
print('x is {}'.format(x))
print(type(x))
1
2
3
x is 5.551115123125783e-17
<class 'float'>
1
2

**You don't want to use floating point numbers for money because you end up with those kinds of problems because 10 cents plus 10 cents plus 10 cents minus thirty cents needs to be zero cents in order for books to balance properly. **


How can we solve this?


Python comes with a built in module called import. And so I'm going to say from decimal, import, everything. And now here I can say a equals decimal. I now have a type point one o. So that's 10 cents in decimal. And you notice I have to put that in quote marks. its going to convert it from a string because we don't want to pass it a floating point number because a floating point number isn't accurate. It has precision but it doesn't have accuracy. And so I say b equals decimal, point three o. And now I can say x equals a plus a plus a minus b, and when I save and run this you notice that the result is zero point zero zero which will balance your books really nicely. And the type is class decimal dot decimal.


So its the decimal class from the lowercase decimal library. So you're going to want to use something like this. It can either be this module or one that you write yourself or another one but you want to use a proper decimal arithmetic module when you're dealing with money.

#!/usr/bin/env python3

from decimal import *

a = Decimal('.10')
b = Decimal('.30')
x = a + a + a - b
print('x is {}'.format(x))
print(type(x))
1
2
3
4
5
6
7
8
9
x is 0.00
<class 'decimal.Decimal'>
1
2

So understanding the numeric types can save you a great deal of trouble for many purposes, the simple integer and float types will work fine for other purposes the decimal module will be necessary or you may want to drive your own class.

# The Bool Type

The bool type is used for logical values and expressions.

#!/usr/bin/env python3

x = True
print('x is {}'.format(x))
print(type(x))
1
2
3
4
5
x is True
<class 'bool'>
1
2

So the bool type is for logical values and expressions. For example here, if I say seven is less than five, I'll get a bool type False. And I'll save and run that, x is False. This is because the comparison operator has returned True or False in the bool type.

#!/usr/bin/env python3

x = 7 < 5
print('x is {}'.format(x))
print(type(x))
1
2
3
4
5
x is False
<class 'bool'>
1
2

its worth noting at this point and I have a reason for this, we're going to use this and you need to understand that there is a special type, None, and its the None type, if I save this and run it, you see the value is None and the class is NoneType. The NoneType is used, and it has only one possible value, which is None and this is used to represent the absence of a value.

#!/usr/bin/env python3

x = None
print('x is {}'.format(x))
print(type(x))
1
2
3
4
5
x is None
<class 'NoneType'>
1
2

There's a reason I am showing you this at this point. If I come down here and I say, if x: print ("True"), else: print ("False"), and these are strings here, so when I save and run this, you'll notice that with the value of None, the logical test here in if, returns False.

#!/usr/bin/env python3

x = None
print('x is {}'.format(x))
print(type(x))

if x:
    print('True')
else:
    print('False')
1
2
3
4
5
6
7
8
9
10
x is None
<class 'NoneType'>
False
1
2
3

So, its worth noting that there are a few things that evaluate as false, any numeric zero, so if I save and run this, x is zero, class is int, and the test is False.

#!/usr/bin/env python3

x = 0
print('x is {}'.format(x))
print(type(x))

if x:
    print('True')
else:
    print('False')
1
2
3
4
5
6
7
8
9
10
x is 0
<class 'int'>
False
1
2
3

Any empty string, so if I just try this, I have an empty string, class string, and it evaluates as false.

#!/usr/bin/env python3

x = ""
print('x is {}'.format(x))
print(type(x))

if x:
    print('True')
else:
    print('False')
1
2
3
4
5
6
7
8
9
10
x is 
<class 'str'>
False
1
2
3

TIP

If I put anything at all in that string is no longer false, so save and run this, and its now true. x = "x"

And if a number is not zero, again, it'll return true, it'll evaluate as true. x = 1

So, like many languages, you can use any type as a logical value, as long as you know the rules.


  • None evaluates as false, zero evaluates as false, and an empty string evaluates as false. Pretty much anything else, at least in the default types will evaluate as true.

So the bool type has two possible values, true and false, and additionally, many other types may be used as logical expressions, and will be evaluated as false, if they're effectively zero or empty, otherwise they will be true. We'll see other examples of this behavior, later in this course.

# Sequence types

Python provides some built-in sequence types, including lists, tuples, and dictionaries.


Here, a list is created with square brackets, like this, here on line four, and if I save and run this, you'll notice that we get this result, I is one, I is two, etc. The for loop is sequencing through the list, and for each item in the list, it assigns I the value of the item, and I is used in the print function.

x = [ 1, 2, 3, 4, 5 ]
for i in x:
    print('i is {}'.format(i))
1
2
3
i is 1
i is 2
i is 3
i is 4
i is 5
1
2
3
4
5

A list is a mutable sequence, so I can reassign one of the values after I have assigned it. I can say X sub two equals three, and so I'm accessing, with this index in the square brackets, I'm accessing a particular specific item in the list. And I'm going to change its value to 42, like that. So, when I save and run it, you'll see that the item number two, which is the third item, because the index starts at zero for all sequence types in Python, and I've reassigned that to the value, 42. So, I can say item number zero, or all the way up to item number four, and it will reassign that value.

x = [ 1, 2, 3, 4, 5 ]
x[2] = 42
for i in x:
    print('i is {}'.format(i))
1
2
3
4
i is 1
i is 2
i is 42
i is 4
i is 5
1
2
3
4
5

So, I can say item number zero, or all the way up to item number four, and it will reassign that value. Now on the other hand, a tuple is exactly like a list, only its immutable, and its indicated by parentheses. And so, if I try and run this with a tuple instead of a list, you notice I get the error, "TypeError: 'tuple' object "does not support item assignment." That's because a tuple is not mutable, you cannot change it, or sometimes its referred to as being immutable.

x = ( 1, 2, 3, 4, 5 )
x[2] = 42
for i in x:
    print('i is {}'.format(i))
1
2
3
4
Traceback (most recent call last):
  File "c:/Users/Thiago Souto/Documents/Python Essential training/Ex_Files_Python_EssT/working.py", line 2, in <module>
    x[2] = 42
TypeError: 'tuple' object does not support item assignment
1
2
3
4

So, if I remove this line, and run it again, you'll notice that it works exactly like the list.

x = ( 1, 2, 3, 4, 5 )
for i in x:
    print('i is {}'.format(i))
1
2
3
i is 1
i is 2
i is 3
i is 4
i is 5
1
2
3
4
5

I find that its generally a good idea to favor the immutable type tuple over the immutable type list, unless you know that you need to change the elements in the list. So, I'll tend to use the parentheses by default, and only use the square brackets when I know that I need to change something.


You can also create a sequence using range. And so instead I can say here range sub five, like that, and when I save and run it, again, it starts at zero, and it ends at the number before what you specify. So, that end is actually the last item minus one. So, when I specify a five, it'll end at four, if I specify a 10, save and run that, you notice I get a sequence only up to nine, and it also starts at zero, I have to scroll backwards to be able to see that.

x = range(5)
for i in x:
    print('i is {}'.format(i))
1
2
3
i is 0
i is 1
i is 2
i is 3
i is 4
1
2
3
4
5

There's actually three possible parameters for range. If you give it just one parameter, it takes this as the end mark, and it starts at zero. I can change the start, I can say start at five instead, and when I save and run this, you notice I get five, six, seven, eight, nine.

x = range(5, 10)
for i in x:
    print('i is {}'.format(i))
1
2
3
i is 5
i is 6
i is 7
i is 8
i is 9
1
2
3
4
5

or I can say end at 50, but step by five. And when I save and run that, I get five, 10, 15, 20, so that third parameter is a step by.

x = range(5, 50, 5)
for i in x:
   print('i is {}'.format(i))
1
2
3
i is 5
i is 10
i is 15
i is 20
i is 25
i is 30
i is 35
i is 40
i is 45
1
2
3
4
5
6
7
8
9

**If you specify all three, they're start, end, and step, and if you specify a just two, it start and end, and if you specify just one, its just end. **


Like the tuple, a range is not mutable, If I try to say X sub two equals 42, save and run, I get this error, "TypeError: 'range' object "does not support item assignment," because its immutable.


If I want a mutable list, I simply construct a list with the results from range using the list constructor, like this, and save and run it, and now I have zero, one, 42, three, and four.

x = list(range(5))
x[2] = 42
for i in x:
    print('i is {}'.format(i))
1
2
3
4
i is 0
i is 1
i is 42
i is 3
i is 4
1
2
3
4
5

A dictionary is a searchable sequence of key value pairs, and you construct it like this. And so that's a dictionary. And if I just save and run this, you notice that I get, I is one, I is two, I is three, I'm just getting the keys, I'm not getting the values.

x = { 'one': 1, 'two': 2, 'three': 3, 'four': 4, 'five': 5 }
for i in x:
    print('i is {}'.format(i))
1
2
3
i is one
i is two
i is three
i is four
i is five
1
2
3
4
5

If I want to get the keys and values, I can say in my for loop, key comma V, in X.items. So, that'll actually return a to tuple of each of the items, with the key and the value. And then I can print it out like this, I can say, key and value, like that, and it will print the key and the value for each element in the dictionary.

x = { 'one': 1, 'two': 2, 'three': 3, 'four': 4, 'five': 5 }
for k, v in x.items():
    print('k: {}, v: {}'.format(k, v))
1
2
3
k: one, v: 1
k: two, v: 2
k: three, v: 3
k: four, v: 4
k: five, v: 5
1
2
3
4
5

Now, each of these values, the keys and the values can be any type, and the same is true for lists and tuples, any element can be any type. And dictionaries are mutable, so I can come up here and I can say, X sub three, use the key to index it, equals 42, and when I save and run it here, you notice that key three now has a value of 42.

x = { 'one': 1, 'two': 2, 'three': 3, 'four': 4, 'five': 5 }
x['three'] = 42
for k, v in x.items():
    print('k: {}, v: {}'.format(k, v))
1
2
3
4
k: one, v: 1
k: two, v: 2
k: three, v: 42
k: four, v: 4
k: five, v: 5
1
2
3
4
5

So, this is an overview of Python's built in sequence types. You'll see a lot of examples of these in the rest of the course. These built-in sequence types are very useful, and flexible.

# type() and id()

In Python, everything is an object, so a Type is the same as a Class.

x = (1, 2, 3, 4, 5)
print('x is {}'.format(x))
print(type(x))
1
2
3
x is (1, 2, 3, 4, 5)
<class 'tuple'>
1
2

Now, where this starts to get interesting is when I make this tuple into something else. We'll change this two, a string that says two, we'll change to the three to a float, change the four, we'll just make that a list, the number four and a string four, and we'll leave the five alone. So when I save and run this, now you'll notice that the string representation printed out on line five is exactly the structure that I've created there, and line six still says type tuple.

x = (1, 'two', 3.0, [4, 'four'], 5)
print('x is {}'.format(x))
print(type(x))
1
2
3
x is (1, 'two', 3.0, [4, 'four'], 5)
<class 'tuple'>
1
2

But if instead I ask, well what's the type of that second element, which would be element number one. Remember that these indices start at zero, and if I save and run this, it now says that's a string. So this is useful information. I can inspect any element in my structure, and I can say what type is that?

x = (1, 'two', 3.0, [4, 'four'], 5)
print('x is {}'.format(x))
print(type(x[1]))
1
2
3
x is (1, 'two', 3.0, [4, 'four'], 5)
<class 'str'>
1
2

On the other hand, if I bring this back to the type of X, and lets just duplicate this and make a Y that's got exactly the same stuff in it, and I'll ask for type of X and type of Y, we get the same result for both of them.

x = (1, 'two', 3.0, [4, 'four'], 5)
y = (1, 'two', 3.0, [4, 'four'], 5)
print('x is {}'.format(x))
print('y is {}'.format(y))
print(type(x))
print(type(y))
1
2
3
4
5
6
x is (1, 'two', 3.0, [4, 'four'], 5)
y is (1, 'two', 3.0, [4, 'four'], 5)
<class 'tuple'>
<class 'tuple'>
1
2
3
4

But instead of type, if I ask the ID, and I'll do that for both of these, I save and run, you'll notice I get two different unique numbers. So these are two different objects. ID function returns a unique identifier for each object.

x = (1, 'two', 3.0, [4, 'four'], 5)
y = (1, 'two', 3.0, [4, 'four'], 5)
print('x is {}'.format(x))
print('y is {}'.format(y))
print(id(x))
print(id(y))
1
2
3
4
5
6
x is (1, 'two', 3.0, [4, 'four'], 5)
y is (1, 'two', 3.0, [4, 'four'], 5)
20517344
20567552
1
2
3
4

Now if I ask for the ID of X sub zero, I have that literal number one there, right? And Y sub zero also has a literal number one, then we save and run this, and we find out that they're exactly the same, because there is only one literal number one object. There's no need for Python to create two different objects for the literal number one.

x = (1, 'two', 3.0, [4, 'four'], 5)
y = (1, 'two', 3.0, [4, 'four'], 5)
print('x is {}'.format(x))
print('y is {}'.format(y))
print(id(x[0]))
print(id(y[0]))
1
2
3
4
5
6
x is (1, 'two', 3.0, [4, 'four'], 5)
y is (1, 'two', 3.0, [4, 'four'], 5)
1503356080
1503356080
1
2
3
4

So I also have an is operator, and I can say well, are these two different things exactly the same? I can say if X sub zero is Y sub zero, this checks to see if they are exactly the same object. So I print. Yep. If they are, and else, print, nope if they are not, and I save and run that, and they are the same object.

x = (1, 'two', 3.0, [4, 'four'], 5)
y = (1, 'two', 3.0, [4, 'four'], 5)
print('x is {}'.format(x))
print('y is {}'.format(y))
print(id(x[0]))
print(id(y[0]))

if x[0] is y[0]:
    print("yep")
else:
    print("nope")
1
2
3
4
5
6
7
8
9
10
11
x is (1, 'two', 3.0, [4, 'four'], 5)
y is (1, 'two', 3.0, [4, 'four'], 5)
1503356080
1503356080
yep
1
2
3
4
5

But I'm just asking if X is the same object as Y, then, oops, that's not where I wanted to do that. Well, I can do that anyway. I wanted to do that here. If I just want to know if X is the same object as Y, then I can save and run, and I can see that nope, they are not the same object, even though they have all the same values.

x = (1, 'two', 3.0, [4, 'four'], 5)
y = (1, 'two', 3.0, [4, 'four'], 5)
print('x is {}'.format(x))
print('y is {}'.format(y))
print(id(x))
print(id(y))

if x is y:
    print("yep")
else:
    print("nope")
1
2
3
4
5
6
7
8
9
10
11
x is (1, 'two', 3.0, [4, 'four'], 5)
y is (1, 'two', 3.0, [4, 'four'], 5)
49484256
49534464
nope
1
2
3
4
5

On the other hand, there's a special function called is instance. Is instance. If is instance X tuple, save and run, and you'll notice is says yes it is, it says yep. X is a tuple.

x = (1, 'two', 3.0, [4, 'four'], 5)
y = (1, 'two', 3.0, [4, 'four'], 5)
print('x is {}'.format(x))
print('y is {}'.format(y))
print(id(x))
print(id(y))

if isinstance(x, tuple):
    print("yep")
else:
    print("nope")
1
2
3
4
5
6
7
8
9
10
11
x is (1, 'two', 3.0, [4, 'four'], 5)
y is (1, 'two', 3.0, [4, 'four'], 5)
57724816
60559840
yep
1
2
3
4
5

So if you want to know, for instance you're going to handle different types differently. If a particular object is of this type or of that type, this is the way that you do it using isinstance(). If you want to print out what type something is, then you can use the type().

And if you want to know exactly what the specific ID is of a object, you can use the id() function, and that will print their ID's.

And you can use the is operator if you want to know if two objects are the same object.


So these functions work by using Python's class introspection, and they do come in handy.

# 4️⃣ Conditionals

# Conditional syntax

The Python syntax for conditional statements is consistent with the rest of the language.

if True:
    print('if true')
elif False:
    print('elif true')
else:
    print('neither true')
1
2
3
4
5
6

Of course, I can have as many elifs as I want to, so I'll number them, elif one, elif two. Let's move this down a little bit so we have more room to edit, three and four, and I can just pick one, and say I'll make this one true, and of course, it will say elif three is true.

if False:
    print('if true')
elif False:
    print('elif 1 true')
elif False:
    print('elif 2 true')
elif True:
    print('elif 3 true')
elif False:
    print('elif 4 true')
else:
    print('neither true')
1
2
3
4
5
6
7
8
9
10
11
12
elif 3 true
1

So you can cascade the else, ifs to as many as you want, and this comes in handy when you want to do something like this, x equals five. If x equals zero, then zero is true, and you need to make that a double equals. If x equals one, and one is true, or we can just leave these numbers here 'cause those won't match up, and so this works very much like a switch statement does in other languages.

x = 4

if x == 0:
    print('Zero true')
elif x == 1:
    print('elif 1 true')
elif x == 2:
    print('elif 2 true')
elif x == 3:
    print('elif 3 true')
elif x == 4:
    print('elif 4 true')
else:
    print('neither true')
1
2
3
4
5
6
7
8
9
10
11
12
13
14

And when I save and run this, we see that elif five is the one that is true, and of course, if we change this to 42, then we get the none of them are true.

elif 4 true
1

So the conditional statement in Python is simple and easy. It uses the familiar if, else, elif keywords, and the typical Python indented block syntax.

# Conditional Operators

Python tends toward a minimalist language, and as such, it provides a minimally complete set of operators for use in conditional expressions. It has comparison operators for equal, not equal, less than, greater than, less than or equal, and greater than or equal. It has logical operators for and, or, and not. Has identity operator that tests if two objects are the same object, and so it has is and is not. And it has a membership operator which is true if a variable is a member of a collection, so it has both in and not in for membership. These operators are commonly used in conditional expressions. You'll see many examples throughout the rest of this course.









# Conditional assignment

Beginning with Version 2.5, Python includes a ternary conditional operator. It's important to note that this operator does not work in versions of Python previous to Python 2.5. Of course, we're in Python 3.6 here.

hungry = True
x = 'Feed the bear now!' if hungry else 'Do not feed the bear.'
print(x)
1
2
3
Feed the bear now!
1
hungry = 0
x = 'Feed the bear now!' if hungry else 'Do not feed the bear.'
print(x)
1
2
3
Do not feed the bear.
1

Now it's important to note that all of this is required if you're going to have the ternary conditional operator, you must have both if and else. You must have both clauses. Of course, if you want to, you can have else, set it to none. So when I save and run it will say None. But you must have an else clause. It does not work with just the if clause. The ternary conditional operator is used occasionally, but it can be very convenient in some circumstances.

# Arithmetic operators

+       Addition
-       Subtration
*       Multiplication
/       Division
//      Integer Division
%       Remainder (Modulo)
**      Exponent
-       Unary negative
+       Unary positive   
1
2
3
4
5
6
7
8
9
x = 5
y = 3
z = x + y

print(f'result is {z}')
1
2
3
4
5
result is 8
1

When I divide, I will always get a floating point. This is new behavior in Python 3. Python 2 does not work this way so it's worth taking note of.

x = 5
y = 3
z = x / y

print(f'result is {z}')
1
2
3
4
5
result is 1.6666666666666667
1

If I want to get the integer division then I use two slash marks for the operator and when I save and run, I get a result of one which is five divided by three without the remainder. If I want the remainder, I use the percent sign which will give me the remainder or the modulus.

x = 5
y = 3
z = x // y

print(f'result is {z}')
1
2
3
4
5
result is 1
1
x = 5
y = 3
z = x % y

print(f'result is {z}')
1
2
3
4
5
result is 2
1

two. There is also unary operators for negating and leaving an operand positive. And so, if I say Z equals minus Z then I get a result here of minus two. And if I say Z equals plus Z then I get the two by itself. And even if the number was already a negative I say Z equals plus Z, it simply doesn't negate it.

z = -z
z = +z
1
2
result is -2
1

So, that's just there for decoration. It doesn't really do anything to the operand. So, these are the arithmetic operators in Python. We'll cover more operators in the rest of this chapter.

# Bitwise operators

These are Python's Bitwise Operators. There's And, Or, Xor, Shift left, and Shift right. These operators operate on bits. They are not the logical operators used for the conditional constructs. These are bitwise operators that operate on numbers, and they operate on the individual bits in the numbers.

&       And
|       Or
^       Xor
<<      Shift left
>>      Shift right
1
2
3
4
5

If you come down here, I have two variables: x and y. And these variables are initialized using hexadecimal literal numbers. And so the first one is 0a and the second one is 02 and then I'll operate on them using an operator, in this case it's the And operator. And then I printed out twice, once in hexadecimal and once in binary. And you'll notice that I'm using f-string, and I'm using these format controls, these format specifiers, in this case 02x. And that gives us two character string, and its in hexadecimal and it has a leading zero. So the zero gives it the leading zero and the two says that the field is two characters wide and the x is for hexadecimal display of an integer value. And you notice I pass up the x here, and I pass up the y here, and the z here for the result. So I do the same thing in the second print call also using an f-string. The difference here is now the field is eight characters wide, still has the leading zero, and in this case it's a binary representation of the value. So, if we look down at our results you'll see that x is 0a in hexadecimal and in binary it's 00001010. So the first four zeros are the hexadecimal zero and the second four digits 1010 is the number a, which in decimal is a 10. And so this allows us to see bitwise what's happening here.

x = 0x0a
y = 0x02
z = x & y

print(f'(hex) x is {x:02x}, y is {y:02x}, z is {z:02x}')
print(f'(bin) x is {x:08b}, y is {y:08b}, z is {z:08b}')
1
2
3
4
5
6
(hex) x is 0a, y is 02, z is 02
(bin) x is 00001010, y is 00000010, z is 00000010
1
2

And so this allows us to see bitwise what's happening here. So, because the operation is an And, you'll notice in x, 1010, and y is 02. So, we have two bit set here in the x and only one bit in the two. So the And operation will only set a bit if both of the operands have a bit set in that position. And so this fourth position here, counting from the right. This fourth position here has a one and it does not have a one here in the y, and so in the z that does not get set. The only one that gets set is that two the second bit. Alright so hopefully this is clear at this point.


Alright so hopefully this is clear at this point. We're going to go ahead and look at some of the other operators. If I take this And and change it to a vertical bar, which is the Or operator. And if I go ahead and run this, you'll notice that all of the bits are set in z, because it will set a bit if either the left operand or the right operand has a bit set in that position. And so that's the bitwise order.

x = 0x0a
y = 0x02
z = x | y

print(f'(hex) x is {x:02x}, y is {y:02x}, z is {z:02x}')
print(f'(bin) x is {x:08b}, y is {y:08b}, z is {z:08b}')
1
2
3
4
5
6
(hex) x is 0a, y is 02, z is 0a
(bin) x is 00001010, y is 00000010, z is 00001010
1
2

If I change this to a five. Save and run here, we get all the bits set because a five is 0101, whereas an a is 1010. So between the two of them, they've got all of the bits set.

x = 0x0a
y = 0x05
z = x | y

print(f'(hex) x is {x:02x}, y is {y:02x}, z is {z:02x}')
print(f'(bin) x is {x:08b}, y is {y:08b}, z is {z:08b}')
1
2
3
4
5
6
(hex) x is 0a, y is 05, z is 0f
(bin) x is 00001010, y is 00000101, z is 00001111
1
2

So Xor, the exclusive or, is the carett symbol and exclusive or means that the result will have a bit set only if one or the other, not both, operands have that bit set. So, I'm going to change this here to an f. And f will have all the bits set. If I save and run this, you notice that it has all the bits set in the right hand operand. But, in the result only the bits are set in the case where one and not the other, so that's a five, right? So looking at them bit by bit, the left hand operand has this bit set and so does the right hand one. So, that is not set in the result. So only if one is set and not the other. So this is an effective way to basically flip the bits by Xor-ing with f, the a bits are flipped and the result is a zero five. We also have Shift left and Shift right. So I'm going to

x = 0x0a
y = 0x0f
z = x ^ y

print(f'(hex) x is {x:02x}, y is {y:02x}, z is {z:02x}')
print(f'(bin) x is {x:08b}, y is {y:08b}, z is {z:08b}')
1
2
3
4
5
6
(hex) x is 0a, y is 0f, z is 05
(bin) x is 00001010, y is 00001111, z is 00000101
1
2

We also have Shift left and Shift right. So I'm going to change this right operand to a 1 and I'm going to use the shift left operator, which looks like that. And when I save and run you see that the bits here are shifted left by 1. So it's 1010 and here it's 10100.

x = 0x0a
y = 0x01
z = x << y

print(f'(hex) x is {x:02x}, y is {y:02x}, z is {z:02x}')
print(f'(bin) x is {x:08b}, y is {y:08b}, z is {z:08b}')
1
2
3
4
5
6
(hex) x is 0a, y is 01, z is 14
(bin) x is 00001010, y is 00000001, z is 00010100
1
2

And likewise we have a shift right operator, which looks like that. And here it'll shift the bits to the right. So the a gets shifted to the right where it becomes a 5.

x = 0x0a
y = 0x01
z = x >> y

print(f'(hex) x is {x:02x}, y is {y:02x}, z is {z:02x}')
print(f'(bin) x is {x:08b}, y is {y:08b}, z is {z:08b}')
1
2
3
4
5
6
(hex) x is 0a, y is 01, z is 05
(bin) x is 00001010, y is 00000001, z is 00000101
1
2

So these are the bitwise operators that are available in Python. It's a fairly complete set. And we'll cover more operators in the rest of this chapter.

# Comparison Operators

These are Python's comparison operators, and there's a complete set of them: less than, greater than, less than or equal, greater than or equal, equal, and not equal. They compare the value of two operands.

<       Less than
>       Greater than
<==     Less than or equal
>=      Greater than or equal
==      Equal
!=      Not equal
1
2
3
4
5
6
x = 42
y = 73

if x < y:
    print('comparison is true')
else:
    print('comparison is false')
1
2
3
4
5
6
7
comparison is true
1

# Boolean operators

These are Python's Boolean Operators. It's an interesting set because it includes the traditional and, or, not. It does not have the traditional XOR, which is not hard to create on your own. And it has a few others for testing if a value is in a set or not in a set, or if testing the identity of two operands whether they're the same object or if they're not the same object. So it's a little different than you might be used to in some other languages, but it's all very useful.

and     And
or      Or
not     Not
in      Value in set
not in  Value no in set
is      Same object identity
is not  Not same object identity
1
2
3
4
5
6
7
a = True
b = False
x = ( 'bear', 'bunny', 'tree', 'sky', 'rain' )
y = 'bear'

if a and b:
    print('expression is true')
else:
    print('expression is false')
1
2
3
4
5
6
7
8
9
expression is false
1

In this case we're testing A and B, which is true and false, the result of which will be false because for and to be satisfied they would both have to be true. When I save this and run it, you see expression is false. I change this operator to or, A or B. Now it will be true because A is true, even though B is not, and that result is now true. I also have the unary not. So if I say if not B, because B is false, the expression is now true. And likewise if I say if not A, we get the expression is false. Now we can test if Y is in X because X is a collection, so we have a value in Y; and we can test if that value exists in the collection by saying Y in X. And if I save this and run it you see that the expression is true. Instead of Y, if I say, leaf, if leaf in X, it'll be false because we do not have a leaf in X. But we do have a tree, so if I say tree, we can see that expression is true. We can also check if Y is X subzero. And this is interesting because you have two literal strings. But if I test their identity, save and run this, you notice that the result is true. This is because strings are not mutable. So there's no reason if you have two literal strings with exactly the same variable, there's no reason to carry around two separate objects. So it gets optimized into one object. So that result is true. They're actually exactly the same object, and we can see that if we print out their IDs. ID of Y, and print ID of X subzero. Save and run that and you see they both have exactly the same ID, and they are indeed the same object. Likewise, I have is not. If I save and run that you'll see that expression is false. But if I say, X of one, then that expression is true because bear is not bunny. So these are the Boolean operators available in Python.

# Operator precedence

Operator precedence describes the order in which operators are evaluated in a compound expression. For example, here's a simple expression, two plus four times five. Do you apply the multiplication first, or the addition?

2 + 4 * 5
2 + ( 4 * 5 )
2 + 20
22
1
2
3
4

If you multiply four times five and then add the two, you get one result. So four times five is 20 plus two is 22. But if you add the two plus four first, two plus four is six times five is 30, you get an entirely different result. This is why operator precedence is important. If you remember your high school math, then you know to do the multiplication first, and get the correct result. In Python, operators have precedence. This is a chart of operator precedence in Python. The higher precedence operators are at the top, and the lower precedence are at the bottom. In many cases, the order of these operators is obvious and follows the same rules as the math we learned in school. In other cases, they may not be so obvious. So it's always a good idea to use parentheses to explicitly specify the order you intend an expression to be evaluated. You can find a complete chart of operator precedence in the Python documentation.



# 6. Loops

# While loop

Python's while loop uses a conditional expression to control its loop. Here in Komodo I've opened a working copy of while.py from chapter six of the exercise files. You see, here's our while loop down on line seven and the condition, the conditional expression, is while this pw variable is not equal to the value of the secret variable. And the body of the loop is this one line of code which assigns the pw variable from the input function which inputs a line of text with a prompt. And so the secret variable is set to a string that say swordfish in all lowercase, and the pw variable is set to the empty string. And so when I save this and run it, it asks me what's the secret word and I can put my cursor down here and I can type something in. And when I press return you see it asks me for the secret word again because the secret word was not correct. And if I type something else in, it's still not correct. Try secret. Still not. Oh I'll type swordfish. And now the loop end because the conditional expression is no longer true. Password is equal to secret when I type in swordfish. And that ends the loop. So obviously you can use any conditional expression here. You just need to make sure that whatever it is you're doing in the loop will eventually resolve that and make that conditional expression no longer true. Otherwise you'll end up with an endless loop.

secret = 'swordfish'
pw = ''

while pw != secret:
    pw = input("What's the secret word? ")
1
2
3
4
5
What's the secret word? father
What's the secret word? cow
What's the secret word? swordfish

Process finished with exit code 0
1
2
3
4
5

So the while loop uses a conditional expression to control the loop. It is simple and elegant and you'll see many examples of it throughout this course. And I'll show you some additional ways to control the loop later in this chapter.

# For loop

The for loop uses a sequence like an iterator list to pull our other sequence object to control the loop. Here in Komodo, I've opened a working copy of for.py from chapter six of the exercise files, and here we have our for loop, which uses this to pull to supply its items, and it assigns each item to the variable pet, and then prints it. And so when I run this, you see we get, open this up a little bit so we get the whole thing, we get our five items from the to pull, bear, bunny, dog, cat, and velociraptor, all cute little animals. So obviously this can be any sequence, it can be a range is very common, you can say range five and that actually supplies an iterator, and so when I save and run, we get zero, one, two, three, and four, or any sequence or any iterator.

animals = ( 'bear', 'bunny', 'dog', 'cat', 'velociraptor' )

for pet in animals:
    print(pet)
1
2
3
4
bear
bunny
dog
cat
velociraptor

Process finished with exit code 0
1
2
3
4
5
6
7

This is extremely powerful and useful and convenient, and you'll see it often. In many cases, you'll see it more often than the while loop. I know that I tend to use this a lot more often than I use a while loop in my code. I'll show you additional ways to control the for loop later in this chapter.

# Additional control

Both the while and for loops have some additional controls that are worth understanding. Three additional controls are available continue, break, and else. The continue clause is used to shortcut a loop and start it again as if it had reached the end of its body of code. The break clause is used to break out of a loop prematurely, execution will continue after the entire loop structure and the else control is not common in other languages so you may not have seen this before. The else block executes only if the loop ends normally. It will not execute if a break is used to end the loop. These same controls are available for both while and for loops.



# 7. Functions

# Defining a function

I just want to describe how this works in a little bit more detail. So, you notice it's an if statement, and the colon is right here, and on the same line we have a function called to main. And so, with an if statement you may have the code on the same line as long as it's just one line of code. It's normally frowned upon but this is one case where it is commonly done this way. And then we have this conditional expression here which compares using the double equal to a test for equality, for value equality. The special variable name with double underscores on either side against a string literal which says main with double underscores on each side. So this name variable, this special variable name, will return the name of the current module.

if __name__ == '__main__': main()
1

So, if this file had been included in another execution unit by the import statement, in other words if somebody had typed import and the name of this file, then this would be running as a module and this name would have the name of the module here, but it's not running as a module. Nobody has imported it and it's running as the main unit of execution. And so, because of that then this main value is a special value which means "No, this is not important, this is the main file." And so we test for that, and you'll see later on when we start talking about modules how this can come in particularly handy. But, it's often used for this purpose because this calls main, which is defined up above and that's okay because you can call function if it's been defined beforehand, but main then calls kitten which is defined after it and so if we didn't have this and we didn't have a function main but we had function to find after it, we would not be able to call it and so that's called for declarations, and Python doesn't support for declarations. And so, this is the standard work around for that. And you'll see a lot of, in fact, it's so common that Komodo here, it has code completion for it. If I type "if and two underscores", you notice that there's a number of choices and one of them is that line of code exactly and I can just type in "main", and boom, it code completes that for me. That's just to show you how common this pattern is, you'll see this a lot especially in modules.







 

def main():
    kitten()

def kitten():
    print('Meow.')

if __name__ == '__main__': main()
1
2
3
4
5
6
7
Meow.
1

So, Python has no distinction between a function and a procedure. Some languages do. Some languages consider a function something that returns a value and a procedure something that does not return a value. In Python, all functions return a value, even if the value is none, so there's no distinction between functions and procedures. Python functions are simple and powerful. And, we'll get into details in the rest of this chapter.

# Function arguments

Function arguments in Python are fairly straightforward. There are a few details that are worth noting. If we run the code above, you notice that down here at the bottom we call main, and then main calls kitten, and then kitten calls print with the literal string 'Meow.' and that prints the string Meow.

Now if I want to pass an argument to kitten, I can do that like this. I can put in a number five, and then down here in kitten, in the parentheses, I can give it a variable name, and then I can print that variable, or I can do whatever I want with it, and you notice that it prints. I can pass multiple arguments, five, six, and seven, by separating them with commas, and likewise I can say a, b, and c, and I can print them, a, b, and c, and when we run this, we get the five, six, and seven printed.

def main():
    kitten(5, 6, 7)


def kitten(a, b, c):
    print('Meow.')
    print(a, b, c)


if __name__ == '__main__': 
    main()
1
2
3
4
5
6
7
8
9
10
11

If I don't pass enough arguments, in other words if I pass a smaller number of arguments in the function call than are expected here in the function definition, then I'll get an error. When I run this you see it says, TypeError: kitten() missing 1 required positional argument and it tells us which one, but actually it doesn't know which one, it just assumes it's the last one. On the other hand, I can give that last one a default value, and now when I run it, it no longer requires three arguments. It'll accept three arguments, so there it's printing the default value of zero, but if I actually give it a seven and run it, now it prints the seven, and I can give default arguments to however many of these I want to, and save and run that, and it's printing the default arguments.

def main():
    kitten(5)


def kitten(a, b=1, c=0):
    print('Meow.')
    print(a, b, c)


if __name__ == '__main__':
    main()
1
2
3
4
5
6
7
8
9
10
11
Meow.
5 1 0
1
2

Any arguments with defaults must be after any arguments without defaults, so in this case I have an a there without a default, and b and c have defaults. If I were to try to have another one that doesn't have a default, even if I were to give it a value, you see I still get an error. It says, non-default argument follows default argument. That's a syntax error, and that's because arguments with defaults must be at the end of the list, and that's really so that the interpreter can understand the argument list. It actually makes sense when you think about it. I consider it a best practice that if any of your arguments have default values, that you provide defaults for all of them. Obviously that doesn't make sense in all cases, but in most cases that's a really good rule of thumb.


Now here's where things get tricky. I'm just going to go back to having one argument here, and we'll save and run that, and we'll see that that works, and I'm going to replace this with a variable, and we'll save and run that. We see that we still get five, and now I'm going to print this. I'm going to use an f-string here: 'in main: x is {x}' and I have to use the same kind of quote marks, don't I? Oops. There we go. And now when I run this, it says in main: x is 5 and that's after the function call, and if I come down in here, and I say a = 3, and we save and run this, we notice that it prints 3 in the kitten function, and x is still five in the main function after the function call, and so this is what they call in the Python documentation call by value, and in most call-by-value languages, when you pass a variable to a function, the function operates on a copy of the variable, so the value is passed, but not the object itself, and this is an important distinction because if the object were actually passed, then if I were to change it here, it would also change there in main, and you notice that it doesn't act like that in Python.

def main():
    x = 5
    kitten(x)
    print(f'in main: x is {x}')


def kitten(a, b=1, c=0):
    a = 3
    print('Meow.')
    print(a)


if __name__ == '__main__':
    main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Meow.
3
in main: x is 5
1
2
3

Well, that's actually not entirely true, and here's where this gets tricky. Integers are immutable, and so this is an integer because I've initialized it with an integer, and if I were to come down here and print the id of x, we see that it has that id there, that ends in 216, and if I come down here in kitten, and I print the id of a before I operate on it, You notice that it's exactly the same, but here's where it gets funny. If I print the id of a after it gets changed, it's now got a different id number, and none of this is really what we'd expect in call by value. If what was being received by kitten was actually a copy of what was being passed to kitten in the calling function, we would expect this to have a different id number, but it has exactly the same id number, so something funny is going on here.

def main():
    x = 5
    print(id(x))
    kitten(x)
    print(f'in main: x is {x}')


def kitten(a, b=1, c=0):
    print(id(a))
    a = 3
    print(id(a))
    print('Meow.')
    print(a)


if __name__ == '__main__':
    main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1425433840
1425433840
1425433808
Meow.
3
in main: x is 5
1
2
3
4
5
6

And here's what it is. If I take two variables, and I assign this same immutable value to both of them, and this is different behavior with mutable values, so I print the id of both of these, and I'm just going to comment out all of this other stuff so that we're not confused here anymore. Actually, we'll just comment out the kitten call, and so now we just have this. So we're printing the id of these two variables, right? And if I change y = 3, now they have two different id numbers, but here's where this gets interesting: if I make x a list, which is mutable, and I change the element of that list, and now I come down here and I print x and I print y, you'll notice that they're still both the same. What I've done here is I've created a list, I've assigned it to x, I've assigned that x = y, and I changed the element of the list only in y. I did not change it in x, and the value of both of them was changed. So when you assign a mutable, you're actually assigning a reference to the mutable, and I have the side effect that when I change an element of that list in one place, it gets changed in both places because it's really just one object, and functions work exactly the same way.

def main():
    x = [5]
    y = x
    y[0] = 3
    print(id(x))
    print(id(y))
    print(x)
    print(y)
    #kitten(x)
    #print(f'in main: x is {x}')


def kitten(a, b=1, c=0):
    print(id(a))
    a = 3
    print(id(a))
    print('Meow.')
    print(a)


if __name__ == '__main__':
    main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
308701704
308701704
[3]
[3]

Process finished with exit code 0
1
2
3
4
5
6

So if I run this, you notice that we have this list, and the list is being printed in kitten as the list with five, and in main as the list with five. It's a one element list.

def main():
    x = [5]
    kitten(x)
    print(f'in main: x is {x}')


def kitten(a, b=1, c=0):
    print('Meow.')
    print(a)


if __name__ == '__main__':
    main()
1
2
3
4
5
6
7
8
9
10
11
12
13
Meow.
[5]
in main: x is [5]

Process finished with exit code 0
1
2
3
4
5

If down here in kitten I change a sub zero equals three, it's changed in both places, so this is not call by value at all. This is actually, strictly, call by reference. What's being passed is a reference to the object, and you can change the object in the caller from the function.


So this is important to understand: an integer is not mutable, so it cannot change, so when you assign a new value to an integer, you're actually assigning an entirely different object to the name. The original integer is not changed, the name simply refers to a new object.


Passing a value to a function acts exactly the same way. A reference to the object is passed and acts exactly like an assignment. So mutable objects may be changed, and those changes will be reflected in the caller. Immutable objects may not be changed.

def main():
    x = [5]
    kitten(x)
    print(f'in main: x is {x}')


def kitten(a, b=1, c=0):
    a[0] = 3
    print('Meow.')
    print(a)


if __name__ == '__main__':
    main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Meow.
[3]
in main: x is [3]

Process finished with exit code 0
1
2
3
4
5

So function arguments in Python act exactly as assignments in Python, with all the quirks of Python's object model. For the most part, things will work as expected, but be careful with lists and other mutable objects.

# Argument lists

Python functions allow variable length, argument lists like the variadic arguments in C and other languages.


from chapter seven of the exercise files. And you'll notice down here, in our definition for our function, we have a variable name with an asterisk before it. This is the variable length argument list. And you'll notice that we can treat it like a sequence, it's actually a tuple. So I check and see if the length is greater than zero. Remember that zero is false, anything greater than zero is one, and if it is then I print each of the items in the list, otherwise, I just print meow.

def main():
    kitten('meow', 'grrr', 'purr')

def kitten(*args):
    if len(args):
        for s in args:
            print(s)
    else: print('Meow.')

if __name__ == '__main__': main()
1
2
3
4
5
6
7
8
9
10

And so when I run this you notice we get the three arguments that we had passed it. And if I call without those arguments, we just get meow with a period, just as it is here in this print argument.

meow
grrr
purr
1
2
3

It's traditional to name your list args, A-R-G-S, and I recommend that you follow that tradition. That way, when people are reading your code, it's obvious what you're doing. And of course you can call this with however many of these you want. And it will print however many you want. It can be just one or it can be many. This feature is useful for situations where you want a function that may be called with different numbers of arguments. You may also call the same function with a prepared list like this.

def main():
    kitten('meow', 'grrr', 'purr', 'hello', 'world', 'earth')

def kitten(*args):
    if len(args):
        for s in args:
            print(s)
    else: print('Meow.')

if __name__ == '__main__': main()
1
2
3
4
5
6
7
8
9
10
meow
grrr
purr
hello
world
earth
1
2
3
4
5
6

You may also call the same function with a prepared list like this. And so if I copy this, that'll give us a tuple. And so if we have a tuple or a list or some other collection, I can call it by putting an asterisk before the variable name, and we'll run it and it gets exactly the same results. That'll simply pass a reference to the same object.

def main():
    x = ('meow', 'grrr', 'purr', 'hello', 'world', 'earth')
    kitten(*x)

def kitten(*args):
    if len(args):
        for s in args:
            print(s)
    else: print('Meow.')

if __name__ == '__main__':
    main()
1
2
3
4
5
6
7
8
9
10
11
12
meow
grrr
purr
hello
world
earth
1
2
3
4
5
6

List arguments are simple to use. They're useful when you need a function that may have different numbers of arguments, and we'll see many examples of this throughout the course.

# Keyword arguments

Key word arguments are like list arguments that are dictionaries instead of tuples. This allows your function to have a variable number of named arguments.

And you notice, down here on line seven, the function argument has two asterisks in front of it instead of one, as we had for the list arguments. And it's named kwargs, which stands for key word arguments. And so when I run this, you'll notice that it says Kitten Buffy says meow, Zilla says grr, Angel says rawr. When we call the function, we have named each of the arguments. Buffy = 'meow', Zilla = 'grr', Angel = 'rawr.' This is the same syntax as we used for dictionary. And, in fact, this is a dictionary.

def main():
    kitten(Buffy = 'meow', Zilla = 'grr', Angel = 'rawr')

def kitten(**kwargs):
    if len(kwargs):
        for k in kwargs:
            print('Kitten {} says {}'.format(k, kwargs[k]))
    else: print('Meow.')

if __name__ == '__main__': 
    main()
1
2
3
4
5
6
7
8
9
10
11
Kitten Buffy says meow
Kitten Zilla says grr
Kitten Angel says rawr
1
2
3

If I wanted to, I could say, x equals and dictionary with those arguments, and then I can pass it x. And just as I would use one asterisk for a list, I use two asterisks for passing at this dictionary, and our result is exactly the same.

def main():
    x = dict(Buffy = 'meow', Zilla = 'grr', Angel = 'rawr')
    kitten(**x)

def kitten(**kwargs):
    if len(kwargs):
        for k in kwargs:
            print('Kitten {} says {}'.format(k, kwargs[k]))
    else: print('Meow.')

if __name__ == '__main__':
    main()
1
2
3
4
5
6
7
8
9
10
11
12
Kitten Buffy says meow
Kitten Zilla says grr
Kitten Angel says rawr
1
2
3

Again, it's traditional to name your keyword argument kwargs, you're not required to follow this convention, but it's generally a good idea to follow a convention. So that when people read your code, they'll know exactly what it means.


Keyword arguments are powerful and simple. They're useful when you need a function that may have different numbers of named arguments. And you'll see many examples later in this course.

# Return values

In Python, there is no distinction between a function and a procedure. All functions return a value.


I'm going to come down here and I'm going to assign the return value from kitten to a variable, and then I'm going to print that, along with its type. And so when I save and run this, you notice that the type is the none type, and the value is none. And that's because if there's no return statement, or an empty return statement, a function returns none.

def main():
    x = kitten()
    print(type(x), x)

def kitten():
    print('Meow.')

if __name__ == '__main__': main()
1
2
3
4
5
6
7
8
Meow.
<class 'NoneType'> None

Process finished with exit code 0
1
2
3
4

If I give it a return statement, and say return a number, then we save and run, and now the type is Int and the number is returned. Likewise I can return a list, and save and run, and I get a list, or I can return a dictionary, and when I save and run, we get that dictionary. So because of Python's effective object model,

def main():
    x = kitten()
    print(type(x), x)

def kitten():
    print('Meow.')
    return dict(x = 42, y = 43, z = 44)

if __name__ == '__main__': main()
1
2
3
4
5
6
7
8
9
Meow.
<class 'dict'> {'x': 42, 'y': 43, 'z': 44}
1
2

Python functions are capable of returning simple or complex values. We'll see many examples of this as we go through this course.

# Generators

A generator is a special class of function that serves as an iterator instead of returning a single value the generator returns a stream of values.

First I want to show you the problem that this generator is solving. If instead of calling my function I call range here and save and run you notice I've specified the number 25 and in a result it counts from zero to 24, which is 25 values starting at zero. I find that a little bit confusing myself at times and so I created a version of range that I call inclusive range. It simply works exactly like range only it returns all of the values from zero all the way up to 25, so when I save and run this you see that my result now includes the 25. You're welcome to use which ever version of this you like, but it's great as an illustration of how a generator works, because that's what range is is it's a generator. So let's take a look at the generator function.

def main():
    for i in inclusive_range(25):
        print(i, end = ' ')
    print()

def inclusive_range(*args):
    numargs = len(args)
    start = 0
    step = 1
    
    # initialize parameters
    if numargs < 1:
        raise TypeError(f'expected at least 1 argument, got {numargs}')
    elif numargs == 1:
        stop = args[0]
    elif numargs == 2:
        (start, stop) = args
    elif numargs == 3:
        (start, stop, step) = args
    else: raise TypeError(f'expected at most 3 arguments, got {numargs}')

    # generator
    i = start
    while i <= stop:
        yield i
        i += step

if __name__ == '__main__': main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 

Process finished with exit code 0
1
2
3

So let's take a look at the generator function. So you notice that we're going to be using a number of the techniques we've been learning so far. First off you'll notice that we use a variable argument list and our number of args is the length and we initialize a couple of variables and then we use this chain of elifs, if and elifs, to decide how to initialize our variables however many arguments we get. You remember the range works if it's just one argument that argument is the stop. If it has two arguments the two arguments are start and stop, and if it has three arguments the arguments are start, stop, and step, and if I have less than one argument I raise an error and if I have more than three arguments I raise an error. Then the actual generator is down here at the bottom. We use a while loop, we initialize I at the beginning and while I is less than or equal to stop then we have this yield. Now yield is like return except it's for a generator. It yields a value and then after it yields the value the function continues until it yields the next value. So when we run this we get this result.


So a generator is a special case of a function that is useful for creating a series of values.

# Decorators

A decorator is a form of metaprogramming and it can be described as a special type of function that returns a wrapper function.

First it's important to realize that in Python everything is an object. So a function is a type of object.

So a function is a type of object. So if I'm here and I define a function and I run it. So I'm calling the function and you notice that it prints out this is f1. But what's interesting here is I can say x equals f1 with the parenthesis and now I'm assigning that function object to the variable x. But everything is an object, so a variable is an object too and I can simply call the function f1 by calling x. So when I save and run, I get exactly the same result.

def f1():
    print('this is f1')

x = f1
x()
1
2
3
4
5
this is f1
1

So this means that I can do silly things like this. So now what I've done, is I've created a function f1 that contains another function f2 and I cannot call f2 directly, because it's scope is inside the function. Remember functions do define scope, locks don't define scope, functions do. And so when I run this, you notice it runs f2, because the return value from f1 is the object f2. And so take that return value, I assign it to x and I call x as if it were a function.

def f1():
    def f2():
        print('this is f2')
    return f2

x = f1()
x()
1
2
3
4
5
6
7
this is f2
1

Now we're going to create a decorator, 'cause this is where this gets actually very interesting. I'll define f3 down here, outside of the scope of the function and I print this is f3. Now f1 is going to take a argument, which it's going to use as a function and I'm going to print, this is before function call and this is after the function call. And we're going to call f, which is the function that we passed in the argument list. And now down here, I'm going to call f1 with f3 as an argument, but I'm going to take the return value and I'm going to call on that return value. Now you remember the return value is f2, which calls the argument that's passed in. Now when I run this, you notice we get this before the function call, then it calls f3 because it was passed. But here's where it becomes a decorator.

def f1(f):
    def f2():
        print('this is before the function call')
        f()
        print('this is after the function call')
    return f2

def f3():
    print('this is f3')

x = f1(f3)
x()
1
2
3
4
5
6
7
8
9
10
11
12
this is before the function call
this is f3
this is after the function call
1
2
3

If I assign this to f3 and then call f3, now f3 in its original form is no longer available, only the wrapper is available and when I run this you see it's the wrapper.

def f1(f):
    def f2():
        print('this is before the function call')
        f()
        print('this is after the function call')
    return f2

def f3():
    print('this is f3')

f3 = f1(f3)
f3()
1
2
3
4
5
6
7
8
9
10
11
12
this is before the function call
this is f3
this is after the function call
1
2
3

Because this is so meta and self-referential and weird and recursive, instead there's a short cut for it, which simply looks like this.

def f1(f):
    def f2():
        print('this is before the function call')
        f()
        print('this is after the function call')
    return f2
@f1
def f3():
    print('this is f3')

#f3 = f1(f3)
f3()
1
2
3
4
5
6
7
8
9
10
11
12

Now when I call f3, we get that result.

this is before the function call
this is f3
this is after the function call
1
2
3

So it takes f3, this syntax here which is called a decorator, it takes the function which is defined directly after it, so the syntax is you have to have the decorator, followed directly by the function definition and it takes that function and it passes it as an argument to the decorator function and it returns and it assigns that name of f3 to the wrapper itself. And so now I can't call f3 directly, I can only call it through the wrapper and f3 now is wrapped inside that decorator function.


So how is this useful, because it's awful meta and weird looking right? Well here's how it's useful.


what it is. I have a function called elapsed_time which wraps the past function in two time stamps and prints out the elapsed time. It runs the function and prints out the elapsed time. I have this function here called big_sum that adds up a whole lot of numbers together and prints the sum. And then down here I simply call big_sum, but because big_sum has this decorator, it's actually wrapped in the elapsed_time wrapper. And so I'll save this and I'll run it and you see there's the result. Big_sum is that big number and it took 1.434 milliseconds to calculate that. So that's one practical example of what a decorator is good for.

import time

def elapsed_time(f):
    def wrapper():
        t1 = time.time()
        f()
        t2 = time.time()
        print(f'Elapsed time: {(t2 - t1) * 1000} ms')
    return wrapper


@elapsed_time
def big_sum():
    num_list = []
    for num in (range(0, 10000)):
        num_list.append(num)
    print(f'Big sum: {sum(num_list)}')

def main():
    big_sum()

if __name__ == '__main__': main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Big sum: 49995000
Elapsed time: 0.9946823120117188 ms
1
2

# 8. Structured Data

# Lists and tuples

The basic sequential types in Python are lists and tuples. Lists are mutable and tuples are not.

And here we define a list, to notice that the square brackets are used to create the list. A list is an ordered collection, it's sequential and it's interval. And you'll notice down here in our print list function, we're iterating with the for loop.

def main():
    game = ['Rock', 'Paper', 'Scissors', 'Lizard', 'Spock']
    print_list(game)


def print_list(o):
    for i in o: print(i, end=' ', flush=True)
    print()


if __name__ == '__main__': 
    main()
1
2
3
4
5
6
7
8
9
10
11
12

So when I run this, it lists in order, each of the elements in the list, rock, paper, scissors, lizard, Spock.

Rock Paper Scissors Lizard Spock 
1

I can access an individual item in the list using an index. So I can say print game sub one, like this. And if I run that, you'll see we get paper. Lists are zero-based so item number zero would be rock, item number one would be paper. So the first number is the beginning of the slice and the second number is the end of the slice noninclusive. So in other words, this would be items number one and two. So this would be paper and scissors. And if I run this, see we get paper and scissors.



 











def main():
    game = ['Rock', 'Paper', 'Scissors', 'Lizard', 'Spock']
    print(game[1:3])
    print_list(game)


def print_list(o):
    for i in o: print(i, end=' ', flush=True)
    print()


if __name__ == '__main__':
    main()
1
2
3
4
5
6
7
8
9
10
11
12
13
['Paper', 'Scissors']
Rock Paper Scissors Lizard Spock 
1
2

And again just like range, I can use the beginning, the end, and a step, and so this would start a paper and it would be every second item ending at the fifth item. And so if I run this, I'll get just paper and lizard.



 











def main():
    game = ['Rock', 'Paper', 'Scissors', 'Lizard', 'Spock']
    print(game[1:5:2])
    print_list(game)


def print_list(o):
    for i in o: print(i, end=' ', flush=True)
    print()


if __name__ == '__main__':
    main()
1
2
3
4
5
6
7
8
9
10
11
12
13
['Paper', 'Lizard']
Rock Paper Scissors Lizard Spock 
1
2

I can search a list using the index method. So I can say, i = game.index and search for paper. And this will return the index, which I can then use as a subscript. So I can say print, game, sub I like this. And when I run it, you see that it returns to paper. A list is mutable, which means I can change it.



 
 











def main():
    game = ['Rock', 'Paper', 'Scissors', 'Lizard', 'Spock']
    i = game.index('Paper')
    print(game[i])
    print_list(game)


def print_list(o):
    for i in o: print(i, end=' ', flush=True)
    print()


if __name__ == '__main__':
    main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Paper
Rock Paper Scissors Lizard Spock 
1
2

A list is mutable, which means I can change it. I can append an item like this. Run that and you see we now have rock paper scissors, lizard, spock, computer. Or I can insert an item, add a particular



 











def main():
    game = ['Rock', 'Paper', 'Scissors', 'Lizard', 'Spock']
    game.append('Computer')
    print_list(game)


def print_list(o):
    for i in o: print(i, end=' ', flush=True)
    print()


if __name__ == '__main__':
    main()
1
2
3
4
5
6
7
8
9
10
11
12
13
Rock Paper Scissors Lizard Spock Computer 
1

Or I can insert an item, add a particular insect, so I'll insert this. And I'll insert it at zero at the beginning. And you see, computer is now at the beginning of the list.


 












def main():
    game = ['Rock', 'Paper', 'Scissors', 'Lizard', 'Spock']
    game.insert(0, 'Computer')
    print_list(game)


def print_list(o):
    for i in o: print(i, end=' ', flush=True)
    print()


if __name__ == '__main__':
    main()
1
2
3
4
5
6
7
8
9
10
11
12
13
Computer Rock Paper Scissors Lizard Spock 
1

I can remove an item by value. And now paper will be omitted from the list, rock, scissors, lizard, Spock.



 











def main():
    game = ['Rock', 'Paper', 'Scissors', 'Lizard', 'Spock']
    game.remove('Paper')
    print_list(game)


def print_list(o):
    for i in o: print(i, end=' ', flush=True)
    print()


if __name__ == '__main__':
    main()
1
2
3
4
5
6
7
8
9
10
11
12
13
Rock Scissors Lizard Spock 
1

Or I can use pop to remove an item from the end of the list. And this will remove an item from the end of the list. Pop also returns the removed value. And so this is useful with append to simulate a stack.



 
 











def main():
    game = ['Rock', 'Paper', 'Scissors', 'Lizard', 'Spock']
    x = game.pop()
    print(x)
    print_list(game)


def print_list(o):
    for i in o: print(i, end=' ', flush=True)
    print()


if __name__ == '__main__':
    main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Spock
Rock Paper Scissors Lizard 
1
2

I can also use pop to remove an item at a particular index. So if I remove item three, that would remove lizard.



 











def main():
    game = ['Rock', 'Paper', 'Scissors', 'Lizard', 'Spock']
    game.pop(3)
    print_list(game)


def print_list(o):
    for i in o: print(i, end=' ', flush=True)
    print()


if __name__ == '__main__':
    main()
1
2
3
4
5
6
7
8
9
10
11
12
13
Rock Paper Scissors Spock 
1

I can use the delete statement to remove an item by index del game[3]. Again that will remove lizard. So we'll get the same result. Or I can remove by a slicedel game[1:3]. And so that will remove paper and scissors or I can remove by a slice like this del game[1:5:2]and that would remove paper and lizard.


I can join a list using the joint method on the string type. So I can say print with a string that'll be used to join and then I can say join and our list. And now it prints our this list joined by comma space in between each of the elements. So that's really useful.



 











def main():
    game = ['Rock', 'Paper', 'Scissors', 'Lizard', 'Spock']
    print(', '.join(game))
    print_list(game)


def print_list(o):
    for i in o: print(i, end=' ', flush=True)
    print()


if __name__ == '__main__':
    main()
1
2
3
4
5
6
7
8
9
10
11
12
13
Rock, Paper, Scissors, Lizard, Spock
Rock Paper Scissors Lizard Spock 
1
2

I can get a raw count of the items in the list using the len function. And this is a function, it is not an object method. So I can say print, len of game. And that will print five which is the length of our list.

print(len(game))
1

Now a tuple works exactly like a list except that it's immutable, and it's indicated by parentheses. So when I run this, you can see it runs exactly the same. But if I try to append, I'll get an error that says tuple object has no attribute append because tuples are immutable and they cannot be changed.


Lists and tuples are fundamental sequence types in Python. The list type is mutable, and the tuple is not.

I tend to favor the tuple unless I absolutely require a mutable sequence.

# Dictionaries

Python's dictionary type is a hashed key value structure. This is comparable to associative arrays in other languages.


You notice that the dictionary is created using curly braces so there's the open curly brace and the closed curly brace and there's key value pairs. That's a key value pair. Each of the key value pairs has a key on the left, a value on the right, and they're separated by a colon. The pairs are separated by commas within the structure sets. One key value pair, that's another one, and et cetera. You notice also that I have a little print dict function which prints the dictionary and when I run it you can see it prints it in a nice readable format.

def main():
    animals = {'kitten': 'meow', 'puppy': 'ruff!', 'lion': 'grrr',
               'giraffe': 'I am a giraffe!', 'dragon': 'rawr'}
    print_dict(animals)


def print_dict(o):
    for x in o: print(f'{x}: {o[x]}')


if __name__ == '__main__': 
    main()
1
2
3
4
5
6
7
8
9
10
11
12
kitten: meow
puppy: ruff!
lion: grrr
giraffe: I am a giraffe!
dragon: rawr
1
2
3
4
5

You can also create the dictionary using the dictionary constructor and keyword arguments and personally I find this a lot more convenient, and now it looks like this. You can see this is a little bit easier to read and it's a lot easier to type and when we save and run it gives us exactly the same results.

animals = dict(kitten='meow', puppy='ruff!', lion='grrr',
               giraffe='I am a giraffe!', dragon='rawr')
1
2

Keys and values may be any type. Keys must be immutable, strings and numbers can always be keys and this keyword argument method of creating the dictionary is obviously most convenient when you're using strings for your keys.


The items method returns a view of key value pairs. This can be used to simplify the loop. So I can say print... Or rather, for, we start with the for loop, for K comma V and so K and V are the key and value in animals.items. I print, we use an F string here and we'll have the key and the value like that and when I save and run this I'll just comment out this here and when I save and run it you see we get exactly the same result.




 
 










def main():
    animals = dict(kitten='meow', puppy='ruff!', lion='grrr',
                   giraffe='I am a giraffe!', dragon='rawr')
    for k, v in animals.items():
        print(f'{k}: {v}')
    #print_dict(animals)


def print_dict(o):
    for x in o: print(f'{x}: {o[x]}')


if __name__ == '__main__':
    main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
kitten: meow
puppy: ruff!
lion: grrr
giraffe: I am a giraffe!
dragon: rawr
1
2
3
4
5

So I can easily replace this here with something a bit more readable. We can use that for a print dict. And I still get the same result.








 
 





def main():
    animals = dict(kitten='meow', puppy='ruff!', lion='grrr',
                   giraffe='I am a giraffe!', dragon='rawr')
    print_dict(animals)


def print_dict(o):
    for k, v in o.items():
        print(f'{k}: {v}')


if __name__ == '__main__':
    main()
1
2
3
4
5
6
7
8
9
10
11
12
13
kitten: meow
puppy: ruff!
lion: grrr
giraffe: I am a giraffe!
dragon: rawr
1
2
3
4
5

The keys method returns a view of dictionary keys, so I can say for K in animals.keys print K, and then we have a view of just the keys.

def main():
    animals = dict(kitten='meow', puppy='ruff!', lion='grrr',
                   giraffe='I am a giraffe!', dragon='rawr')
    for k in animals.keys(): 
        print(k)
    #print_dict(animals)


def print_dict(o):
    for k, v in o.items():
        print(f'{k}: {v}')


if __name__ == '__main__':
    main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
kitten
puppy
lion
giraffe
dragon
1
2
3
4
5

or I can just get the values like this with the values method and that'll give us just a list of the values.

for v in animals.values():
    print(v)
1
2
meow
ruff!
grrr
I am a giraffe!
rawr
1
2
3
4
5

A dictionary is indexed by its keys so you can easily pick a particular element, you can say print animals sub lion and that will just print the lion. It's just the value so the subscript returns a value just like it does with a list only in this case you're using the key instead of an index.

def main():
    animals = dict(kitten='meow', puppy='ruff!', lion='grrr',
                   giraffe='I am a giraffe!', dragon='rawr')
    print(animals['lion'])
    #print_dict(animals)


def print_dict(o):
    for k, v in o.items():
        print(f'{k}: {v}')


if __name__ == '__main__':
    main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
grrr
1

In fact, you may use this to assign a different value to the lion, you can have it say, I am a lion. And now when we print it we get lion is I am a lion. Or you may add a new item, say monkey.

animals['lion'] = 'I am a lion'
1

Or you may add a new item, say monkey. And monkeys of course say haha. And when I run this I now have monkey at the end of my list.

animals['monkey'] = 'haha'
1
kitten: meow
puppy: ruff!
lion: grrr
giraffe: I am a giraffe!
dragon: rawr
monkey: haha
1
2
3
4
5
6

You can search for a key by using the in operator. So say lion in animals and you'll notice that that returns a true value see that at the top of this there, true. Or you can use that with any conditional expression.




 











def main():
    animals = dict(kitten='meow', puppy='ruff!', lion='grrr',
                   giraffe='I am a giraffe!', dragon='rawr')
    print('lion' in animals)
    print_dict(animals)


def print_dict(o):
    for k, v in o.items():
        print(f'{k}: {v}')


if __name__ == '__main__':
    main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
True
kitten: meow
puppy: ruff!
lion: grrr
giraffe: I am a giraffe!
dragon: rawr
1
2
3
4
5
6

Or you can use that with any conditional expression. And so that'll say found and if I'm looking for something that's not there you'll see it says nope.




 











def main():
    animals = dict(kitten='meow', puppy='ruff!', lion='grrr',
                   giraffe='I am a giraffe!', dragon='rawr')
    print('found!' if 'lion' in animals else 'nope!')
    print_dict(animals)


def print_dict(o):
    for k, v in o.items():
        print(f'{k}: {v}')


if __name__ == '__main__':
    main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
found
kitten: meow
puppy: ruff!
lion: grrr
giraffe: I am a giraffe!
dragon: rawr
1
2
3
4
5
6

you'll see it says nope. If you try to access a key that doesn't exist you'll get a key error exception so if I say print animals sub Godzilla you notice that I get the exception of key error Godzilla because that key does not exist. Or you can use the get method to return a value when you don't know if the key exists. I can instead say animals.get like that and I get the none value because that key doesn't exist so if you don't want the exception, you want just a none value, you can use the get method.

def main():
    animals = dict(kitten='meow', puppy='ruff!', lion='grrr',
                   giraffe='I am a giraffe!', dragon='rawr')
    print(animals.get('godzilla'))
    print_dict(animals)


def print_dict(o):
    for k, v in o.items():
        print(f'{k}: {v}')


if __name__ == '__main__':
    main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
None
kitten: meow
puppy: ruff!
lion: grrr
giraffe: I am a giraffe!
dragon: rawr
1
2
3
4
5
6

So Python's dictionary type is both simple and useful. You'll see many examples in the rest of this course.

# Sets

Python has a datatype for sets. A set is like a list that does not allow duplicate elements.


You'll notice down here on line five and six I've defined two different sets and I've defined them just using strings and when I run this you'll notice using my print_set function below that what I get is an unordered list of the unique characters in each string. There's no duplicates because a set does not allow duplicates.

def main():
    a = set("We're gonna need a bigger boat.")
    b = set("I'm sorry, Dave. I'm afraid I can't do that.")
    print_set(a)
    print_set(b)


def print_set(o):
    print('{', end = '')
    for x in o: print(x, end = '')
    print('}')


if __name__ == '__main__': 
    main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

And the lists are unordered, each time I run it you'll notice they come up in a different order.

{o'ei bWn.datrg}
{'mIo.ivrfse Dd,chynat}
1
2
{edg.'Wai brnto}
{.ioIehf'vrDs,mcyda nt}
1
2
{otgrd.'Wa iben}
{rDt,d.ife'omya hcIsvn}
1
2

Of course it's entirely possible to sort them. If you really want them in order and now you see that they are both sorted and each time I run it they come up in the same sorted order.

print_set(sorted(a))
print_set(sorted(b))
1
2
{ '.Wabdeginort}
{ ',.DIacdefhimnorstvy}
1
2

But mostly if you're using sets, you're going to be checking the membership of the set and using them as sets not as lists. If you want an ordered list, you'll use a list or a string. So for example, I can check for the members that are in set a but not in set b by using the minus operator. And that's the members of set a that are not also in set b.

def main():
    a = set("We're gonna need a bigger boat.")
    b = set("I'm sorry, Dave. I'm afraid I can't do that.")
    print_set(a - b)


def print_set(o):
    print('{', end = '')
    for x in o: print(x, end = '')
    print('}')


if __name__ == '__main__':
    main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{bgW}
1

I can find the members that are in set a or set b or both by using the vertical bar operator and that's all of the members that are in one or both of the sets.

print_set(a - b)
1
{rbgts'emv,yiIaohcf Wn.Dd}
1

Or I can look for the exclusive or using the caret operator. So that's a or b but not both and when I save and run that I get an entirely different list.

print_set(a ^ b)
1
{WyDvbIcmfsgh,}
1

Or I can look for only the members that are in both and when I save and run that, you see we get that different list.

print_set(a & b)
1
{'n er.oiadt}
1

So like the other collections in python, members of a set can be any type. In this case I found that the strings were convenient way to get a bunch of data into them easily so we can look at the intersections of the sets. And a set is like a list that does not allow duplicate elements and it's a useful tool in many circumstances.

# List comprehension

A list comprehension is a list created based on another list or iterator. This list comprehension is a very common technique in Python, so it's good to understand it.

Here on line five, I've created a sequence, based on the range function, the range generator, so it will be zero to 10. If I save and run this, you'll see it says zero through 10 in our output. I have this little print_list, which is for printing our sequence.

def main():
    seq = range(11)
    print_list(seq)


def print_list(o):
    for x in o:
        print(x, end=' ')
    print()


if __name__ == '__main__':
    main()
1
2
3
4
5
6
7
8
9
10
11
12
13
0 1 2 3 4 5 6 7 8 9 10 
1

And let's go ahead and create a list comprehension. I'll create a second sequence, I'll call it seq2. And this one will be a list and it'll be a list of each of the elements of our first list, multiplied by two. So I'll just say x times two for x in seq. It's as simple as that.

def main():
    seq = range(11)
    seq2 = [x * 2 for x in seq]
    print_list(seq)
    print_list(seq2)


def print_list(o):
    for x in o:
        print(x, end=' ')
    print()


if __name__ == '__main__':
    main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
0 1 2 3 4 5 6 7 8 9 10 
0 2 4 6 8 10 12 14 16 18 20
1
2

We can do a lot of things with this. We can get a list of all the elements of our first list that are not divided by three, by saying x for x in seq if x module 03 is not equal to zero. And now I'll run it and you see we get only the elements that are not divisible by three. You'll notice I have this extra if clause, and that's allowed only after the for clause.

seq2 = [x for x in seq if x % 3 != 0]
1
0 1 2 3 4 5 6 7 8 9 10 
1 2 4 5 7 8 10 
1
2

I can also create a list of tuples, if I want to. X comma x squared. So this will be a list of each element of our sequence squared, and the element squared. So there will be two in each element here, there will be a tuple, for x in sequence. And when I run it, you see we get a list of tuples, with the original element from the sequence and the squared element.

seq2 = [(x, x**2) for x in seq]
1
0 1 2 3 4 5 6 7 8 9 10 
(0, 0) (1, 1) (2, 4) (3, 9) (4, 16) (5, 25) (6, 36) (7, 49) (8, 64) (9, 81) (10, 100) 
1
2

You can call functions if you want to. Here I'm going to import from math. I don't need the import there. From math import pi, so I have this constant. And our seq2 is now, calls the function round, pi, i for i in sequence. And so now I'll have pi rounded to the number of places for each element in the sequence. So pi rounded to zero, pi rounded to one, pi rounded to two, three, four, five, et cetera.

def main():
    seq = range(11)
    from math import pi
    seq2 = [round(pi, i) for i in seq]
    print_list(seq)
    print_list(seq2)


def print_list(o):
    for x in o:
        print(x, end=' ')
    print()


if __name__ == '__main__':
    main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0 1 2 3 4 5 6 7 8 9 10 
3.0 3.1 3.14 3.142 3.1416 3.14159 3.141593 3.1415927 3.14159265 3.141592654 3.1415926536 
1
2

You can even create a dictionary. And so I'm going to create a dictionary here, instead of a list. I'll say x: x squared for x in sequence. And for this I'm not going to be able to use my print_list, I'll just say print. And there's our dictionary for each element. We have the key, which is the original element for our sequence, and its square.



 

 











def main():
    seq = range(11)
    seq2 = {x: x**2 for x in seq}
    print_list(seq)
    print(seq2)


def print_list(o):
    for x in o:
        print(x, end=' ')
    print()


if __name__ == '__main__':
    main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
0 1 2 3 4 5 6 7 8 9 10 
{0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81, 10: 100}
1
2

And you may also create a set. I can say x for x in superduper, a little string there, if x not in pd like that, and that'll give me a set. And I can use print_list for this. This is fun. That'll give me a set of all the letters in the string superduper that are not also in the string pd. So everything that's not a p or a d in superduper. This is a little bit surprising, because a set is of course just the unique members, and so when I run it, there's just four letters there that are not p or d in superduper.



 

 











def main():
    seq = range(11)
    seq2 = {x for x in 'superduper' if x not in 'pd'}
    print_list(seq)
    print_list(seq2)


def print_list(o):
    for x in o:
        print(x, end=' ')
    print()


if __name__ == '__main__':
    main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
0 1 2 3 4 5 6 7 8 9 10 
r e u s 
1
2

So list comprehension is a powerful technique for creating lists and other collections. You'll see it often in Python code and you'll see other examples here in our course, as well.

# Mixed structures

In Python, everything is an object and variables store references to objects. This means that it's possible to store anything in a data structure.


And you notice here in my main function, I have a rather complicated set of stuff. I have r is a range, l is a list, and that list includes integers, strings, a dictionary, and all kinds of stuff. T is a tuple, which again, includes strings and a None value in the middle of it. S is a set, d is a dictionary. And then I have this mixed which is a list of all of these different structures. And then I have this display function, which I'll show you in a minute which will display the mixed. And I'm just going to run it right now and you can see down here, here's all our different elements.

# globals
dlevel = 0 # manage nesting level


def main():
    r = range(11)
    l = [1, 'two', 3, {'4': 'four'}, 5]
    t = ('one', 'two', None, 'four', 'five')
    s = set("It's a bird! It's a plane! It's Superman!")
    d = dict(one=r, two=l, three=s)
    mixed = [l, r, s, d, t]
    disp(mixed)


def disp(o):
    global dlevel

    dlevel += 1
    if   isinstance(o, list):  print_list(o)
    elif isinstance(o, range): print_list(o)
    elif isinstance(o, tuple): print_tuple(o)
    elif isinstance(o, set):   print_set(o)
    elif isinstance(o, dict):  print_dict(o)
    elif o is None: print('Nada', end=' ', flush=True)
    else: print(repr(o), end=' ', flush=True)
    dlevel -= 1

    if dlevel <= 1: print() # newline after outer


def print_list(o):
    print('[', end=' ')
    for x in o: disp(x)
    print(']', end=' ', flush=True)


def print_tuple(o):
    print('(', end=' ')
    for x in o: disp(x)
    print(')', end=' ', flush=True)


def print_set(o):
    print('{', end=' ')
    for x in sorted(o): disp(x)
    print('}', end=' ', flush=True)


def print_dict(o):
    print('{', end=' ')
    for k, v in o.items():
        print(k, end=': ' )
        disp(v)
    print('}', end=' ', flush=True)


if __name__ == '__main__': 
    main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

So first is the list, and you remember the list has one, two, and it's got a dictionary in the middle of it. Then is the range. Then I have the set. And so that's all of the characters in the set. Then I have the dictionary, and see the dictionary has one is r, which is the range, two is l which is the list. Three is s which is the set. So we have all this stuff in that dictionary there. And then we have the tuple with the one, two, none, which is displaying as Nada because of the way that I have my display function set up. So let's take a look at the display function.

[ [ 1 'two' 3 { 4: 'four' } 5 ] 
[ 0 1 2 3 4 5 6 7 8 9 10 ] 
{ ' ' '!' "'" 'I' 'S' 'a' 'b' 'd' 'e' 'i' 'l' 'm' 'n' 'p' 'r' 's' 't' 'u' } 
{ one: [ 0 1 2 3 4 5 6 7 8 9 10 ] two: [ 1 'two' 3 { 4: 'four' } 5 ] three: { ' ' '!' "'" 'I' 'S' 'a' 'b' 'd' 'e' 'i' 'l' 'm' 'n' 'p' 'r' 's' 't' 'u' } } 
( 'one' 'two' Nada 'four' 'five' ) 
]  
1
2
3
4
5
6

So let's take a look at the display function. You can see that this is a really interesting data structure and what I've done is I've used isinstance to determine is it a list, is it a range, is it a tuple, is it a set? And I have different print functions for each of those. And you notice this function is called display and each of these print_list, print_tuple, they each call display for the elements within it. And so it can test and see if it has to print another structure or if it's none, it's going to print the word Nada. Or otherwise, if it's none of those things, it's just going to print a representation. I also have this

def disp(o):
    global dlevel

    dlevel += 1
    if   isinstance(o, list):  print_list(o)
    elif isinstance(o, range): print_list(o)
    elif isinstance(o, tuple): print_tuple(o)
    elif isinstance(o, set):   print_set(o)
    elif isinstance(o, dict):  print_dict(o)
    elif o is None: print('Nada', end=' ', flush=True)
    else: print(repr(o), end=' ', flush=True)
    dlevel -= 1

    if dlevel <= 1: print() # newline after outer
1
2
3
4
5
6
7
8
9
10
11
12
13
14

I also have this global variable here, dlevel to keep track of my nesting level.


You can see that our structures can be as complex as we want them to be and it's possible to iterate through these structures and introspect each element of each of these structures and decide what to do with it. And so I just wanted to show you this. I know it's kind of complex, but this is the power of Python's object model. Everything is an object. Objects may contain references to other objects including your own defined classes and objects. And we'll get to that in a later chapter. And this allows you to create virtually any structured data.

# 9. Classes

# Creating a class

A class is the basis of all data in Python, everything is an object in Python, and a class is how an object is defined.


Beginning on line one we see our class definition beginning with the keyword class, and the name of the class, and a colon, and then everything that defines the class is indented under that declaration.


We have data in the form of these variables, and we have methods in the form of these function definitions. You'll notice that the first parameter of a method is always self. Self is not a keyword. You can actually name that first parameter whatever you want to, but self is traditional and I highly recommend that you use self so that as people are reading your code, they know what you're talking about. So the first argument is self, which is a reference to the object, not the class, to the object. And so, when an object is created from the class, self will reference that object. And then everything that references anything defined in the class is dereferenced off itself to get the instantiated object version of it. And the period operator is used to dereference the object.

class Duck:
    sound = 'Quack quack.'
    movement = 'Walks like a duck.'

    def quack(self):
        print(self.sound)

    def move(self):
        print(self.movement)


def main():
    donald = Duck()
    donald.quack()
    donald.move()


if __name__ == '__main__':
    main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Quack quack.
Walks like a duck.
1
2

And the same is true outside of the class. So here we've defined a variable donald from the class Duck, and then in line 16, we invoke the object method quack on the object donald. So donald is the object, because it was instantiated from the class, the dot operator dereferences the object so that you can get to the method in this case quack. And so quack and move, as you can see, they print the sound and movement variables from up here, and so when I save this and run it we get those two strings printed. In fact, if you wanted to, although it's not recommended, you could simply print donald.sound, a variable itself, and get exactly the same result.


So this is the basics of how a class is defined.

# Constructing an object

An instance of a class is called an object. It's creating by calling the class itself as if it were a function.

class Animal:
    def __init__(self, type, name, sound):
        self._type = type
        self._name = name
        self._sound = sound

    def type(self):
        return self._type

    def name(self):
        return self._name

    def sound(self):
        return self._sound

def print_animal(o):
    if not isinstance(o, Animal):
        raise TypeError('print_animal(): requires an Animal')
    print('The {} is named "{}" and says "{}".'.format(o.type(), o.name(), o.sound()))


def main():
    a0 = Animal('kitten', 'fluffy', 'rwar')
    a1 = Animal('duck', 'donald', 'quack')
    print_animal(a0)
    print_animal(a1)
    print_animal(Animal('velociraptor', 'veronica', 'hello'))

if __name__ == '__main__': main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

You see down here in main, I create two objects, a0 and a1 from the animal class. And I'm initializing it with various parameters. Type of the animal, the name of the animal, and the sound that it makes. I'm also simply calling "print animal", so "print animal" is this function here that expects an animal object and prints the animal. And I'm calling it directly here from the constructor. From the class, without creating an intermediary object. This works exactly the same way because if you'll remember, function parameters work exactly like assignments in Python.


def main():
    a0 = Animal('kitten', 'fluffy', 'rwar')
    a1 = Animal('duck', 'donald', 'quack')
    print_animal(a0)
    print_animal(a1)
    print_animal(Animal('velociraptor', 'veronica', 'hello'))
1
2
3
4
5
6
7

And so when I run this, you see I get three animals, a kitten named Fluffy that says "rwar", and if we scroll down here you can see that. A duck named Donald that says "quack", and a velociraptor named Veronica that says "hello".

The kitten is named "fluffy" and says "rwar".
The duck is named "donald" and says "quack".
The velociraptor is named "veronica" and says "hello".
1
2
3

So let's take a look at the class constructor. There's a special class method name called init, with double underscores before and after, so those are two underscore characters. One and two, both before and after the word init. And that's a special name for a class function which operates as an initializer, or a constructor. And so you pass it three arguments, of course the first argument is always self, and that's what makes it a method, an object method, because this self points at the object. And then the three parameters type, name, and sound. And those are used to initialize object variables. And these are object variables because they're never initialized until after the object is defined, so they don't exist in the class without having been constructed into an object. And you notice that the object variables all have an underscore at the beginning of the name. Again, this is traditional, and this discourages users of the object from accessing these variables directly.

def __init__(self, type, name, sound):
    self._type = type
    self._name = name
    self._sound = sound
1
2
3
4

Instead, you have these accessors, or getters, I call them getters, some people call them accessors. Which simply return the value of those object variables. self._type, self._name and self._sound. And then down here when we print the animal, we use those getters in order to access the variables.


So the object is created by using the class name as if it were a function name. And this calls the constructor.


Now you'll notice that I had these three parameters, and I don't have an easy way of really remembering the order of them, so a lot of times instead, somebody will do it this way. We'll use "kwargs", and then each of these can be "kwargs" sub type, and name, and then down here, I can say, "type = 'kitten'", "name = 'fluffy'", and "sound = 'rwar'", and I'll go ahead and do this without the extra spaces around the equal sign. And do the same thing here. And here, and now when I run it, of course, I have exactly the same result, but now it's a little bit easier to remember these parameters, and I can put them in in any order if I want to.

class Animal:
    def __init__(self, **kwargs):
        self._type = kwargs['type']
        self._name = kwargs['name']
        self._sound = kwargs['sound']

    def type(self):
        return self._type

    def name(self):
        return self._name

    def sound(self):
        return self._sound

def print_animal(o):
    if not isinstance(o, Animal):
        raise TypeError('print_animal(): requires an Animal')
    print('The {} is named "{}" and says "{}".'.format(o.type(), o.name(), o.sound()))


def main():
    a0 = Animal(type='kitten', name='fluffy', sound='rwar')
    a1 = Animal(type='duck', name='donald', sound='quack')
    print_animal(a0)
    print_animal(a1)
    print_animal(Animal(type='velociraptor', name='veronica', sound='hello'))

if __name__ == '__main__': main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
The kitten is named "fluffy" and says "rwar".
The duck is named "donald" and says "quack".
The velociraptor is named "veronica" and says "hello".
1
2
3

In fact, I can even give them default values. I can say "if 'type' in kwargs else", and give it a default value. And now, if I want to, I can come down here, and I can say "print animal", and just give it the animal class with simply parentheses, which will give it the default object. And now when I save and run it, you see, we get the last one here, which is a kitten named Fluffy that says "rwar".

class Animal:
    def __init__(self, **kwargs):
        self._type = kwargs['type'] if 'type' in kwargs else 'kitten'
        self._name = kwargs['name'] if 'name' in kwargs else 'fluffy'
        self._sound = kwargs['sound'] if 'sound' in kwargs else 'rawr'

    def type(self):
        return self._type

    def name(self):
        return self._name

    def sound(self):
        return self._sound

def print_animal(o):
    if not isinstance(o, Animal):
        raise TypeError('print_animal(): requires an Animal')
    print('The {} is named "{}" and says "{}".'.format(o.type(), o.name(), o.sound()))


def main():
    a0 = Animal(type='kitten', name='fluffy', sound='rwar')
    a1 = Animal(type='duck', name='donald', sound='quack')
    print_animal(a0)
    print_animal(a1)
    print_animal(Animal(type='velociraptor', name='veronica', sound='hello'))
    print_animal(Animal())
    
if __name__ == '__main__': main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
The kitten is named "fluffy" and says "rwar".
The duck is named "donald" and says "quack".
The velociraptor is named "veronica" and says "hello".
1
2
3

So that's just a little bit about the constructor. An object is an instance of a class. An object is created by calling the class as if it were a function, and the constructor is used to initialize the object.

# Class methods

A function that is associated with a class is called a method. This provides the interface to the class and its objects.


If we look down here, we'll find this method called type which serves as both a getter and a setter, and so I call it a getter setter.







 
 
 





















class Animal:
    def __init__(self, **kwargs):
        self._type = kwargs['type'] if 'type' in kwargs else 'kitten'
        self._name = kwargs['name'] if 'name' in kwargs else 'fluffy'
        self._sound = kwargs['sound'] if 'sound' in kwargs else 'meow'

    def type(self, t = None):
        if t: self._type = t
        return self._type

    def name(self, n = None):
        if n: self._name = n
        return self._name

    def sound(self, s = None):
        if s: self._sound = s
        return self._sound

    def __str__(self):
        return f'The {self.type()} is named "{self.name()}" and says "{self.sound()}".'

def main():
    a0 = Animal(type = 'kitten', name = 'fluffy', sound = 'rwar')
    a1 = Animal(type = 'duck', name = 'donald', sound = 'quack')
    print(a0)
    print(a1)

if __name__ == '__main__': 
    main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

You'll notice that the first argument to the function is self, and that's what makes this a method and not just a plain function. This is filled in automatically, when I call this method on the object, I don't provide this argument. So I'll just provide one argument, and the fact that it's being called on the method will provide the first argument, self. It's common to name this argument self, although it's not required, you can name it whatever you'd like. But it's a really good idea to always use the word self, because it's traditional, and when people are reading your code, they'll know what it means.


So in this case, the second argument is t, and you notice it has a default value of none, so if there is no value, or if it's none, then this if will fail and it'll just return the value of type. If there is a value, then it'll go ahead and it'll set type, before returning it. So that's what makes it a setter getter.


You'll also notice that the object variables are named with a leading underscore, and this again is traditional. Python doesn't have private variables, and so there's no way to actually prevent somebody from using these. But this indicates that it's a private variable, and it should not be set or retrieved outside of the setter getter.


So when we run this, you'll notice that we have the same output as we did in previous lessons.

The kitten is named "fluffy" and says "rwar".
The duck is named "donald" and says "quack".
1
2

And if I go down here to the bottom, you'll notice that I'm creating two objects, a0 and a1, for the two animals. And I'm printing them out. And we'll get, in a moment, to how this is happening, that I'm just printing them out with print directly like that.

def main():
    a0 = Animal(type = 'kitten', name = 'fluffy', sound = 'rwar')
    a1 = Animal(type = 'duck', name = 'donald', sound = 'quack')
    print(a0)
    print(a1)
1
2
3
4
5

But if I take, say, a0 and I want to change its sound, I simply call the sound function and I change it to, say, a bark.

def main():
    a0 = Animal(type = 'kitten', name = 'fluffy', sound = 'rwar')
    a1 = Animal(type = 'duck', name = 'donald', sound = 'quack')
    a0.sound('bark')
    print(a0)
    print(a1)
1
2
3
4
5
6

And now, when I run this, you'll notice that fluffy says bark, instead of rwar, like it was initialized to. And that's because I have set this variable, using the setter getter for sound, which is right there actually, at the bottom.

The kitten is named "fluffy" and says "bark".
The duck is named "donald" and says "quack".
1
2

Now you'll also notice this special method called str, with two underscores before and two underscores after. We've seen this before, in our constructor init, with two underscores before and two underscores after. So this is a specially-named method, which provides the string representation of the object. And this allows us to print it with simply this print and the object like that, without needing a special function, like we had in the previous lessons. You can find a list of all the special method names here in the documentation, under data model. And if you click on special method names, you'll see a whole bunch of them, with all of their descriptions. And there's str right there, the informal or nicely printable string representation of an object. And there's just a lot more of them. There's all the comparison operators, and this list goes on for quite a while.

def __str__(self):
    return f'The {self.type()} is named "{self.name()}" and says "{self.sound()}".'
1
2

Methods are the primary interface for classes and objects. They work exactly like functions, except they are bound to the object through their first argument, commonly named self.

# Object data

Data may be associated with a class or an object and it's important to understand the distinction.


And here in the init method, which is our constructor you notice that I'm initializing three different variables type, name, and sound. And these are object variables. They only exist, when the object is created from the class they do not exist in the class itself. And so, I'm going to go ahead and run this and you can see where using those variables

class Animal:
    def __init__(self, **kwargs):
        self._type = kwargs['type'] if 'type' in kwargs else 'kitten'
        self._name = kwargs['name'] if 'name' in kwargs else 'fluffy'
        self._sound = kwargs['sound'] if 'sound' in kwargs else 'meow'

    def type(self, t=None):
        if t: self._type = t
        return self._type

    def name(self, n=None):
        if n: self._name = n
        return self._name

    def sound(self, s=None):
        if s: self._sound = s
        return self._sound

    def __str__(self):
        return f'The {self.type()} is named "{self.name()}" and says "{self.sound()}".'


def main():
    a0 = Animal(type='kitten', name='fluffy', sound = 'rwar')
    a1 = Animal(type='duck', name='donald', sound = 'quack')
    print(a0)
    print(a1)


if __name__ == '__main__': 
    main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
The kitten is named "fluffy" and says "rwar".
The duck is named "donald" and says "quack".
1
2

But what's important to realize here is that if I come in and I change one of them, a zero name = 'Joe' Now this is not a good idea, and you don't normally want to access these underscore variables directly. The underscore means don't do that. And so I'm just doing this for demonstration purposes. So that we can see that when I print a zero dot name I'm going to move this stuff to the end here Oops, copied it, I didn't move it, there we go. So you see that a zero name is 'Joe', and a one name did not get affected. A one name is still "Donald".

def main():
    a0 = Animal(type='kitten', name='fluffy', sound = 'rwar')
    a1 = Animal(type='duck', name='donald', sound = 'quack')
    print(a0)
    print(a1)

    a0._name = 'Joe'
    print(a0._name)
    print(a1._name)
1
2
3
4
5
6
7
8
9
The kitten is named "fluffy" and says "rwar".
The duck is named "donald" and says "quack".
Joe
donald
1
2
3
4

So these are object variables, they don't exist in the class and are bound to the object, not to the class itself.


Now if instead, I come up here and I create something mutable so we can actually demonstrate this I'm going to create a list. one, two, three. This is a class variable, not an object variable. Because it's defined in the class, it's not defined in any method.


 








class Animal:
    x = [1, 2, 3]

    def __init__(self, **kwargs):
        self._type = kwargs['type'] if 'type' in kwargs else 'kitten'
        self._name = kwargs['name'] if 'name' in kwargs else 'fluffy'
        self._sound = kwargs['sound'] if 'sound' in kwargs else 'meow'

    ...
1
2
3
4
5
6
7
8
9

And so If I come down here now, and I print a zero dot x, and you see I have that list one, two, three If I now change in a one, not in a zero, in a one the first element of that list to a seven let's say. And then I'll print it again. You'll see that I've changed the value but, I'm printing from a zero, and I changed it in a one. That's because this variable is associated with the class and only exists in the class. The object actually doesn't have this, it's drawing it from the class, not from the object. And that's a very important distinction. So this comes to something called encapsulation. Encapsulation is one of the major benefits of object oriented programing. If my variables are encapsulated, that means that they belong to the object, and not to the class. This variable here, this x variable is not encapsulated and so it exists, it's exactly the same object in every instance of the class. And every object that's created by the class, because it only exists in the class. So that makes it not encapsulated. As a general rule, except for things like constants that you are never going to change, and those should be immutable not mutable. You're really never going to want to put mutable data in the class. That's generally speaking a very bad idea.

    ...

def main():
    a0 = Animal(type='kitten', name='fluffy', sound='rwar')
    a1 = Animal(type='duck', name='donald', sound='quack')
    print(a0)
    print(a1)

    print(a0.x)
    a1.x[0] = 7
    print(a0.x)


if __name__ == '__main__':
    main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
The kitten is named "fluffy" and says "rwar".
The duck is named "donald" and says "quack".
[1, 2, 3]
[7, 2, 3]
1
2
3
4

more about these setters and getters, again this is one of the principles of encapsulation is that you never want to access these variables directly. And that's why they have the underscore in front of them. In many languages there's a concept of private variables that simply cannot be accessed or changed from outside of the class definition. Python doesn't have that. Instead, we have a convention. And the convention is to use this underscore and that means, do not touch this. And so you never want to set that directly from outside of the class methods. And so here I have this method which sets it and gets it, it's set in the constructor here and even when I read it in my string function, I'm reading it using the getter because that's just safe. And so it's important to understand and to adhere to this convention of never setting or getting these object variables directly to always use a method to do that.









 
 
 









 
 












class Animal:
    x = [1, 2, 3]

    def __init__(self, **kwargs):
        self._type = kwargs['type'] if 'type' in kwargs else 'kitten'
        self._name = kwargs['name'] if 'name' in kwargs else 'fluffy'
        self._sound = kwargs['sound'] if 'sound' in kwargs else 'meow'

    def type(self, t=None):
        if t: self._type = t
        return self._type

    def name(self, n=None):
        if n: self._name = n
        return self._name

    def sound(self, s=None):
        if s: self._sound = s
        return self._sound

    def __str__(self):
        return f'The {self.type()} is named "{self.name()}" and says "{self.sound()}".'


def main():
    a0 = Animal(type='kitten', name='fluffy', sound='rwar')
    a1 = Animal(type='duck', name='donald', sound='quack')
    print(a0)
    print(a1)


if __name__ == '__main__':
    main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

So it's important to understand the distinction between class variables and object variables and how to properly encapsulate data in your objects.

# Inheritance

Class inheritance is a fundamental part of object-oriented programming. This allows you to extend your classes by driving properties and methods from parent classes.


And you'll notice in our init method, we're no longer providing any default values. And that's because this is now just the base class, and it's going to be inherited in order to be used. Because of that, now we need to do a little bit of extra checking in our setter getters. We can't just return a value, we need to check and see if the value's actually there.



 
 
 















































class Animal:
    def __init__(self, **kwargs):
        if 'type' in kwargs: self._type = kwargs['type']
        if 'name' in kwargs: self._name = kwargs['name']
        if 'sound' in kwargs: self._sound = kwargs['sound']

    def type(self, t = None):
        if t: self._type = t
        try: return self._type
        except AttributeError: return None

    def name(self, n = None):
        if n: self._name = n
        try: return self._name
        except AttributeError: return None

    def sound(self, s = None):
        if s: self._sound = s
        try: return self._sound
        except AttributeError: return None


class Duck(Animal):
    def __init__(self, **kwargs):
        self._type = 'duck'
        if 'type' in kwargs: del kwargs['type']
        super().__init__(**kwargs)


class Kitten(Animal):
    def __init__(self, **kwargs):
        self._type = 'kitten'
        if 'type' in kwargs: del kwargs['type']
        super().__init__(**kwargs)


def print_animal(o):
    if not isinstance(o, Animal):
        raise TypeError('print_animal(): requires an Animal')
    print(f'The {o.type()} is named "{o.name()}" and says "{o.sound()}".')


def main():
    a0 = Kitten(name = 'fluffy', sound = 'rwar')
    a1 = Duck(name = 'donald', sound = 'quack')
    print_animal(a0)
    print_animal(a1)


if __name__ == '__main__': 
    main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

And so, I'm using exceptions for this. This is the normal way to do this. We're going to cover exceptions in a later chapter. But just understand that what this does, is it attempts to return the value, and if that fails, it returns none instead.




 
 


    ...
def type(self, t = None):
    if t: self._type = t
    try: return self._type
    except AttributeError: return None
    ...
1
2
3
4
5
6

So, to actually use this class, we're now inheriting it to create other classes. We've got a duck class, and a kitten class. And in each of these cases, we set the type to, in this case, duck, and we check and we see if there is a type in the keyword arguments and if so, we delete that, and then through the super function, we call the parent class initializer with our kwargs. So super always calls the parent class.





 
 





 
 


    ...
class Duck(Animal):
    def __init__(self, **kwargs):
        self._type = 'duck'
        if 'type' in kwargs: del kwargs['type']
        super().__init__(**kwargs)


class Kitten(Animal):
    def __init__(self, **kwargs):
        self._type = 'kitten'
        if 'type' in kwargs: del kwargs['type']
        super().__init__(**kwargs)
    ...
1
2
3
4
5
6
7
8
9
10
11
12
13
14

And then down here, we initialize a zero with a kitten, and a one with a duck, and when I run this, you notice we get the result that we expect.

    ...
def main():
    a0 = Kitten(name = 'fluffy', sound = 'rwar')
    a1 = Duck(name = 'donald', sound = 'quack')
    print_animal(a0)
    print_animal(a1)
    ...
1
2
3
4
5
6
7
The kitten is named "fluffy" and says "rwar".
The duck is named "donald" and says "quack".
1
2

So this allows me to do things like a special case, for example, here in kitten, we can define a method that doesn't exist in any of the other subclasses of animal, and so in this case we'll call this one kill. Because we know that kittens are actually predators, and are always trying to kill something, right? And again, we start with self, and s, s will be the string that will identify the target of its predatation, I don't know if that's a word or not, the target of its killing. And we'll use print, and we'll use an f string, self.name, will now kill all and the string. Exclamation point because that's a pretty important thing to know. And then down here, I can simply say, print a zero is the kitten, and I actually don't need to print 'cause I have a print in my method there, I can just say a0.kill humans. And now when I run it, fluffy will now kill all humans.







 
 













 





class Kitten(Animal):
    def __init__(self, **kwargs):
        self._type = 'kitten'
        if 'type' in kwargs: del kwargs['type']
        super().__init__(**kwargs)

    def kill(self, s):
        print(f'{self.name()} will now killl all {s}!')


def print_animal(o):
    if not isinstance(o, Animal):
        raise TypeError('print_animal(): requires an Animal')
    print(f'The {o.type()} is named "{o.name()}" and says "{o.sound()}".')


def main():
    a0 = Kitten(name = 'fluffy', sound = 'rwar')
    a1 = Duck(name = 'donald', sound = 'quack')
    print_animal(a0)
    print_animal(a1)
    a0.kill('humans')


if __name__ == '__main__':
    main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
The kitten is named "fluffy" and says "rwar".
The duck is named "donald" and says "quack".
fluffy will now killl all humans!
1
2
3

will now kill all humans. But I cannot run that on a one, because I get a attribute error, duck object has no attribute kill. 'Cause that only exists in the kitten, and not in the duck.


So, you can also, of course, extend built in classes, because in Python, everything is an object. Here's an example here, I have working copy of string.py, also from chapter nine of the exercise files, and here, you'll see that I'm inheriting from str. And it does not have a capital first letter. This is a built-in class, built-in to Python, and that's our standard string class. And I'm inhering from that, and I'm overwriting the string representation method, to instead of returning the string itself, to return a slice of the string where the step goes backwards. And so, this will reverse the string So this is a type of a string that every time I print it, it'll print backwards. And here I create one with hello world, and I print it. And when I print it, you'll notice that it prints hello world backwards.

class RevStr(str):
    def __str__(self):
        return self[::-1]

def main():
    hello = RevStr('Hello, World.')
    print(hello)

if __name__ == '__main__': main()
1
2
3
4
5
6
7
8
9
.dlroW ,olleH
1

So you can see that class inheritance is a very powerful thing to be able to do. And you can inherit and extend, and even as in this case, change built-in classes or classes that you define yourself. Class inheritance is a vital part of object-oriented programming and in Python, you can see it's relatively easy and intuitive.

# Iterator objects

An iterator is a class that provides a sequence of items, generally used in a loop.


to the generator from the functions chapter. So, let's take a look at how this works. Here we have our constructor and our constructor simply sets up all of the variables and it checks the arguments, it checks how many arguments we have, if there's just one, that's the stop, if there's two, it's start and stop, and if there's three arguments, it's start, stop, and step. Just like the range function, the built-in range function. And if we have the wrong number of arguments, we get a TypeError exception, again, we'll cover exceptions in detail later on in this course. And we initialize the starting point of our iterator, and then we have this special iterator method I-T-E-R with the double underscores on either side of it, and this simply identifies this object as an iterator object, and then there's the __next__ function, which is the iteration itself, a construct like the for loop is going to look for this __next__ function in order to treat this as an iterator, and in order to use it for iteration. If we've reached stop, we raise the StopIteration exception, otherwise, we go ahead and increment and return the value, and you'll notice there's no yield here, yield was implemented later than iterators, and generator functions are actually a simpler way to do exactly the same thing, but iterators are still very common, and so here we have our application, for n in inclusive range, and we use this just like we would with range.

class inclusive_range:
    def __init__(self, *args):
        numargs = len(args)
        self._start = 0
        self._step = 1
        
        if numargs < 1:
            raise TypeError(f'expected at least 1 argument, got {numargs}')
        elif numargs == 1:
            self._stop = args[0]
        elif numargs == 2:
            (self._start, self._stop) = args
        elif numargs == 3:
            (self._start, self._stop, self._step) = args
        else: raise TypeError(f'expected at most 3 arguments, got {numargs}')

        self._next = self._start
    
    def __iter__(self):
        return self

    def __next__(self):
        if self._next > self._stop:
            raise StopIteration
        else:
            _r = self._next
            self._next += self._step
            return _r

def main():
    for n in inclusive_range(25):
        print(n, end=' ')
    print()

if __name__ == '__main__': main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

and so when I run this, you see we get our range and come down here to the end again, and see where we've passed it 25, and so we get zero through 25 including the 25, and if we use this with just the built-in range function, you notice that we get that without the, it's not inclusive, it doesn't include the 25.

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 
1

I can set this up to start, simply cells, so I can say start at five, and when I run this, it's five through 25, and I can tell it to step by five, and when I run this, you notice it steps by five instead of by one, or I can do some other number, here I can have it step by three, or whatever.


So, an iterator is functionally identical to a generator function, a generator function is often easier to implement and will work just as well, but it's good to understand how iterators work, and you'll certainly see them in the wild.