Python
Python
Python
Programming
Learning Python the Smart Way
—
Gabor Guta
Pragmatic Python
Programming
Learning Python the Smart Way
Gabor Guta
Pragmatic Python Programming: Learning Python the Smart Way
Gabor Guta
Budapest, Hungary
Acknowledgments������������������������������������������������������������������������������xv
Introduction��������������������������������������������������������������������������������������xvii
v
Table of Contents
vi
Table of Contents
Inheritance����������������������������������������������������������������������������������������������������������61
Nested Classes���������������������������������������������������������������������������������������������������64
Special Methods�������������������������������������������������������������������������������������������������65
Classes in Practice����������������������������������������������������������������������������������������������68
Advanced Details������������������������������������������������������������������������������������������������70
Class Variables, Class Methods, and Static Methods������������������������������������70
Abstract Base Classes�����������������������������������������������������������������������������������72
Immutable Objects and Data Classes������������������������������������������������������������73
Methods of Identifying Classes���������������������������������������������������������������������76
Class Diagrams����������������������������������������������������������������������������������������������77
Key Takeaways����������������������������������������������������������������������������������������������������78
vii
Table of Contents
viii
Table of Contents
Bibliography�������������������������������������������������������������������������������������189
Index�������������������������������������������������������������������������������������������������193
ix
About the Author
Gabor Guta studied and carried out research
at the Research Institute for Symbolic
Computation, Johannes Kepler University,
Linz, to gain an understanding of the formal
meaning of programming languages. He
worked on complex technology transfer,
cheminformatics, and bioinformatics projects
where both strong theoretical background
and practical software development skills were crucial. Currently, he is
developing distributed software for an open data project. Besides his
software development work, he has been continuously training people
both in academic and industrial settings. He has been actively teaching
Python since 2017.
xi
About the Technical Reviewer
Joshua Willman began using Python in 2015
when he needed to build neural networks
using machine learning libraries for image
classification. While building large image
datasets for his research, he needed to build
a program that would simplify the workload
and labeling process, which introduced him
to PyQt. Since then, he has tried to dive into
everything that is Python.
He currently works as a Python developer, building projects to help
others learn more about coding in Python for game development, AI, and
machine learning. Recently, he set up the site redhuli.io to explore his and
others’ interests in utilizing programming for creativity.
He is the author of Modern PyQt: Create GUI Applications for Project
Management, Computer Vision, and Data Analysis and Beginning PyQt:
A Hands-on Approach to GUI Programming, both published by Apress.
xiii
Acknowledgments
I am thankful to István Juhász, my former diploma supervisor; without his
encouragement and enthusiastic support, this book could not have come
into existence. I am grateful to my friend László Szathmáry, who helped
me as an experienced Python developer and trainer with his insightful
feedback.
xv
Introduction
Communication gaps can build up in IT workplaces between developers
and other roles not requiring programming skills. This gap frequently
hurts the project’s progress and makes cooperation between participants
difficult. My intention is to bridge this gap with this book by explaining the
common mental models with which humans think. I will also demonstrate
the way these models are applied during the programming process.
The book is based on more than two decades of training and software
development experience. Python is not only a popular and modern
programming language, but it is also an easy-to-learn and efficient tool to
reach your goals.
I will not provide too many hands-on exercises and technical details
(how an operating system is built, the way a networking protocol works,
etc.). Regrettably, I cannot offer a quick option to acquire these skills, as
the only way to achieve these skills is with extensive amounts of practice
and troubleshooting. This book will give you a strong basis for starting that
practice.
xvii
Introduction
Figures use the UML notation, and a short description of their meaning
is shown at the end of Chapters 1, 3, 4, and 5. In the source code examples,
I deviate sometimes from the Python coding standard due to the page
layout constraints. These appear mostly with short or shortened names
and 40- to 50-character lines. Program code examples are conceptually
independent units, but they assume former examples of the book have
been run, since they may refer to definitions in them. The examples are
based on the most up-to-date Python 3.10.2 version available when writing
the book. Most of the examples can be run in versions 3.8.x and 3.9.x,
which are the most widespread at this time. Results of the examples are
intentionally not published in the book. The source code in the book is
available for download and you can experiment with it.
xviii
Introduction
Source Code
All source code used in this book is available for you to download from
https://github.com/Apress/pragmatic-python-programming.
Installing an Environment
The installation steps for the most widespread operating systems
(Windows 10, macOS, Ubuntu Linux 22.04 LTS) are described here so you
can run the examples in this book. For another operating system, you can
find assistance on the Internet.
Installation on Windows 10
Follow these steps to install and run Python on Windows 10:
xix
Introduction
xx
Introduction
Installation on macOS
Follow these steps to install and run Python on macOS:
xxi
Introduction
xxii
Introduction
xxiii
CHAPTER 1
Expression: How to
Compute
“Expressions are formed from operators and operands.”
500*2 + 200
2
Chapter 1 Expression: How to Compute
500:int 2:int
((500*2) + 200)
3
Chapter 1 Expression: How to Compute
4
Chapter 1 Expression: How to Compute
The task solved by the example is the following: if the amount of the
order is greater than 1000, a string containing the discount rate would be
created with the values 5% or none. Figure 1-2 shows the expression trees
formed upon evaluation of the expression.
5
Chapter 1 Expression: How to Compute
if-else:operator if-else:operator
500:int 2:int
1200:int 1000:int
0.05 == 5e-2
(0+1j)**2 == -1
During the design of the Python language, it was a vital design decision
that it should contain a small number of built-in types, and at first sight,
the behavior of those types is the same as that of the other, not built-in
types. Only five data types are examined more thoroughly here; the rest
will be discussed in Chapters 5 and 6.
7
Chapter 1 Expression: How to Compute
Variable Names
Now that we have seen how to carry out computation in specific cases,
let’s look at how a computation can be generalized. For this case, objects
are assigned to variable names. Variable names may be thought of most
simply as labels. Variable names usually begin with a letter and continue
with letters or digits. The expression in Listing 1-5 could be rewritten by
first assigning the numbers to variable names, as shown in Listing 1-8.
The advantage of this approach is that in case we were to compute the
expression for other numbers, we simply change only the assignment
statements in lines 1–3 and would not have to modify the expressions.
PRICE = 500
QUANTITY = 2
LIMIT = 1000
total_amount = PRICE * QUANTITY
d_available = total_amount >= LIMIT
discount = '5%' if d_available else 'none'
To express the intent that the variable names to which the value once
assigned will not require change, the variable names are capitalized. This
notation is not more than a convention, and the Python language does not
prevent them from being changed.
The resulting expression is thus much easier to understand.
Importantly, not the formula itself is assigned to the variable name, but
an object created upon computation of the expression instead. It is always
necessary to assign an object to the variable name before it is used (it
must appear on the left side of the equation before it can appear on the
right side).
8
Chapter 1 Expression: How to Compute
a+b a + b Identical.
a=2 a = 2 Identical.
ab a b Adding a space between alphabetic characters turns
the single name into two separate names, which are
in most cases syntactically incorrect (an important
exception is when one of the names is a keyword).
12 1 2 Adding a space between numeric characters turns
a single literal into two separate literals, which are
in most cases syntactically incorrect.
(continued)
9
Chapter 1 Expression: How to Compute
S
tatements
Listing 1-8 showed a program consisting of multiple lines. Each line is one
statement, more precisely an assignment statement. Programs consist of
statements that are executed one after the other, thereby exerting some effect
on their environment. The default in Python is that one line is one statement.
The recommended maximal line length is 79 characters. Listing 1-9 shows
how a long statement can be stretched to multiple lines (lines 3 to 7) or
multiple statements can be condensed into a single line (line 7). When a line
is too short for a statement, part of the statement can be brought over to the
following line after an end-of-line backslash (\). Should we want to compact
multiple statements in a line, the statements would have to be separated by a
semicolon (;). Both notations should generally be avoided.
10
Chapter 1 Expression: How to Compute
PRICE = 500
QUANTITY = 2
PRICE = \
500
QUANTITY = \
2
PRICE = 500; QUANTITY = 2
11
Chapter 1 Expression: How to Compute
12
Chapter 1 Expression: How to Compute
13
Chapter 1 Expression: How to Compute
14
Chapter 1 Expression: How to Compute
PRICE1 = 10
PRICE2 = 2500
Difference of f’{PRICE1} and {PRICE2} is {PRICE2-PRICE1}'
15
Chapter 1 Expression: How to Compute
f'{PRICE1:.2f}, {PRICE2:05d}'
Advanced Details
This section describes technical details in reference manual style and
advanced concepts that may need more technical background.
Names
You saw that names (also called identifiers) can be given to objects. The
following characters can be present in a name: beginning with a letter or
an underscore and continuing with a letter-like character, underscore,
or digit. Letter-like means that other categories are added to characters
considered letters in the Unicode standard, namely, the “nonspacing
mark,” the “spacing combining mark,” and the “connector punctuation”
categories. Within names Python discriminates lowercase and uppercase
letters, but certain character combinations can be regarded the same
according to the NFKC standard. It is recommended to use only letters of
the English alphabet in names.
16
Chapter 1 Expression: How to Compute
Literals
Literals represent objects corresponding to their meanings. Rules applied
to the bool, complex, float, int, and str type values are as follows:
17
Chapter 1 Expression: How to Compute
18
Chapter 1 Expression: How to Compute
19
Chapter 1 Expression: How to Compute
\ and “new line” The backslash and the new line will be ignored.
\\ The backslash itself (\).
\a ASCII bell character (BEL).
\b ASCII backspace (BS).
\f ASCII form feed (FF).
\n ASCII linefeed (LF).
\r ASCII carriage return (CR).
\t ASCII horizontal tabulation (TAB).
\v ASCII vertical tabulation (VT).
\ooo Character code with a value of ooo in an octal number system.
\xhh Character code with a value of hh in a hexadecimal number system.
20
Chapter 1 Expression: How to Compute
The dot can appear within a decimal fraction. Three consecutive dots
may be present one after the other, which is called an ellipsis literal. This is
not used in core Python, only in some extensions (e.g., NumPy).
Table 1-5 shows that the precedence of the operators is shown
downward increasing. Stronger operations will be performed first.
If strengths were equal, applying the operations takes place from left
to right. An exception is exponentiation with a reverse direction of
application.
21
Chapter 1 Expression: How to Compute
x := y Assignment expression
x if y else z Conditional expression
x or y Logical or
x and y Logical and
not x Logical negation
x in y, x not in y, Membership tests,
x is y, x is not y, identity tests,
x < y, x <= y, x > y, and comparisons
x >= y, x != y, x == y
x | y Bitwise or
x ^ y Bitwise exclusive or
x & y Bitwise and
x << y, x >> y Bitwise shift
x + y, x - y Addition, subtraction
x / y, x // y, x % y Division, integer division,
remainder
+x, -x, ~x Positive, negative, bitwise negation
x**y Raising to the power
(x) Expression in parentheses
22
Chapter 1 Expression: How to Compute
Python Standards
The Python language is defined in The Python Language Reference. The
content of the book covers basically this document. In addition, several
language-related standards will be described in the Python Enhancement
Proposals; they are usually referenced as PEP plus a number. An often
mentioned PEP document is PEP 8, which contains a recommendation for
formatting the Python source code.
Key Takeaways
• In the chapter, you learned about the concept of an
expression, which is one of the most important building
blocks of programming languages. An expression
describes operations between objects. Usually, the goal
of their usage is to construct a new object needed for
the next step of processing (e.g., calculating the sum of
the price of products).
23
Chapter 1 Expression: How to Compute
24
CHAPTER 2
The Function:
Programs as a Series
of Statements
“It is usual in mathematics—outside of mathematical logic—to use the
word function imprecisely and to apply it to forms such as y2 + x. Because
we shall later compute with expressions for functions, we need a distinction
between functions and forms and a notation for expressing this distinction.
This distinction and a notation for describing it, from which we deviate
trivially is given by Church.”
C
alling a Function
Let’s start by looking at how to use the built-in functions that already exist
in Python. Using a function usually means executing a function call. The
result of the function is referred to as the returned value. The computer
executes the statements assigned to the function name with the specified
objects, and the result object will be obtained. This is expressed by
placing a pair of parentheses after the function name, in which parameter
objects are enumerated optionally. The enumerated objects are called the
arguments of the functions. Calling the absolute value function visible in
Listing 2-1 will result in a number object with a value of 1. This is exactly
what you would expect from the |-1| expression.
abs(-1)
round(3499 * 1.1)
26
Chapter 2 The Function: Programs as a Series of Statements
OLD_NET = 5000
NEW_NET = 6500
VAT_RATE = 1.1
27
Chapter 2 The Function: Programs as a Series of Statements
print('Hello World!')
28
Chapter 2 The Function: Programs as a Series of Statements
Function Arguments
Arguments have so far been specified to functions by listing them in due
order. This method of argument specification is referred to as positional
parameter specification. However, it is possible to specify an argument not
only according to position but also as a keyword argument.
There are two important arguments of the print() function that can
be specified as keyword arguments: the sep and end arguments. These
arguments can be strings: the first one specifies which separator character
should be printed between the printed values, and the second one
specifies what is printed at the end of the line. Among the arguments of
the function, the positional arguments should always precede the keyword
parameters.
The Python language allows positional or keyword arguments to be
optional. In other words, the function will assign them a default value if
they are not specified. The keyword arguments of print() can be omitted
because they have default values. The default value for the sep parameter
is a space, and the default value for the end parameter is a new line
(linefeed) character. See Listing 2-7.
PRODUCT_NAME = 'Cube'
PRICE = 100
print('The product:', PRODUCT_NAME, end=' ')
print('(', PRICE, ' USD)', sep='')
The other feature you can observe is that the print() function has any
number of positional parameters. We speak of a variable-length parameter
list, and at the function call you specify them as normal fixed parameters.
This is useful in cases when it is not known in advance how many
arguments will be specified.
29
Chapter 2 The Function: Programs as a Series of Statements
Defining a Function
Now that you have seen how to call functions, you will learn how to
define your own function. Defining a function always begins with the
def keyword, followed by the name of the function, and the parentheses
optionally with parameters; then the statements constituting the function
come on new lines after a colon. Parameters are listed in parentheses
separated by commas, and the values of these variables will be defined by
the arguments during the call of the function.
30
Chapter 2 The Function: Programs as a Series of Statements
total_sum(1000, 8)
31
Chapter 2 The Function: Programs as a Series of Statements
Keyword Arguments
If you want to pass arguments as keyword arguments, you can do it as
shown in Listing 2-11. Parameters can be passed either as positional
arguments or as keyword arguments, or as combination of the two,
until the constraint of specifying positional arguments before keyword
arguments is met. (There is no third variation in the example due to this
constraint.)
total_sum(price=1000, quantity=8)
total_sum(1000, quantity=8)
For the function definition in Listing 2-12, parameters get the default
values. The parameters with default values are not allowed to be followed
by parameters without default values. This is demonstrated in lines 1, 2, 3,
and 4 of Listing 2-13, where for a default value the function can be called
according to the previous one; this time arguments specified at the call
will be passed. In lines 5, 6, 7, and 8, a single argument is specified, or no
arguments are specified, and in these cases the not-specified parameters
will be assigned with the default values. This is the way parameters
modified on rare occasions can be made optional.
32
Chapter 2 The Function: Programs as a Series of Statements
total_sum_v2(1000, 8)
total_sum_v2(1000, quantity=8)
total_sum_v2(price=1000, quantity=8)
total_sum_v2(quantity=8, price=1000)
total_sum_v2(1000)
total_sum_v2(price=1000)
total_sum_v2(quantity=8)
total_sum_v2()
Visibility of Names
After a variable name has been defined in the earlier examples, it can
be referred to from the point of the definition. In the functions, in turn,
if a variable name is defined, it cannot be referred to from “outside.”
If the defined total value variable name is referred to from outside the
definition of the function (in our case after the definition in the line where
the statements are not indented) in Listing 2-14, an error will be raised.
Visibility can be summarized in three points.
33
Chapter 2 The Function: Programs as a Series of Statements
PRICE = 2000
def total_sum_v3(quantity):
return PRICE * quantity
total_sum_v3(100)
PRICE = 2000
def total_sum_v4(quantity):
PRICE = 3000
return PRICE * quantity
total_sum_v4(100)
34
Chapter 2 The Function: Programs as a Series of Statements
Functions as Parameters
Functions are objects as well, like with numbers. Therefore, a function
can be given as a value of a variable name, and it can be invoked. This
is useful when there is behavior to be transferred. In Listing 2-16, three
methods are defined: the first and second functions calculate the discount
rate from the unit price and quantity, and the third function calculates the
reduced price from the unit price, quantity, and discount rate calculation
function. The last two lines show examples of how the reduced_price_p
can be called. The discount_30 and the discount_4p1 return a 30 percent
discount value if the unit price is higher than 500 and a rate that makes one
item out of free free, respectively. The reduce_price_p function calculates
the total_value from the price and quantity as a first step. Then it calls
its discount parameter, which is a function, to retrieve the rate of the
discount. Finally, it calculates the reduced price from the total_value and
discount_rate. In the first and second examples, the results are 3500 and
4000, respectively.
35
Chapter 2 The Function: Programs as a Series of Statements
print(reduced_price_p(1000, 5, discount_30))
print(reduced_price_p(1000, 5, discount_4p1))
def d_available(total_value):
return total_value >= limit
36
Chapter 2 The Function: Programs as a Series of Statements
Functions in Practice
Three types of notations can help you when defining functions: define the
parameter and return types, specify the preconditions with respect to the
parameters, and include a detailed documentation string. These three
notations are completely optional and primarily do not affect the behavior
of a function.
Similar to the way type hints were defined for variable names in
Chapter 1, the types of function parameters can be written after the name,
separated by a colon. The return type of the function can be written after
the function parameters, separated from it by an arrow (->).
Function can begin with a so-called documentation string (docstring),
which provides a description of the function, that can be queried. The
documentation string is a multiline comment that contains the task of
the function briefly, on one line; its detailed description separated by
one empty line; and finally, a description of the parameters after the
Args: word.
After the documentation string, the preconditions can be represented
to describe the conditions necessary for the function to work properly.
These can be described with the assert statement: an assert keyword
followed by a Boolean expression and optionally by an error message
separated by a comma. If the preconditions expressed by the Boolean
expression is not fulfilled, an error is given.
By convention, there is no space between the function name and the
parentheses when calling the functions, as you saw in the examples. As to
the commas, it is recommended to put a space only after the comma.
Listing 2-18 describes the process shown in Figure 2-1. Each step in the
figure corresponds to one line of the example. This example demonstrates
in practice how the source code can differ from the documentation. The
gap can be narrowed by inserting comments into the source code identical
to the ones in the figure and giving the function a name consistent with the
documentation. The example uses the definitions of Listing 2-19.
37
Chapter 2 The Function: Programs as a Series of Statements
Args:
price: the unit price
quantity: the quantity of the product
Returns:
the result of the computation, which is the value of
the product
"""
assert price >= 0, "the price cannot be negative"
assert quantity >= 0, "the quantity cannot be negative"
total_sum = price * quantity
return total_sum
Args:
value: the total price of the product
limit: a new limit if it differs from 5000
Returns:
True if discount is available, otherwise False
"""
38
Chapter 2 The Function: Programs as a Series of Statements
Args:
value: the total price of the product
discount: amount of the discount in fraction
(e.g. 0.5 equals 50%)
Returns:
The discounted price if discount is available,
otherwise the original value
"""
assert value >= 0, "the value cannot be negative"
assert 1 >= discount >= 0, "discount is not in the
valid range"
multiplier = 1.0 - (discount
if discount_available(value)
else 0.0)
return round(value * multiplier)
39
Chapter 2 The Function: Programs as a Series of Statements
Start
End
Advanced Details
This section describes some technical details in reference manual style
and advanced concepts that may need more technical background.
40
Chapter 2 The Function: Programs as a Series of Statements
41
Chapter 2 The Function: Programs as a Series of Statements
f(1, 2, c=3)
f(1, b=2, c=3)
42
Chapter 2 The Function: Programs as a Series of Statements
Lambda Expression
For functions expecting another function as their parameter, the definition
of the function to be passed as a parameter is frequently clumsy. The lambda
function can solve this problem; it’s a simple anonymous function definition
containing a single expression. Listing 2-22 shows a discount_50() function
that is transferred as an argument in line 4 to the reduced_price_p function,
as shown in Listing 2-16. Listing 2-23 shows the replacement of the function
calculating the previous discount by a lambda expression. A lambda
expression in this example has two parameters: the price and the quantity to
maintain the compatibility with the earlier function definition conventions,
but only the quantity parameter used in the expression. As the lambda
expression returns a function object, it can be written directly as a function
argument of the reduced_price_p function.
Decorator
A decorator is a special notation to modify the behavior of the function.
The decorator is in fact a function receiving a function as a parameter
and returning a function as a result. It can be used in two typical ways: it
records the obtained function into some global data structure and returns
43
Chapter 2 The Function: Programs as a Series of Statements
def limit_discount(discount):
def check(price, quantity):
pre_calculation = discount(price, quantity)
return pre_calculation if pre_calculation<0.75
else 0.75
return check
@limit_discount
def discount_40(price, quantity):
rate = 0.4 if price*quantity > 500 else 0
return rate
@limit_discount
def sell_for_free(price, quantity):
rate = 1.0 if quantity == 1 else 0
return rate
print(reduced_price_p(1000, 1, discount_40))
print(reduced_price_p(1000, 1, sell_for_free))
44
Chapter 2 The Function: Programs as a Series of Statements
Key Takeaways
• A function call is an expression that can be denoted
by writing parentheses (this is called a function
call operator) after the name of the function. The
parentheses can contain further expressions that
are the arguments of the function. Calling a function
means executing the statements assigned to the
function. The function usually describes some
calculations or changes its environment. The function
call usually returns a value that can be used.
45
Chapter 2 The Function: Programs as a Series of Statements
46
CHAPTER 3
David West
product 1 program
price = 1000 product 2 name = main.py
name = small cube old price = 15000
code = T001 price = 20000 documentation
name = large cube name = manual.pdf
code = T002
description
product 3 name = README
name = nano cube
code = T004 documentation
price = 2000 name = manual.html
48
Chapter 3 The Class: How to Model the World
What Is a Class?
A class describes the common blueprint of similar objects. In the Python
language, these classes are the types of the objects. If, for example, in an
order management program products are to be represented by objects,
a Product class must be defined. This definition of class describes how
instance variables of the new objects are formed, and the methods that
read or write these instance variables are also defined here.
Determining the classes during the development process is crucial: if
the responsibilities of a class is clear, i.e., which aspects of reality are to be
modeled, the program will be much easier to understand. Specifying the
responsibilities of the classes can be represented only informally in the
documentation, but it is the consequence of this decision that defines what
kind of instance variables and methods appear in the classes. The product
class would be quite different if, say, the program manages chemicals,
and, besides its name, the hazard classification or storage requirements of
the material would also have to be represented as the product’s instance
variables. See Figure 3-2.
product_1: Product
old_price = 1000 «instanceOf»
price = 1000 Product
name = small cube
code = T001 product_2: Product + code: str
old_price = 15000 «instanceOf» + name: str
price = 20000 + old_price: int
name = large cube + price: int
product_3: Product code = T002 + __init__(str, str, int): void
old_price = 2000 «instanceOf»
+ discount(int): void
price = 2000
name = nano cube
code = T003
49
Chapter 3 The Class: How to Model the World
Creating Objects
In examples cited so far, objects of a particular class have been created
by defining them as values or results of expressions. Objects can also be
explicitly created from classes. Object formation is called instantiation, as
it is like creating an actual representation/instance of an abstract concept/
category/generalization. When an object is to be generated from a class,
parentheses are put after the class name, as if a function were to be called.
Depending on the class, parameters can be specified, which usually affect
the values of the instance variables of the generated object.
For example, when class int is instantiated, an int type object will be
formed with a value of 0. A string can be a parameter at the instantiation of
the int type, which—if containing a number—will be an integer with the
same value. Listing 3-1 shows these examples.
INTEGER_NUMBER_0 = int()
INTEGER_NUMBER_5 = int('5')
50
Chapter 3 The Class: How to Model the World
complex
imag: float
real: float
conjugate(): complex
I = complex(0, 1) # 0+1j
real_part = I.real
imaginary_part = I.imag
In the case of methods, a pair of parentheses will follow the dot and
the method name. These parentheses can remain empty (as shown for the
functions), or parameters can be present in the parentheses.
conjugate_value = I.conjugate()
Defining Classes
You saw earlier how a class construct is used. Let’s look at an example of
how to define your own class. The class representing the product shown
in Figure 3-4 is defined in Listing 3-3. The instantiation of this class is
demonstrated in Listing 3-4.
51
Chapter 3 The Class: How to Model the World
Product
code
name
old_price
price
__init__(code, name, price): void
reduce_price(amount): void
The definition of a class begins with the keyword class and a colon,
and the block containing definitions of the class elements follows. The
class usually has an initialization method named __init__(), which
sets up the default state of the instance (this is similar to the concept of
a constructor in other programming languages). The definition of the
class contains a discount() method, which modifies the price instance
variables of the Product type objects in the case of adding a discount.
The first parameter of methods defined in the class always refers to the
object itself, conventionally called self. To access or modify an instance
variable of the object from a method of the class, the self parameter
must be used. In Listing 3-3 lines 3 to line 6 show the instance variable
definitions of the Product object. In line 9, the value of the price instance
variable is assigned to the old_price instance variable. In line 10, the new_
price variable will take the value of the calculated price. The new_price
variable is just a “normal” method variable, which is not assigned to the
object in any form. The last line assigns the value of this method variable
to the price instance variable. The methods of the object can be called in a
similar way as instance variables are referenced: the reduce_price method
could be called like self.reduce_price(0) from an additional method of
the same class.
52
Chapter 3 The Class: How to Model the World
class Product:
def __init__(self, code, name, price):
self.code = code
self.name = name
self.price = price
self.old_price = price
You can see in the first row of Listing 3-5 how to define a novel c02
object. The prices and names of object c01 defined earlier, and the newly
defined c02, are printed in the second line. The price of object c01 in the
third line and the name of c02 in the fourth line are changed. Both objects’
names and values are printed in the last line. This example demonstrates
well that although instance variables of both objects were described in the
same class definition, their values are specific to the objects and can be
changed independently.
53
Chapter 3 The Class: How to Model the World
In line 1 of Listing 3-6, the names and values of the objects are also
printed. The method implementing the discount is called on line 2. This
changes only the instance variables of c01 as it can be verified based on
the result of print statements in the last line.
An extreme case can also come up when a class contains only instance
variables. A good example of this case is the Address class shown in
Figure 3-5, since it consists only of instance variables: country, postcode,
city, and address. Listing 3-7 shows the definition of the class.
Address
address
city
country
zip_code
54
Chapter 3 The Class: How to Model the World
class Address:
def __init__(self, country, zip_code, city, address):
self.country = country
self.zip_code = zip_code
self.city = city
self.address = address
Objects have unique identifiers that do not change after their creation.
This identifier can be queried by the id() function, the result of which
is an integer. If the identifiers of two objects are equal, the objects are
the same. The methods and the instance variables of the objects can be
queried by the dir() function. Listing 3-8 shows the calls of the previous
two functions and the output of their results.
55
Chapter 3 The Class: How to Model the World
Customer Address
invoice address
email address
name shipping address city
phone country
zip_code
The definition of the class of Address was shown in Listing 3-7. As you
can see from the definition of the Customer class in Listing 3-9, references
to the addresses are realized as simple instance variables.
class Customer:
def __init__(self, name, email, phone,
shipping_address,
billing_address=None):
self.name = name
self.email = email
self.phone = phone
self.shipping_address = shipping_address
self.billing_address = billing_address
56
Chapter 3 The Class: How to Model the World
Order Product
quantity product code
state name
close(): void old_price
post(): void price
__init__(code, name, price): void
reduce_price(amount): void
customer
Customer Address
shipping_address
email address
name invoice_address city
phone country
zip_code
Let’s suppose for the sake of simplicity that only one product is ordered
in a single order, in any quantity. Listing 3-10 shows the definition of the
class modeling the order. The definitions of the other classes are already
known from the earlier examples.
class Order:
def __init__(self, product, quantity, customer):
self.product = product
self.quantity = quantity
self.customer = customer
self.state = 'CREATED'
def close(self):
self.state = 'READYTOPOST'
def post(self):
self.state = 'SENT'
57
Chapter 3 The Class: How to Model the World
Properties
In the query of the instance variable values or in the assignment of values,
it may be required to execute some operations by the object before the
operation happens. In these cases, so-called properties can be defined.
58
Chapter 3 The Class: How to Model the World
class Address:
def __init__(self, zip_code, city, address, country):
self._zip_code = zip_code
self.city = city
self.address = address
self.country = country
@property
def full_address(self):
return (f'{self.zip_code} {self.city}, '
+ f'{self.address}, {self.country}')
59
Chapter 3 The Class: How to Model the World
class Address:
def __init__(self, zip_code, city, address, country):
self._zip_code = zip_code
self.city = city
self.address = address
self.country = country
@property
def full_address(self):
return (f'{self._zip_code} {self.city}, '
+ f'{self.address}, {self.country}')
@property
def zip_code(self):
return str(self._zip_code)
@zip_code.setter
def zip_code(self, zip_code):
self._zip_code = int(zip_code)
Listing 3-14 shows how to use the properties. They are accessed and
modified like instance variables. In reality, the methods with the property
decorator are called to get the value. The method with the zip_code.
setter decorator is called to set the value of the zip_code property, and
the actual value is assigned to the _zip_code instance variable.
60
Chapter 3 The Class: How to Model the World
Inheritance
Inheritance basically serves to reuse the code already written. If a new
class differs from an existing one only because it extra instance variables
and methods, it is not necessary to define the recurring parts again;
instead, the existing class can be referenced. In this case, the existing class
will be called a base class (or superclass), and the newly specified one will
be called a derived class (or subclass). The nature of the connection is
inheritance, and it can be said that the base class is a generalization of the
derived class. The opposite is specialization.
As shown in Figure 3-8, and as demonstrated earlier in Listing 3-13, the
Product class is extended to store the quantity of the product.
61
Chapter 3 The Class: How to Model the World
Product
code
name
old_price
price
__init__(code, name, price)
discount(amount)
QuantifiableProduct
amount
unit
__init__(code, name, price, amount, unit)
The original Product class was defined in Listing 3-3. The derived class
is defined by specifying the name of the base class in parentheses after the
class keyword and the class name. When a base class is not specified, the
object class will be the base class of the defined class. The object class
implements default functionalities, and it is—directly or indirectly—the
base class of all classes. The base class can be accessed by the super()
function. This function is used in the example shown in Listing 3-15 to call
the initialization method of the base class.
class QuantifiableProduct(Product):
def __init__(self, code, name, price,
quantity, unit):
super().__init__(code, name, price)
self.quantity = quantity
self.unit = unit
62
Chapter 3 The Class: How to Model the World
63
Chapter 3 The Class: How to Model the World
Nested Classes
Like in the case of functions, the definition of classes may also be present
within other classes. These nested classes (sometimes called inner classes)
are usually applied for storing some internal data of the containing class,
as shown in Listing 3-17.
class Order:
class Item:
def __init__(self, product, quantity):
self.product = product
self.quantity = quantity
64
Chapter 3 The Class: How to Model the World
def close(self):
self.state = 'CLOSED'
def post(self):
self.state = 'SENT'
Note Classes are also objects in the Python language, and they are
instances of the type class. Classes that can create other classes
are called meta classes. Meta classes are the derived classes of the
type and not the object.
Special Methods
You saw that when specifying a number or Boolean value with the print()
function that the value will somehow become a string. Up to now, when we
defined an object, it was printed on the screen as a string containing the
name and the identifier of the class without its instance variables. You will see
how to make your own object capable of returning an informative string. In
Listing 3-19 the __str__() method carrying out the conversion to the string
representation is defined. The method __str__() has a counterpart called
__repr__(), which is called when the resulting string will be displayed for the
developer.
65
Chapter 3 The Class: How to Model the World
class Product:
def __init__(self, code, nev, price):
self.code = code
self.name = nev
self.price = price
self.old_price = price
def __str__(self):
return (f'{self.name} ({self.code}): '
+ f'{self.old_price}=>{self.price}')
def __repr__(self):
return (f'<Product code={self.code}, '
+ f'name={self.name}, '
+ f'price={self.price}, '
+ f'old price={self.old_price}>')
Methods that both begin and end with double underscores, like the
previous methods, are called special methods in the Python language.
These methods often determine the behavior of a class when they are
used with built-in functions or operators applied on the object of the class.
One important method is __eq__(), which performs the comparison of
two objects. When we write a == b, in reality object a is performing an
operation, parametrized by object b as described in Chapter 1, and a new
Boolean type object is generated as a result. The previous comparison
is carried out exactly as if a method call to a.__eq__(b) was in its place.
Method __eq__() in Listing 3-20 works so that it compares only a single
66
Chapter 3 The Class: How to Model the World
instance variable when comparing products: the product code. As its exact
description and its current price are irrelevant from the point of view of
the match, these instance variables will be ignored. When the object is
removed, method __del__() of the class may be called, similarly to the
destructor of other programming languages. Python does not guarantee
that this method will be called before completing the program; therefore, it
is recommended to take this into account when implementing this method.
class Product:
def __init__(self, code, name, price):
self.code = code
self.name = name
self.price = price
self.old_price = price
def __str__(self):
return (f'{self.name} ({self.code}): '
+ f'{self.old_price}=>{self.price}')
def __repr__(self):
return (f’<Product {self.name}({self.code}): '
+f'{self.old_price}=>{self.price}>')
67
Chapter 3 The Class: How to Model the World
Classes in Practice
Figure 3-9 shows the model of the classes we’ve defined thus far with type
information. In a larger system, it is important that the responsibilities of
the classes are properly selected.
Order Product
quantity: int product code: str
s tate: str name: str
close(): void old_price: int
post(): void price: int
__init__(code: str, name: str, price: int): void
reduce_price(amount: int): void
customer
Customer Address
shipping_address
email: str address: str
name: str invoice_address city: str
phone: str country: str
zip_code: str
Figure 3-9. The order and the associated classes with types
68
Chapter 3 The Class: How to Model the World
class Order:
"""The data and state belong to the Order
Attributes:
product: ordered product
quantity: the quantity of the product
customer: the customer
state: state of the order; 'CREATED',
'SENT' or 'CLOSED'
"""
product: Product
quantity: int
customer: Customer
allapot: str
69
Chapter 3 The Class: How to Model the World
Advanced Details
This section describes some technical details in reference manual style
and advanced concepts that may need more technical background.
70
Chapter 3 The Class: How to Model the World
class Product:
counter = 1
@classmethod
def product_count(cls):
return cls.counter
@staticmethod
def generate_str(code, name, price, old_price):
return f'{name} ({code}): {old_price}=>{price}'
def __str__(self):
product_str = self.generate_str(self.code, self.name,
self.price, self.old_price)
return f'{product_str}'
def __repr__(self):
product_str = self.generate_str(self.code, self.name,
71
Chapter 3 The Class: How to Model the World
self.price, self.old_price)
return f'<Product {product_str}>'
72
Chapter 3 The Class: How to Model the World
class Sales(ABC):
@abstractmethod
def calculate_discount(self, price, pieces):
...
def discount_price(self, price, pieces):
value = price * pieces
discount = self.calculate_discount(price, pieces)
return value * (1-discount)
class Sales4p1(Sales):
def calculate_discount(price, pieces):
return ((pieces//5) / pieces)
73
Chapter 3 The Class: How to Model the World
class Address:
def __init__(self, zip_code, city, address, country):
self._zip_code = zip_code
self._city = city
self._address = address
self._country = country
@property
def full_address(self):
return (f’{self._zip_code} {self.city}, ‘
+ f’{self.address}, {self.country}’)
@property
def zip_code(self):
return str(self._zip_code)
@property
def city(self):
return str(self._city)
@property
def address(self):
return str(self._address)
@property
def country(self):
return str(self._country)
74
Chapter 3 The Class: How to Model the World
def __hash__(self):
return hash((self.zip_code, self.city, self.address,
self.country))
Classes that are used only to store data and do not have any behavior
(i.e., do not have any methods) are called data classes. Automatic creation
of initialization, string conversion, and other special methods is made
possible by the @dataclass decorator. Listing 3-25 shows a version of the
Address class, wherein the @dataclass decorator is used.
@dataclass(frozen=True)
class Address:
postcode: int
city: str
address: str
country: str = field(default='HUNGARY')
@property
def full_address(self):
return (f'{self.postcode} {self.city}, '
+ f'{self.address}, {self.country}')
75
Chapter 3 The Class: How to Model the World
76
Chapter 3 The Class: How to Model the World
Class Diagrams
Class diagrams are the most well-known and widespread type of UML
diagram. The most fundamental element in the diagram is the rectangle
representing the classes; it is generally divided into three parts. The three
parts contain the following textual contents: the name of the class, its
instant variables, and its methods. Instant variables and methods are
written in single lines each usually by indicating their type too, similar
to the definition usual in programming languages. These lines can be
appended by further extra information, such as the default values of the
variables.
Connections between the classes are presented in the class diagram.
In diagrams, associations are marked by a continuous line. Inheritance,
in turn, is marked by a continuous line with an empty, closed arrowhead.
If the association has a direction (the connection is essential for one of
the classes only), an open arrowhead is present at the end of the line.
Two important types of connections between classes exist beyond the
former: dependence and containment. Dependence means that one class
somehow refers to another one. This is marked by a dashed line, at the
end of which an open arrowhead points to the reference. Containment
denotes that one class models some part of the other class. A continuous
line signifies this with a rhombus at the end of the containing class. The
strength of the containment is marked by a solid or empty rhombus,
depending on whether the connection is permanent (aggregation) or
temporary (composition), respectively.
77
Chapter 3 The Class: How to Model the World
Key Takeaways
• In Python everything is an object: integers, strings,
functions, files, or any concept modeled by the
developer. The definition according to which objects
are initially constructed is called a class. In Python, the
words class and type are synonyms. Additionally, a class
is also an object.
78
Chapter 3 The Class: How to Model the World
79
CHAPTER 4
if Statement
The simplest control structure is the if statement. The if statement allows
a series of statements to be executed depending on a condition formulated
in a Boolean expression. This control structure is an if statement followed
by statements depending on the condition with indentation (in the form
of a block). The if statement begins with an if keyword followed by the
Boolean expression and closed by a colon.
Optionally, another branch can be connected to this control structure,
which runs if the condition is not satisfied. This is denoted by an else
keyword located in the same indentation level as the if statement, with a
succeeding colon; the statements that follow are executed if the condition
is not satisfied.
You’ll recall the Product class was defined in Listing 3-20. If the value
of the discount is between the acceptable range (i.e., it is higher than 0
and lower than 99), the price of the product will be reduced. Otherwise, an
error message is displayed. Figure 4-1 shows the flow.
82
Chapter 4 The Control Structure: How to Describe the Workflow
Start
Defining product
Reading amount of
discount
[else]
[0 < discount value <= 99]
Message: Discount
Reduce the price value is too low or
too high
End
In the first two lines of Listing 4-1, a product object is instantiated, and
the extent of the discount will be asked for.
83
Chapter 4 The Control Structure: How to Describe the Workflow
Start
Defining product
Reading amount of
discount [discount value < 0]
Message: Discount value is negative
[discount value == 0]
Message: No discount
[99 < discount value <= 100]
Message: Discount value is too high
[0 < discount value <= 99]
[else]
Message: Price will be negative
Reduce the price
End
84
Chapter 4 The Control Structure: How to Describe the Workflow
85
Chapter 4 The Control Structure: How to Describe the Workflow
match Statement
The match statement is similar to the if statement. The main difference is
that it matches the resulting object of an expression to patterns to select
the branch to be executed, instead of evaluating Boolean expressions.
The match statement was introduced in Python 3.10. This statement starts
with a match keyword followed by the expression to which the patterns
are compared. After a colon, the block contains the list of the patterns.
The pattern in the simplest case can be a string, integer, Boolean value, or
None. Multiple patterns can be listed connected with the | symbol, which
indicates an “or” connection (i.e., at least one of the patterns must match).
The pattern is written between a case keyword and a colon, followed by
the block that will be executed if there is a successful match.
The pattern can contain variable names and an if keyword followed
by a guard condition (a Boolean expression), which can already
reference the variable names. This notation of guard condition enables
the practical combination of simple matching with the comparison, as
shown in the if statement. What else can be a pattern will be detailed
in the “Advanced Details” section.
You can also follow its functionality in Figure 4-3. In the first case, the
discount_value must be equal to 0, and in this situation the “No discount”
text will be displayed. In the second case, the discount_value must be
equal to 1, and in this situation, the “Only 1%” text will be displayed, and
the price will be reduced by 1 percent. The third case is similar to the
second case with the difference that here the discount_value must be
equal to 5 or 10 and the displayed text will be slightly different. The last
case contains the underscore character, which means that it will match
any value.
86
Chapter 4 The Control Structure: How to Describe the Workflow
Start
Defining product
Reading amount of
[discount value == 0]
discount
Message: No discount
[discount value == 1]
Message: Only 1%
End
In Listing 4-4, you can see a match statement in which one of the four
cases can be selected.
87
Chapter 4 The Control Structure: How to Describe the Workflow
print('Only 1%')
product.reduce_price(discount_value)
case 5|10:
print(f'{discount_value}% is reasonable')
product.reduce_price(discount_value)
case _:
print('We allow only 0%, 1%, 5% or 10% discounts')
88
Chapter 4 The Control Structure: How to Describe the Workflow
Start
Defining product
Reading amount of
discount
[discount value == 0]
Message: No discount
[0 < discount value <= 99]
Message: Within allowed range
In Listing 4-5, you can see the combination of fixed patterns with
guard conditions, which enables the handling of more complex cases. In
the second, third, and fourth cases, the variable name x is assigned to the
integer expected to be evaluated in the guard conditions.
89
Chapter 4 The Control Structure: How to Describe the Workflow
while Statement
The while statement is intended to repeat a series of statements until a
condition is met. Such conditions can be, for example, reaching a value or
a certain event occurring. The common property is that you do not know
how many repetitions will lead to the desired result.
Figure 4-5 shows an example that can be implemented in Python with
the while statement.
90
Chapter 4 The Control Structure: How to Describe the Workflow
Start
Defining product
Reading Amount of
Discount
End
In Listing 4-6 reading the extent of the discount is repeated until the
read value falls within the expected range. When this occurs, the repetition
will end, and line 6 will be executed.
91
Chapter 4 The Control Structure: How to Describe the Workflow
There are two lines in the previous example where data is read: the
value is read once always at the start, and second time it is read only when
it was not possible to obtain a value meeting the conditions on the first try.
If you wanted to read data only at one point, the read statement will have
to be placed inside the loop. Additionally, the condition must be evaluated
in a way that its value should be determined after the reading. Figure 4-6
represents this process visually.
Start
Defining Product
[need to rad
new value] [0 < discount value <= 99]
Reading Amount Need to read new
[else] of Discount value = False
[else]
Reduce the Price Displaying
Massage
End
The condition of the loop has been modified in Listing 4-7 so now it
depends on a read_next variable. This is first true; then if the read value is
found to be inside the range, it changes to false. If its value is false, the next
repetition will not run. If it is not within range, its value will still be true;
hence, the reading is repeated.
92
Chapter 4 The Control Structure: How to Describe the Workflow
93
Chapter 4 The Control Structure: How to Describe the Workflow
Start
Defining Product
[else]
Reading Amount Displaying
of Discount Message
[0 < discount value <= 99]
End Terméket
leértékel
94
Chapter 4 The Control Structure: How to Describe the Workflow
else:
print(‘Discount abount is too low or too high’)
product.reduce_price(discount_value)
Start
Defining Product
95
Chapter 4 The Control Structure: How to Describe the Workflow
product.reduce_price(discount_value)
After the while statement, there can be an else keyword and a block
that runs once if the condition is not met. The else branch is not executed
if the loop is ended with the break statement. Listing 4-11 shows this.
96
Chapter 4 The Control Structure: How to Describe the Workflow
else:
print('No more try')
discount_value = 0
product.reduce_price(discount_value)
for Statement
In a for loop, you can specify a repetition that corresponds to a range of
values (the range can even be infinite). This statement is started with a for
keyword followed by a target variable name; then an in keyword followed
by an expression that generates values assigned to the variable; finally, a
colon and the statements to be repeated with indentations appear. The
simplest and most frequent iterator is the range function. This function
makes the loop repeated by a fixed number, and the target variable name
will take a value from a fixed range of numbers. The range function can
have one, two, or three integer numbers as arguments.
Chapter 5 will cover what kind of expression can be used in the for
statement instead of the range function. The for statement can contain an
else branch that behaves similarly to those previously covered.
The upcoming examples will show that the price change of the product
is calculated for each value of discount in a specific range. Figure 4-9
shows the essence of this behavior.
97
Chapter 4 The Control Structure: How to Describe the Workflow
Start
Reading next
amount of
discount
[Till amount of discount
to process available]
Defining Product Reduce the Price Displaying new price
[no more]
End
Listing 4-13 shows the value of the discount running between 1 and
10. This is done by providing two arguments to the range function: the first
value and the one after the last value.
98
Chapter 4 The Control Structure: How to Describe the Workflow
99
Chapter 4 The Control Structure: How to Describe the Workflow
TOO_LOW_PRICE = 900
for discount_value in range(10):
product = Product('K01', 'cube', 1000)
product.reduce_price(discount_value)
if product.price < TOO_LOW_PRICE:
break
print('Cost of the product:', product.price)
else:
print('All discount values are acceptable')
Exception Handling
Exception handling helps to manage errors or exceptional cases separately.
Thus, you do not have to insert error handling sections between statements
executed during the normal executions. This helps to separate clearly
which statements belong to the normal operation and which belong to the
handling of exceptional or faulty operation. Errors can range from “division
by zero” to “file not found.” When handling an exception, the type of error
or problem is signaled by the system with the instantiation of an exception
object (or alternatively such an object created by the program itself as well).
The exception handling is built around four keywords: in the block
after the try keyword, there is the code, which is attempted to be executed;
in the blocks after the except keywords comes the statements managing
the exceptional cases; the block after the else keyword contains the
statements managing the nonexceptional cases; and statements in the
block after the finally keyword always run. The except keyword can
be followed by a class determining the type of the exception. Then it will
be executed only when the exception belongs to the particular class;
otherwise, it can be executed for any exception. Let’s start with the
simplest case in which only a try and an except branch exists.
100
Chapter 4 The Control Structure: How to Describe the Workflow
Start
End
try:
discount_value = int(input('Amount of the discount
(in %)?'))
except ValueError as e:
print(f'Error: {e}')
discount_value = 0
The except and the following block can be repeated with different
exception types, but then they have to be organized in a way that the
more specific class would be the preceding one. The block after the else
keyword is executed if no exception occurred. The finally block will
run even when the block belongs to the try statement, which can be
interrupted by break, continue, or return statements.
101
Chapter 4 The Control Structure: How to Describe the Workflow
try:
discount_value = int(input('Amount of the discount
(in %)?'))
except ValueError as e:
print(f'Error converting the input: {e}')
discount_value = 0
except Exception as e:
print(f'Error: {e}')
discount_value = 0
else:
print(f'The input value is a valid integer')
finally:
print(f'The amount of discount will be {discount_value}')
102
Chapter 4 The Control Structure: How to Describe the Workflow
def reduce_price(product):
try:
amount = input('Amount of the discount (in %)?')
discount_value = int(amount)
except ValueError as e:
raise ValueError('Not an integer')
if discount_value > 0 and discount_value <= 99:
product.reduce_price(discount_value)
else:
raise ValueError('Discount abount is too low or too high')
try:
product = Product('K01', 'cube', 1000)
reduce_price(product)
103
Chapter 4 The Control Structure: How to Describe the Workflow
except ValueError as e:
print('Modification is failed for the following
reason:', str(e))
Context Management
It occurs frequently in a program that some resources are needed to
be reserved and then released again. Such resources can be files or
network connections that are opened and then closed. As the resource
reservation and releasing pairs are easily messed up, this can be provided
automatically by the context management device. A with keyword is
followed by the creation of the object representing the resource to be
reserved and then released. In the block that follows, the resource can be
used; then when leaving the block, this is automatically closed or released.
Listing 4-19 shows how to open a file that you write to; then after
leaving the block, it will be automatically closed.
The open function takes two parameters: a filename and a file mode
(wt means writing to a text file). It returns a file object that will be assigned
to the orders_doc variable name. In the second line, the Orders: string
will be written in the opened files. Without the context management
statement, the write statement must be followed by the method call
order_doc.close() to explicitly close the file.
104
Chapter 4 The Control Structure: How to Describe the Workflow
Recursion
Recursion is being covered with the control structures since its behavior is
similar to what can be achieved with a while statement; one has to use only
a combination of a function call and an if statement. From a function, it is
possible to invoke not only another function, but itself as well. This option is
used typically in cases where the task can be described as an initial step and
repetition of a step with changing parameters. Listing 4-20 solves recursively
the following problem: how many times can a product be discounted by 10
percent to make its price lower than a given sum? A function calculating this
105
Chapter 4 The Control Structure: How to Describe the Workflow
must examine whether the current price of the product is lower than the
expected price. If it is lower, it does not have to be discounted; if it is not true,
these steps have to be repeated with the discounted price (i.e., the result
will be that the price of the product will be reduced one more time). In this
second case, the function calls itself with modified arguments. After some
calls, these modifications in the arguments are expected to reach a value for
which the price of the product will fall below the expected price; thus, the
recursion will terminate. If the conditions are not properly specified or the
step does not make the values converge fast enough to fulfill the conditions,
the recursion will terminate after a certain number of calls (the default value
of allowed function call depth is typically 1000).
Loops in Practice
For loops, it is worthy to explain in the comments the reasoning behind
their expected termination and what the invariant is (a Boolean expression
that will always be true upon executing the loop). As shown in Listing 4-21,
106
Chapter 4 The Control Structure: How to Describe the Workflow
If you are unsure about the termination of the loop, you can consider
introducing a counter to specify an upper number of attempts, as shown in
Listing 4-22.
107
Chapter 4 The Control Structure: How to Describe the Workflow
It is important to note that after the failed attempts the loop is left by
raising an exception; this prevents the execution of the statement after
the loop, which expects discount_value and does have a valid value.
Naturally, a loop with a counter can be expressed with a for statement too.
Advanced Details
This section describes mostly technical details in reference manual style
and some advanced concepts that may need more technical background.
108
Chapter 4 The Control Structure: How to Describe the Workflow
case. The sole purpose of this syntax is to describe that the expected
object belongs to a certain class and to specify its expected data attribute
values. The parameters are listed as keyword arguments, or in the case of
positional arguments the class definition must contain a special __match_
args__ class variable. This variable must contain the list attributes relevant
during the matching and their expected order in the pattern.
Listing 4-23 shows that the Product class contains the __match_
args__ variable to specify that the code, name, and price attributes can
be matched and in this order. In the first case, you expect an object in
which all three attributes match with values specified in the pattern. The
second pattern matches an object with fixed code and value attributes, but
any name. The third pattern matches an object with fixed code and name
attributes, but any value. The fourth pattern matches if the object does
meet any of the earlier criteria but has at least a K01 code.
class Product:
__match_args__ = ("code", "name", "price")
def __init__(self, code, name, price):
self.code = code
self.name = name
self.price = price
self.old_price = price
109
Chapter 4 The Control Structure: How to Describe the Workflow
110
Chapter 4 The Control Structure: How to Describe the Workflow
Exception Classes
As described earlier, exceptions are also objects. An exception will be
instantiated from classes, the base class of which is the BaseException
class or a derived class of it, the Exception. In Listing 4-26, a class
named ProductcodeError is defined with two extra instance variables
(the code and the message) storing error-specific information. The last
line is necessary to make the message instance variable to the string
representation of the exception.
class ProductCodeError(Exception):
def __init__(self, code):
self.code = code
self.message = f'Code {code} does not exists'
super().__init__(self.message)
111
Chapter 4 The Control Structure: How to Describe the Workflow
try:
raise ProductCodeError('B1')
except ProductCodeError as e:
print(f'Error: {e}')
class PriceReductionTry:
def __init__(self, product):
self.product = product
def __enter__(self):
self.price = self.product.price
self.old_price = self.product.old_price
return self.product
112
Chapter 4 The Control Structure: How to Describe the Workflow
with PriceReductionTry(sub_cube):
sub_cube.reduce_price(20)
print(sub_cube)
print(sub_cube)
Evaluating Strings
The Python language contains a built-in tool for calculating an expression
stored in the form of an arbitrary string. Listing 4-29 demonstrates the
evaluation of the expression covered in Chapter 1. It can be used to store
the expression to be calculated in a configuration file or in other form. As
the eval() makes possible the calculation of an arbitrary expression, you
should be careful regarding the source of the string to be calculated so as
not to give an opportunity to run malicious code.
eval('5000 * 2 + 2000')
Activity Diagram
The activity diagram is the workflow diagram of the UML standard. The
activity on the diagram—denoting typically a function call or execution
of an operation—is denoted by rectangles with rounded edges. The
beginning and end of the process are denoted by solid and empty circles,
respectively. These elements can be connected by a solid line having an
open arrowhead at one end, which denotes the succession of the elements.
The arrow points at the element later in time. The branches are denoted
by rectangles, and conditions are written between square brackets with
arrows pointing outward.
113
Chapter 4 The Control Structure: How to Describe the Workflow
Key Takeaways
• if and match statements allow you to execute
different parts of the program based on the state of
the environment. If a statement evaluates a Boolean
expression and the result is true, it executes a block of
code. Additionally, any number of elif branches and a
single else branch can be attached to the if statement.
The match statement selects a branch based on the
matching of a pattern to the specified object, and the
branch containing the first matching pattern will be
selected.
114
CHAPTER 5
Donald E. Knuth
Several data types have been described so far, such as numbers, Boolean
values, and strings. In addition, you learned how to define your own types,
i.e., classes. In the case of the Order class, you might realize that allowing it
to reference more than one product (or an arbitrary number of products)
in the model would be useful. To make this possible, you need a notation
to reference the objects with an index number or other identifiers. Such
classes or types able to reference a variable number of objects—based on
some structural property—are called data structures.
When elements are represented after each other in a row and you can
refer to them based on their position in the row, they are called sequences.
Two important sequence types exist in the Python language: one is the
list; the other is the so-called tuple. Lists can be modified, while tuples
have a fixed number of elements and cannot be modified after creation.
The sequence is rather a natural concept when you think of the many
situations when data to be processed is listed sequentially.
For the sequence, the succession of the objects provides the structure
of the class. Data structures have another important group in addition
to the sequences. In that group, it matters only whether a reference by a
key can be made or not to a particular object. The two most important
examples of this kind of structuring of data are the dictionary and the set.
They will be examined in this chapter too.
116
Chapter 5 The Sequence: From Data to the Data Structure
product_name:list 0 :str
2 :str
4 :str
:str
:str
117
Chapter 5 The Sequence: From Data to the Data Structure
products:list 0 :Product
2 :Product
4 :Product
:Product
:Product
mixed_list = [
1,
'cube',
Product('K1', 'cube', 1000)
]
118
Chapter 5 The Sequence: From Data to the Data Structure
mixed_list:list :int
:str
:Product
119
Chapter 5 The Sequence: From Data to the Data Structure
Listing 5-5. Accessing Objects in the List with Indexing from the
End of the List
120
Chapter 5 The Sequence: From Data to the Data Structure
The length of the list (the number of elements in it) can be queried
by the len() built-in function. As shown in Listing 5-8, it is possible to
append by the append() and extend() methods, depending on whether
one element or another list is appended. In the case of these methods,
the change takes place in the existing list; in other words, they create an
in-place modification of the list. When a new list is intended to be created
by appending two existing ones, using the + (concatenation) operator is
expected.
The deletion of the list elements can be carried out by the del keyword.
The remove(x) method of the list can be used to remove elements based
on their values. For example, as shown in Listing 5-9, the first element is
removed from the product_names in line 2 and the element with the value
cube v2.0 in line 3.
121
Chapter 5 The Sequence: From Data to the Data Structure
Lists can be easily processed in a for loop. Namely, sequences can also
be present instead of the range object. In this case, the cycle variable of the
for takes the values of this sequence. Listing 5-10 shows how to print each
product of a list.
Lists, like the numbers, are also objects; therefore, they can be
elements of other lists, too. (A list can also be added to itself technically,
but this doesn’t make too much sense.) It also must be noted that these
embeddings can be arbitrarily deep.
Listing 5-11 shows a list, and the elements of the list are other lists.
These embedded lists contain groups of product names. In the first
print call, the first group is displayed. Then in the next print call, the first
element of the first group is displayed. In this example, you can see that
the embedded list element is accessed the same way as the top-level
list element; in other words, the first index operator returns the first list,
and the second index operator its first element. After the first two print
statements, the second list is modified. After selecting the second element
of the top-level list, a new element is appended to the accessed list object.
After that, the second list (the group of medium-sized cube names) is
printed. Finally, the two elements of the second group are printed.
122
Chapter 5 The Sequence: From Data to the Data Structure
product_name_groups = [
['small cube', 'tiny cube'],
['cube'],
['large cube', 'XL cube'],
]
print('First group:', product_name_groups[0])
print('First element of the first group:',
product_name_groups[0][0])
product_name_groups[1].append("Cube M+")
print('2nd group:', product_name_groups[1])
print('First and second elements of the 2nd group:',
product_name_groups[1][0], 'and',
product_name_groups[1][1])
As you can see, the list is also an object, the type of which is list.
Hence, it can also be generated by directly instantiating the list()
expression. From among the types shown so far, this is the first embedded
type that is mutable (i.e., it can be modified).
Processing of a List
The Python language has a so-called list comprehension statement that
generates a list from a list-like object. This statement is usually applied
to execute some operation on the objects in the existing list and place
the result objects in a new list. In the list comprehension statement,
only the operations that have no side effects are recommended. The list
comprehension statement consists of the following elements written
between square brackets ([]): an expression, for keyword, target variable
name, in keyword, and iterator object. This has a meaning similar to the
123
Chapter 5 The Sequence: From Data to the Data Structure
for statement: the target variable takes the next element of the iterator
and calculates the expression before the for keyword. The result of the
calculation is placed in a newly generated list, which will be the result of
the whole list comprehension expression.
In Listing 5-12, from line 1 to line 3, from the product type objects
a product name list is generated with the notation previously covered.
The same thing is performed in line 4, but with the “list comprehension”
notation. The two equivalent lists are printed in line 5 to enable
verification of their equivalence.
names = []
for product in products:
names.append(product.name)
names_v2 = [product.name for product in products]
print(names, names_v2)
Listing 5-14 shows that extra filtering is added to the processing: it will
be part of the resulting list only if the expression after the if is evaluated
as true.
124
Chapter 5 The Sequence: From Data to the Data Structure
Listing 5-15 shows how to prepare the product name pairs from
all combinations of the elements of the two lists where the price of the
first product is lower than that of the second. This example produces all
combinations without the if keyword as follows: the first variable of the
pair (p), which first references the first product object, will “step” to the
next object only after all the objects were enumerated as the value of the
second variable (p2); then objects are enumerated as the value of the
second variable repeatedly after the first variable takes a new value as long
as the last object is selected for the first variable.
125
Chapter 5 The Sequence: From Data to the Data Structure
Listing 5-17 shows functions applicable for lists of number objects: the
selection of the maximum and minimum elements and the calculation of
the total amount.
print('Highest price:',
max([p.price for p in products]))
print('Lowest price:',
min([p.price for p in products]))
print('Avarage price:',
sum([p.price for p in products])/len(products))
126
Chapter 5 The Sequence: From Data to the Data Structure
Tuples
The tuples (or they are also called ordered lists or records) are read-only
lists with N elements. The tuples with two elements will be called pairs in
the book, those with three elements triples, and so on. A tuple’s definition
is similar to that of a list, but round parentheses are used instead of square
ones. The same operations can be used as operations to read the list.
Listing 5-19 shows a definition of a pair.
The Python language makes it easy to use tuples in its syntax because
it is not necessary to write the brackets in certain cases. As shown in
Listing 5-20, with two Product objects separated by a comma, you get a
tuple. This behavior can be checked according to Listing 5-21 by reading
its first element (or alternatively querying its type by using the type()
function).
127
Chapter 5 The Sequence: From Data to the Data Structure
products_fix2[0]
The unpacking notation can be also used in for statements, as you will
see in the following sections.
128
Chapter 5 The Sequence: From Data to the Data Structure
Dictionaries
Dictionaries are useful types when elements are intended to be referenced
by an arbitrary value (usually called a key) instead of their index numbers.
You can define a dictionary by listing the key-value pairs in braces,
separating a key from a value by colons, and using a comma between the
different key-value pairs. It is important that the keys are immutable. Five
key-value pairs are listed in Listing 5-23. The keys are strings containing
two characters, while the values are Product objects.
codes['K1']
129
Chapter 5 The Sequence: From Data to the Data Structure
You can form the union with the | operator, as shown in Listing 5-26.
Elements of the dictionary can be updated also in groups by the update
method, as shown in lines 1 to 4 of Listing 5-27. Lines 5 to 8 have the same
effect, but an operator is used.
codes |= ({
'K17': Product('K17', 'cube v12.0', 12000),
'K18': Product('K18', 'cube v13.0', 12900)
})
130
Chapter 5 The Sequence: From Data to the Data Structure
for k in codes.keys():
codes[k].reduce_price(3)
print(f"New price of the product with {k} code is
{codes[k].price}")
131
Chapter 5 The Sequence: From Data to the Data Structure
for k, p in codes.items():
p.reduce_price(3)
print(f"New price of the product with {k} code is
{p.price}")
Not only lists but dictionaries can be generated similarly to the list
comprehension. The difference compared to the notation of the list
comprehension is that the dictionary comprehension requires the usage of
braces instead of the square parentheses, and there must be independent
expressions for the calculation of the key-value pairs, separated by a colon.
In Listing 5-31, a dictionary is generated from the list of Product type
objects, which contains codes assigned to the product names.
Sets
The behavior of the set type is equivalent to the mathematical set
concept. You can also think of a set as a dictionary without a value, which
corresponds to its notation. Listing 5-32 shows three set definitions.
132
Chapter 5 The Sequence: From Data to the Data Structure
labels = SIZES.union(OTHER)
the_size = THE_PRODUCT.intersection(SIZES)
print(the_size)
opposite = labels.difference(THE_PRODUCT)
print(opposite)
133
Chapter 5 The Sequence: From Data to the Data Structure
Copying Sequences
It is important to be aware that these data structures can be modified.
Since variable names are just references to the objects in Python, in the
case of an assignment to another, the variable name refers to the same
object. This means that after the modification through referencing with the
new variable name, the modification will be visible also when accessing
the object with the original variable name.
If you want to assign a copy of an object to a new variable name, a new
object has to be instantiated in a way that the content of source is copied.
Objects in the data structure generated in this way continue to be identical,
but now, in turn, in case the data structure itself got changed, this will have
no effect on the original data structure. This means that new elements
can be added, elements can be removed, and the order or other structural
features can be changed, and they will apply only to the newly instantiated
object. Listing 5-36 demonstrates this theory and shows various ways of
copying lists. In the assignment in line 2, the object referenced by the b
variable name is identical with that referenced by a. In the assignment in
line 3, the object referenced by the c variable name is a copy of the object
referenced by a. An alternative notation for copying the a list is shown in
134
Chapter 5 The Sequence: From Data to the Data Structure
line 4. This alternative notation means that a partial list is generated from
the list that contains the elements from the first element of the list to the
last one. The identity of the references is examined in the last line: based
on the above, a and b are identical, while the rest are different.
Since the objects referenced by the copied lists are identical, any
changes in the mutable elements will continue to be visible in the source
data structure. Thus, the previous copy mechanisms are called a shallow
copy. Listing 5-37 shows the effect of the shallow copying mutable
objects. In this example, you are creating a list from lists of strings called
orig_names and shallow copying it to the list copied_list. Elements are
added to the list orig_names and its last embedded list. When the lists
are printed, the first addition is visible only in the original list, while the
second addition is also visible in the copied list. This is because even the
list copied_name is a new list; it references the same objects as the original
list, and some of the referenced elements were mutable.
135
Chapter 5 The Sequence: From Data to the Data Structure
If you want to prevent this behavior, you have to duplicate all of the
elements, not just the top-level object. This is called a deep copy; you can
find further information in the “Advanced Details” section.
Sequences in Practice
For data structures, what is worth documenting are the constraints on
the elements in the comments (for example, the price of every product
is at least $100 USD) or their layout (for example, the list always contains
an element, and they are ordered). Listing 5-38 shows three comments
on lists: the second comment is useful for its reader as it confirms the
properties of the list, which can be inferred from the initial values; and the
first and third ones are useful as the described properties are hard to guess
from the values.
136
Chapter 5 The Sequence: From Data to the Data Structure
Advanced Details
This section describes mostly technical details in reference manual style
and some advanced concepts that may need more technical background.
Iterable Objects
Iterability as a feature of objects has been mentioned several times without
its exact definition. A class is iterable if it is able to return a so-called iterator
object; i.e., it has an __iter__() method. Iterator objects, in turn, are
objects that have a __next__() method giving a “next” element. The iterator
objects also have a method, similarly to the iterable classes; this method is
able to return an iterator—in this case, it means itself. Listing 5-39 shows an
example.
class Items:
class Item_iter:
def __init__(self, item):
self.i=iter(item)
def __iter__(self):
return self
def __next__(self):
return next(self.i)
def __iter__(self):
return Items.Item_iter(self.items)
137
Chapter 5 The Sequence: From Data to the Data Structure
item_iter = iter(items)
print(next(item_iter))
print(next(item_iter))
print(next(item_iter))
print(next(item_iter))
138
Chapter 5 The Sequence: From Data to the Data Structure
139
Chapter 5 The Sequence: From Data to the Data Structure
def discount_price():
for discount_value in range(10):
product = Product('K01', 'cube', 1000)
product.reduce_price(discount_value)
yield product.price
140
Chapter 5 The Sequence: From Data to the Data Structure
Main discount_price()
function
print(price)
next
yield price
print(price)
next
yield price
print(price)
141
Chapter 5 The Sequence: From Data to the Data Structure
142
Chapter 5 The Sequence: From Data to the Data Structure
143
Chapter 5 The Sequence: From Data to the Data Structure
products
Order list Product
1 0..*
products
Order Product
1 0..*
Sequence Diagram
The sequence diagram can depict how objects are communicating with
each other. A so-called lifeline on the diagram—denoting typically an
executing function object or an object that has methods that are called—
is denoted by rectangles with a vertical line under them. The horizontal
lines with arrowheads show the communication between the lifelines.
The timing of the communication is represented by the vertical order
of the arrows as the first call is the topmost one and the last one is the
bottommost one. The vertical lines show that the objects are available, and
their parts can be thickened to show they are actively executed.
144
Chapter 5 The Sequence: From Data to the Data Structure
Key Takeaways
• Lists are the simplest and most frequently used data
structures, which stores objects with before/after
relationships. Objects in the lists are referenced by their
index number.
145
CHAPTER 6
The Module:
Organization of
Program Parts into
a Unit
... reuse components that are already available, compose applications
from big chunks of premade libraries, glue them together, and make sure
it works, even without fully understanding how. Although many would
reject this point of view, it is the de facto style, mostly unconsciously, behind
today’s biggest software projects.
Jaroslav Tulach
into a module. In this chapter, we will discuss the concepts of modules and
packages, how they can be imported, the built-in and third-party packages,
how packages can be created, and what kind of tools can help to make
packages high quality.
Built-in Modules
Python comes with more than 200 built-in modules, including everything
from specialized data structures to relational database management
functionality. This is also the reason behind one of the slogans of Python,
namely, “batteries included.”
You reference a module by using the import keyword and specifying
the name of the module. The module containing the date type is imported
on line 1 of Listing 6-1 and used on line 2. Modules are also objects, so, for
example, classes defined in them can be accessed by putting a dot between
the module name and the class name.
import datetime
datetime.date(2020,2,2).strftime('%Y.%m.%d.')
import datetime as dt
dt.date(2020,2,2).strftime('%Y.%m.%d.')
148
Chapter 6 The Module: Organization of Program Parts into a Unit
The float type used so far is not suitable to store financial data, as
it performs rounding of the decimal places based on standards usual in
mathematics. This is the reason why only an integer type is used for this
purpose so far. Therefore, to store financial data, the decimal package
is recommended. This package defines the decimal type that can be
used, as shown in Listing 6-6. Decimal numbers are specified generally
as strings, and the value of the decimal object will match exactly with
the number described by the string. Listing 6-7 compares the float type
and decimal type. The first value will be a number, which approximates
1.100000000000000088, while the second one will be exactly 1.1.
149
Chapter 6 The Module: Organization of Program Parts into a Unit
FLOAT_NUM = 1.1
FINANCIAL_NUM = Decimal('1.1')
print(f'{FLOAT_NUM:.50f}, {FINANCIAL_NUM:.50f},')
The result of an operation between two decimals (i.e. how the result is
rounded and the number of its stored digits) depends on the environment of
the calculation. In Listing 6-8, the environment of the calculation is accessed
by the getcontext() function. The listing shows how to use the two instance
variables from among the numerous settings: the prec variable specifies
the number of stored digits (before and after the decimal point together),
and the rounding variable controls the rounding rules (to apply the rules
used in standard rounding or in banker’s rounding, the ROUND_HALF_UP or
ROUND_HALF_EVEN value has to be set, respectively). These settings affect
operations only on decimal numbers. During the operations it can happen
that the decimal digits produced by the calculation exceed the precision of
the original numbers. This can be restored to the accuracy specified in the
parameter by the quantize() method.
150
Chapter 6 The Module: Organization of Program Parts into a Unit
index_numbers.popleft()
Python searches for the module to be imported first among the built-
in modules. If it cannot be found here, try to load it from the location
listed in the path variable of the sys module. (Its value can be checked by
printing the value of the path variable name after executing the from sys
import path statement.) These are as follows: the current directory (i.e.,
the directory from which your Python program is started); the directories
in the environmental variable PYTHONPATH, which was set by the operating
system; and the directories specified during the installation.
151
Chapter 6 The Module: Organization of Program Parts into a Unit
Defining Modules
It is simple to prepare your own module in Python as the file containing
the source code is considered a module by default. Importing a module
in Python means that the source code of that module is executed. To turn
a stand-alone Python script into a reusable module, you must make its
functionalities accessible through functions or classes. Additionally, the
statements that are not intended to be executed when the file is used as a
module must be guarded by an if statement. The conditional expression
of this if statement is typically __name__=='__main__'. The exact meaning
of this if statement is as follows: if the file is imported as a module, the
name of the module is assigned to the __name__ variable name, while if the
file is executed directly as a script, its value is the __main__ string.
The upcoming listings contain only the most relevant fragments of the
files from this point. Listing 6-11 references the classes associated with
the Order class (in Listings 3-7, 3-13, 3-17, and 3-20) organized into a file
named model.py. You can download the complete file of this module. The
first line of the fragment is the condition, which is needed to ensure the
rest of the code runs only when launched as an independent program.
152
Chapter 6 The Module: Organization of Program Parts into a Unit
Packages
Packages are modules containing other modules. They can be used to
organize the modules into further units. One package is usually one
directory with modules with additionally a file named __init__.py in
it. This package can also be placed into another package, and this can
be repeated arbitrarily. If this package has to be executable directly, a
__main__.py file can be placed in the directory that will contain the code
to be executed in such a case only.
153
Chapter 6 The Module: Organization of Program Parts into a Unit
Future Package
The Python language has a statement that can switch on and off new
language additions or change their behavior. This statement is the from
__future__ import followed by the name of the feature and can be present
only at the beginning of the source file. This statement is a different
statement than the earlier reviewed import statement. For compatibility
reasons, the __future__ package exists and can be used with other forms
of import statements, but this is not to be confused with the previous
statement.
Since version 3.7, the only active feature that can be turned on is the
delayed evaluation of the type annotations, and the name of this feature is
annotations (see PEP563; type annotations will be discussed in Appendix C).
This functionality is turned on by default starting from version 3.11, and in
later versions this statement will not change the behavior of the language
anymore.
154
Chapter 6 The Module: Organization of Program Parts into a Unit
Package Management
The Python environment supports managing third-party packages with
a package managing tool named pip. This package manager is able to
download versions of the package together with their dependencies from
the Python Package Index and make it available to your machine.
Listing 6-12 shows the most important package management
commands.
The first two commands list all the installed packages and all packages
having a more up-to-date version than the one installed. The command
in line 3 lists packages from the Python Package Index that match the
requested word. Lines 4 and 5 show how simple it is to install a package (in
the second case, a version number is also specified; a relation sign can also
be used here to express the required package version more loosely). The
command in line 6 shows information about the installed package, such
as the list of packages this one depends on. The last two lines show how to
save the list of the installed packages into a file and how to install packages
based on a dependency file.
155
Chapter 6 The Module: Organization of Program Parts into a Unit
156
Chapter 6 The Module: Organization of Program Parts into a Unit
The first step of the first scenario is to import the packages as shown
in Listing 6-14. The next step is to download the web page, as shown in
Listing 6-15, followed by printing the response code of the download
request, type of the content, and format of the text coding. Then the
downloaded web page is processed. Listing 6-16 and Listing 6-17 show
how the header element of the processed web page and the text of the
header element can be accessed, respectively.
import requests
from bs4 import BeautifulSoup
APRESS = 'https://apress.github.io'
Q = APRESS + 'pragmatic-python-programming/quotes.html'
r = requests.get(Q, timeout=1)
print(r.status_code, r.headers['content-type'],
r.encoding)
site = BeautifulSoup(r.text, 'html.parser')
site.head.title
site.head.title.text
Listing 6-18 shows fragments of the data obtained from the website.
Listing 6-19 shows the data being processed. This is implemented by
iterating through all the tr elements; the class is book, and the text part of
the two td elements are printed during this procedure.
157
Chapter 6 The Module: Organization of Program Parts into a Unit
<tr class="book">
<td class="auth">Donald E. Knuth</td>
<td class="title">TAOCP</td>
</tr>
Listing 6-19. Extracting Data from the Body of the Web Page
In the second scenario, the pandas package is imported, and the Excel
table is loaded according to Listing 6-20. To display the loaded table, the
orders variable is printed.
import pandas as pd
orders = pd.read_excel('orders.xlsx',
index_col=0)
orders.sort_values(by='Order value')
158
Chapter 6 The Module: Organization of Program Parts into a Unit
orders.groupby('Customer id').sum()
M
odules in Practice
Modules are the highest-level organizational unit in the Python language.
For the design of the modules, it is important that the related definitions
are located in one module following some organizational principle. It is
important for the organizational intent (frequently called the responsibility
of the module) to be documented as well. In Listing 6-23, the beginning
and end fragments of the models.py file are shown. At the beginning the
module, the documentation includes short and long descriptions of the
module and then presents the use of the module. At the end of the module,
there is the idiom-like structure, which runs only when the module is
directly launched. This section is usually applied when you want your
module to work also as an independent command-line program. This
example also contains the version number of the module, assigned to the
__version__ variable name.
Example usage:
159
Chapter 6 The Module: Organization of Program Parts into a Unit
__version__ = '1.0.0'
...
if __name__=='__main__':
...
Advanced Concepts
This section describes some technical details in reference manual style
and some advanced concepts that may need more technical background.
160
Chapter 6 The Module: Organization of Program Parts into a Unit
161
Chapter 6 The Module: Organization of Program Parts into a Unit
To transition to the new approach, you need the install the build
package and update the setuptools package. This can be achieved
with the pip install --upgrade setuptools build command. The
contents of pyproject.toml and setup.cfg are shown in Listing 6-25
and Listing 6-26, respectively. The command python -m build can
be used to generate the registry-1.0.0-py2.py3-none-any.whl and
registry-1.0.0.tar.gz compressed package files.
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
[metadata]
name = registry
version = 1.0.0
description = Order Management System
author = Gabor Guta, PhD
author_email = [email protected]
license = GPL
[options]
package_dir =
= src
packages = find:
python_requires = >=3.10
[options.packages.find]
where=src
162
Chapter 6 The Module: Organization of Program Parts into a Unit
Virtual Environments
The virtual environment can be useful when the exact reproducibility
of the environment is important or you have to use various Python
versions and package versions in parallel. The virtual environment
can be created by the python -m venv ENVIRONMENT_NAME command,
where ENVIRONMENT_NAME is the name of the environment to be created.
The environment will be created in a directory named the same as the
name specified in the command. The directory will contain a pyvenv.
cfg configuration file; an include directory for the C header files; a lib
directory, which contains the site-packages directory for the third-party
packages; and finally, a bin or Scripts directory—depending on whether
the installation is under Linux or Windows—containing program files
of the Python environment. The environment can be activated with the
source ENVIRONMENT_NAME/bin/activate command on Linux, while the
same can be accomplished by the ENVIRONMENT_NAME\script\activate.
bat command on Windows 10. The environment can be switched off by
the deactivate command. (The macOS commands are identical to the
commands used for Linux.)
Other tools are also available to manage the virtual environments.
The most popular alternatives for the built-in tools are the pipenv and
poetry tools.
163
Chapter 6 The Module: Organization of Program Parts into a Unit
class TestProduct(TestCase):
def setUp(self):
self.prod = Product('K01', 'Standard Cube', 1000)
def test_product_creation(self):
self.assertEqual(self.prod.code, 'K01')
self.assertEqual(self.prod.name, 'Standard Cube')
self.assertEqual(self.prod.price, 1000)
self.assertEqual(self.prod.old_price, 1000)
def test_price_reduction(self):
self.prod.reduce_price(10)
self.assertEqual(self.prod.price, 900)
self.assertEqual(self.prod.old_price, 1000)
def test_invalid_input(self):
with self.assertRaises(TypeError):
self.prod.reduce_price("A")
164
Chapter 6 The Module: Organization of Program Parts into a Unit
If you are interested in testing, two topics may be worth further study:
165
Chapter 6 The Module: Organization of Program Parts into a Unit
Preparation of Documentation
The program Sphynx can be used to generate documentation for your
package. Preparing the documentation consists of two steps: the tool
extracts the documentation comments from the Python source files,
and then it combines them with the documentation files and creates
documentation of your package. Documentation files provide the frame
of the documentation of the package, and they can also contain further
documentation about the package. As an example, a file containing the
user guide will be created. A format called reStructuredText can be used
to add formatting to the text in the documentation files. Some formatting
notation for example: the text highlighting can be denoted by stars put
around the text; or the chapter title can be marked by underlining (a series
of equal signs in the next line as long as the title text).
166
Chapter 6 The Module: Organization of Program Parts into a Unit
Thereafter, you can add the user guide shown in Listing 6-31 to your
project, by copying the content of the listing into the userguide.rst file
in the docs directory. The files that are generated by default require the
following modifications: references to the documentation files have to be
added in the index.rst file, as shown in Listing 6-32; in the conf.py file,
the hash marks before the first three lines have to be removed, the source
directory path have to be fixed, and lines 7–10 (the section beginning with
the extensions word) should be rewritten as shown in Listing 6-33. These
modifications are necessary for Sphynx to be able to automatically load
the comments (docstrings) from the Python source files. The command in
line 4 will generate the description of the module in HTML format. If the
files are changed, the documentation can be regenerated with the last two
commands.
167
Chapter 6 The Module: Organization of Program Parts into a Unit
User Guide
=====================
.. toctree::
:maxdepth: 2
:caption: Contents:
docs/userguide
docs/modules
import os
import sys
sys.path.insert(0, os.path.abspath('./src/'))
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.napoleon',
]
168
Chapter 6 The Module: Organization of Program Parts into a Unit
Key Takeaways
• Functions, classes, and other definitions can be
organized into modules to make it easier to navigate
between them. A Python source file itself is a module
already. A package is a module that contains further
modules in it. Modules must be imported before they
can be used.
169
APPENDIX A
Binary Representation
This appendix discusses how 0s and 1s can be understood by the
computer to represent the simple or complicated types shown in the book.
For a computer, the 0s and 1s—called bits—are organized into groups of
eight. This is called a byte, which can take 256 different values: 00000000,
00000001, 00000010, 00000011, ..., 11111111.
After refreshing our knowledge on the concept of numeral systems,
these bits can be treated as digits in the binary system, behaving similarly
to their counterparts in the decimal system. The two main differences are
that the highest digit here is the 1 and that the place values of the digits
are 1, 2, 4, 8, 16, etc., instead of 1, 10, 100, 1000, etc. That is, 101 is a binary
number corresponds to 5 since there is a 1 in the place values of 1 and
4. You can see in Listing A-1 how to display the decimal number 42 as a
binary number and how to specify integers as binary numbers.
print(bin(42))
print(0b101010, int('101010', 2))
OR, it is 1 when only one of the numbers is equal to 1. The AND operator is
usually applied for masking: those place values that we would like to keep
will be given the 1 value, and those to be deleted will be given a 0. In our
example, we want to keep only the lowest four place values.
DATA = b'temp'
data_rw = bytearray()
data_rw.extend(DATA)
data_rw.append(5)
print(DATA, data_rw)
172
Appendix A Binary Representation
It was already explained how numbers are represented. Let’s look now
at characters. Characters are stored such that each letter has a number
assigned to it. The code for the letter t is 116, as shown in Listing A-5. The
code of a character can be queried by the ord() built-in function, while
the character belonging to the particular code can be queried by the chr()
function.
print(ord('t'), chr(116))
print(b'temp'.decode())
print('temp ÁÉÍÓÚ'.encode())
173
Appendix A Binary Representation
174
Appendix A Binary Representation
one. The former solution is called little endian (the lower one goes first) and
the latter one big endian (the higher one goes first) encoding, as shown in
Listing A-7. (If the two encoding systems are exchanged and the values of
the two bytes are not identical, the result obtained will be incorrect.) The
currently used machines with x86 processors use the little endian encoding,
while the network standards use the big endian encoding.
INTEGER = 768
byte_repr = INTEGER.to_bytes(2, byteorder='little')
reverse = int.from_bytes(byte_repr, byteorder='little')
print(byte_repr, reverse)
175
APPENDIX B
Type Annotations
Type annotations are currently only for documentation purposes. The
Python language does not use this information in any form. These
annotations are usually applied for two reasons: static type checking
programs are able to verify whether the particular variable name will make
reference only to the denoted type of objects without running the program;
and the integrated development environments (IDEs) can provide better
type hinting and code completion.
Annotations of the most important built-in types were shown in the
first three chapters. For the more complex type annotations, you need to
import the typing module. This module contains definitions of the data
structure and the various type modifiers. More complex type annotations
are shown in Listing B-1.
• a: A list of integers (the types of the list elements are in
square brackets)
• b: A set of strings (the types of the set elements are in
square brackets)
• c: A dictionary with string keys and float values (the
types of dictionary keys and values are in square
brackets)
• d: A pair, the first element of which is a string, and its
second element is an integer (the types of the elements
by position are in square brackets)
178
Appendix B Type Annotations
The generic types are suitable for defining a function or a class in a way
types of certain parameters or returning values could be specified later. In
the case of a generic type definition, it is specified only which ones have to
be identical from among the types to be specified later or whether some
kind of restriction applies to the deferred type definitions. In Listing B-5,
types of the price and of the function calculating the reduced price should
be the same and suitable to store the financial value. This is attained in the
example by allowing only the specification of int or Decimal types.
179
Appendix B Type Annotations
180
APPENDIX C
Asynchronous
Programming
In general, asynchronous behavior means that we do not wait for the effect
of a triggered event. As an example, after sending a message, the sender
will not wait for the response but will continue to the next step, and the
response will be handled when it arrives. This makes sense because it’s the
opposite of the usual synchronous behavior when the process blocks until
the arrival of the response.
Executing slow parts of a program (functions reading data from a hard
disk or from the network) asynchronously makes it possible to execute
further program parts until the requested data arrives. To implement
this, Python provides various language constructs and the asyncio
package. Additionally, several third-party packages support asynchronous
functionality such as aiofile, aiohttp, asyncpg, etc. In this appendix, the
goal is to explain the behavior of the language constructs connected to the
native coroutines and the necessary parts of the asyncio package.
182
Appendix C Asynchronous Programming
Listing C-1 shows how a coroutine called main can be executed. Note
that if main is simply called with a function call operator like main(), it
won’t execute; it will just return a coroutine object. In Figure C-1, the
process of executing the main coroutine can be traced: when the coroutine
is called, it returns a coroutine object, which is passed to the asyncio.
run function for execution. This function internally wraps the coroutine
object into a Task and runs it on the so-called event loop. The event loop is
responsible for switching to the next executable Task if there are any.
import asyncio
asyncio.run(main())
main()
main()
coroutine object
print("Hello World!")
183
Appendix C Asynchronous Programming
Listing C-2 shows that a coroutine has to use the await statement to
execute another coroutine. The await statement can be used only inside a
coroutine and expects an awaitable object. This listing practically chains
two coroutines. The hello coroutine prints the greeting with the specified
name 10 times if it is not parameterized differently.
import asyncio
asyncio.run(main())
import asyncio
asyncio.run(main())
184
Appendix C Asynchronous Programming
import asyncio
asyncio.run(main())
185
Appendix C Asynchronous Programming
In Listing C-5 the explicit creation of Tasks can be seen. Tasks can be
imagined as handlers that store the execution states of the coroutines.
In this example, two coroutines are executed one after each other. The
variable names task_a and task_b reference the corresponding tasks, and
the print(task_a.done(), task_b.done()) calls print the status of the
two tasks. The statement await task_b will wait until the hello coroutine,
which greets Bob five times, finishes. It is expected that by reaching the last
status message both tasks are completed.
186
Appendix C Asynchronous Programming
import asyncio
asyncio.run(main())
187
Appendix C Asynchronous Programming
import asyncio
asyncio.run(main())
188
B
ibliography
1. Kent Beck, Ward Cunningham. A laboratory for
teaching object oriented thinking. Proceedings of
OOPSLA ’89, Pages 1-6, ACM, 1989
2. Michael R. Blaha, James R. Rumbaugh. Object-
Oriented Modeling and Design with UML (2nd
edition). Pearson, 2004
3. Robert L. Glass. Facts and Fallacies of Software
Engineering. Addison-Wesley Professional, 2002
4. Brian W. Kernighan, Dennis M. Ritchie. The C
programming language (2nd ed.). Prentice Hall, 1988
5. Russ Miles, Kim Hamilton. Learning UML 2.0.
O’Reilly Media, 2006
6. Donald E. Knuth. The Art of Computer
Programming, Vol. 1: Fundamental Algorithms (3rd
ed.). Addison Wesley Professional, 1997
7. John McCarthy. LISP I programmer’s manual.
Massachusetts Institute of Technology,
Computation Center and Research Laboratory of
Electronics, 1960
8. Guido van Rossum. Language Design Is Not
Just Solving Puzzles. https://www.artima.com/
weblogs/viewpost.jsp?thread=147358, 2006
190
Bibliography
191
Index
A Boolean expression, 4, 37, 81,
82, 84, 86
Absolute value, 26
Boolean type object, 66
Abstract base classes (ABC
Boolean types, 3–5
classes), 72, 73
break statement, 96
Accessing an object, 116
Bytes, 172–175
Activity diagram, 113
all() function, 125
Annotations, 177, 179 C
Arguments, 26, 29
Characters, special
ASCII code, 173
meaning, 21
Assert statement, 37
Class
Assignment expression, 93
ABC, 72, 73
Asynchronous behavior, 142,
data classes, 73–75
181, 182
defining, 51, 52, 154
Asynchronous functionality, 181
diagrams, 77
Asynchronous generator, 187, 188
identification methods, 76
Asynchronous iterators, 187
immutable objects, 73–75
asyncio package, 181, 182
inheritance, 61–63
Attributes, 48, 68, 78
instance variables, 49
Await statement, 184
instantiation, 53–55
nested classes, 64, 65
B objects, 49
BaseException class, 111 in practice, 68, 69
Big endian, 175 relationships, 55–58
Binary representation, 174–178 responsibilities, 49
Bits, 171, 172 special methods, 65–67
Blocks, 1, 23, 25, 31 variables, 70
194
INDEX
195
INDEX
196
INDEX
U
S Unified Modeling
Sales abstract base class, 72 Language (UML), 23
Scripts, 12, 163 unittest.mock package, 165
197
INDEX
198