Python Basics
This section presents the basic principles of using the Python programming language to create machine-learning models. Python is a high-level programming language with extensive capabilities. It does not require compilation, yet at the same time it provides a certain level of efficiency comparable to machine-code-level programming languages. Python has established itself as the preferred tool for creating tools and applications in the field of machine learning. Thanks to its extensive collection of open and...
Key ideas
- Required Software
- Python Modules
- Brief Information about Python
- Variable Types in Python: Basics and Examples
- Strings
- Converting Data Types in Python
Practice assignment
Take a small list or dictionary of data, convert it into a NumPy array, and compute two simple statistics. Connect the result with "Python Basics".
Python Basics
2.1 Required Software
This section presents the basic principles of using the Python programming language to create machine-learning models. Python is a high-level programming language with extensive capabilities. It does not require compilation, yet at the same time it provides a certain level of efficiency comparable to machine-code-level programming languages. Python has established itself as the preferred tool for creating tools and applications in the field of machine learning. Thanks to its extensive collection of open and constantly updated libraries, Python is distinguished by its functional power. It is ideally suited to effective work in programming and code writing. Python is also fast and has a structure that is optimally suited for working with large amounts of data, which is a key element in machine learning.
This chapter is not a formal course in Python; rather, it is intended to help readers begin studying and practically applying the materials in later chapters more easily. We will focus on some useful simple tricks and on certain subtleties of behavior that often affect coding in ML. All the code presented in this textbook, and in this chapter in particular, can be found at https://sohoware.ru/SohoBook/. Readers who are familiar with Python may simply skip this chapter.
To install Python, go to the official website at https://www.python.org/downloads/, select version 3.10.X, and download the installer for your operating system. During installation, it is important to select the “Add Python to PATH” option so that Python can be accessed from the command line. After installation is complete, check it by entering the following in the command line:

python --version
For macOS or Linux, use:
python3 --versionThis should display the Python version number.
To install Visual Studio Code (VS Code), visit https://code.visualstudio.com/ and download the version for your operating system. After installation, open VS Code and install the Python extension through the Marketplace using the Extensions side panel. This will optimize the environment for Python development and provide convenient tools and functions for writing and testing code.
2.2 Python Modules
Let us begin with a less traditional introduction. Unlike other books on computer languages, we start the discussion with how to use our own code, which we may develop during the learning process.
First of all, it is important to become familiar with Python’s module system, which makes it possible to extend the functionality of your code by using predeveloped libraries. For example, the sys module provides access to certain variables and functions that interact with the Python interpreter.
import sys # Import the sys module to work with system parameters
sys.path.append('grbin') # Add the path to the directory with user modules
# The following line can be uncommented to view the current list of paths
# print(sys.path) # Print the list of paths where Python searches for modulesComments in code begin with the # symbol and are intended to explain how the code works, making it more understandable for other developers.
For longer explanations or documentation in code, multiline comments are used. They are written as strings enclosed in triple quotation marks. This makes it possible to insert extensive descriptions without starting each line with #.
'''
This is a multiline comment, where more complex logic can be described
or detailed instructions can be given.
Using multiline comments improves code readability and makes the code easier to maintain.
'''For demonstration, we will create a simple module named grcodes.py, which will contain a function called printx. This function is intended to demonstrate how to print a variable’s name and value.
import sys # Import the sys module to work with system parameters
# Definition of a function in the grcodes.py module
def printx(name):
"""
The function prints the variable name and its value.
Example of use: name = 88.0; printx('name')
"""
frame = sys._getframe(1)
print(name, '=', repr(eval(name, frame.f_globals, frame.f_locals)))Using the printx function demonstrates how output can be simplified by minimizing repetition and reducing the probability of errors when code is changed.

import grcodes as gr # Import the user module and assign it an alias for convenience
x = 1.0 # Assign a value to the variable
print(x) # Standard output of the variable value
gr.printx('x') # Use the printx function from the gr module to print the variable name and value
# Get help for the printx function
help(gr.printx)Note that when from grcodes import printx is used, the reference gr. before printx becomes unnecessary. This makes the code more concise and easier to read.
2.3 Brief Information about Python
Python is a high-level interpreted programming language developed by Guido van Rossum in the late 1980s and first released to the public in 1991. The main purpose of creating Python was to simplify the programming process by making code easier to read and write. Python supports many programming paradigms, including procedural, object-oriented, and functional programming, which makes it suitable for different types of projects.
Python’s philosophy is based on the desire for simplicity and elegance, as well as on high code readability. This is achieved through meaningful indentation and minimalist syntax. “The Zen of Python” (PEP 20) sets out the key principles underlying Python’s design, for example, “Simple is better than complex” and “Readability counts.” These principles are aimed at encouraging programmers to write concise and understandable code.
Python is actively developed and supported by a global community of developers. The Python Software Foundation (PSF) is a nonprofit organization that promotes the development of the language and supports its open ecosystem.
Since its creation, Python has undergone significant changes. Python 3, introduced in 2008, was a major update that introduced many improvements and changes incompatible with Python 2. This led to a long transition period during which both versions coexisted, but support for Python 2 was ultimately discontinued in 2020 so that the community could focus its efforts on developing Python 3.
In modern Python development, integrated development environments (IDEs) play a significant role by providing developers with extensive tools for writing, testing, and debugging code. Among the many available IDEs, Visual Studio Code (VS Code) occupies a special place because of its flexibility, power, and extensibility. Developed by Microsoft, VS Code offers a wide range of features for Python development, including Git integration, debugging, dependency management, code completion, and much more.

VS Code supports working with Python through extensions such as Python Extension for Visual Studio Code, which provides IntelliSense support, debugging, testing, code formatting, and virtual-environment management. This makes VS Code an ideal choice for Python developers of any skill level, from beginners to professionals.
To improve interaction with code and increase productivity in VS Code, it is recommended to install a set of basic extensions, starting with the Python extension, which simplifies many development tasks, including writing, testing, and debugging code. Extensions in VS Code are installed through the built-in extension marketplace, which can be accessed directly from the IDE.
To activate the Python extension and start working with Python in VS Code, follow these steps:
Open VS Code and go to the extension marketplace by clicking the Extensions icon in the side panel or by using the keyboard shortcut Ctrl+Shift+X. 2. Type “Python” in the search bar and select the Python extension from Microsoft. 3. Click “Install” to add the extension to your development environment.
After the extension is installed, VS Code will automatically detect the Python interpreters installed on your system and offer them for use in your projects. You will also be able to configure the development environment according to your preferences, including selecting the interpreter, setting paths to executable files, configuring the debugger, and much more.
2.4 Variable Types in Python: Basics and Examples
Python is considered an object-oriented language. Every variable in Python is an object. Python is dynamically typed: the type of a variable is determined at the moment the variable is assigned. You do not need to declare variables in advance or specify their types beforehand. Python has several basic variable types: numbers and strings. These variables may exist independently or form tuples, lists, dictionaries, sets, NumPy arrays, and so on. Various operations can be performed on all these types in code, including arithmetic operations, logical operations, formatting, and others. Note that a variable is defined rather freely: it may be a tuple, a list, a dictionary, and so on. For example, a list may be inside a list, a tuple inside a list, or a list inside a tuple.
Python supports three types of numbers: integers, floating-point numbers, and complex numbers.
To define an integer variable, you can simply assign it an integer value.
my_int = 48 # With this assignment, my_int becomes an integer
print(my_int)
printx('my_int')48
my_int = 48type(my_int) # Check the type of the variable; print() is not needed
# if this is the last line in the cellintmy_int = 5.0 # Now my_int becomes a floating-point number
type(my_int)floatmy_complex = 8.0 + 5.0j # Thus, my_complex is a complex number
print(my_complex)
printx('my_complex')(8+5j)
my_complex = (8+5j)my_int, my_float, my_string = 20, 10.0, "Hello!"
if my_string == "Hello!": # comparison operators: ==, !=, <, <=, >, >=
print("String: %s" % my_string) # Indentation of 4 spaces
if isinstance(my_float, float) and my_float == 10.0:
# isinstance(): returns whether an object is an instance of a class
print("This is a floating-point number, and it is equal to %f" % my_float)
if isinstance(my_int, int) and my_int == 20:
print("This is an integer, and it is equal to: %d" % my_int)String: Hello!
This is a floating-point number, and it is equal to 10.000000
This is an integer, and it is equal to: 20The type of a variable can be converted:
my_float = 5.0 # Assign the value 5.0 to my_float, making the variable a float
print(my_float) # Print the value of my_float
my_float = float(6) # Create a floating-point number by converting an integer with float()
print(my_float) # Print the value of my_float
print(int(7.0)) # Convert a float to an int and print it
print('int(7.0)') # Print the string 'int(7.0)'5.0
6.0
7
int(7.0)To check the memory address of a variable, use:
a = 1.0
print('a =', a, 'at memory address:', id(a))a = 1.0 at memory address: 140703208811472b = a
print('b =', b, 'at memory address:', id(b))b = 1.0 at memory address: 140703208811472Note that b has the same address as a.
a, b = 2.0, 3.0
print('a =', a, 'at memory address:', id(a))
print('b =', b, 'at memory address:', id(b))a = 2.0 at memory address: 4365829648
b = 3.0 at memory address: 4365829264Note the change in address when the value of a variable changes.
The underscore, or underscore character _, plays several unique roles in Python. Let us consider some of them.
n1 = 100000000000
n_1 = 100_000_000_000 # for readability
# Ternary conditional expression
print('Yes, n1 is the same as n_1') if n1 == n_1 else print('No')
n2 = 1_000_000_000
print('Total =', n1 + n2)
# f-string (Python 3.6 or later)
print('Total =', f'{n1 + n2:,}')
# The following line prints the number using underscores as separators
print('Total =', f'{n1 + n2:_}')Yes, n1 is the same as n_1
Total = 101000000000
Total = 101,000,000,000
Total = 101_000_000_0002.4.1 Strings
Strings are fragments of text used to create labels and filenames for program results. A string can be defined as any sequence of characters enclosed in quotation marks. Either a pair of single quotation marks or a pair of double quotation marks may be used.
my_string = "How are you? How is your mood?"
# A string has been defined; the characters in it can be indexed
print(my_string, my_string[13], my_string[14], my_string[15], my_string[16], my_string[17:])
my_string = 'Hello,' + " hello!" + " I am here."
# Note that the + operator performs concatenation for strings
print(my_string)How are you? How is your mood? H o w is your mood?
Hello, hello! I am here.Although both single and double quotation marks may be used, if a string contains apostrophes, double quotation marks should be used; otherwise the apostrophes will terminate the string if single quotation marks are used, and vice versa. For example:
my_string = "Do not worry; just use double quotation marks to 'escape' apostrophes."
print(my_string)Do not worry; just use double quotation marks to 'escape' apostrophes.The two types of quotation marks can also be swapped:
my_string = 'Do not worry about "double quotation marks".'
print(my_string)Do not worry about "double quotation marks".Consult the Python documentation when it is necessary to include such elements as backslashes and Unicode characters. Below are some convenient and clean operations that apply to numbers and strings. You can try them in practice and gain experience.
one, two, three = 1, 2, 3 # Assign values to variables
summation = one + two + three
print('sum =', summation) # Print the sumsum = 6one, two, three = 1, 2, 3.0 # Variable types may differ
summation = one + two + three
print('Summation =', summation) # Print the summationSummation = 6.0one3 = two3 = three3 = 3 # Assign the same value to variables
print(one3, two3, three3)3 3 3More convenient operations:
hello, world = "Hello,", "world!"
# Concatenate strings
helloworld = hello + " " + world + "!!"
print(helloworld, ' ', hello + " " + world)
# Get the length of the string, including spaces and punctuation marks
lhw = len(helloworld)
print('The length of the string "helloworld" is', lhw)Hello, world!!! Hello, world!
The length of the string "helloworld" is 15You can split a string into a list of strings, each of which is a word.
hello, world = "Hello,", "world!"
helloworld = hello + " " + world + "!!"
the_words = helloworld.split(" ") # Creates a list of strings
# Similar operations on lists will be discussed later
print("Splitting the words of the string: %s" % the_words)
print('Joining them back together using a space as the separator:', ' '.join(the_words))Splitting the words of the string: ['Hello,', 'world!!!']
Joining them back together using a space as the separator: Hello, world!!!To find a letter, or character, in a string, try the following:
my_string = "Helloworld!"
print('"o" is immediately after the', my_string.index("o"), 'th letter.')
print('The first letter "l" is immediately after the', my_string.index("l"), 'th letter.')"o" is immediately after the 4 th letter.
The first letter "l" is immediately after the 2 th letter.In this example we use the + operator for string concatenation in order to avoid extra spaces between the number and the suffix:
my_string = "Helloworld!"
# + performs concatenation
print('The position of the letter "o" is immediately after the ' + str(my_string.index("o")) + 'th letter.')
print('The first letter "l" is immediately after the ' + str(my_string.index("l")) + 'th letter.')The position of the letter "o" is immediately after the 4th letter.
The first letter "l" is immediately after the 2th letter.You may need to find the frequency of each element in a list.
from collections import Counter # Import the Counter module
my_list = ['a', 'a', 'b', 'b', 'b', 'c', 'd', 'd', 'd', 'd', 'd']
count = Counter(my_list) # A Counter object is a dictionary
print(count) # Print the frequency of each element in the list
# See also the later discussion of dictionaries{'a': 2, 'b': 3, 'c': 1, 'd': 5}In this example, the letter a occurs 2 times in the list, the letter b occurs 3 times, the letter c occurs 1 time, and the letter d occurs 5 times.
print('The frequency of "b" is', count['b'])
# Frequency of the element indexed by its keyThe frequency of "b" is 3Thus, the letter b occurs 3 times in the given list.
Note that Python, like many other programming languages, starts counting from 0 rather than from 1.
Below we list other operations that may be useful.
Converting between uppercase and lowercase letters in a string:
my_string = "Hello, world!" # Define a string
print(my_string.upper(), my_string.lower(), my_string.title()) # Convert to uppercase, lowercase, and title caseHELLO, WORLD! hello, world! Hello, World!Returning a string by slicing:
my_string = "ABCDEFG"
reversed_string = my_string[::-1]
print(reversed_string)GFEDCBAThe string ABCDEFG has been reversed, and the result is the string GFEDCBA.
The title() function of the string class:
my_string = "russia is a country of opportunity"
new_string = my_string.title()
print(new_string)Russia Is A Country Of OpportunityThe title() method is applied to the string russia is a country of opportunity, causing the first letter of each word to become uppercase and all other letters to become lowercase.
Using repetitions:
n = 8
my_list = [1] * n
print(my_list)[1, 1, 1, 1, 1, 1, 1, 1]The list my_list contains 8 elements, each of which is the number 1.
# Declare the variable my_string and assign it the value "abcdefg"
my_string = "abcdefg"
# Print the string my_string repeated twice
print(my_string * 2)abcdefgabcdefgThe operator in Python is used to multiply strings. In this case, mystring 2 means that the string mystring will be repeated twice. Thus, this code prints the string abcdefg twice.
# Declare the variable lotsofhellos and assign it the value "Hello " repeated 5 times
lotsofhellos = "Hello " * 5
# Print the value of the variable lotsofhellos
print(lotsofhellos)Hello Hello Hello Hello HelloThis program works as follows. The first line declares the variable lotsofhellos and assigns it the value Hello repeated 5 times. The multiplication operator (*) in Python is used for string concatenation in this case: it repeats the string Hello 5 times.
The length of a given argument using the len() function:
# Length of the string "ABCDE"
print("The length of the string \"ABCDE\" is", len("ABCDE"))The length of the string "ABCDE" is 5In this code, we use the len() function to obtain the length of a string. The len() function takes a string as its argument and returns an integer equal to the number of characters in the string.
# Program for printing the length of a string
# Initialize the variable my_string
my_string = "Hello, world!"
# Print the length of the string my_string
print("The length of the string", my_string, "is", len(my_string))The length of the string "Hello, world!" is 13# Declare two variables containing lists
even_numbers, odd_numbers = [2, 4, 6, 8], [1, 3, 5, 7]
# Get the length of the list even_numbers using the len() function
length = len(even_numbers)
# Combine two lists using the + operator
all_numbers = odd_numbers + even_numbers
# Print the combined list, as well as the original and new lengths
print(all_numbers, 'The original length is', length, '. The new length is', len(all_numbers))[1, 3, 5, 7, 2, 4, 6, 8] The original length is 4 . The new length is 8Sort elements using the sorted() function:
# Create a list
all_numbers = [5, 3, 9, 1, 4]
# Print sorted characters and the characters of "ABCDE" in reverse order
print(sorted('DCBAE'), sorted('ABCDE', reverse=True))
# Print the list all_numbers sorted in ascending and descending order
print(sorted(all_numbers), sorted(all_numbers, reverse=True))['A', 'B', 'C', 'D', 'E'] ['E', 'D', 'C', 'B', 'A']
[1, 3, 4, 5, 9] [9, 5, 4, 3, 1]Multiplying each element of a list by the same number:
# Original list and variable
original_list, n = [1, 2, 3, 4], 2
# Create a new list using a list comprehension,
# where each element of the original list is multiplied by n
new_list = [n * x for x in original_list]
# Print the new list
print(new_list)[2, 4, 6, 8]Creating an index for a list using the enumerate() function:
my_list = ['a', 'b', 'c'] # Create a list with the elements 'a', 'b', and 'c'
# Iterate over the list, obtaining the index and value of each element
for index, value in enumerate(my_list):
# Print the index (increased by 1) and the value of each list element
print('{0}: {1}'.format(index + 1, value))1: a
2: b
3: cmy_list = ['a', 'b', 'c'] # Create a list with the elements 'a', 'b', and 'c'
for index, value in enumerate(my_list): # Generate indices and values
print(f'{index + 1}: {value}') # Print the index and value using an f-string1: a
2: b
3: cThe try-except-finally exception-handling construction monitors code and prevents execution from stopping unexpectedly:
a, b = 1, 2 # Assign values to variables a and b
try:
print(a / b) # Try to perform division
except ZeroDivisionError:
print("division by zero") # Handle division-by-zero error
else:
# The else block is executed if there were no exceptions in the try block
print("no exceptions occurred")
finally:
# The finally block is executed in any case
print("Whatever happened, we always execute this")0.5
no exceptions occurred
Whatever happened, we always execute thisObtaining memory size in bytes:
import sys # Import the sys module
num = "AAA"
print('The memory size is %d' % sys.getsizeof(num), 'bytes')
# Memory size of the stringThe memory size is 52 bytesThis is an interesting result because it demonstrates that the memory occupied by a string in Python depends not only on the number of characters in the string, but also includes additional metadata, such as the length of the string and the data type. A three-character string occupies considerably more memory than it might seem at first glance. This is important to take into account when working with large volumes of text data in Python.
num = 12345
print('The memory size is %d' % sys.getsizeof(num), 'bytes')
# Memory size of the integerThe memory size is 28 bytesThis result demonstrates that integers in Python also occupy more memory than simply the number of bytes needed to store the numbers themselves. Additional bytes are also used to store metadata about the variable, such as its type.
Check whether a string starts with something or ends with something:
# Define a string
astring = "Hello, world!"
# Check whether the string starts with "Hello" and whether it ends with "abcde" or "!"
starts_with_hello = astring.startswith("Hello")
ends_with_abcde = astring.endswith("abcde")
ends_with_exclamation = astring.endswith("!")
print(starts_with_hello, ends_with_abcde, ends_with_exclamation)True False TrueWe checked whether the string astring, containing the text Hello, world!, begins with the substring Hello and ends with the substrings abcde or !.
The results of the code are as follows:
The string astring begins with the substring Hello: True. - The string astring ends with the substring abcde: False. - The string astring ends with the character !: True.
More similar functions:
# Check whether all characters are numbers or letters
# Check whether all characters are letters
# Check whether the string is written in uppercase
my_string = "Hello, world!"
my_string1 = "HelloWorld"
my_string2 = "HELLO WORLD!"
print(my_string.isalnum()) # whether all characters are letters or digits
print(my_string1.isalpha()) # whether all characters are letters
print(my_string2.isupper()) # whether everything is uppercaseFalse
True
TrueChecking the type and other attributes of variables:
n, x, s = 1234, 1234.0, 'this string'
print(type(n), type(x), type(s)) # Check the object type
print(len(s), len(str(n)), len(str(x))) # Determine the length of the string and string representations of numbers<class 'int'> <class 'float'> <class 'str'>
11 4 6The results of the code are as follows.
Variable types:
n has type int, which means an integer. - x has type float, which means a floating-point, or real, number. - s has type str, which means a string.
String lengths:
The length of the string s (this string) is 11 characters. - The length of the string representation of the number n (1234) is 4 characters. - The length of the string representation of the number x (1234.0) is 6 characters, because it includes the digits, the decimal point, and the zero after the point.
These results correctly reflect the data types and string lengths for the specified variables.
2.4.2 Converting Data Types in Python
When one of the variables in an operation with other integers is a floating-point number, the result becomes a floating-point number.
a = 2 # Initialize the variable a with the value 2
print('a =', a, 'type of a:', type(a)) # Print the value and type of a
b = 3.0 # Initialize the variable b with the value 3.0
a = a + b # Add the variables a and b; the result is stored in a
# Print the values and types of variables a and b
print('a =', a, 'type of a:', type(a))
print('b =', b, 'type of b:', type(b))a = 2 type of a: <class 'int'>
a = 5.0 type of a: <class 'float'>
b = 3.0 type of b: <class 'float'>The type of a variable can be converted to other types.
n, x, s = 1234, 1234.5, 'this string' # Initialize variables n, x, and s
sfn = str(n) # Convert the integer n to a string
print(sfn, type(sfn)) # Print the value and type of sfn
sfx = str(x) # Convert the floating-point number x to a string
print(sfx, type(sfx)) # Print the value and type of sfx1234 <class 'str'>
1234.5 <class 'str'>n, x, s = 1234, 1234.5, 'this string' # Initialize variables n, x, and s
xfn = float(n) # Convert the integer n to a floating-point number
print(xfn, type(xfn)) # Print the value and type of xfn
nfx = int(x) # Convert the floating-point number x to an integer
print(nfx, type(nfx)) # Print the value and type of nfx1234.0 <class 'float'>
1234 <class 'int'># a = int('Hello') # Converting the string 'Hello' to an integer will cause an error
# ValueError
# a = int('2.5') # Converting the string '2.5' to an integer will also cause an error
# ValueError
a = int('25') # Converting the string '25' to an integer works correctly
print(a, type(a)) # Print the value and type of a after conversion25 <class 'int'>The comments in the code also explain that attempting to convert the strings Hello or 2.5 to an integer will produce a ValueError, because these strings cannot be correctly interpreted as integers.
# Perform an addition operation
result = 8.0 + float("8.0") # Add 8.0 and the string "8.0" converted to a float
print(result) # Print the result16.0An addition operation has been performed on the number 8.0 and the string 8.0 converted to a floating-point number. The result of this operation is 16.0.
a = int(False) # Convert the Boolean value False to an integer
print(a, type(a)) # Print the value and type of a after conversion0 <class 'int'>However, operations with mixed numbers and strings are not allowed, and this causes a TypeError:
# Define a floating-point number and a string
my_float = 3.14
my_string = "This is an example string"
# Attempt to add them together
try:
my_mix = my_float + my_string # Attempt to add a floating-point number and a string
except TypeError as e:
print(f"Error: {e}") # Print the errorError: unsupported operand type(s) for +: 'float' and 'str'2.4.3 The Art of Data Formatting in Python
Formatting is extremely useful when outputting variables. Python uses C-style string formatting to create new formatted strings. The % operator is used to format a set of variables enclosed in a tuple, which is a fixed-size list and will be discussed later. This places ordinary text at the position indicated by one of the argument specifiers, such as %s, %d, %f, and so on. The best way to understand this is through examples:
name = "Ivan"
print("Hello, %s! How are you?" % name) # %s is used for strings
# For two or more arguments, use a tuple:
name, age = "Alexey", 23
print("%s is %d years old." % (name, age)) # %d is used for numbersHello, Ivan! How are you?
Alexey is 23 years old.The following example demonstrates the use of an f-string in Python, available starting with version 3.6. F-strings allow variable values to be inserted directly into a string, which makes the code more readable and convenient.
# Define variables
name = "Ivan" # Name
age = 23 # Age
# Print a greeting and age information
# Use an f-string to insert variables directly into the string
print(f"{name} is {age} years old.") # f-strings are available in Python 3.6 and laterIvan is 23 years old.Any object that is not a string, for example a list, can also be formatted using the %s operator. The %s operator formats an object as a string using the repr method and returns it. For example:
list1, list2, x = [1, 2, 3], ['Anna', 'Maria'], 12.5 # Multiple assignment
print("List1:%s; List2:%s\nx=%s, x=%f, x=%.3f, x=%e" % (list1, list2, x, x, x, x))List1:[1, 2, 3]; List2:['Anna', 'Maria']
x=12.5, x=12.500000, x=12.500, x=1.250000e+01The second output line displays the values of the variable x in different formats:
x=%s: string format. - x=%f: floating-point format with decimal places. - x=%.3f: floating-point format with three digits after the decimal point. - x=%e: exponential format.
list1, list2, x = [1, 2, 3], ['Anna', 'Maria'], 12.5 # Multiple assignment
print(f"List1:{list1}; List2:{list2}; x={x}, x={x:.2f}, x={x:.3e}")
# This line prints the values of list1, list2, and x.
# Output formatting is performed using an f-string.List1:[1, 2, 3]; List2:['Anna', 'Maria']; x=12.5, x=12.50, x=1.250e+01Commonly used formatting argument specifiers, when an f-string is not used:
Python has a special way to insert variable values into a string, making it more dynamic and informative. This method is called string formatting.
Formatting symbols:
%s: a string or an object that can be converted to a string, such as a number. - %d: integers. - %f: floating-point numbers. - %.f: floating-point numbers with a specified number of digits after the decimal point. For example, %.2f prints a number with two digits after the decimal point. - %e: scientific notation: a floating-point number multiplied by the specified power of 10. - %x / %X: integers in hexadecimal notation, using lowercase or uppercase letters.
Example of use:
name = "Ivan"
age = 30
height = 1.82
print(f"Hello, {name}! Your age is {age} years, and your height is {height:.2f} meters.")Hello, Ivan! Your age is 30 years, and your height is 1.82 meters.In this example:
The symbol f before the string indicates that string formatting is being used. - Values are inserted directly into the string inside braces, such as {name} and {age}. - The expression {height:.2f} formats the value of height with two digits after the decimal point.
Additional options:
A field width can be specified for outputting a value: %10d prints an integer in a field 10 characters wide. - Fill characters can be specified for empty positions: %05d prints an integer padded with leading zeros to a width of 5 characters. - Value alignment can be specified: %-5d left-aligns an integer in a field 5 characters wide.
2.5 Arithmetic Operators
The addition, subtraction, multiplication, and division operators can be used with numbers.
2.5.1 Addition, Subtraction, Multiplication, Division, and Exponentiation
— addition
— subtraction

— multiplication
/ — division
// — integer division (with the fractional part discarded)
% — remainder after integer division (remainder or modulo)
** — exponentiation
Here are several examples of how these operators are used:
# Addition
print(1 + 2) # 3
# Subtraction
print(5 - 3) # 2
# Multiplication
print(2 * 3) # 6
# Division
print(10 / 2) # 5
# Integer division
print(10 // 2) # 5
# Remainder after integer division
print(10 % 2) # 0
# Exponentiation
print(2 ** 3) # 83
2
6
5.0
5
0
8# Declare the variable number and assign it the value 1 + 2 * 3 / 4.0
number = 1 + 2 * 3 / 4.0
# Print the value of the variable number to the screen
print(number)2.5In Python, the modulo operator (%) returns the integer remainder after division: dividend % divisor = remainder.
# Declare two variables:
# numerator — the numerator
# denominator — the denominator
numerator, denominator = 11, 2
# Compute the quotient using integer division
# floor division
floor = numerator // denominator
# Print the result to the screen
print(str(numerator) + '//' + str(denominator) + '=', floor)
# Compute the remainder after division
remainder = numerator % denominator
# Print the result to the screen
print(str(numerator) + '%' + str(denominator) + '=', remainder)
# Compute the sum of the quotient and the remainder
print(floor * denominator + remainder)11//2= 5
11%2= 1
11In Python, using two multiplication signs denotes exponentiation.
# Declare two variables:
# squared — the square of the number 7
# cubed — the cube of the number 2
squared, cubed = 7 ** 2, 2 ** 3
# Print the values of the variables to the console
print('7 ** 2 =', squared, ', and 2 ** 3 =', cubed)7 ** 2 = 49 , and 2 ** 3 = 8# Bitwise exclusive OR (XOR) between the numbers 7 and 2
test72 = 7 ^ 2
# Print the result to the console
print(test72)5The ^ operator performs the bitwise XOR operation on operands.
It compares the corresponding bits of two numbers and returns 1 if the bits differ and 0 if they match.
The result of the XOR operation (5 in this case) is assigned to the variable test72.
Note that in Python the ^ operator is used for bitwise XOR, not for exponentiation.
Python allows a simple swap operation between two variables.
# Declare variables a and b and assign them the values 100 and 200, respectively
a, b = 100, 200
# Print the values of a and b
print('a=', a, 'b=', b)
# Swap the values of a and b using assignment operators
a, b = b, a
# Print the values of a and b
print('a=', a, 'b=', b)a= 100 b= 200
a= 200 b= 100As can be seen from the output, after the code is executed, the values of the variables a and b have been swapped.
Python provides a number of built-in functions and types that are always available. For a quick overview, refer to the following table or find more detailed information at the official Python documentation for built-in functions.
2.6 Boolean Values and Operators
Boolean values are two constant objects: True and False. When used as an argument of an arithmetic operator, they behave as the integers 1 and 0, respectively. The built-in function bool() can be used to cast any value to a Boolean value.
The definitions are given below:
# Prints True for all values except 0 and an empty value
print(bool(5), bool(-5), bool(0.2), bool(-0.1), bool(str('a')), bool(str('0')))
# True True True True True True
# Prints False for 0 and -0.0
print(bool(0), bool(-0.0))
# False False
# Prints False for empty values
print(bool(''), bool([]), bool({}), bool(()))
# False False False FalseThe bool() function returns False only if the value is zero or the container is empty. Otherwise, it returns True. Note that str('0') is neither zero nor empty.
Boolean operators include and and or.
# The first line of code prints three values:
# - True and True — logical AND of two true values; the result is True
# - False or True — logical OR of two values, one of which is true; the result is True
# - True or True — logical OR of two true values; the result is True
print(True and True, False or True, True or True)
# The second line of code prints two values:
# - False and False — logical AND of two false values; the result is False
# - False and True — logical AND of two values, one of which is false; the result is False
print(False and False, False and True)True True True
False False2.7 Lists: Mutable Containers
We have already encountered lists several times. In this section, we will examine them in more detail.
A list is a set of variables, and it is very similar to an array. A list can contain variables of any type and any number of variables. These variables are stored inside a pair of square brackets [].
When necessary, lists can be iterated over to perform operations. A list is one of the “iterable” objects.
2.7.1 Creating, Appending, Concatenating, and Updating Lists
# Declare the variable x_list of type list
x_list = []
# Print the contents of x_list to the screen
print(f"x_list={x_list}")
# Print the address of x_list in hexadecimal format
print(hex(id(x_list)))x_list=[]
0x104983300x_list = []
x_list.append(1) # Adds the number 1 to the end of x_list
x_list.append(2) # Adds the number 2 to the end of x_list
x_list.append(3.) # Adds the number 3.0 (floating point) to the end of x_list
print(x_list[0]) # Prints the first element of the list (index 0), which is 1
print(x_list[1]) # Prints the second element of the list (index 1), which is 2
print(x_list[2]) # Prints the third element of the list (index 2), which is 3.0
print(x_list) # Prints the entire list1
2
3.0
[1, 2, 3.0]for x in x_list: # A for loop for iterating over the elements of x_list
print(x, end=',') # Print each element without a line break
print('\n') # Print a new line after the loop
x_list2 = x_list * 2 # Create a new list x_list2 by concatenating x_list with itself
print(x_list2) # Print the new list1,2,3.0,
[1, 2, 3.0, 1, 2, 3.0]print(id(x_list), id(x_list2))4367086336 4367678592Explanation:
print(id(xlist), id(xlist2)): this line of code prints the unique identifiers (memory addresses) of two lists: xlist and xlist2.
Different addresses confirm that xlist and xlist2 are different objects in memory, even though they contain the same elements. This happens because the operation xlist2 = xlist * 2 created a new list instead of modifying the original list x_list.
Important points:
In Python, every variable refers to an object in memory.
An object identifier (its memory address) can be obtained with the id() function.
When you assign one list to another, you create a new reference to the same object in memory.
When you perform operations on lists that create new lists (for example, list multiplication or list concatenation), you create new objects in memory, and they have different identifiers.
# Returns the identifier (memory address) of the second element of x_list
id(x_list[1])1594536160id(xlist[1]): this line of code returns the identifier (memory address) of the second element of the list xlist.
x_list3 = x_list # Assign the list x_list to the variable x_list3
print(x_list, ' ', x_list3) # Print the values of both variables[1, 2, 3.0] [1, 2, 3.0]Explanation:
xlist3 = xlist: this line does not create a new list. Instead, the variable xlist3 simply becomes another name for the same list that xlist points to. This means that both variables refer to the same object in memory.
print(xlist, ' ', xlist3): this line prints the values of both variables; they are identical because they refer to the same list.
Important points:
Assigning lists in Python does not create a copy of the list. Instead, it creates another reference to the same list in memory.
This means that any changes made to one list will also be reflected in the other list, since both refer to the same object.
To create an independent copy of a list, you need to use copying methods such as list.copy() or list slices.
print(id(x_list), id(x_list3))1847992120648 1847992120648Explanation:
print(id(xlist), id(xlist3)): this line of code prints the unique identifiers (memory addresses) of the variables xlist and xlist3.
The identical addresses confirm that both variables refer to the same object in memory. This happens because assigning xlist3 = xlist did not create a new list; it only created another reference to the same list.
x_list4 = x_list.copy() # Create an independent copy of x_list using the copy() method
print(x_list, ' ', x_list4) # Print the values of both lists[1, 2, 3.0] [1, 2, 3.0]Explanation:
xlist4 = xlist.copy(): the copy() method creates a new independent copy of the list xlist and assigns it to the variable xlist4. This means that xlist4 will have the same elements as xlist, but it will be stored in a separate area of memory.
print(xlist, ' ', xlist4): this line prints the values of both lists. They are identical in content but independent of each other.
print(id(x_list), id(x_list4))1847992120648 1847993186760Different addresses confirm that xlist and xlist4 are different objects in memory, even though they contain the same elements. This happens because the copy() method created a new independent copy of the list x_list.
# Assign the value 4.0 to the first element of the list x_list
x_list[0] = 4.0
# Print the list to the screen
print(x_list)[4.0, 2, 3.0]# Create the list x_list
x_list = [1, 2, 3.0]
# Create the list x_list2, which is a copy of x_list
x_list2 = x_list.copy()
# Change the value of the first element of x_list
x_list[0] = 4.0
# Print x_list2 to the screen
print(x_list2)[1, 2, 3.0]In this case, we see that changing the value of an element in the list xlist does not affect the value of the corresponding element in xlist2. This happens because xlist2 is a separate copy of xlist.
Creating a list by unpacking a string of digits:
num = 19345678
# Method 1: using the map() function and converting to a list
list_of_digits = list(map(int, str(num)))
print(list_of_digits) # Output: [1, 9, 3, 4, 5, 6, 7, 8]
# Method 2: using list comprehension
list_of_digits = [int(x) for x in str(num)]
print(list_of_digits) # Output: [1, 9, 3, 4, 5, 6, 7, 8]Explanations:
Method 1: using map() and converting to a list.
map(int, str(num)): applies the int() function to each character in the string str(num), converting those characters to integers. map() returns an iterator that needs to be converted to a list using list().
Method 2: using list comprehension.
[int(x) for x in str(num)]: creates a new list in which each element is the result of applying the int() function to the corresponding character in the string str(num). This is a more compact and often more readable way to create lists based on existing iterations.
Both methods produce the same result:
listofdigits: a list containing the digits of the number num as separate integers: [1, 9, 3, 4, 5, 6, 7, 8].
The choice between methods depends on coding style and readability preferences:
map() may be more concise for simple transformations.
List comprehension can be more expressive and flexible for complex transformations and filtering.
Element-wise addition of lists requires a small trick. Better methods, including the use of NumPy arrays, will be discussed in the section on list comprehension.
# Declare two variables containing lists
list1, list2 = [20, 30, 40], [5, 6, 8]
# Print the original lists to the screen
print(list1, " ", list2, " ", list1 + list2)
# Print the original lists to the screen as strings
print("Original string 1: " + str(list1))
print("Original string 2: " + str(list2))
# Print a message saying that "+" is not addition
print('"+" is not addition, but concatenation:', list1 + list2)[20, 30, 40] [5, 6, 8] [20, 30, 40, 5, 6, 8]
Original string 1: [20, 30, 40]
Original string 2: [5, 6, 8]
"+" is not addition, but concatenation: [20, 30, 40, 5, 6, 8]# Declare two lists
list1 = [1, 2, 3, 4, 5]
list2 = [6, 7, 8, 9, 10]
# Declare an empty list to store element sums
add_list = []
# Use a for loop for element-wise addition of lists
for i in range(0, len(list1)):
add_list.append(list1[i] + list2[i])
# Print the result to the screen
print("Element-wise addition of two lists: " + str(add_list))Element-wise addition of two lists: [7, 9, 11, 13, 15]# Print the address of the first element of add_list
print("Address of the first element of the list:", id(add_list[0]))Address of the first element of the list: 4363141552# Print the address of the first element of list1
print("Address of the first element of the list:", id(list1[0]))Address of the first element of the list: 4377526512# Declare two lists
list1 = [1, 2, 3, 4, 5]
list2 = [6, 7, 8, 9, 10]
# Declare an empty list for storing results
add_list = []
# A for loop that iterates over elements of two lists at the same time
for i1, i2 in zip(list1, list2):
# Add element i2 to element i1
add_list.append(i1 + i2)
# Print the results to the screen
print("Sum of the elements of two lists element by element: ", add_list)Sum of the elements of two lists element by element: [7, 9, 11, 13, 15]Code explanation:
add_list = []: declare an empty list for storing results.
for i1, i2 in zip(list1, list2):: a for loop that iterates over elements of two lists simultaneously.
zip(): a built-in function that returns an iterator containing tuples of elements of two lists located at the same index.
i1: the first element of the tuple returned by zip().
i2: the second element of the tuple returned by zip().
add_list.append(i1 + i2): add element i2 to element i1.
print("Sum of the elements of two lists element by element: ", add_list): print the results to the screen.
2.7.2 Slicing Strings and Lists
Slicing is a useful and efficient operation for working with parts of strings, lists, or arrays (we will discuss arrays later). We will start with string slicing and then move on to lists.
A string slice means obtaining part of a string specified by indices or a range of indices.
Indices in Python start from zero, so the first character in a string has index 0, the second character has index 1, and so on.
A colon (:) is used to denote a range of indices in a slice.
Negative indices are used to count from the end of a string.
The step parameter allows you to specify the interval between selected characters in a slice.
An IndexError exception occurs when you try to access an element by an index that is outside the length of a string or list.
my_string = "Every hut has its own rattles"
# Print the original string
print(my_string)
# Print the fifth character of the string
print('5th =', my_string[4])
# Print the seventh through eleventh characters of the string
print('from 7th to 11th =', my_string[6:11])Every hut has its own rattles
5th = y
from 7th to 11th = hut# Create a string variable
my_string = "Hello, world!" # You can change the string to any other string
# Print string slices with explanations:
print('[6:-1]=', my_string[6:-1]) # Slice from the 7th element (inclusive) to the last, not including the last
print('[:]=', my_string[:]) # All characters of the string
print('[6:]=', my_string[6:]) # Slice from the 7th element (inclusive) to the end of the string
print('[:-1]=', my_string[:-1]) # All characters of the string except the last[6:-1]= world
[:]= Hello, world!
[6:]= world!
[:-1]= Hello, world# Declare the variable my_string and assign it the value "Hello world!"
my_string = "Hello world!"
# Print the text "Characters from the 4th to the 9th with step 2"
print("Characters from the 4th to the 9th with step 2")
# Use a slice to obtain a substring from my_string starting with the 4th character,
# ending with the 9th, with a step of 2.
# The result will be the string "l o".
print("[3:9:2]=", my_string[3:9:2])
# The string "Hello world!" consists of 12 characters:
# H — 1st character
# e — 2nd character
# l — 3rd character
# l — 4th character
# o — 5th character
# — 6th character (space)
# w — 7th character
# o — 8th character
# r — 9th character
# l — 10th character
# d — 11th character
# ! — 12th character
# Slice syntax: [start:stop:step]
# start — index of the first character in the substring
# stop — index of the character before which the slice stops
# step — interval in the sequence of characters in the substringCharacters from the 4th to the 9th with step 2
[3:9:2]= l oUsing a negative step, we can easily reverse a string, as we saw earlier:
my_string = "Here we go! (c)"
# Print the original string
print("string:", my_string)
# Print the string in reverse order
print("[::-1]=", my_string[::-1])string: Here we go! (c)
[::-1]= )c( !og ew ereHIn brief, if a single number is indicated in square brackets, the slice takes the character located at position (number + 1). This is because Python starts counting from zero. A colon means “everything available.” If it is used alone, the slice captures the entire string. If a number is placed to its left, the slice is taken from that number to the end of the string, and vice versa. A negative number means that counting is done from the end of the string: -3 means “the third character from the end.” The step parameter can also be used to skip characters.
Note that when you access a string character using an index that does not exist, an IndexError is generated.
# Declare the variable my_string without assigning a value
# in order to demonstrate an error
my_string = ""
# Attempt to print the element with index 14, which is outside the string
print('[14]=', my_string[14]) # This will print an IndexError# Create the list my_list containing mixed variable types:
my_list = [0, 1, 2, 3, "4E", 5, 6, 7, [8, 8], 9]
# Print the list my_list using the print() operator
# In the range from 0 to 9 with step 1
print(my_list[0:10:1])
# Print the list my_list using the print() operator
# From the beginning of the list to the end
print(my_list[:])[0, 1, 2, 3, '4E', 5, 6, 7, [8, 8], 9]
[0, 1, 2, 3, '4E', 5, 6, 7, [8, 8], 9]2.7.3 Using “_” Wildcards in Lists
# Create a list with mixed values
nlist = [10, 20, 30, 40, 50, 6.0, '7H']
# Unpack the list elements, ignoring the first two and the fifth element
_, _, n3, _, *nn = nlist
# Print the third element and the remaining elements after the fifth, with text translation
print("n3=", n3, "remaining numbers", *nn)
# Create another list with numeric values
nlist = [10, 20, 30, 40, 50, 60, 70]
# Unpack elements, leaving the third, the last, and the rest in a separate list nn
_, _, n3, *nn, nlast = nlist
# Print the third element, the last element, and the remaining elements with text translation
print("n3=", n3, ", last=", nlast, ", and all other numbers", *nn)n3= 30 remaining numbers 50 6.0 7H
n3= 30 , last= 70 , and all other numbers 40 50 602.7.4 Nested Lists (Lists Inside Lists)
# Create a nested list of mixed variable types
nested_list = [[11, 12], ['2B', 22], [31, [32, 3.2]]]
# Print the nested list to the screen
print('Nested list:')
print(nested_list)
# Print the number of nested lists
print('Number of nested lists:', len(nested_list))Nested list:
[[11, 12], ['2B', 22], [31, [32, 3.2]]]
Number of nested lists: 3# Print the first nested list
# nested_list[0] — the first element of nested_list
# Print the second nested list
# nested_list[1] — the second element of nested_list
# Print the third nested list
# nested_list[2] — the third element of nested_list
# Print the entire nested list
# print(nested_list) — the function for printing data to the screen
print(nested_list[0])
print(nested_list[1])
print(nested_list[2])
print(nested_list)[11, 12]
['2B', 22]
[31, [32, 3.2]]
[[11, 12], ['2B', 22], [31, [32, 3.2]]]print(nested_list[0][0]) # prints the first element (index 0) in the first nested list (also index 0). Thus, it accesses the number 11.
print(nested_list[0][1]) # prints the second element (index 1) in the first nested list (index 0). Thus, it accesses the number 12.11
12print(nested_list) # prints the entire nested list
print(nested_list[1][0]) # prints the first element (index 0) in the second nested list (index 1). Thus, it prints the string '2B'.
print(nested_list[2][1]) # prints the second element (index 1) in the third nested list (index 2). This second element is itself a list: [32, 3.2].
print(nested_list[2][1][0]) # prints the first element (index 0) in the list located in the second element (index 1) of the third nested list (index 2). Thus, it prints the number 32.[[11, 12], ['2B', 22], [31, [32, 3.2]]]
2B
[32, 3.2]
322.8 Tuples
After discussing lists, examining tuples becomes simple. This is because they are essentially the same, and the main difference is as follows:
A tuple is usually enclosed in parentheses (), while a list is enclosed in square brackets [].
A tuple is immutable, while a list is mutable. This means that tuples cannot be changed after they are created. Values in tuples are preserved.
2.8.1 Storing Values
Because a tuple is immutable, we use it to store data that needs to be preserved. Thus, its use is very limited. It is used to store constants, preventing them from being changed. In addition, working with tuples is faster.
Except for these differences, a tuple behaves like a list. It can be accessed by index, iterated over, and assigned to other variables. Several examples are given below.
# Create a tuple
ttuple = (10, 20, 30, 40, 50, 6.0, '7H')
# Print the tuple
print('ttuple =', ttuple)
# Assign the first value of the tuple to the variable `aa`
aa = ttuple[0]
# Print the value of the variable `aa`
print('aa =', aa)
# Print the values of tuple elements with indices 1, 6, and -1
print(ttuple[1], ' ', ttuple[6], ' ', ttuple[-1])ttuple = (10, 20, 30, 40, 50, 6.0, '7H')
aa = 10
20 7H 7H# Declare a tuple
ttuple = ('a', 'b', 'c', 'd', 'e')
# A for loop using the enumerate() function
# The enumerate() function returns a tuple of the element index and value
for i, data in enumerate(ttuple):
# Check whether the index is less than 3
if i < 3:
# Print the index and the element value
print(i, ':', data)0 : a
1 : b
2 : c# Create a tuple
ttuple = (1, 2, 3)
# Print the tuple
print("Terminal output:", ttuple)
# Try to change the value of a tuple element
try:
ttuple[2] = 300
except TypeError:
print("Error: the value of a tuple element cannot be changed")This is probably all we need to know about tuples.
2.9 Dictionaries: Indexed by Keys
Dictionaries are a data type intended for storing and organizing information as “key-value” pairs. They provide a flexible and efficient way to work with data, which makes them an indispensable tool in many programming tasks.
2.9.1 Main Characteristics of Dictionaries
“Key-value” pairs: in dictionaries, data is stored as pairs in which each key corresponds to a specific value. Keys serve as unique identifiers used to access the corresponding values.
Indexing by keys: instead of numerical indices, as in lists, dictionaries use unique keys to refer to values. This allows direct access to the desired elements without iterating over the entire collection.
Diverse value types: values in dictionaries can be objects of any data type, including strings, numbers, lists, other dictionaries, and even functions. This gives dictionaries broad flexibility for different purposes.
No duplicate keys: each key in a dictionary must be unique. Two or more pairs with the same keys are not allowed. This guarantees unambiguous references and prevents conflicts when accessing values.
Possibility of duplicate values: while keys must be unique, values in a dictionary may be repeated. This makes it possible to use the same value for multiple keys if necessary.
2.9.2 Examples of Dictionary Use
Storing contact information: dictionaries are ideal for storing data where a unique name or identifier must be associated with a set of properties. For example, a dictionary can be used to store a person’s name, phone number, address, and email.
Representing data structures: dictionaries make it possible to model various data structures, such as trees, graphs, and sets, where elements are connected to one another by different relationships.
Counting word frequency in text: dictionaries are indispensable for counting the number of occurrences of words in a text. Each word can be used as a key, and its value will represent the number of times that word appears in the text.
Settings and configurations: dictionaries are often used to store program parameters and settings, allowing their values to be changed easily without rewriting the code.
Dictionaries are a powerful and versatile tool in Python. Their ability to efficiently store and manage data in the form of “key-value” pairs makes them indispensable in many areas of programming.
2.9.3 Assigning Data to a Dictionary
For example, phone numbers can be assigned to a dictionary in the following format:
# Create the dictionary phonebook1
phonebook1 = {}
# Add entries to the dictionary
phonebook1["Masha"] = 513476565
phonebook1["Lyosha"] = 513387234
phonebook1["Nastya"] = 513682762
phonebook1["Ded Moroz"] = 513387234
# Print the dictionary to the screen
print(phonebook1){'Masha': 513476565, 'Lyosha': 513387234, 'Nastya': 513682762, 'Ded Moroz': 513387234}A dictionary can also be initialized as follows:
# Create the dictionary phonebook2
phonebook2 = {'Lera': 656477456, 'Nastya': 656377243, 'Timofey': 656662798}
# Print the dictionary to the screen
print(phonebook2){'Lera': 656477456, 'Nastya': 656377243, 'Timofey': 656662798}# Declare the dictionary phonebook0
phonebook0 = {
"Kostya": [788567837, 788347278], # Kostya has two phone numbers
# this is a list
"Inna": 513683222,
"Vladik": 656477456
}
# Print phonebook0 to the screen
print("Phone book:")
print(phonebook0)Phone book:
{'Kostya': [788567837, 788347278], 'Inna': 513683222, 'Vladik': 656477456}2.9.4 Iterating over Dictionaries
Like lists, dictionaries can also be iterated over. Since keys and values are stored as pairs, we can use a for loop to process them.
# Create a phone-book dictionary
phonebook1 = {
"Ivanov Ivan": 1234567,
"Petrov Petr": 89012345678,
"Sidorov Sidor": 7890123456
}
# Iterate over the dictionary
for name, number in phonebook1.items():
# Print the subscriber's name and phone number
print("The phone number of %s is %d" % (name, number))The phone number of Ivanov Ivan is 1234567
The phone number of Petrov Petr is 89012345678
The phone number of Sidorov Sidor is 7890123456# Create a phone-book dictionary
phonebook1 = {
"Ivanov Ivan": 1234567,
"Petrov Petr": 89012345678,
"Sidorov Sidor": 7890123456
}
# Iterate over the dictionary
for key, value in phonebook1.items():
print(key, value)Ivanov Ivan 1234567
Petrov Petr 89012345678
Sidorov Sidor 7890123456# Create a phone-book dictionary
phonebook1 = {
"Ivanov Ivan": 1234567,
"Petrov Petr": 89012345678,
"Sidorov Sidor": 7890123456
}
# Iterate over the dictionary
for key in phonebook1.keys():
print(key)Ivanov Ivan
Petrov Petr
Sidorov Sidor# Create a phone-book dictionary
phonebook1 = {
"Ivanov Ivan": 1234567,
"Petrov Petr": 89012345678,
"Sidorov Sidor": 7890123456
}
# Iterate over the dictionary
for value in phonebook1.values():
print(value)1234567
89012345678
78901234562.9.5 Deleting a Value
To delete a pair of entries, we use the built-in del operator or pop, using keys.
The del operator deletes an item from a dictionary by the specified key. For example, the following code deletes the item with the key "Ivanov Ivan" from the dictionary phonebook:
phonebook = {
"Ivanov Ivan": 1234567,
"Petrov Petr": 89012345678,
"Sidorov Sidor": 7890123456
}
del phonebook["Ivanov Ivan"]
print(phonebook){'Petrov Petr': 89012345678, 'Sidorov Sidor': 7890123456}The pop operator deletes an item from a dictionary by the specified key and returns the value of that item. For example, the following code deletes the item with the key "Ivanov Ivan" from the dictionary phonebook and stores the value of that item in the variable number:
phonebook = {
"Ivanov Ivan": 1234567,
"Petrov Petr": 89012345678,
"Sidorov Sidor": 7890123456
}
number = phonebook.pop("Ivanov Ivan")
print(phonebook)
print(number){'Petrov Petr': 89012345678, 'Sidorov Sidor': 7890123456}
12345672.9.6 Merging Two Dictionaries
First, use the update() method.
# Create two phone books
phonebook1 = {
"Ivanov Ivan": 1234567,
"Petrov Petr": 89012345678,
"Sidorov Sidor": 7890123456
}
phonebook2 = {
"Vasilyev Vasily": 9123456789,
"Pupkin Pupkin": 4567890123
}
# Update phone book 1
# Use the `update()` method to merge two dictionaries
phonebook1.update(phonebook2)
# Print the updated phone book 1
print(phonebook1){'Ivanov Ivan': 1234567, 'Petrov Petr': 89012345678, 'Sidorov Sidor': 7890123456, 'Vasilyev Vasily': 9123456789, 'Pupkin Pupkin': 4567890123}Now use a simpler tool called the double-star. It allows you to create a third new dictionary that is a combination of two dictionaries without affecting the two original dictionaries.
# Create two phone books
phonebook1 = {
"Ivanov Ivan": 1234567,
"Petrov Petr": 89012345678,
"Sidorov Sidor": 7890123456
}
phonebook0 = {
"Vasilyev Vasily": 9123456789,
"Pupkin Pupkin": 4567890123
}
# Create a new phone book 3
# Use the `**` operator to merge two dictionaries
phonebook3 = {**phonebook1, **phonebook0}
# Print the new phone book 3
print(phonebook3){'Ivanov Ivan': 1234567, 'Petrov Petr': 89012345678, 'Sidorov Sidor': 7890123456, 'Vasilyev Vasily': 9123456789, 'Pupkin Pupkin': 4567890123}Duplicate keys, if any, are removed from the new dictionary:
# Create two dictionaries
dict_1 = {
"Apple": 7,
"Banana": 5
}
dict_2 = {
"Banana": 3,
"Orange": 4
}
# Merge two dictionaries
# Use the `**` operator
combined_dict = {**dict_1, **dict_2}
# Print the merged dictionary
print(combined_dict){'Apple': 7, 'Banana': 3, 'Orange': 4}As a result of executing the code, a new dictionary combineddict is created, containing all elements from the dictionaries dict1 and dict_2.
The ** operator is used to merge two dictionaries. It combines the elements of two dictionaries based on their keys. If the same key occurs in both dictionaries, the value from the second dictionary is used in the merged result.
In this case, the key "Banana" is present in both dictionaries. As a result of merging, the value for this key is taken from the second dictionary, that is, 3.
2.10 Sets
In Python, a set is a collection that is similar to a list, but every element in a set is unique and cannot be repeated.
2.10.1 Avoiding Duplication
Consider an example:
# Create the variable sentence1 and assign it a string with repeated words
sentence1 = "His name is Semyon and Semyon is his name"
# Use the split() method to convert the string to a list of words
# The default separator is a space, so it splits the string into words
words1 = sentence1.split()
# Print the list of words for clarity
# The output should show all words, including duplicates
print(words1)
# Convert the list of words to a set, which automatically removes all duplicates
word_set1 = set(words1)
# Print the set of words to see that duplicates have been removed
print(word_set1)['His', 'name', 'is', 'Semyon', 'and', 'Semyon', 'is', 'his', 'name']
{'His', 'his', 'and', 'name', 'is', 'Semyon'}As you can see, after converting the list to a set, all duplicate words disappeared. Only unique elements remain in the set.
Using a set allows us to remove repetitions easily without writing additional code to check for duplicates. This is very convenient, for example, in data processing when repeated values need to be removed from a list.
2.10.2 Intersection of Two Sets
# Define the first set of words
word_set1 = {'hello', 'world', 'name', 'is', 'first', 'python', 'and'}
# Define the second set of words
word_set2 = {'this', 'is', 'a', 'test', 'name', 'first', 'and'}
# Use the intersection() method to find the intersection between two sets.
# The intersection of sets returns a new set that contains all elements
# that are present in both original sets.
common_elements = word_set1.intersection(word_set2)
# Print the common elements of the sets to the screen
print(common_elements) # Prints a set with common elements{'name', 'is', 'and', 'first'}2.10.3 Difference of Two Sets
In Python, sets are collections that contain non-repeating elements in unordered form. One of the unique operations for sets is the ability to find their difference, which makes it possible to identify elements present in one set and absent from another.
# Define two sets with different words
word_set1 = {'hello', 'his', 'Her', 'His', 'Nastya'}
word_set2 = {'hello', 'Her', 'her', 'Anya'}
# Get and print the words that are in word_set1 but not in word_set2
# Expected output: {'his', 'His', 'Nastya'}
print(word_set1.difference(word_set2)){'his', 'His', 'Nastya'}In the example above, the words 'his', 'His', and 'Nastya' are unique to wordset1 compared with wordset2.
Now, if we want to find the elements that are unique to each of the sets, that is, present only in one of the sets, we can use the symmetric difference operation.
# Print the symmetric difference between word_set1 and word_set2
# Expected output: {'his', 'His', 'her', 'Anya', 'Nastya'}
print(word_set1.symmetric_difference(word_set2)){'his', 'His', 'her', 'Anya', 'Nastya'}In this case, the symmetric difference returns the words 'his', 'His', 'her', 'Anya', and 'Nastya', which are unique to both sets but are not in their intersection.
A common question is whether set operations can be applied to lists. Python lists do not support set operations directly because they are ordered collections and may contain duplicate elements.
# Create two lists of words
words1 = ['hello', 'his', 'Her', 'His', 'Nastya']
words2 = ['hello', 'Her', 'her', 'Anya']
# Attempting to use the intersection method will raise an error because lists do not have such a method.
# This code, if uncommented, will raise AttributeError:
# print(words1.intersection(words2)) # AttributeError: 'list' object has no attribute 'intersection'To use set operations on lists, you first need to convert the lists to sets using the set() function, perform the required operation, and then, if necessary, convert the result back to a list using the list() function.
# Create two sets with unique words
word_set1 = {'hello', 'his', 'Her', 'His', 'Nastya'}
word_set2 = {'hello', 'Her', 'her', 'Anya'}
# Print the difference between word_set1 and word_set2.
# The difference method returns a set containing elements that are in word_set1 but not in word_set2.
# Expected output: {'his', 'His', 'Nastya'}
print('Difference between word_set1 and word_set2:', word_set1.difference(word_set2))
# Print the symmetric difference between word_set1 and word_set2.
# The symmetric_difference method returns a set containing elements that are in only one of the sets.
# Expected output: {'his', 'His', 'her', 'Anya', 'Nastya'}
print('Symmetric difference between word_set1 and word_set2:', word_set1.symmetric_difference(word_set2))
# Lists in Python do not support set operations directly, so we cannot use methods like intersection.
# However, we can convert lists to sets, perform the operation, and then convert the result back to a list if needed.
# Create two lists of words
words1 = ['hello', 'his', 'Her', 'His', 'Nastya']
words2 = ['hello', 'Her', 'her', 'Anya']
# Convert the lists to sets and find their symmetric difference,
# then convert the result back to a list.
sym_diff_list = list(set(words1).symmetric_difference(set(words2)))
# Print the result of the symmetric difference of the lists
# Expected output: ['his', 'His', 'her', 'Anya', 'Nastya']
print('Symmetric difference of the lists words1 and words2:', sym_diff_list)Difference between word_set1 and word_set2: {'His', 'his', 'Nastya'}
Symmetric difference between word_set1 and word_set2: {'Nastya', 'Anya', 'His', 'his', 'her'}
Symmetric difference of the lists words1 and words2: ['Nastya', 'Anya', 'His', 'his', 'her']In this code, we create two sets and two lists. We demonstrate difference and symmetric-difference operations on sets and explain how lists can be adapted to perform similar operations by converting them to sets. The code comments describe each step and the expected output of the operations in detail.
2.11 Conditions and Loops
Conditions are the basis for decision-making in programming. You use conditions to determine whether a particular block of code should be executed. For example: “if today is Monday, perform task A.”
for and while loops are constructs used to repeat a block of code several times. A for loop is usually used when the number of repetitions is known, while a while loop is used when the repetitions depend on a particular condition being satisfied. For example, for is used to iterate over elements of a list, while while can be used to execute a block of code until a particular condition is reached.
2.11.1 Operators
Comparison operators in Python include:
== (equal to)
!= (not equal to)
< (less than)
<= (less than or equal to)
(greater than)
= (greater than or equal to)
When we use these operators, Python compares the values on both sides of the operator and returns the Boolean value True or False.
These operators form the basis for creating conditional expressions that allow your program to respond to different data and scenarios. They can be used to check user input, compare calculation results, and control program behavior depending on internal states and data.
Example:
# Assign the value 2 to the variable x
x = 2
# Compare x with 2; we expect True because x is indeed equal to 2
print(x == 2) # Result: True
# Compare x with 3; we expect False because x is not equal to 3
print(x == 3) # Result: False
# Check whether x is less than 3; we expect True because 2 is less than 3
print(x < 3) # Result: TrueNow let us use a comparison operator in a conditional construct. Conditional constructs allow a certain block of code to be executed if the condition is true (True).
Example of use in a conditional construct:
# Assign the value 2 to the variable x
x = 2
# Check whether x is equal to 2
if x == 2:
# If the condition is true, print the message "x is equal to 2!"
print("x is equal to 2!") # Expected output: x is equal to 2!
else:
# If the condition is false, print the message "x is not equal to 2."
print("x is not equal to 2.") # This code will not be executed because the condition is trueIn this code, we see the use of the == operator in an if condition. If the condition is true, that is, if the value of the variable x is indeed equal to 2, Python executes the code in the if block. If the variable x were equal to any other value except 2, the code in the else block would be executed.
The main important note is that when we write x == 2, we are not assigning the value 2 to x; we are checking whether the current value of x is equal to the number 2. This is comparison, not assignment.
The in operator is used to check whether a specified object is present in a container of iterable objects, such as a list. This operator returns True if the element is present in the container and False otherwise.
Consider the following code example, where we define two variables, name1 and name2, containing names, as well as a list groupA consisting of several names:
# Declare two string variables
name1, name2 = "Sasha", "Lyosha"
# Create the list groupA with names
groupA = ["Sasha", "Masha"]
# Check whether name1 (Sasha) is in the groupA list
if name1 in groupA:
print(name1, "found in group A")
# Check whether name2 (Lyosha) is in the groupA list
if name2 in groupA:
print(name2, "found in group A")
else:
# The else branch is executed if name2 (Lyosha) is not found in groupA
print(name2, "not found in group A")
# Add one more name for demonstration
name3 = "Lena"
# Check whether name3 (Lena) is in the groupA list
if name3 in groupA:
print(name3, "found in group A")
else:
# The else branch is executed if name3 (Lena) is not found in groupA
print(name3, "not found in group A")Sasha found in group A
Lyosha not found in group A
Lena not found in group AThis example demonstrates how the in operator can be used effectively to check whether elements belong to a list.
The is operator, unlike the equality operator ==, does not check operand values. It checks whether both operands refer to the same object. For example:
x, y = ['a', 'b'], ['a', 'b']
z = y # Assign z a reference to the same object as y
print(x == y, ' x=', x, 'y=', y, hex(id(x)), hex(id(y))) # Prints True because the values in x and y are equal
print(x is y, 'x=', x, 'y=', y, hex(id(x)), hex(id(y))) # Prints False because x and y are different objects (different IDs)
print(y is z, ' y=', y, 'z=', z, hex(id(y)), hex(id(z))) # Prints True; y and z refer to the same object
# Change the second element in the list y
y[1] = 'x'
print('After changing one value in y')
print(x == y, 'x=', x, 'y=', y, hex(id(x)), hex(id(y))) # Prints False; now their values are not equal
print(x is y, 'x=', x, 'y=', y, hex(id(x)), hex(id(y))) # Prints False; x and y are different objects
print(y is z, ' y=', y, 'z=', z, hex(id(y)), hex(id(z))) # Prints True; y and z still refer to the same object
# Add list x to list y
y.append(x)
print('After adding list x to list y')
print(x == y, 'x=', x, 'y=', y, hex(id(x)), hex(id(y))) # Prints False; the values of x and y differ
print(x is y, 'x=', x, 'y=', y, hex(id(x)), hex(id(y))) # Prints False; x and y are different objects
print(y is z, ' y=', y, 'z=', z, hex(id(y)), hex(id(z))) # Prints True; y and z still refer to the same objectTrue x= ['a', 'b'] y= ['a', 'b'] 0x100a77300 0x100b07c80
False x= ['a', 'b'] y= ['a', 'b'] 0x100a77300 0x100b07c80
True y= ['a', 'b'] z= ['a', 'b'] 0x100b07c80 0x100b07c80
After changing one value in y
False x= ['a', 'b'] y= ['a', 'x'] 0x100a77300 0x100b07c80
False x= ['a', 'b'] y= ['a', 'x'] 0x100a77300 0x100b07c80
True y= ['a', 'x'] z= ['a', 'x'] 0x100b07c80 0x100b07c80
After adding list x to list y
False x= ['a', 'b'] y= ['a', 'x', ['a', 'b']] 0x100a77300 0x100b07c80
False x= ['a', 'b'] y= ['a', 'x', ['a', 'b']] 0x100a77300 0x100b07c80
True y= ['a', 'x', ['a', 'b']] z= ['a', 'x', ['a', 'b']] 0x100b07c80 0x100b07c80In this example, we see how the == and is operators work differently. == compares the values in lists, while is checks whether variables refer to the same object in memory. This is demonstrated using the id() function, which returns a unique identifier of an object in memory. At the beginning, x and y have identical values, but they refer to different objects, as can be seen from the different IDs. When we assign y to z, z begins to refer to the same object as y.
Changing an element in the list y does not affect x, because they are different objects. However, since z still refers to the same object as y, changes in y are also reflected in z. Adding x to the list y makes y longer and changes its contents, but it does not affect x and does not change the fact that y and z refer to the same object.
The not operator is a logical operator that inverts the value of a Boolean expression: if the expression is true (True), the result is False, and vice versa.
The not operator is especially useful when forming complex logical conditions. It can be used in combination with other operators such as is (to check object identity) and in (to check whether an element is present in a sequence).
Consider several examples:
# Example 1: simple use of `not`
print(not False) # Prints True because `not` inverts False to True
# Example 2: comparison using `not`
print(not False == False) # Prints False; equivalent to the expression True == False
# Example 3: working with the == and is operators in combination with not
x, y = [1, 2, 3], [1, 2, 3]
z = y # Assign z a reference to the same object as y
# Comparing list values
# Use the `==` operator to check equality of values in lists
print(x == y, 'x=', x, 'y=', y, hex(id(x)), hex(id(y)))
# Prints True because the values in x and y are identical even though they are different objects
# Comparing object identity with `is not`
# Use `is not` to check whether variables point to different objects
print(x is not y, 'x=', x, 'y=', y, hex(id(x)), hex(id(y)))
# Prints True because x and y, although they have identical values, are different objects
# Using `is` to check object identity
print(y is z, 'y=', y, 'z=', z, hex(id(y)), hex(id(z)))
# Prints True because y and z point to the same objectTrue
False
True x= [1, 2, 3] y= [1, 2, 3] 0x104b5b300 0x104bebc80
True x= [1, 2, 3] y= [1, 2, 3] 0x104b5b300 0x104bebc80
True y= [1, 2, 3] z= [1, 2, 3] 0x104bebc80 0x104bebc802.11.2 Using if Conditional Constructs
As in other programming languages, the if construct is the main tool for executing conditional logic. We have already seen several examples earlier. Below are additional examples of using conditions in Python with the if construct, demonstrating work with code blocks:
# Set a comfortable temperature in degrees Celsius.
temp_good = 22.0
# Current temperature; this value can be changed for experimentation.
temp_now1 = 48.0
# Define conditions for different temperature states.
statement1 = (temp_now1 <= (temp_good + 10.0)) & (temp_now1 >= (temp_good - 10.0)) # comfortable
statement2 = temp_now1 < (temp_good - 10.0) # cold
statement3 = temp_now1 > (temp_good + 10.0) # hot
# Apply conditional operators to print the corresponding messages.
if statement1: # Do not forget the colon.
print("Okay, it is", temp_now1, "degrees Celsius. Comfortable. Let's go.")
pass # Additional actions can be added here.
elif statement2: # Do not forget the colon.
print("No, it is", temp_now1, "degrees Celsius. Too cold. We cannot go.")
pass # Additional actions can be added here.
elif statement3:
print("No, it is", temp_now1, "degrees Celsius. Too hot. We cannot go.")
pass # Additional actions can be added here.
else:
print("Let's check the temperature.") # Perform another action
passNo, it is 48.0 degrees Celsius. Too hot. We cannot go.Note that in Python there is no limit on the number of blocks that can be used in a single if conditional statement.
2.11.3 for Loops
A for loop is a control structure in programming that allows a specific block of code to be repeated a given number of times. This is especially useful when you need to perform operations on each element in a collection (for example, a list or array) or repeat a task a fixed number of times.
In Python, the syntax of a for loop is as follows:
for variable in sequence:
# code block to execute
passHere, the variable takes the value of each element from the sequence one by one, and the code block inside the loop is executed for each of these elements.
Iterating over list elements:
for element in [1, 2, 3, 4, 5]:
print(element)This prints each element of the list.
Using range:
for number in range(10):
print(number)This prints the numbers from 0 to 9.
for loops play an important role in artificial intelligence (AI) programming, especially in data processing, model training, and task automation:
Data processing: loops are often used for data preprocessing, for example, to normalize or standardize datasets before using them in machine-learning models.
Model training: in some algorithms, such as gradient descent, loops are used to repeatedly update model weights during training.
Automation and testing: for loops are useful for automating repetitive tasks, such as testing models on different datasets or with different parameters.
Special algorithms in AI: in deep learning, for loops are used in recurrent neural networks (RNNs) to iterate through sequences of data, such as text.
primes = [2, 3, 5] # Define a list of prime numbers
for prime in primes: # Loop over the list of prime numbers
print(prime, end=' ') # Print each prime number on one line
print('\nElements in range(10) are')
for n in range(10): # Create a range from 0 to 9
print(n, end=',') # Print each number on one line, separated by commas
print('\nElements in range(3, 8) are')
for n in range(3, 8): # Create a range from 3 to 7
print(n, end=',') # Print each number on one line
print('\nElements in range(-3, 10, 2) are')
for n in range(-3, 10, 2): # Create a range from -3 to 9 with step 2
print(n, end=',') # Print each number on one line
print('\nElements in range(10, -3, -2) are')
for n in range(10, -3, -2): # Create a reverse range from 10 to -2 with step -2
print(n, end=',') # Print each number on one line2 3 5
Elements in range(10) are
0,1,2,3,4,5,6,7,8,9,
Elements in range(3, 8) are
3,4,5,6,7,
Elements in range(-3, 10, 2) are
-3,-1,1,3,5,7,9,
Elements in range(10, -3, -2) are
10,8,6,4,2,0,-2,2.11.4 while Loops
A while loop is a control structure in programming that allows a block of code to be executed while a certain condition is satisfied. This loop is useful when the number of iterations is not known in advance or depends on dynamic factors during program execution.
In Python, the syntax of a while loop is as follows:
while condition:
# code block to execute
passHere, the code block inside the while loop will be executed as long as the condition remains true. As soon as the condition becomes false, loop execution stops.
Waiting for a state change:
while limit_not_reached:
# perform some operations
passThe loop continues until a certain limit is reached.
An infinite loop with an exit condition:
while True:
# perform operations
if exit_condition:
breakThe loop executes indefinitely until a specific exit condition is satisfied.
while loops are repeated as long as a certain Boolean condition is satisfied. This condition controls the execution of operations inside the loop. Consider an example:
# Initialize the counter
count = 0
# The loop continues while count is less than 10
while count < 10:
print(count, end=',')
count += 1 # This is equivalent to count = count + 10,1,2,3,4,5,6,7,8,9,Using the break and continue statements
break is used to exit a for or while loop.
continue skips the current block of code and returns execution to the loop condition.
Example with break:
# Print all numbers up to a specified limit
count = 0
while True:
print(count, end=',')
count += 1
if count >= 10: # If the limit has been reached, interrupt the loop
break
print('\n') # Move to a new line after the loop is completed0,1,2,3,4,5,6,7,8,9,Example with continue:
# Print only even numbers: 0, 2, 4, 6, 8
for n in range(10):
if n % 2 != 0: # If the number is odd, skip it
continue
print(n, end=',')0,2,4,6,8,Loops with an else block
When the loop condition is not satisfied, the code in the else block is executed. However, if a break statement was executed inside the loop, the else block is skipped. It is important to note that the else block will be executed even if a continue statement occurred before it.
Example with else:
# Print numbers from 0 to 4 and then a message that the limit has been reached
count = 0
nlimit = 5
while count < nlimit:
print(count, end=',')
count += 1
else:
print("the counter value has reached %d" % (nlimit))0,1,2,3,4,the counter value has reached 52.11.5 Ternary Conditional Operators
A ternary conditional operator in Python is a compact way to perform operations based on a condition. Unlike traditional multi-line if-else conditional constructs, the ternary operator allows all the logic to be encapsulated in one line. This makes the code cleaner, more concise, and consistent with the DRY principle (“Don’t Repeat Yourself”).
As an example, consider a four-line conditional-operator construct:
if condition:
variable = value_1
else:
variable = value_2This construct can be efficiently reduced to one line using the ternary conditional operator, which follows the general syntax:
variable = value_1 if condition else value_2This approach not only reduces the amount of code but also makes it easier to read and understand while preserving the full functionality of conditional logic. Ternary operators are especially useful in situations where it is necessary to assign a value to a variable depending on some condition.
# Define variables for demonstration
value_1 = "Value when the condition is true"
value_2 = "Value when the condition is false"
# Define the condition to check
# In this case, the condition checks whether the number is even
number = 10 # This can be changed to test different conditions
# Use the ternary conditional operator to assign a value to the variable 'result'
# If 'number' is even (number % 2 equals 0), assign 'value_1'; otherwise, assign 'value_2'
result = value_1 if number % 2 == 0 else value_2
# Print the result
# Prints 'value_1' if the number is even and 'value_2' if it is odd
print(result)Value when the condition is trueThe ternary operator value1 if number % 2 == 0 else value2 is used to assign one of these values to the variable result, depending on whether the number is even. At the end, result is printed to the screen.
2.12 Functions (Methods)
Functions are a convenient way to structure code into blocks that can be called an unlimited number of times as needed. This significantly reduces code repetition and makes code cleaner, more readable, and easier to maintain. In addition, functions are an excellent way to define interfaces for convenient code exchange between programmers.
2.12.1 Block Structure for Defining Functions
Functions in Python are defined using the block keyword def, followed by the function name, which is also the block name. A function is called using its name followed by parentheses (), which contain arguments if there are any. Try the simplest possible function:
def hi():
# Prints a welcome message to the screen
print("Hello, welcome to this simple function!")
# Calling the greeting function
hi()Hello, welcome to this simple function!In this example, the hi function takes no arguments and, when called, prints the message “Hello, welcome to this simple function!” to the screen. This demonstrates the basic principle of creating and using functions in Python.
2.12.2 Functions with Arguments
In the simplest case considered earlier, no function arguments were required. However, functions are often created with required arguments, which are variables passed from the calling code to the function. This allows the function to work with different input data and makes its behavior more flexible.
# Define a function with two parameters: username and greeting
def greeting_student(username, greeting):
# Print a greeting message using an f-string to insert variables
print(f"Hello, {username}, congratulations! We wish you {greeting}")
# Call the function and pass two string arguments
greeting_student("Stepan", "an exciting journey in using functions!")Hello, Stepan, congratulations! We wish you an exciting journey in using functions!Functions can also return values to the calling code using the return keyword.
2.12.3 Example of a Function with a Return Value
# Define a function for adding two numbers
def sum_two_numbers(a, b):
return a + b # Return the sum of the two function arguments
# Assign values to two variables and call the function to add them
x, y = 2.0, 8.0
apb = sum_two_numbers(x, y)
print(f'{x} + {y} = {apb}') # Output: "2.0 + 8.0 = 10.0"
# Change the variable values and call the function again
x, y = 20, 80
apb = sum_two_numbers(x, y)
print(f'{x} + {y} = {apb}, excellent!') # Output: "20 + 80 = 100, excellent!"2.0 + 8.0 = 10.0
20 + 80 = 100, excellent!2.12.4 Variable Scope
Python uses the LEGB rule (Local, Enclosing, Global, Built-in), which determines the order in which the interpreter searches for a variable. The search ends when the variable is found.
# Define a function with a local change to the argument value
def sum_two_numbers(a, b):
a += 1 # Locally increase the value of argument a by 1
print('Inside the function, a =', a) # Print the local value of a
return a + b # Return the sum of the modified a and b
x, y = 2.0, 8.0
print('Before calling the function, x =', x, 'y =', y) # Print x and y before the function call
apb = sum_two_numbers(x, y) # Call the function and save the result
print('After calling the function, %f + %f = %f' % (x, y, apb)) # Print the results after the callBefore calling the function, x = 2.0 y = 8.0
Inside the function, a = 3.0
After calling the function, 2.000000 + 8.000000 = 11.0000002.12.5 Lambda Functions (Anonymous Functions)
An anonymous function is a function without a name. In most programming languages, when you define a function, you also give it a name so that you can call it by that name later in your code. Anonymous functions differ in that they are defined without a name and are usually used at the place where they are created.
In Python, anonymous functions are created with the lambda keyword, which is why they are also called lambda functions. Lambda functions can take any number of arguments, but they can contain only one expression. Their syntax is simplified and intended for creating small functions; these functions do not require an explicit return statement to return a value because the result of the expression is returned automatically.
Example of using an anonymous function in Python:
result = (lambda x, y: x + y)(10, 20)
print(result)30In this example, a lambda function that adds two numbers is defined and immediately called with the arguments 10 and 20. The function has no name and is used only once, directly at the place where it is defined.
Anonymous functions are especially useful when a simple function must be passed as an argument to another function. This often occurs in functional programming, for example when using higher-order functions such as map(), filter(), and reduce(), where lambda functions can serve as compact and efficient handlers.
A lambda function is a compact one-line representation of a function. It is one of the simplest and at the same time most useful forms of functions. Because of its conciseness, it is ideally suited for creating small anonymous functions directly at the place of use.
Lambda functions are useful for defining mathematical expressions. For example, a linear function can be defined as f(x) = ax + b, where a and b are constants. In Python, this can be expressed through a lambda function as follows:
linear_function = lambda x, a, b: a * x + b
print("Linear function: f(x) = 2x + 3 at x = 5 gives", linear_function(5, 2, 3))Linear function: f(x) = 2x + 3 at x = 5 gives 13Similarly, a quadratic function expressed as f(x) = ax^2 + bx + c can be defined through a lambda function as follows:
quadratic_function = lambda x, a, b, c: a * x**2 + b * x + c
print("Quadratic function: f(x) = 2x^2 + 3x + 1 at x = 2 gives", quadratic_function(2, 2, 3, 1))Quadratic function: f(x) = 2x^2 + 3x + 1 at x = 2 gives 15Lambda functions are often used together with ordinary functions, especially when a value must be returned from a compact function. For example, they can be used as arguments for higher-order functions such as map(), filter(), and reduce().
Example of using a lambda function together with map():
numbers = [1, 2, 3, 4, 5]
squared_numbers = list(map(lambda x: x**2, numbers))
print("Squares of numbers:", squared_numbers) # Output: [1, 4, 9, 16, 25]Squares of numbers: [1, 4, 9, 16, 25]2.13 Classes and Objects
A class is a fundamental construct in object-oriented programming; it serves as a template for creating objects. A class encapsulates variables and functions (or methods), providing structure and behavior for its objects. An unlimited number of objects can be created from a class, and each of them inherits the structure, variables (attributes), and functions (methods) of the class.
2.13.1 The Simplest Class
This example presents the simplest class named C. The class defines the attribute ca, which is a class attribute. Comments inside the class enclosed in triple quotation marks are intended to provide class documentation or explain its purpose.
class C:
'''The simplest possible class named "C".'''
ca = "class attribute" # Define a class attribute
# Use the help() function to obtain information about class C
help(C)
# Create two instances of class C
i1 = C() # Instance i1 of class C is created
i2 = C() # Instance i2 of class C is created
# Demonstrate access to the class attribute through instances and through the class itself
print('i1.ca =', i1.ca) # Access the class attribute through instance i1
print('i2.ca =', i2.ca) # Access the class attribute through instance i2
print('C.ca =', C.ca) # Access the class attribute directly through the class itself
# Change the class attribute and observe its effect on the instances
C.ca = "This is the modified class attribute 'ca'"
print('After the change:')
print('C.ca =', C.ca) # Shows the modified value of the class attribute
print('i1.ca =', i1.ca) # Instance i1 reflects the change to the class attribute
print('i2.ca =', i2.ca) # Instance i2 reflects the change to the class attributeThis code shows how class instances are created and how they are connected to class attributes. It also demonstrates that changing a class attribute is reflected in all its instances, emphasizing the property of shared attributes in object-oriented programming.
Class attributes and object-instance attributes are stored in separate dictionaries. For class C, the dictionary C.dict contains the class attributes, while i1.dict and i2.dict store the attributes of the corresponding object instances. When we modify an instance attribute, such as i1.ca, a new dictionary is created for that instance, reflecting its modified attributes. If no changes are made, as in the case of i2 before assigning a value to i2.ca, the instance dictionary remains empty and the attributes are still inherited from the class. After a value is assigned to i2.ca, an attribute dictionary is also created for i2, reflecting changes at the instance level. It is important to note that any future changes to class-level attributes will not affect the attributes of instances that already have their own attribute dictionaries.
class C:
ca = "Initial value of the class attribute 'ca'"
# Create instances of class C
i1 = C()
i2 = C()
# Change the class attribute 'ca'
C.ca = "Second modification of the class attribute 'ca'"
# Print the dictionary of class C
print("Dictionary of class C:", C.__dict__)
# Change the instance attribute 'ca' for i1
i1.ca = "This is the modified instance attribute 'ca'"
# Print the dictionary of instance i1
print("Dictionary of instance i1:", i1.__dict__)
# Print the dictionary of instance i2 before changes
print("Dictionary of instance i2 before changes:", i2.__dict__)
# Change the instance attribute 'ca' for i2
i2.ca = "Now we modify the instance attribute 'ca'"
# Print the dictionary of instance i2 after changes
print("Dictionary of instance i2 after changes:", i2.__dict__)Dictionary of class C: {'__module__': '__main__', 'ca': "Second modification of the class attribute 'ca'", '__dict__': <attribute '__dict__' of 'C' objects>, '__weakref__': <attribute '__weakref__' of 'C' objects>, '__doc__': None}
Dictionary of instance i1: {'ca': "This is the modified instance attribute 'ca'"}
Dictionary of instance i2 before changes: {}
Dictionary of instance i2 after changes: {'ca': "Now we modify the instance attribute 'ca'"}2.13.2 A Class for Scientific Computing
class Circle:
"""
The Circle class is intended for computations related to a circle.
In particular, it allows the area of a circle to be computed for a given radius.
"""
pi = 3.14159 # Class attribute containing the value of pi used by all class instances
def __init__(self, radius):
"""
Class constructor that initializes a new Circle instance with a given radius.
:param radius: circle radius
"""
self.radius = radius # Instance attribute containing the radius of the circle
def circle_area(self):
"""
Method for computing the area of a circle.
:return: circle area
"""
return self.pi * self.radius**2 # Return the circle area using the formula πr²
# Create an instance of class Circle with radius 10
r = 10
c10 = Circle(r)
# Print initial values
print('Circle.pi before change:', Circle.pi)
print('Radius:', c10.radius)
print('c10.pi before change:', c10.pi)
# Change the value of pi only for the c10 instance
c10.pi = 3.14
# Print values after the change
print('c10.pi after change through c10.pi:', c10.pi)
print('Circle.pi after change through c10.pi:', Circle.pi)
print('Area of circle c10 =', c10.circle_area())
print('Area of a circle with radius 100 =', Circle(100).circle_area())Circle.pi before change: 3.14159
Radius: 10
c10.pi before change: 3.14159
c10.pi after change through c10.pi: 3.14
Circle.pi after change through c10.pi: 3.14159
Area of circle c10 = 314.0
Area of a circle with radius 100 = 31415.899999999998In this code:
The Circle class describes a circle and can compute its area. - The pi attribute is a class attribute shared by all instances and represents the constant pi. - The init method initializes a class instance by setting its radius. - The circle_area method computes the area of the circle using the instance attributes. - The example demonstrates that changing the pi attribute for a specific instance (c10) does not affect the class attribute pi, thereby emphasizing the difference between class attributes and instance attributes.
This example also illustrates the basic concepts of OOP, such as encapsulation (hiding data inside a class) and inheritance (when subclasses are created).
2.13.3 Subclass (Class Inheritance)
Subclasses are often used to implement inheritance in Python, which allows new classes to be created while fully using the structure of an existing class (attributes and functions) without affecting the current use of that class. This is also useful for modernizing existing programs by reducing code duplication.
Suppose that the code for the Circle class created earlier has already been distributed and is used by many people. Now we decide to create another class for computing the area of a sector of a circle based on the value of the fraction of the circle. To do this, we can create a subclass named PartialCircle, which will not affect the use of the already distributed Circle class.
class PartialCircle(Circle):
'''Subclass "PartialCircle" based on class "Circle": computes the area of a circle sector.'''
def __init__(self, radius, portion):
'''Initialization with the attributes: radius and portion.'''
super().__init__(radius) # Inherit attributes from the base class "Circle".
self.portion = portion # Subclass attribute that defines the fraction of the circle.
def sector_area(self):
'''Compute the area of a circle sector.'''
return self.portion * self.circle_area() # Use the base-class method to compute the circle area.Readers may run this code and spend time studying the information about the subclass structure, its relationship with the base class, the use of self to prepare connections with future objects, and which attributes and functions were inherited from the base class and created in the subclass.
pc10 = PartialCircle(10., 0.5) # Create a subclass object instance with radius 10 and a 50% fraction.
print(pc10.pi) # Use an attribute from the base class
print(pc10.radius) # Also an attribute from the base class
print(pc10.sector_area()) # Compute the area of a 50% circle sectorNow change the value of the constant pi through the subclass instance:
pc10.pi = 3.14
print(pc10.pi) # The value changed to 3.14Check the value of pi through the base class Circle:
print(c10.pi) # The value remains unchanged. Changes in the subclass do not affect the base class.Let us prepare a complete code listing with detailed comments demonstrating class inheritance in Python using the example of the Circle and PartialCircle classes.
# Define the base class Circle
class Circle:
'''Class "Circle" for representing a circle and computing its area.'''
def __init__(self, radius):
'''Initialize a class instance with one attribute: radius.'''
self.radius = radius # Instance attribute storing the radius of the circle.
self.pi = 3.14159 # Attribute storing the value of π.
def circle_area(self):
'''Method for computing the area of a circle.'''
return self.pi * (self.radius ** 2) # Return the area of the circle using the formula πr^2.
# Define the subclass PartialCircle, which inherits Circle
class PartialCircle(Circle):
'''Subclass "PartialCircle" based on class "Circle": computes the area of a circle sector.'''
def __init__(self, radius, portion):
'''Initialization with the attributes: radius and portion (fraction of the circle).'''
super().__init__(radius) # Call the base-class initializer to set the radius.
self.portion = portion # Subclass attribute defining the circle fraction for which the area must be computed.
def sector_area(self):
'''Compute the area of a circle sector.'''
return self.portion * super().circle_area() # Use the base-class method and multiply by the fraction.
# Create an instance of the base class Circle
c10 = Circle(10) # Instance of class Circle with radius 10
print("Area of the circle:", c10.circle_area()) # Compute and print the area of the circle
# Create an instance of the subclass PartialCircle
pc10 = PartialCircle(10, 0.5) # Instance of class PartialCircle with radius 10 and a 50% fraction
print("Area of a 50% circle sector:", pc10.sector_area()) # Compute and print the area of the sector
# Change the value of π through the subclass instance
pc10.pi = 3.14
print("Modified value of pi in the subclass:", pc10.pi)
# Check whether the value of π changed in the base class
print("Value of pi in the base class:", c10.pi) # The value of π in the base class remains unchangedArea of the circle: 314.159
Area of a 50% circle sector: 157.0795
Modified value of pi in the subclass: 3.14
Value of pi in the base class: 3.141592.14 NumPy Arrays: Convenience for Scientific Computing
NumPy arrays are similar to lists, and they are much easier to work with for scientific computations. Operations with NumPy arrays are usually much faster when working with large-scale data.
2.14.1 Lists and NumPy Arrays and Their Difference from the Classical List
In Python, lists and NumPy arrays are data structures intended for storing sets of elements. Despite superficial similarities, they have important differences that must be taken into account when choosing a suitable tool for specific tasks. In this chapter, we will examine the common features and distinctive characteristics of lists and NumPy arrays in detail, paying special attention to their applicability in different computational contexts.
Common characteristics:
Mutability: Both types of structures can be modified after creation, allowing elements to be added, deleted, or changed through so-called destructive operations. - Indexing: Individual elements are accessed by their numeric index, which reflects their position in the structure. - Slicing: New structures containing selected fragments of the original data are created using the slicing mechanism.
Key differences:
Origin: NumPy arrays are available as part of the external NumPy library, which must be imported, whereas lists are a built-in Python data type. - Element-wise operations: NumPy arrays support direct mathematical and logical operations on all elements without using loops, which is not available for lists. - Type homogeneity: NumPy arrays require data of a single type, whereas lists allow elements of different types to be stored. - Multidimensionality: NumPy arrays can represent multidimensional structures, including matrices and tensors, which goes beyond the capabilities of one-dimensional lists. - Performance: Operations with NumPy arrays are usually significantly faster than analogous actions with lists because of computational optimization and the use of low-level C code. - Memory efficiency: NumPy arrays provide more compact data storage compared with lists, which is especially important when working with large amounts of information. - Applicability in different fields: NumPy arrays are widely used in mathematical computations, array-data processing, and machine-learning algorithms because of their performance advantages and support for multidimensional operations.
A conscious choice of the appropriate data structure makes it possible to optimize performance, memory usage, and development convenience in different computational tasks.
2.14.2 NumPy Array Structure
First, we will briefly examine the structure of a NumPy array in comparison with the list discussed earlier. To begin the discussion, we import the NumPy package.
import numpy as np # Import the NumPy module and give it the alias np
x1 = np.array([28, 3, 28, 0]) # Create a one-dimensional NumPy array
print('x1 =', x1) # Print array x1
# A NumPy array is similar to a listx1 = [28 3 28 0]As shown above, a NumPy array is “framed” by a pair of square brackets, just like a list.
import numpy as np
# Import the NumPy module and give it the alias np
x2 = np.array([[51, 22.0], [0, 0], (18 + 9j, 3.)]) # Mixed types
# Create array x2 from three rows and two columns. The first two rows contain integers,
# and the third row contains a complex number and a real number.
print('x2 =', x2) # All elements become complex numbers
# Since complex numbers are present in the array, all numbers in the array become complex numbers.x2 = [[51.+0.j 22.+0.j]
[ 0.+0.j 0.+0.j]
[18.+9.j 3.+0.j]]This is a two-dimensional NumPy array. It is framed by a double pair of square brackets. A list has no multidimensionality except in the form of nesting: lists inside lists.
We can also create NumPy arrays from lists. In the following example, we first create two lists and then create NumPy arrays from them:
# List of people's weights (in kg)
list_w = [57.5, 64.3, 71.6, 68.2]
# List of people's heights (in m)
list_h = [1.5, 1.6, 1.7, 1.65]
# Print the list of weights
print('People’s weights:', list_w)
# Print the list of heights
print('People’s heights:', list_h)People’s weights: [57.5, 64.3, 71.6, 68.2]
People’s heights: [1.5, 1.6, 1.7, 1.65]# Import the NumPy library
import numpy as np
# Declare lists of people’s weights and heights
list_w = [57.5, 64.3, 71.6, 68.2]
list_h = [1.5, 1.6, 1.7, 1.65]
# Convert the lists to NumPy arrays
narray_w = np.array(list_w)
narray_h = np.array(list_h)
# Print the arrays
print('Array of weights:', narray_w)
print('Array of heights:', narray_h)Array of weights: [57.5 64.3 71.6 68.2]
Array of heights: [1.5 1.6 1.7 1.65]Let us create a function that prints information about a given NumPy array.
First, we create a function named getArrayInfo(). This function takes a NumPy array as an argument and prints the following information about it:
the value of the first array element; - the array type; - the number of array dimensions; - the array shape; - the number of elements in the array; - the array data type; - the memory address of the array.
Here is the code for the getArrayInfo() function:
def getArrayInfo(a):
"""Get information about a given array: getArrayInfo(array)."""
# Print the elements of the first axis of the array
print('Elements of the first array axis:', a[0])
# Print the array type
print('Type:', type(a))
# Print the number of array dimensions
print('Number of dimensions, a.ndim:', a.ndim)
# Print the array shape (sizes along each axis)
print('Array shape, a.shape:', a.shape)
# Print the total number of elements in the array
print('Number of elements, a.size:', a.size)
# Print the data type of the array elements
print('Data type of elements, a.dtype:', a.dtype)
# Print the memory address where the array is stored (debugging information)
print('Memory address:', a.data)Now we can use getArrayInfo() to obtain information about any NumPy array. For example, here is how we can obtain information about the narray_w array:
narray_w = np.array([57.5, 64.3, 71.6, 78.9])
getArrayInfo(narray_w)Elements of the first array axis: 57.5
Type: <class 'numpy.ndarray'>
Number of dimensions, a.ndim: 1
Array shape, a.shape: (4,)
Number of elements, a.size: 4
Data type of elements, a.dtype: float64
Memory address: <memory at 0x102f5a680>2.14.3 Slices for NumPy Arrays
Slices also work for NumPy arrays, similarly to lists. Let us take a slice of an array.
import numpy as np
list_w = [57.5, 64.3, 71.6, 68.2]
list_h = [1.5, 1.6, 1.7, 1.65]
narray_w = np.array([57.5, 64.3, 71.6, 78.9])
print(list_w[1:3]) # Slice between the second and third elements
print(narray_w[1:3]) # Slice between the second and third elements[64.3, 71.6]
[64.3 71.6]Now let us add an element to both the list and the NumPy array.
list_w.append(59.8)
print(list_w)
# For a NumPy array, we use:
print(np.append(narray_w, 59.8))[57.5, 64.3, 71.6, 68.2, 59.8]
[57.5 64.3 71.6 78.9 59.8]print(list_w, ', ', narray_w)
print(type(list_w), ', ', type(narray_w))
print(len(list_w), ', ', narray_w.ndim) # Use len() to get the length[57.5, 64.3, 71.6, 68.2, 59.8] , [57.5 64.3 71.6 78.9]
<class 'list'> , <class 'numpy.ndarray'>
5 , 1nwh = (narray_w, narray_h) # This forms a tuple of NumPy arrays
print(nwh)
# (array([57.5, 64.3, 71.6, 68.2]), array([1.5, 1.6, 1.7, 1.65]))
# To form a multidimensional array, we can use the following (more on this later):
arr = np.array([narray_w, narray_h])
# array([[57.5, 64.3, 71.6, 68.2 ],
# [ 1.5, 1.6, 1.7, 1.65]])In NumPy, the main data structure is the multidimensional array. Consider the array arr with two dimensions. The array dimensions are specified as the tuple (2, 4), where the first number is the number of rows (records along axis 0), and the second is the number of columns (records along axis 1).
Transposition is the operation of changing the order of an array’s axes. In NumPy, the .T method is used to transpose an array. This operation swaps rows and columns, resulting in a change in the array shape.
import numpy as np
# Original array
arr = np.array([[57.5, 64.3, 71.6, 68.2],
[ 1.5, 1.6, 1.7, 1.65]])
# Transpose the array
arrT = arr.T
# Print the transposed array
print(arrT)[[57.5 1.5 ]
[64.3 1.6 ]
[71.6 1.7 ]
[68.2 1.65]]As a result of executing this code, the array shape changes from the original (2, 4) to (4, 2). This means that the array, which originally contained 2 rows and 4 columns, will contain 4 rows and 2 columns after transposition.
As can be seen from the example, the first column of the original array became the first row of the transposed array, and so on for the remaining elements. This process is widely used in mathematical computations, data processing, and machine learning for preparing and transforming data.
2.14.4 Reference and Copy of a NumPy Array
Let us consider how changes in a NumPy array affect other variables that refer to the same array.
import numpy as np
# Create a NumPy array with two rows and four columns
arr = np.array([[57.5, 64.3, 71.6, 68.2],
[1.5, 1.6, 1.7, 1.65]])
# Create a second variable that refers to the same array
arrb = arr
# Print the contents of arrb
print(arrb)
# Change the first element of arr
arr[0, 0] = 888.0
# Print the contents of arrb again and see that it has changed
print(arrb)[[57.5 64.3 71.6 68.2 ]
[ 1.5 1.6 1.7 1.65]]
[[888. 64.3 71.6 68.2 ]
[ 1.5 1.6 1.7 1.65]]As can be seen from the result, after an element in arr is changed, the contents of arrb also change. This happens because arrb is not a copy of arr; it refers to the same data object. Any changes made through one variable will also be visible through the other.
Note: to create an independent copy of an array in NumPy, use the copy() method as follows:
arrc = arr.copy()Using copy(), you create a new data object that can be changed independently of the original.
When working with arrays in NumPy, it is important to understand the difference between assignment and creating a copy. Consider the following example:
import numpy as np
# Create a two-dimensional array
arr = np.array([[57.5, 64.3, 71.6, 68.2],
[1.5, 1.6, 1.7, 1.65]])
# Try to create a "copy" by assignment
arrc = arrIn this case, arrc is not an independent copy of arr. It is merely a new reference to the same array, and changes in arrc will affect arr. To create a real copy, the copy() method must be used:
# Create an independent copy of the array
arrc = arr.copy()
Now arrc is a fully independent copy of arr, and changes in one will not affect the other. However, it should be remembered that creating copies can be resource-intensive, so it should be used wisely:
# Change an element in the original array
arr[0, 0] = 77.0
# Print both arrays for comparison
print('arr:', arr)
print('arrc:', arrc)After executing this code, we will see that arr has changed, but arrc has remained the same.
Note: use the copy() method only when it is truly necessary in order to avoid unnecessary memory and execution-time costs.
import numpy as np # Import the NumPy library under the alias np
# Create a two-dimensional array arr using np.array
arr = np.array([[57.5, 64.3, 71.6, 68.2],
[1.5, 1.6, 1.7, 1.65]])
# Create an independent copy of array arr using the copy method
arrc = arr.copy()
# Print the contents of array arrc
print('arrc', arrc)
# Change the element at position [0, 0] of array arr to 77.0
arr[0, 0] = 77.0
# Print the modified array arr
print('arr', arr)
# Print array arrc to show that it has not changed
print('arrc', arrc)arrc [[57.5 64.3 71.6 68.2 ]
[ 1.5 1.6 1.7 1.65]]
arr [[77. 64.3 71.6 68.2 ]
[ 1.5 1.6 1.7 1.65]]
arrc [[57.5 64.3 71.6 68.2 ]
[ 1.5 1.6 1.7 1.65]]2.14.5 Axis in a NumPy Array
An axis is one of the fundamental concepts used when working with arrays in the NumPy library. An axis indicates a direction in an array. Thus, a one-dimensional array has one axis (axis 0), a two-dimensional array has two axes (axis 0 and axis 1), and so on.
Multidimensional arrays can be constructed from one-dimensional arrays by combining them. For this purpose, the np.stack() method is used; it allows sequences of arrays to be combined along a specified axis. The method syntax is as follows:
np.stack(sequence_of_arrays, axis=axis_number)where sequenceofarrays is a list or tuple of NumPy arrays, and axis_number is the index of the axis along which the combination must be performed.
Suppose we have two one-dimensional arrays that we want to combine into a two-dimensional array by placing them one above the other:
import numpy as np # Import the NumPy library under the alias np
# Assumed one-dimensional arrays to combine
narray_w = np.array([57.5, 64.3, 71.6, 68.2])
narray_h = np.array([1.5, 1.6, 1.7, 1.65])
# Combine arrays along axis 0 (vertically)
arr = np.stack([narray_w, narray_h], axis=0)
# Print the result
print(arr)[[57.5 64.3 71.6 68.2 ]
[ 1.5 1.6 1.7 1.65]]It is often necessary to transform a multidimensional array into a one-dimensional array. NumPy has the np.ravel() method for this purpose; it “flattens” a multidimensional array into a one-dimensional form.
import numpy as np # Import the NumPy library under the alias np
# Assumed one-dimensional arrays to combine
narray_w = np.array([57.5, 64.3, 71.6, 68.2])
narray_h = np.array([1.5, 1.6, 1.7, 1.65])
# Combine arrays along axis 0 (vertically)
arr = np.stack([narray_w, narray_h], axis=0)
# Print the original two-dimensional array
print(arr)
# Transform the two-dimensional array into a one-dimensional array
rarr = np.ravel(arr)
# Print the result
print(rarr)[[57.5 64.3 71.6 68.2 ]
[ 1.5 1.6 1.7 1.65]]
[57.5 64.3 71.6 68.2 1.5 1.6 1.7 1.65]Figure: Introduction to numerical computations with NumPy.
When working with arrays in NumPy, situations often arise where the dimensionality of an array must be changed. This may mean transforming a two-dimensional array with shape (2, 4) into a one-dimensional array of length 8.
In machine learning, it is often necessary to sum array elements along a specific axis. NumPy provides the np.sum function for this purpose; it efficiently sums elements along a specified axis.
import numpy as np # Import the NumPy library under the alias np
# Suppose we have the following two-dimensional array:
arr = np.array([[57.5, 64.3, 71.6, 68.2],
[ 1.5, 1.6, 1.7, 1.65]])
# Print the original array for clarity
print(arr)
# Sum array elements by columns (axis=0)
column_sum = np.sum(arr, axis=0)
print('Sum by columns:', column_sum, column_sum.shape)
# Sum array elements by rows (axis=1)
row_sum = np.sum(arr, axis=1)
print('Sum by rows:', row_sum, row_sum.shape)[[57.5 64.3 71.6 68.2 ]
[ 1.5 1.6 1.7 1.65]]
Sum by columns: [59. 65.9 73.3 69.85] (4,)
Sum by rows: [261.6 6.45] (2,)Sum by columns: the result is a one-dimensional array, each element of which represents the sum of the numbers in the corresponding column of the original array. The dimensionality of this array is (4,).
Sum by rows: the result is also a one-dimensional array, each element of which is the sum of the numbers in the corresponding row of the original array. The dimensionality of this array is (2,).
Summing along different axes of an array reduces its dimensionality and changes its shape.
2.14.7 Element-Wise Computations
In Python, there are different types of data structures that support different operations. Two of the most frequently used types for storing collections of numbers are lists (list) and NumPy arrays (numpy.array). They behave differently when mathematical operations are performed.
Lists in Python can be joined together using the + operator. This operation is called concatenation. Concatenation is the process of attaching one list to the end of another, which creates a new list containing the elements of both original lists.
If we have two lists, listw and listh, then executing listw + listh creates a new list in which all elements from listw come first, followed by all elements from listh.
list_w = [57.5, 64.3, 71.6, 68.2, 59.8]
list_h = [1.5, 1.6, 1.7, 1.65]
listwh = list_w + list_h
print('Concatenated list:', listwh)Concatenated list: [57.5, 64.3, 71.6, 68.2, 59.8, 1.5, 1.6, 1.7, 1.65]Unlike lists, NumPy arrays support element-wise operations. This means that when the + operator is used between two NumPy arrays, Python adds the corresponding elements of these arrays.
If narrayw and narrayh are NumPy arrays of the same length, the operation narrayw + narrayh creates a new NumPy array in which each element is the sum of the corresponding elements of the original arrays.
import numpy as np # Import the NumPy library under the alias np
narray_w = np.array([57.5, 64.3, 71.6, 68.2])
narray_h = np.array([1.5, 1.6, 1.7, 1.65])
narraywh = narray_w + narray_h
print('Element-wise added array:', narraywh)Element-wise added array: [59. 65.9 73.3 69.85]NumPy arrays are especially convenient when element-wise multiplication is needed. This is often used to convert units of measurement, for example when converting weight from kilograms to pounds.
To convert weight from kilograms to pounds, we can multiply the array of weights in kilograms by the number 2.20462, because 1 kilogram is equal to 2.20462 pounds.
import numpy as np # Import the NumPy library under the alias np
weights_kg = np.array([57.5, 64.3, 71.6, 68.2])
weights_lbs = weights_kg * 2.20462
print('Weight in pounds:', weights_lbs)Weight in pounds: [126.76565 141.757066 157.850792 150.355084]Thus, NumPy arrays provide powerful and flexible tools for performing mathematical operations on large data.
Earlier, we discussed the use of ordinary lists and functions such as zip() for element-wise operations, but this can be inefficient, especially for large amounts of data. Instead, we can use NumPy capabilities, which significantly speed up computations.
import numpy as np
# Example of using ordinary lists to compute the sum of elements of two lists
list1 = [20, 30, 40, 50, 60]
list2 = [4, 5, 6, 2, 8]
sum_lists = [x + y for x, y in zip(list1, list2)]
print('Sum of lists:', sum_lists)
# Now perform the same operation using NumPy arrays
sum_arrays = (np.array(list1) + np.array(list2)).tolist()
print('Sum of NumPy arrays:', sum_arrays)Sum of lists: [24, 35, 46, 52, 68]
Sum of NumPy arrays: [24, 35, 46, 52, 68]In the NumPy example, we convert the lists to NumPy arrays, perform element-wise addition, and then convert the result back to a list. This is much more efficient, especially when working with large datasets.
2.14.8 Practical Methods for Creating Multidimensional Arrays
In areas such as machine learning and mathematical computations, multidimensional arrays are an essential tool. This is because, when working with large amounts of data, efficient tools are needed for generating, manipulating, and processing them. The NumPy library provides a number of functions for working with data arrays of different dimensionalities.
The function np.arange(start, stop, step, dtype) creates a one-dimensional array with a sequence of numbers starting from start and ending at stop (not including stop), with step step. The dtype parameter determines the data type of the array elements. If it is not specified, the data type will be determined from the type of the input data.
np.arange(2, 8, 0.5, dtype=np.float)array([2. , 2.5, 3. , 3.5, 4. , 4.5, 5. , 5.5, 6. , 6.5, 7. , 7.5])This code returns an array of real numbers with an interval of 0.5, starting at 2 and ending with a number less than 8.
Unlike np.arange, the function np.linspace(start, stop, num) returns an array that contains num evenly distributed numbers in the interval from start to stop, including both endpoints. This is ideal when it is necessary to obtain an exact number of elements.
np.linspace(1., 4., 6)array([1. , 1.6, 2.2, 2.8, 3.4, 4. ])This code generates an array of 6 numbers evenly distributed between 1 and 4 inclusive.
Using these functions simplifies the process of creating arrays for testing algorithms, visualizing data, and solving other computational tasks. They help avoid manual value entry and automate data preprocessing processes.
Initialization of arrays with a specified value:
import numpy as np
a = np.array([1., 2., 3.])
a.fill(9.9) # Fill all elements with the same value
print(a)[9.9 9.9 9.9]Initially, a one-dimensional array a is created with three elements [1., 2., 3.]. Then the .fill(9.9) method is used, which replaces all array elements with the value 9.9. As a result, an array consisting of identical values 9.9 is obtained.
Creating an array without initializing values:
import numpy as np
x = np.empty((3, 4)) # Shape (dimension) (3, 4) is specified, without value initialization
print(x)[[0. 0. 0. 0.]
[0. 0. 0. 0.]
[0. 0. 0. 0.]]The function np.empty((3, 4)) creates a 3x4 array (3 rows and 4 columns), but does not initialize its values, which means that it will contain arbitrary numbers that were already in this memory area.
Initializing an array with zeros:
import numpy as np
x = np.zeros((6, 6)) # Initialized with zeros
print(x)[[0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0.]]The function np.zeros((6, 6)) creates an array of size 6x6, where each element is initialized to 0.
Creating an array, initializing it with ones, and then multiplying it:
import numpy as np
y = np.ones((2, 2)) * 2 # A 2x2 array with 1.0 in all elements
print(y)[[2. 2.]
[2. 2.]]The function np.ones((2, 2)) creates a 2x2 array in which each element is initialized to 1.0. After that, the array is multiplied by 2, and the result is an array in which every element is equal to 2.
Assigning one array to a slice of another array:
import numpy as np # Import the NumPy library
# Create the first 6x6 array initialized with zeros
x = np.zeros((6, 6))
print("Original array x:")
print(x)
# Create the second 2x2 array initialized with ones
y = np.ones((2, 2))
print("\nArray y:")
print(y)
# Multiply each element of array y by 2
y *= 2
print("\nArray y after multiplication by 2:")
print(y)
# Assign array y to a slice of array x.
# Here we take a slice from the 3rd to the 5th row (not including 5)
# and from the 3rd to the 5th column (not including 5)
# and assign the values of array y to it.
x[3:5, 3:5] = y
print("\nArray x after assigning array y to the slice:")
print(x)Original array x:
[[0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0.]]
Array y:
[[1. 1.]
[1. 1.]]
Array y after multiplication by 2:
[[2. 2.]
[2. 2.]]
Array x after assigning array y to the slice:
[[0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0.]
[0. 0. 0. 2. 2. 0.]
[0. 0. 0. 2. 2. 0.]
[0. 0. 0. 0. 0. 0.]]These examples demonstrate various functions of the NumPy library for creating and manipulating data arrays in Python.
2.14.9 Working with an External Package: TensorFlow
The toolkit of a modern machine-learning specialist includes many libraries and packages specifically designed to simplify and accelerate the process of building scalable machine-learning models. One such powerful tool is TensorFlow, which we will use in this training course.
TensorFlow is an external library that is not included in the standard Python distribution, so it must be installed before you begin working with it. This can be done with the pip package-management system by running the following command in the command line or terminal:
pip install tensorflowAfter this command is executed, pip automatically downloads and installs the latest version of TensorFlow and all required dependencies.
While studying and working with code, you may encounter errors related to missing modules. In that case, install the missing module in the same way, using pip.
After TensorFlow has been installed successfully, it can be imported into your Python script with the standard import command:
import tensorflow as tf # Import TensorFlow under the alias tf for convenienceThis gives you access to all TensorFlow functions and classes that can be used to build and train machine-learning models.
TensorFlow also provides the tensorflow.numpy (tfnp) module for working with arrays in a way similar to NumPy. It provides compatibility with NumPy and allows many of the same functions and methods to be used. Example of creating an array:
import tensorflow.experimental.numpy as tfnp # Import the module for working with arrays
# Create a 3-by-4 array filled with zeros
x = tfnp.zeros((3, 4)) # Use the zeros function to create an array filled with zeros
# Convert the TensorFlow NumPy array to a standard NumPy array for output
x_np = x.numpy() # Conversion to a standard NumPy array
# Print the array using standard NumPy output-formatting options
print(x_np)
# Print the size of the array; it should be 12, since there are 3 rows and 4 columns
print(tfnp.size(x))[[0. 0. 0. 0.]
[0. 0. 0. 0.]
[0. 0. 0. 0.]]
12In this example, tfnp.zeros creates an array with 3 rows and 4 columns filled with zeros. We then print it and show the total number of elements in the array.
A tensor is a mathematical object that generalizes the concepts of a scalar, vector, and matrix to higher dimensions. Tensors are widely used in physics, engineering, and, of course, in machine learning and deep learning. They are multidimensional arrays of data that can have any number of dimensions, also called ranks.
Tensor rank: The rank of a tensor, or its order, determines the number of dimensions of the tensor. For example, a scalar, meaning a single number, is a rank-0 tensor; a vector, meaning a list of numbers, is a rank-1 tensor; and a matrix, meaning a table of numbers, is a rank-2 tensor. Tensors with rank higher than 2 are often called multidimensional tensors.
Tensor shape: The shape of a tensor describes its size in each dimension. For example, a 3x4 matrix has the shape [3, 4], which means that the matrix contains 3 rows and 4 columns.
Tensor elements: Each tensor element is a number, which may be an integer, a real number, or even a more complex data type, depending on the context of use. Tensor elements are usually processed together by mathematical operations.
Tensors can be used to represent many different types of data:
Scalar data, such as temperature, price, and so on, can be represented by rank-0 tensors.
Vector data, such as forces, velocities, and so on, or time series can be represented by rank-1 tensors.
Images, which can be represented as 2D matrices of pixels, can be represented by rank-2 tensors, or rank-3 tensors if color channels are taken into account.
Video data, where each frame is an image, can be represented by rank-4 tensors: time, height, width, and color channels.
import tensorflow as tf
y = tf.random.normal([3, 4], mean=0.0, stddev=1.0)The function tf.random.normal generates a tensor of the specified shape ([3, 4] means 3 rows and 4 columns) with elements drawn from a normal, or Gaussian, distribution. The parameters mean and stddev determine the mean value and standard deviation of the distribution, respectively. In this case, they are set to 0.0 and 1.0, which corresponds to the standard normal distribution.
x = tf.exp(y)The function tf.exp is applied to each element of the tensor y. The exponential function exp(x) is computed as e^x, where e is the base of natural logarithms and is approximately equal to 2.71828. This transformation is often used in science and engineering.
y_transposed = tf.transpose(y)The tf.transpose function swaps the dimensions of a tensor. In the case of a matrix, that is, a 2D tensor, this turns rows into columns and vice versa. Transposition is often used to prepare tensors for mathematical operations that require a particular alignment of dimensions, such as matrix multiplication.
result_matrix = tf.matmul(x, y_transposed)The function tf.matmul multiplies two tensors by interpreting them as matrices. Matrix multiplication is not elementwise, as multiplication in ordinary algebra is; instead, each element of the resulting matrix is computed as the dot product of the corresponding row of the first matrix and column of the second. For multiplication to be possible, the number of columns in the first matrix must match the number of rows in the second. The result is a new matrix whose size is determined by the number of rows in the first matrix and the number of columns in the second.
import tensorflow as tf
# Initialize a 3x4 tensor with values from the standard normal distribution
y = tf.random.normal([3, 4], mean=0.0, stddev=1.0)
# Print the value of tensor y
print('y')
print(y.numpy())
# Exponentiate each element of tensor y
x = tf.exp(y) # Compute e raised to the power of each element of tensor y
print(x.numpy())
# Transpose tensor y
y_transposed = tf.transpose(y) # Transpose tensor y
print(y_transposed.numpy())
# Multiply tensor x by the transposed tensor y
result_matrix = tf.matmul(x, y_transposed) # Compute the matrix product
print(result_matrix.numpy())y
[[ 0.71528995 2.1142166 -1.1875231 -0.16214192]
[-0.1809633 -0.21809964 1.1487513 -0.02445018]
[ 1.6564487 0.6891605 1.2565229 1.3516059 ]]
[[2.0447795 8.283093 0.30497572 0.8503205 ]
[0.834466 0.8040453 3.1542516 0.9758463 ]
[5.240667 1.9920425 3.5131845 3.863625 ]]
[[ 0.71528995 -0.1809633 1.6564487 ]
[ 2.1142166 -0.21809964 0.6891605 ]
[-1.1875231 1.1487513 1.2565229 ]
[-0.16214192 -0.02445018 1.3516059 ]]
[[18.474825 -1.847019 10.62796 ]
[-1.6071612 3.273221 7.218715 ]
[ 3.161762 2.5584767 19.690228 ]]In TensorFlow, tensors are the main objects for working with data and are designed for efficient computation, especially on graphics processing units (GPUs) and in distributed systems. Tensor operations in TensorFlow make it possible to perform various data manipulations, including mathematical operations, shape changes, and much more.
NumPy arrays remain the main objects in NumPy, the central library for scientific computing in Python. NumPy and TensorFlow are often used together in machine-learning projects, because NumPy provides many useful operations for preprocessing data that can then be used in TensorFlow.
To interact between TensorFlow tensors and NumPy arrays, you can use the methods tf.converttotensor() and .numpy(). These methods make it easy to transform data from NumPy arrays into TensorFlow tensors and back, providing convenient integration and data compatibility.
Let us consider a code example where we use TensorFlow to work with tensors and NumPy to compute a dot product:
# Import the required libraries
import tensorflow as tf # Import TensorFlow for working with tensors
import numpy as np # Import NumPy for working with NumPy arrays
# Create tensors in TensorFlow
x = tf.constant([[1, 2], [3, 4]], dtype=tf.float32) # Create a 2x2 tensor x
y = tf.constant([[5, 6], [7, 8]], dtype=tf.float32) # Create a 2x2 tensor y
# Convert TensorFlow tensors to NumPy arrays
x_np = x.numpy() # Convert tensor x to a NumPy array
y_np_transposed = tf.transpose(y).numpy() # Transpose tensor y and convert it to a NumPy array
# Compute the dot product between NumPy arrays
result = np.dot(x_np, y_np_transposed) # Compute the dot product between x_np and y_np_transposed
# Print the dot-product result
print("Dot product of x and transposed y:")
print(result)Dot product of x and transposed y:
[[17. 23.]
[39. 53.]]The following code demonstrates the main functions for working with TensorFlow, including creating and manipulating tensors, elementwise operations, transposition, matrix multiplication, and interaction with NumPy. Note that in TensorFlow, the .numpy() method is used to obtain tensor values as a NumPy array:
# Step 1: Import the required libraries
# Import TensorFlow and NumPy
import tensorflow as tf
import numpy as np
# Step 2: Work with tensors in TensorFlow
# Tensors are the main data type in TensorFlow for storing and processing numerical data
# Create a tensor with uninitialized values of size 3x4
x = tf.Variable(tf.zeros((3, 4)))
print("Uninitialized tensor X:\n", x.numpy())
# Initialize another tensor with random values from a normal distribution
y = tf.random.normal((3, 4))
print("Tensor Y with random values:\n", y.numpy())
# Step 3: Elementwise operations
# Exponentiate all elements of tensor y
x_exp = tf.exp(y)
print("x_exp = e^y:\n", x_exp.numpy())
# Step 4: Transposition and matrix multiplication
# Transpose tensor y
y_transposed = tf.transpose(y)
print("Transposed Y:\n", y_transposed.numpy())
# Multiply x_exp by the transposed y
result_matrix = tf.matmul(x_exp, y_transposed)
print("Result of matrix multiplication of x_exp by transposed Y:\n", result_matrix.numpy())
# Step 5: Interaction with NumPy
# Convert tensors to NumPy arrays to perform NumPy-compatible operations
x_np = x_exp.numpy()
y_np_transposed = y_transposed.numpy()
# Perform a dot product with NumPy
np_result = np.dot(x_np, y_np_transposed)
print("Result of the dot product with NumPy:\n", np_result)Uninitialized tensor X:
[[0. 0. 0. 0.]
[0. 0. 0. 0.]
[0. 0. 0. 0.]]
Tensor Y with random values:
[[ 0.4931968 -0.31107786 1.0522738 -0.91028893]
[ 0.7813833 -0.3026102 -1.2939718 0.28406715]
[ 0.6851111 -1.1481444 -0.64575064 -0.14548281]]
x_exp = e^y:
[[1.6375427 0.73265684 2.864156 0.40240794]
[2.1844919 0.7388871 0.27417964 1.3285221 ]
[1.9839922 0.3172249 0.52426887 0.8646048 ]]
Transposed Y:
[[ 0.4931968 0.7813833 0.6851111 ]
[-0.31107786 -0.3026102 -1.1481444 ]
[ 1.0522738 -1.2939718 -0.64575064]
[-0.91028893 0.28406715 -0.14548281]]
Result of matrix multiplication of x_exp by transposed Y:
[[ 3.227286 -2.5339868 -1.6273712 ]
[-0.07329397 1.5059394 0.27794188]
[ 0.6444511 1.0214796 0.53070307]]
Result of the dot product with NumPy:
[[ 3.227286 -2.5339868 -1.6273712 ]
[-0.07329397 1.5059394 0.27794188]
[ 0.6444511 1.0214796 0.53070307]]2.14.10 In-Place Operations
In machine learning, we often need to process large amounts of data. To make this work efficient, we try to use operations that do not lead to unnecessary movement of data in computer memory. Such operations are called in-place operations.
import tensorflow as tf
# Create two variables in TensorFlow
x = tf.Variable([1, 2, 3], dtype=tf.float32)
y = tf.Variable([4, 5, 6], dtype=tf.float32)
# Print object identifiers before the operation
print('id(x) before the operation:', id(x))
print('id(y) before the operation:', id(y))
# A standard addition operation that creates a new tensor
temp_y = y + x # In TensorFlow this creates a new tensor
# If you want to keep using y as a variable, create a new variable with the result
y = tf.Variable(temp_y)
print('id(y) after the standard addition operation:', id(y))
# In-place operation using the assign_add method
y.assign_add(x) # This adds x to y in place without changing the identifier of y
print('id(y) after the in-place operation using assign_add:', id(y))
# In-place operation using the += operator
x.assign_add(y) # This adds y to x in place, using the += operator
print('id(x) after the in-place operation using the += operator:', id(x))id(x) before the operation: 6079315920
id(y) before the operation: 6069364304
id(y) after the standard addition operation: 6069363968
id(y) after the in-place operation using assign_add: 6069363968
id(x) after the in-place operation using the += operator: 60793159202.14.11 Working with Slices in Multidimensional Arrays in NumPy
Specific rows and columns can be read from a multidimensional array by using slices. Slices allow us to select subsets of an array for reading or modification.
import numpy as np # Import the NumPy library for working with arrays
# Create a two-dimensional floating-point array (matrix) for the example
x = np.array([[1.343, 3.358, 6.210, 6.438],
[7.653, -2.504, 12.811, 1.550],
[26.785, 0.124, 4.275, -2.182]])
# Print the original array
print("Original array x:")
print(x)
# Read the second and third rows from array x
print("\nSecond and third rows of array x:")
second_and_third_rows = x[1:3] # Slice from the second row (index 1) to the third row (index 2)
print(second_and_third_rows)
# Read the elements of the second row from the second to the third column
print("\nElements of the second row from the second to the third column:")
second_row_second_to_third_column = x[1:2, 1:3] # Slice for the second row and columns with indices 1 and 2
print(second_row_second_to_third_column)
# Change the value in the second row, third column to 88.0
x[1, 2] = 88.0 # Assign the value 88.0 to the element with index [1, 2]
# Print the array after the change
print("\nArray x after changing one value:")
print(x)
# Change the values in the second row from the second to the third column to 88.0
x[1, 1:3] = 88.0 # Slice for changing values in the second row and columns with indices 1 and 2
# Print the array after changing a slice of values
print("\nArray x after changing a slice of values:")
print(x)In this code listing, array x is used as an example to demonstrate slicing operations and changes to values in a NumPy array.
To extract the second and third rows of array x, use the following code:
print(x) # Print the entire array for clarity
x[1:3] # Obtain a slice for the second and third rowsIn this example, x[1:3] refers to the rows of array x with indices 1 and 2. It is important to remember that indexing in Python starts at zero and the end boundary of a slice is not included. That is, x[1:3] returns rows with indices 1 and 2, but not 3.
If we want to read the elements of the second row from the second to the third columns, we can do this as follows:
x[1:2, 1:3] # Select the second row and a slice of columns with indices from 1 to 2Here x[1:2, 1:3] creates a slice that selects the elements of the second row (index 1) and the columns with indices 1 and 2.
To change specific values of an array, for example, the value in the second row and third column to 88.0, execute the following:
x[1, 2] = 88.0 # Assign a new value to the element
print(x) # Print the modified arraySuch an assignment changes the specific array element without changing the other elements.
Finally, if we want to change several values at once, for example all values in the second row from the second to the third column, use:
x[1:2, 1:3] = 88.0 # Assign a new value to several elements at once
print(x) # Print the array with updated valuesThis causes the elements in the second row from the second to the third column to now have the value 88.0.
2.14.12 Broadcasting Operations
Let us explain in detail the concept of broadcasting operations in Python using the NumPy library. This process makes it possible to perform arithmetic operations on arrays that are initially not size-compatible.
NumPy arrays have an attribute called shape, which is a tuple showing the size of each dimension of the array. For example, the shape of array y, created with the function np.arange(6), is (6,), which means that it is a one-dimensional array with six elements.
When working with arrays of different shapes, we often have to bring them to a compatible shape before performing operations on them. The reshape method is used for this. In the example, array x is first created with np.arange(24) and has the shape (24,); then it is transformed into a two-dimensional array (4, 6) with x.reshape((4, 6)).
Now that the shapes of arrays x and y are compatible (x has the shape (4, 6) and y has the shape (6,)), broadcasting operations can be performed. NumPy automatically “stretches” array y along the first dimension, the dimension with index 0, so that its shape corresponds to the shape of x. As a result, y is repeated four times to match the four rows in x.
After broadcasting is applied, the addition operation x + y is performed elementwise: each element of y is added to the corresponding element of each row of x.
import numpy as np
# Create a one-dimensional array y
y = np.arange(6)
print('y = ', y, 'Shape of y:', y.shape)
# Create a one-dimensional array x and reshape it
x = np.arange(24).reshape((4, 6))
print('X = \n', x, 'Shape of X:', x.shape)
# Perform addition using broadcasting operations
result = x + y
print('X + y = \n', result)The result will be a two-dimensional array in which each column has been increased by the corresponding element of array y.
# Import the NumPy library under the alias np
import numpy as np
# Create a one-dimensional array y using the arange function, which generates a sequence of numbers from 0 to n-1
y = np.arange(6)
# Print array y and its shape
print('y = ', y, 'Shape of y:', y.shape)
# Create a one-dimensional array x of the same length as y, but with numbers from 0 to 23
x = np.arange(24)
# Change the shape of array x with the reshape method to obtain a two-dimensional array
# with 4 rows and 6 columns, which matches the shape of array y for broadcasting
x = x.reshape((4, 6))
# Print array x and its new shape
print('X = \n', x, 'Shape of X:', x.shape)
# Perform the addition of x and y. Thanks to broadcasting operations, y is automatically
# expanded to the shape of x, and the addition is performed elementwise.
result = x + y
# Print the addition result
print('X + y = \n', result)If you execute this code in Python, it will demonstrate how NumPy processes broadcasting operations by automatically “expanding” the smaller array for compatibility with the larger array during elementwise operations.
Multiplication of arrays and reshaping them:
# Import the NumPy library
import numpy as np
# Suppose arrays X and y have been defined earlier
# Print the shapes of arrays X and y
print('Shape of X:', X.shape, 'Shape of y:', y.shape)
# Compute the dot product of arrays X and y
# The np.dot() function returns the sum of the products of corresponding array elements
print(np.dot(X, y))
# Change the shape of array X to the three-dimensional array Z using the np.reshape() method
# The parameters (2, 3, 4) define the new array shape
Z = np.reshape(X, (2, 3, 4))
# Print the new three-dimensional array Z
print(Z)Dot product (np.dot()): The operation np.dot(X, y) is used to compute the dot product between X and y, which in mathematics and linear algebra means summing the products of the corresponding elements of two sequences of numbers.
Array reshaping (reshape): The reshape method allows the shape of an array to be changed without changing its data. In this example, array X is transformed from its original shape into a three-dimensional array Z with shape (2, 3, 4).
Operations for filling an array with one value and broadcasting-based addition of arrays of different shapes:
# Create a two-dimensional array a using arange and reshape
# The array is filled with consecutive numbers from 0 to 11 and reshaped to (3, 4)
a = np.arange(12).reshape(3, 4)
# Fill the entire array a with the value 100 using the fill method
a.fill(100)
# Print array a after filling
print(a)
# It is assumed that array Z was defined earlier as a three-dimensional array
# Adding arrays a and Z demonstrates broadcasting operations
# Array a is expanded to the shape of array Z, and elementwise addition is performed
print(Z + a)
# Create and fill a one-dimensional array a
a = np.arange(4)
a.fill(100)
# Print the one-dimensional array a
print(a)
# Add the one-dimensional array a to the three-dimensional array Z
# This shows NumPy's ability to perform broadcasting operations when arrays of different shapes
# can be added through automatic expansion of the smaller arrays
print(Z + a)Let us create a full Python code listing with detailed comments that demonstrates array operations using the NumPy library:
# Import the NumPy library
import numpy as np
# Create a one-dimensional array y with consecutive numbers from 0 to 5
y = np.arange(6)
# Print information about array y
print('y = ', y, 'Shape of y:', y.shape)
# Create a one-dimensional array x with consecutive numbers from 0 to 23
x = np.arange(24)
# Reshape array x into a two-dimensional array (4, 6)
x = x.reshape((4, 6))
# Print information about array x
print('x = ', x, 'Shape of x:', x.shape)
# Compute the dot product of arrays x and y using the np.dot function
# It is important to note that the arrays must have compatible dimensions for such an operation
dot_product = np.dot(x, y)
print('Dot product of x and y:', dot_product)
# Reshape array x into a three-dimensional array z (2, 3, 4)
z = x.reshape((2, 3, 4))
# Print the three-dimensional array z
print('z =\n', z)
# Create a two-dimensional array a with arange and reshape it to (3, 4)
a = np.arange(12).reshape((3, 4))
# Fill array a with the value 100
a.fill(100)
# Print the two-dimensional array a after filling
print('a =\n', a)
# Demonstrate broadcasting operations when adding array a and the three-dimensional array z
# Array a is automatically “expanded” to the shape of array z for elementwise addition
broadcasted_sum = z + a
# Print the addition result
print('Result of adding z and a:\n', broadcasted_sum)
# Create and fill a one-dimensional array a with the value 100
a = np.arange(4)
a.fill(100)
# Print the one-dimensional array a
print('a =', a)
# Add the one-dimensional array a to the three-dimensional array z
# NumPy automatically “expands” array a to perform the addition operation
broadcasted_sum = z + a
# Print the addition result
print('Result of adding z and a:\n', broadcasted_sum)2.14.13 Rules for Dimension Compatibility
When you plan to perform an operation between two arrays, NumPy must compare their shapes. This comparison starts from the last dimension, the rightmost one, and moves toward the first, the leftmost dimension. For two dimensions to be considered compatible, one of the following conditions must hold:
The dimensions are equal.
The dimension of one of the arrays in this dimension is equal to 1.
If at least one of these conditions is not satisfied, NumPy raises a ValueError indicating incompatible shapes.
To illustrate broadcasting operations, let us imagine several examples:

If we add a one-dimensional array to a two-dimensional array, NumPy “expands” the one-dimensional array by repeating it along each row of the two-dimensional array.
When adding two one-dimensional arrays of different lengths, if the length of one of the arrays is 1, NumPy “stretches” it along the length of the other array.
If a one-dimensional array consisting of one column is added to a two-dimensional array, NumPy “expands” the one-dimensional array by repeating it along each column of the two-dimensional array.
The figure clearly shows how NumPy implements broadcasting operations, demonstrating the original arrays, how the arrays are “expanded,” and the final result of the addition operation.
Applying these rules makes it possible to simplify many types of computations, especially when we need to perform an operation on arrays of different shapes. This is especially relevant when arrays can have different sizes and shapes.
Using broadcasting operations, we can reduce the need to write redundant code and loops, which makes programs more efficient and easier to understand.
In the following code, we will consider three examples showing different aspects of broadcasting operations in NumPy:
import numpy as np
# Example 1: Adding one-dimensional and two-dimensional arrays
# Create a one-dimensional array
one_d_array = np.array([1, 2, 3])
# Create a two-dimensional array
two_d_array = np.array([[0, 0, 0], [10, 10, 10], [20, 20, 20], [30, 30, 30]])
# Broadcasting operations allow these two arrays to be added despite the difference in their shapes
broadcasted_sum_1 = one_d_array + two_d_array
print("Example 1: Adding one-dimensional and two-dimensional arrays")
print(broadcasted_sum_1)
# Example 2: Adding two one-dimensional arrays of different lengths
# Create a one-dimensional array with one element
single_element_array = np.array([100])
# Broadcasting “expands” the smaller array so that it can be added to one_d_array
broadcasted_sum_2 = single_element_array + one_d_array
print("\nExample 2: Adding two one-dimensional arrays of different lengths")
print(broadcasted_sum_2)
# Example 3: Adding a one-dimensional array consisting of one column to a two-dimensional array
# Create a one-dimensional array representing a column
column_array = np.array([[0], [10], [20], [30]])
# Broadcasting “expands” the column horizontally so that addition can be performed
broadcasted_sum_3 = column_array + one_d_array
print("\nExample 3: Adding a one-dimensional array consisting of one column to a two-dimensional array")
print(broadcasted_sum_3)Figure: Adding arrays.
2.14.14 Conversion between TensorFlow and NumPy Arrays
# Import the NumPy library under the alias np for working with arrays
import numpy as np
# Import TensorFlow for working with tensors and deep learning
import tensorflow as tf
# Create a one-dimensional array of 24 numbers, starting with 0 and ending with 23
x = np.arange(24)
# Transform the one-dimensional array into a two-dimensional array (matrix) of size 4x6
x = x.reshape(4, 6)
# Print matrix x to the screen for checking
print("NumPy matrix x:")
print(x)
# Convert the NumPy array x into a TensorFlow tensor
tfx = tf.convert_to_tensor(x, dtype=tf.float32)
# Print tensor tfx to the screen for checking
print("\nTensorFlow tensor tfx:")
print(tfx)
# Convert the TensorFlow tensor back into a NumPy array
npa = tfx.numpy()
# Check that npa is indeed a NumPy array
print("\nArray npa converted back to NumPy:")
print(npa)
print("Type of npa:", type(npa))NumPy matrix x:
[[ 0 1 2 3 4 5]
[ 6 7 8 9 10 11]
[12 13 14 15 16 17]
[18 19 20 21 22 23]]
TensorFlow tensor tfx:
tf.Tensor(
[[ 0. 1. 2. 3. 4. 5.]
[ 6. 7. 8. 9. 10. 11.]
[12. 13. 14. 15. 16. 17.]
[18. 19. 20. 21. 22. 23.]], shape=(4, 6), dtype=float32)
Array npa converted back to NumPy:
[[ 0. 1. 2. 3. 4. 5.]
[ 6. 7. 8. 9. 10. 11.]
[12. 13. 14. 15. 16. 17.]
[18. 19. 20. 21. 22. 23.]]
Type of npa: <class 'numpy.ndarray'>This code listing demonstrates the process of creating an array in NumPy, converting it into a TensorFlow tensor, and converting it back into a NumPy array.
# Import the required libraries
import numpy as np
import tensorflow as tf
# Create a NumPy array with consecutive numbers from 0 to 23
npa = np.arange(24).reshape(4, 6) # Change the shape of the array to 4x6
# Print the original NumPy array
print("Original NumPy array:")
print(npa)
# Convert the NumPy array into a TensorFlow tensor
tfy = tf.convert_to_tensor(npa, dtype=tf.float32)
# Print the TensorFlow tensor
print("\nConverted into a TensorFlow tensor:")
print(tfy)
# Convert the TensorFlow tensor back into a NumPy array
npa_from_tfy = tfy.numpy()
# Print the NumPy array obtained from the TensorFlow tensor
print("\nConverted back into a NumPy array:")
print(npa_from_tfy)
# To check whether the original and resulting NumPy arrays match,
# you can use the np.array_equal() function
print("\nThe NumPy arrays are identical:", np.array_equal(npa, npa_from_tfy))Original NumPy array:
[[ 0 1 2 3 4 5]
[ 6 7 8 9 10 11]
[12 13 14 15 16 17]
[18 19 20 21 22 23]]
Converted into a TensorFlow tensor:
tf.Tensor(
[[ 0. 1. 2. 3. 4. 5.]
[ 6. 7. 8. 9. 10. 11.]
[12. 13. 14. 15. 16. 17.]
[18. 19. 20. 21. 22. 23.]], shape=(4, 6), dtype=float32)
Converted back into a NumPy array:
[[ 0. 1. 2. 3. 4. 5.]
[ 6. 7. 8. 9. 10. 11.]
[12. 13. 14. 15. 16. 17.]
[18. 19. 20. 21. 22. 23.]]
The NumPy arrays are identical: TrueThis code demonstrates how a NumPy array can be converted into a TensorFlow tensor, operations can be performed on it in the TensorFlow context, and then the tensor can be converted back into a NumPy array. This is useful when NumPy and TensorFlow operations need to be combined in a single workflow.
2.14.15 Extracting Subarrays in NumPy
Another feature of NumPy arrays is that they are easy to divide into subarrays.
print(bmi[bmi > 25]) # Print only those values whose BMI is greater than 25[ 25.556 25.117 25.051]This code demonstrates the ability to use Boolean indexing to filter array elements. Here bmi is a hypothetical NumPy array containing body-mass-index values for various individuals. The expression bmi > 25 creates a new array containing Boolean values (True or False) for each element of the original bmi array, where True corresponds to elements whose value is greater than 25. When this Boolean expression is used inside square brackets, NumPy returns only those elements of the original bmi array for which the corresponding value in the Boolean array is True, that is, all bmi values greater than 25.
import numpy as np # Import the NumPy library
# Create a bmi array with test data
# For demonstration, we will use random numbers from 20 to 30
bmi = np.random.uniform(20, 30, 10)
# Print the original bmi array
print("Original BMI array:", bmi)
# Apply Boolean indexing to filter values greater than 25
# This works by creating a Boolean array in which each element is checked against the condition > 25
# and then this Boolean array is used to select elements from the original array.
filtered_bmi = bmi[bmi > 25]
# Print the filtered values
print("Filtered BMI values > 25:", filtered_bmi)This code first imports NumPy, creates array bmi using random values for the example, and then uses Boolean indexing to filter and print the array values that are greater than 25.
import numpy as np # Import the NumPy library
# Create a bmi array with test data
# For demonstration, use an array with fixed values from 20 to 30
bmi = np.array([21.3, 24.5, 27.9, 30.1, 23.4, 25.6, 27.4, 22.0, 28.5, 26.1])
# Print the original bmi array
print("Original BMI array:", bmi)
# Apply Boolean indexing to filter values greater than 25
# A Boolean array is created in which each element is checked against the condition > 25
# This Boolean array is used to select elements from the original array
filtered_bmi = bmi[bmi > 25]
# Print the filtered values
print("Filtered BMI values > 25:", filtered_bmi)Original BMI array: [21.3 24.5 27.9 30.1 23.4 25.6 27.4 22. 28.5 26.1]
Filtered BMI values > 25: [27.9 30.1 25.6 27.4 28.5 26.1]2.14.16 NumPy and Universal Functions (ufunc)
Universal functions, or ufunc, are an element of the NumPy library designed to perform operations on the elements of multidimensional arrays element by element. These functions have several important characteristics:
Elementwise processing: ufunc functions are applied to each array element separately, which makes it possible to perform vectorized operations efficiently without explicitly using loops.
Broadcasting support: This means that ufunc functions can correctly process arrays of different sizes by expanding smaller arrays to match the shape of a larger array.
Type casting: ufunc functions support automatic casting of array-element data types to perform operations.
Standard functions: As a rule, ufunc functions implement standard mathematical and logical operations.
In NumPy, universal functions are instances of the numpy.ufunc class. Many built-in NumPy functions are implemented as ufunc functions and are written in the compiled C language, which provides high performance.
Types of ufunc
Basic ufunc: Work with scalar values.
Generalized ufunc: Work with subarrays, such as vectors, matrices, and so on, applying operations across different dimensions.
Examples of ufunc
exp(x, /[, out, where, casting, order, ...]): computes the exponential of all elements of the input array.
log(x, /[, out, where, casting, order, ...]): computes the natural logarithm of each array element elementwise.
NumPy provides the ability to create custom ufunc functions with frompyfunc. This makes it possible to wrap existing Python functions as universal functions, thereby expanding the capabilities of the library.
For a deeper study of universal functions and their use in NumPy, it is recommended to consult the NumPy documentation.
2.14.17 NumPy Arrays and Vectors/Matrices
NumPy arrays are the main building block of the NumPy library and are widely used in calculations. Understanding the differences between the traditional concepts of vectors and matrices in linear algebra and the behavior of NumPy arrays makes effective programming and code debugging possible.
Features of NumPy arrays
Shape flexibility: NumPy arrays can have any number of dimensions, unlike the strict one-dimensional vectors and two-dimensional matrices of linear algebra. This makes it possible to represent more complex data structures.
Type homogeneity: In a NumPy array, all elements must have the same type. This differs from data structures in other programming languages, where elements may have different types.
Operation vectorization: Operations on NumPy arrays are performed elementwise and can be vectorized to improve performance, which differs from mathematical operations on vectors and matrices.
Dimensionality and shape: The dimensionality of a NumPy array determines its shape. For example, a one-dimensional array resembles a vector, and a two-dimensional one resembles a matrix. However, in NumPy, these structures are treated in the same way as ndarray.
NumPy mainly uses an array structure. The behavior of a NumPy array is similar to, but at the same time significantly different from, the traditional notions of vector and matrix that we study in linear algebra. Let us analyze this in more detail.
Unlike simple one- and two-dimensional structures such as vectors and matrices, NumPy arrays can have up to 32 dimensions by default, and this number can be changed. This allows them to be used for complex tasks that require processing data with a large number of dimensions.
Because of their multidimensionality and flexibility, NumPy arrays are ideal for data processing in complex machine-learning models that require work with multidimensional datasets.
A one-dimensional NumPy array is similar to the usual concept of a vector in linear algebra. The difference is that a one-dimensional array does not distinguish between a row vector and a column vector. It is simply a one-dimensional array with shape (n,), where n is the length of the array. In most cases, it behaves like a row vector, but not exactly. For example, the transpose() function has no effect on it. This is because transpose() swaps two axes of an array with two or more axes.
In NumPy, a column vector in linear algebra should be regarded as a special case of a two-dimensional array with only one column. You can create an array similar to a column vector in linear algebra by adding an additional axis to a one-dimensional array. See the following examples.
To begin, let us consider creating a one-dimensional array, transposing it, and adding a new axis to transform it into a two-dimensional array in NumPy:
import numpy as np # Import the NumPy library
# Create a one-dimensional array with three elements
a = np.array([1, 2, 3])
# Print the array, its shape, the transposed array, and its shape
# For a one-dimensional array, transposition does not change the shape of the array
print("Original array:", a)
print("Shape of the original array:", a.shape)
print("Transposed array:", a.T)
print("Shape of the transposed array:", a.T.shape)
# Add a new axis to transform the array into a two-dimensional array
# This is done to create a column vector
an = a[:, np.newaxis]
# Print the new two-dimensional array and its shape
print("\nTwo-dimensional column vector:", an)
print("Shape of the column vector:", an.shape)
# Now we can transpose the two-dimensional array
# Transposition turns the column vector into a row vector
print("\nTransposed two-dimensional array (row vector):", an.T)
print("Shape of the row vector:", an.T.shape)Original array: [1 2 3]
Shape of the original array: (3,)
Transposed array: [1 2 3]
Shape of the transposed array: (3,)
Two-dimensional column vector: [[1]
[2]
[3]]
Shape of the column vector: (3, 1)
Transposed two-dimensional array (row vector): [[1 2 3]]
Shape of the row vector: (1, 3)In NumPy, an array is created with the function np.array(). A one-dimensional array is essentially a list of elements. In our example, array a is created with the values [1, 2, 3].
a = np.array([1, 2, 3])When we print array a, its shape, and the shape of its transposed version a.T, we see the following:
print(a, a.T, a.shape, a.T.shape)In the output, [1 2 3] is our original array, and (3,) is its shape, showing that the array contains 3 elements and is one-dimensional. In NumPy, one-dimensional arrays have no distinction between rows and columns, so transposition does not affect them, and the shape of a.T also remains (3,).
Now let us try to turn a one-dimensional array into a two-dimensional one by adding a new axis. This is done with slicing and np.newaxis. The slice : means that we take all elements in this dimension, while np.newaxis adds a new axis, making the array two-dimensional.
an = a[:, np.newaxis]Then we print the new array an and its shape:
print(an, an.shape)In the output, [[1] [2] [3]] shows our new two-dimensional array, and (3, 1) is its shape, showing that the array has 3 rows and 1 column, which makes it a column vector.
Now that we have a two-dimensional array, we can transpose it with .T. Transposition turns rows into columns and vice versa.
print(an.T, an.T.shape)In the output, [[1 2 3]] is the transposed array, where the original rows became columns. The shape (1, 3) indicates that we now have 1 row and 3 columns, which makes it a row vector.
To begin, let us consider how to create a two-dimensional array from a one-dimensional one. Suppose we have a one-dimensional array a with the values [1, 2, 3]. We want to transform it into a two-dimensional array in order to use operations intended for matrices.
import numpy as np
# Original one-dimensional array
a = np.array([1, 2, 3])
# Add a new dimension to the end of array `a`
a1 = a.reshape(a.shape + (1,))
print(a1, a1.shape) # Prints a two-dimensional array and its shape (3, 1)[[1]
[2]
[3]] (3, 1)In the code above, the reshape method changes the shape of the array by adding one dimension. We use a.shape + (1,) to add one more dimension to the current shape of the array (3,).
Now let us consider another way to add a dimension using the None object.
# Add a new dimension without specifying elements
aN = a[:, None]
print(aN, aN.shape) # Similar to the previous output: a two-dimensional array and its shape (3, 1)Here a[:, None] uses slicing syntax and None to tell NumPy to add a new dimension. This is equivalent to using np.newaxis, which we will consider next.
Dimensions can also be added not only to the end of an array, but also to its beginning.
import numpy as np
# Add a new axis to the beginning of array `a`
a0 = a[np.newaxis, :]
print(a0, a0.shape) # Prints a two-dimensional array in the form of a row vector (1, 3)We used np.newaxis to add a new dimension. In this case, we add the dimension at the beginning, so the shape of the array becomes (1, 3), which is a two-dimensional array with one row and three columns, a row vector.
# Import the NumPy library under the alias np
import numpy as np
# Create a one-dimensional array of three elements
a = np.array([1, 2, 3])
# Add a new dimension to the end of array `a` to turn it into a two-dimensional array
# This is done by changing the array shape with the reshape method
a1 = a.reshape(a.shape + (1,))
# Print the result and the shape of the resulting array
# Expected output: a two-dimensional array with three rows and one column
print("Adding a dimension to the end of the array:")
print(a1, a1.shape)
# Add a new dimension using the None object
# This is equivalent to adding a new axis to the end of the array
aN = a[:, None]
# Print the result and the shape of the resulting array
# Expected output: similar to the previous one, a two-dimensional array with three rows and one column
print("\nAdding a dimension using None:")
print(aN, aN.shape)
# Add a new axis to the beginning of array `a` with np.newaxis
# This creates a row vector
a0 = a[np.newaxis, :]
# Print the result and the shape of the resulting array
# Expected output: a two-dimensional array as a row vector with one row and three columns
print("\nAdding an axis to the beginning of the array:")
print(a0, a0.shape)Adding a dimension to the end of the array:
[[1]
[2]
[3]] (3, 1)
Adding a dimension using None:
[[1]
[2]
[3]] (3, 1)
Adding an axis to the beginning of the array:
[[1 2 3]] (1, 3)2.14.18 Hankel Matrix
A Hankel matrix is a matrix whose elements on the anti-diagonals, running from right to left, are constant. In this context, adding a column array and a row array gives a matrix where each element is the sum of the corresponding elements of the original one-dimensional array a.
# Import the NumPy library under the alias np
import numpy as np
# Create a one-dimensional array of three elements
a = np.array([1, 2, 3])
# Add a new dimension using the None object
aN = a[:, None]
print("\nAdding a dimension using None:")
print(aN, aN.shape)
# Add a new axis to the beginning of array `a` with np.newaxis
a0 = a[np.newaxis, :]
print("\nAdding an axis to the beginning of the array:")
print(a0, a0.shape)
# Apply a trick for creating a Hankel matrix by adding aN and a0
# Broadcasting is used here: a NumPy mechanism that allows arithmetic operations
# on arrays with different shapes. The result is a Hankel matrix.
print("\nAn interesting trick for creating a Hankel matrix:")
hankel_matrix = aN + a0
print(hankel_matrix)Adding a dimension using None:
[[1]
[2]
[3]] (3, 1)
Adding an axis to the beginning of the array:
[[1 2 3]] (1, 3)
An interesting trick for creating a Hankel matrix:
[[2 3 4]
[3 4 5]
[4 5 6]]Although two-dimensional NumPy arrays are similar to matrices in linear algebra, there are key differences that are important for understanding their use. Differences in multiplication operations are especially important: the dot product is performed with np.dot() or the @ operator, while the * operator is used for elementwise multiplication. Some operations can change the dimensionality of arrays, which requires special care and an understanding of the effect on data analysis. NumPy also has a matrix class that imitates the behavior of linear-algebra matrices, but its use is not recommended because it may become obsolete.
2.15 Plot Generation
Python has graph-creation capabilities because various visualization modules are available. As an example, let us consider creating a demonstration plot with scattered circles of different sizes and colors.
First, import the required modules:
import numpy as np # Module for working with arrays
import matplotlib.pyplot as plt # Module for creating plotsNext, generate a dataset:
n = 80 # Number of points
x = np.random.rand(n) # Randomly generated coordinates along the X axis
y = np.random.rand(n) # Randomly generated coordinates along the Y axis
colors = np.random.rand(n) # Randomly generated colors for each point
areas = np.pi * (18 * np.random.rand(n))**2 # Circle areas randomly generated in the radius range from 0 to 20Now, using the generated data, create a plot:

plt.scatter(x, y, s=areas, c=colors, alpha=0.8) # Create a plot with circles
plt.show() # Display the plotIn this plot, each point is a circle whose size and color are generated randomly. The alpha parameter controls the transparency of the circles, which adds visual depth when they overlap.
# Import the required libraries
import numpy as np # For working with arrays and performing mathematical operations
import matplotlib.pyplot as plt # For data visualization
# Generate data for the plot
n = 80 # Number of circles
x = np.random.rand(n) # Random X-axis coordinates for the centers of the circles
y = np.random.rand(n) # Random Y-axis coordinates for the centers of the circles
colors = np.random.rand(n) # Random colors for the circles
areas = np.pi * (18 * np.random.rand(n))**2 # Random radii for the circles, converted into areas
# Create the plot
plt.scatter(x, y, s=areas, c=colors, alpha=0.8) # Use the scatter function to draw circles
# where x and y are the coordinates of the centers of the circles, s is the circle size specified by areas,
# c is the circle color, and alpha is the transparency of the circles.
plt.show() # Display the plotFigure: Randomly generated circular areas filled with different colors.


import numpy as np
import matplotlib.pyplot as plt
# Import the NumPy library for working with arrays and matplotlib.pyplot for data visualization.
n = 80 # Number of points to generate
# Generate random coordinates for n points.
x = np.random.rand(n) # Random coordinates along the X axis
y = np.random.rand(n) # Random coordinates along the Y axis
# Generate random colors and sizes for the points.
colors = np.random.rand(n) # Random colors for each point
areas = np.pi * (18 * np.random.rand(n))**2 # Random point sizes, representing circle areas
# Create a scatter plot using the generated data.
plt.scatter(x, y, s=areas, c=colors, alpha=0.8) # 's' denotes size, 'c' denotes color, 'alpha' denotes point transparency
plt.show() # Display the scatter plot
# Generate data for plotting a curve.
x = range(1000) # Generate a sequence of numbers from 0 to 999
y = [i ** 2 for i in x] # Create a list of y values, where each value is the square of the corresponding x value
# Plot the function y = x^2
plt.plot(x, y) # 'plot' is used to draw a line graph
plt.show() # Display the graph
# Generate data for a histogram.
x = np.linspace(0, 1, 1000)**1.5 # Create 1000 points between 0 and 1 and raise the values to the power of 1.5 to distort the distribution
# Plot a histogram of the distribution of x values.
plt.hist(x) # 'hist' is used to draw a histogram
plt.show() # Display the histogram2.16 Evaluating Code Efficiency
Code performance can be evaluated in two ways. Typical code examples that readers can use to analyze the computational efficiency of their programs are given below. These examples use the time and numpy modules to demonstrate performance differences between standard Python operations and optimized operations implemented in the NumPy library.
import time
import numpy as np
# Create a large list of integers
g = list(range(10_000_000))
# Convert the list into a NumPy array with the float64 data type
q = np.array(g, 'float64')
# Measure the execution time of summation using the built-in Python sum function
start = time.process_time() # Record the start time
sg = sum(g) # Sum the list elements
t_elapsed = (time.process_time() - start) # Compute the elapsed time
print(f'Sum: {sg}, Elapsed time: {t_elapsed}')
# Measure the execution time of summation using the np.sum function for a NumPy array
start = time.process_time() # Record the start time again
sq = np.sum(q) # Sum the NumPy array elements
t_elapsed = (time.process_time() - start) # Compute the elapsed time
print(f'Sum: {sq}, Elapsed time: {t_elapsed}')
# Use the %%timeit magic command to automatically measure the execution time of code in a cell
# %%timeit
# sg = sum(g) # This line demonstrates how to use timeit in interactive environments such as JupyterSum: 49999995000000, Elapsed time: 0.030974000000000057
Sum: 49999995000000.0, Elapsed time: 0.006296999999999997In this code example, we first import the required modules, then create a large list of integers and convert it into a NumPy array. Next, we measure the time required to sum the elements of the list and the array using the standard sum function and the np.sum function, respectively. The results demonstrate the performance differences between standard Python operations and operations optimized in NumPy. The comments in the code help explain what each step does and how to interpret the output.
2.17 Conclusion
Having mastered the basic knowledge of the Python programming language, its modules, and the main programming techniques, we have reached the stage where we are ready to apply this knowledge to solve computational problems and master machine-learning techniques. This process will not only make it possible to implement complex computational tasks, but will also contribute to the gradual improvement of Python programming skills.
It is important to emphasize that practical experience and the constant application of acquired knowledge in real projects are key factors for deeply mastering the language and its capabilities. Regularly solving problems of varying complexity will help not only consolidate basic knowledge, but also become familiar with advanced programming techniques and approaches.
Check yourself
Which idea best describes the focus of "Python Basics"?
In machine learning, theoretical definitions are useful to check with numerical examples and visualizations.
Which actions help reinforce the chapter material?
Take quiz