Part 5

Дополнительные списки

Списки с данными разных типов

В предыдущей части мы в основном работали со списками, содержащими целые числа, но в списках можно хранить значения любых типов. Список строк может выглядеть так:

names = ["Marlyn", "Ruth", "Paul"]
print(names)
names.append("David")
print(names)

print("Number of names on the list:", len(names))
print("Names in alphabetical order:")
names.sort()
for name in names:
  print(name)
Пример вывода

['Marlyn', 'Ruth', 'Paul'] ['Marlyn', 'Ruth', 'Paul', 'David'] Number of names on the list: 4 Names in alphabetical order: David Marlyn Paul Ruth

Числа с плавающей точкой также могут быть элементами списка:

measurements = [-2.5, 1.1, 7.5, 14.6, 21.0, 19.2]

for measure in measurements:
    print(measure)

mean = sum(measurements) / len(measurements)

print("The mean is:", mean)
Пример вывода

-2.5 1.1 7.5 14.6 21.0 19.2 The mean is: 10.15

Напоминание: использование глобальных переменных внутри функций

Мы знаем, что внутри определений функций можно присваивать значения новым переменным, но функция также видит переменные, присвоенные вне нее, в главной функции. Такие переменные называются глобальными.

Использовать глобальные переменные внутри функций обычно плохая идея. Помимо прочего, это может приводить к ошибкам, которые трудно отследить.

Ниже пример функции, которая использует глобальную переменную "по ошибке":

def print_reversed(names: list):
    # using the global variable instead of the parameter by accident
    i = len(name_list) - 1
    while i >= 0:
        print(name_list[i])
        i -= 1

# here the global variable is assigned
name_list = ["Steve", "Jean", "Katherine", "Paul"]
print_reversed(name_list)
print()
print_reversed(["Huey", "Dewey", "Louie"])
Пример вывода

Paul Katherine Jean Steve

Paul Katherine Jean Steve

Хотя оба вызова функции получают аргумент правильного типа, функция всегда выводит то, что хранится в глобальной переменной name_list.

Чтобы не запутать ситуацию еще сильнее, помните, что весь код для тестирования ваших функций должен находиться в блоке if __name__ == "__main__":, чтобы автоматические тесты работали правильно. Предыдущий пример следует изменить:

def print_reversed(names: list):
    # using the global variable instead of the parameter by accident
    i = len(name_list) - 1
    while i>=0:
        print(name_list[i])
        i -= 1

# All the code for testing the function should be within this block
if __name__ == "__main__":
    # here the global variable is assigned
    name_list = ["Steve", "Jean", "Katherine", "Paul"]
    print_reversed(name_list)
    print()
    print_reversed(["Huey", "Dewey", "Louie"])

Обратите внимание, что теперь глобальная переменная присваивается внутри блока if.

Автоматические тесты в системе TMC выполняются без запуска кода внутри блока if. Поэтому в этом последнем примере функция даже теоретически не работала бы, так как она обращается к переменной name_list, которой вообще не существует во время выполнения тестов.

Предупреждение: перезапись параметра и слишком ранний возврат

Перед тем как перейти к упражнениям этой части, стоит рассмотреть пару новых источников ошибок. Посмотрим на функцию, которая сообщает, найдено ли целое число в списке. И список, и число определены как параметры функции:

def number_in_list(numbers: list, number: int):
    for number in numbers:
        if number == number:
            return True
        else:
            return False

Кажется, что эта функция всегда возвращает True. Причина в том, что цикл for перезаписывает значение, хранящееся в параметре number. Поэтому условие в операторе if всегда истинно.

Переименование параметра решает проблему:

def number_in_list(numbers: list, searched_number: int):
    for number in numbers:
        if number == searched_number:
            return True
        else:
            return False

Теперь условие в операторе if выглядит лучше. Но появляется новая проблема, потому что функция все еще работает неправильно. Следующий пример проявляет ошибку:

found = number_in_list([1, 2, 3, 4], 3)
print(found)  # prints out False

Проблема здесь в том, что функция возвращает результат слишком рано, не проверив все числа в списке. Фактически функция берет только первый элемент списка и возвращает True или False в зависимости от его значения. Мы не можем знать, что числа нет в списке, пока не проверим все элементы списка. Команду return False нужно разместить вне цикла for:

def number_in_list(numbers: list, searched_number: int):
    for number in numbers:
        if number == searched_number:
            return True

    return False

Рассмотрим еще одну ошибочную функцию:

def unique_numbers(numbers: list):
    # a helper variable to store all the numbers we've already checked
    numbers = []
    for number in numbers:
        # have we seen this number already?
        if number in numbers:
            return False
        numbers.append(number)

    return True

unique = unique_numbers([1, 2, 2])
print(unique)  # prints out True

Эта функция должна проверять, все ли числа в списке отличаются друг от друга, но она всегда возвращает True.

Здесь функция снова по ошибке перезаписывает значение, хранящееся в ее параметре. Функция пытается использовать переменную numbers для хранения уже проверенных чисел, но этим перезаписывает исходный список-аргумент. Простое исправление — переименовать вспомогательную переменную:

def unique_numbers(numbers: list):
    # a helper variable to store all the numbers we've already checked
    numbers_checked = []
    for number in numbers:
        # have we seen this number already?
        if number in numbers_checked:
            return False
        numbers_checked.append(number)

    return True

unique = unique_numbers([1, 2, 2])
print(unique)  # prints out False

Такие проблемы и многие другие можно находить и исправлять с помощью отладчика или инструмента визуализации. Трудно переоценить, насколько важно научиться эффективно ими пользоваться.

Loading

Списки внутри списков

Элементы списка сами могут быть списками:

my_list = [[5, 2, 3], [4, 1], [2, 2, 5, 1]]
print(my_list)
print(my_list[1])
print(my_list[1][0])
Пример вывода

[[5, 2, 3], [4, 1], [2, 2, 5, 1]] [4, 1] 4

Зачем могут быть полезны списки внутри списков?

Помните, что списки могут содержать элементы разных типов. Например, информацию о человеке можно хранить в списке. В качестве первого элемента можно указать имя, второго — возраст, третьего — рост в метрах:

["Anne", 12, 1.45]

Тогда база данных людей может быть списком, элементы которого являются списками с информацией об одном человеке:

persons = [["Betty", 10, 1.37], ["Peter", 7, 1.25], ["Emily", 32, 1.64], ["Alan", 39, 1.78]]

for person in persons:
  name = person[0]
  age = person[1]
  height = person[2]
  print(f"{name}: age {age} years, height {height} meters")
Пример вывода

Betty: age 10 years, height 1.37 meters Peter: age 7 years, height 1.25 meters Emily: age 32 years, height 1.64 meters Alan: age 39 years, height 1.78 meters

Цикл for проходит по элементам внешнего списка один за другим. То есть каждый список с информацией об одном человеке по очереди присваивается переменной person.

Списки не всегда лучший способ представлять данные, например информацию о человеке. Скоро мы познакомимся со словарями Python, которые часто лучше подходят для таких ситуаций.

Матрицы

Двумерный массив, или матрица, также естественно представлять списком внутри списка.

Например, следующую матрицу

5 1 1

можно представить в Python как двумерный список следующим образом:

my_matrix = [[1, 2, 3], [3, 2, 1], [4, 5, 6]]

Поскольку матрица — это список, содержащий списки, к отдельным элементам матрицы можно обращаться с помощью последовательных квадратных скобок. Первый индекс относится к строке, а второй — к столбцу. Индексация начинается с нуля, поэтому, например, my_matrix[0][1] обращается ко второму элементу первой строки.

my_matrix = [[1, 2, 3], [3, 2, 1], [4, 5, 6]]

print(my_matrix[0][1])
my_matrix[1][0] = 10
print(my_matrix)
Пример вывода

2 [[1, 2, 3], [10, 2, 1], [4, 5, 6]]

Как и любой другой список, строки матрицы можно обходить циклом for. Следующий код выводит каждую строку матрицы на отдельной строке:

my_matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

for row in my_matrix:
    print(row)
Пример вывода

[1, 2, 3] [4, 5, 6] [7, 8, 9]

Аналогично, вложенные циклы можно использовать для доступа к отдельным элементам. Следующий код выводит каждый элемент матрицы на отдельной строке с помощью двух циклов for:

my_matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

for row in my_matrix:
    print("a new row")
    for element in row:
        print(element)
Пример вывода

a new row 1 2 3 a new row 4 5 6 a new row 7 8 9

Визуализация кода со списками внутри списков

Программы со списками внутри списков поначалу могут казаться трудными для понимания. Инструмент визуализации от Python Tutor очень помогает понять, как они работают. Ниже показана визуализация примера выше:

5 1 2

Изображение выше показывает, что матрица 3 на 3 технически состоит из четырех списков. Первый список представляет всю матрицу. Остальные три списка являются элементами первого списка и представляют строки.

Поскольку многомерные списки можно обходить вложенными циклами, естественно думать, что сами списки тоже вложены, но изображение выше показывает, что это не совсем так. Вместо этого список, представляющий всю матрицу, "указывает" на каждый отдельный список, представляющий строку матрицы. Это называется ссылкой, и в следующем разделе эта идея будет рассмотрена подробнее.

На изображении выше выполнение дошло до второй строки матрицы, и именно на этот список сейчас ссылается переменная row. Переменная element содержит элемент, на котором сейчас находится выполнение. Значение, хранящееся в element, — это средний элемент списка, то есть 5.

Доступ к элементам матрицы

Получить доступ к одной строке матрицы просто — достаточно выбрать нужную строку. Следующая функция вычисляет сумму элементов выбранной строки:

def sum_of_row(my_matrix, row_no: int):
    # choose the desired row from within the matrix
    row = my_matrix[row_no]
    row_sum = 0
    for item in row:
        row_sum += item

    return row_sum

m = [[4, 2, 3, 2], [9, 1, 12, 11], [7, 8, 9, 5], [2, 9, 15, 1]]

my_sum = sum_of_row(m, 1)
print(my_sum) # prints out 33 (which equals 9 + 1 + 12 + 11)

Работать со столбцами матрицы немного сложнее, потому что матрица хранится по строкам:

def sum_of_column(my_matrix, column_no: int):
    # go through each row and select the item at the chosen position
    column_sum = 0
    for row in my_matrix:
        column_sum += row[column_no]

    return column_sum

m = [[4, 2, 3, 2], [9, 1, 12, 11], [7, 8, 9, 5], [2, 9, 15, 1]]

my_sum = sum_of_column(m, 2)
print(my_sum) # prints out 39 (which equals 3 + 12 + 9 + 15)

Обрабатываемый здесь столбец состоит из элементов с индексом 2 в каждой строке.

Инструмент визуализации определенно рекомендуется для понимания того, как работают эти функции.

Изменить значение отдельного элемента матрицы просто: выберите строку внутри матрицы, а затем столбец внутри строки:

def change_value(my_matrix, row_no: int, column_no: int, new_value: int):
    # choose the desired row
    row = my_matrix[row_no]
    # select the correct item within the row
    row[column_no] = new_value

m = [[4, 2, 3, 2], [9, 1, 12, 11], [7, 8, 9, 5], [2, 9, 15, 1]]

print(m)
change_value(m, 2, 3, 1000)
print(m)
Пример вывода

[[4, 2, 3, 2], [9, 1, 12, 11], [7, 8, 9, 5], [2, 9, 15, 1]] [[4, 2, 3, 2], [9, 1, 12, 11], [7, 8, 9, 1000], [2, 9, 15, 1]]

Обратите внимание, что выше мы использовали индексы строки и столбца, чтобы получить доступ к выбранному элементу. Если мы хотим изменить содержимое матрицы, нужно обращаться к элементам по индексам. Это означает, что мы не можем использовать простой цикл for item in list для обхода матрицы, если хотим изменить ее содержимое.

Вместо этого нужно отслеживать индексы элементов, например с помощью цикла while или цикла for с функцией range. Следующий код увеличивает значение каждого элемента матрицы на один:

m = [[1,2,3], [4,5,6], [7,8,9]]

for i in range(len(m)): # using the number of rows in the matrix
    for j in range(len(m[i])): # using the number of items on each row 
        m[i][j] += 1

print(m)
Пример вывода

[[2, 3, 4], [5, 6, 7], [8, 9, 10]]

Внешний цикл проходит по индексам от нуля до длины матрицы, то есть до количества строк в матрице. Внутренний цикл проходит по индексам от нуля до длины каждой строки внутри матрицы.

Loading

Двумерный массив как структура данных в игре

Матрица может быть очень полезной структурой данных во многих разных играх. Например, сетку судоку на изображении ниже

5 1 3

можно представить в виде матрицы так:

sudoku = [
  [9, 0, 0, 0, 8, 0, 3, 0, 0],
  [0, 0, 0, 2, 5, 0, 7, 0, 0],
  [0, 2, 0, 3, 0, 0, 0, 0, 4],
  [0, 9, 4, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 7, 3, 0, 5, 6, 0],
  [7, 0, 5, 0, 6, 0, 4, 0, 0],
  [0, 0, 7, 8, 0, 3, 9, 0, 0],
  [0, 0, 1, 0, 0, 0, 0, 0, 3],
  [3, 0, 0, 0, 0, 0, 0, 0, 2]
]

Здесь значение ноль представляет пустую клетку, так как ноль не является допустимым значением в завершенной головоломке судоку.

Вот простая функция для вывода сетки судоку выше:

def print_grid(sudoku):
    for row in sudoku:
        for square in row:
            if square > 0:
                print(f" {square}", end="")
            else:
                print(" _", end="")
        print()

print_grid(sudoku)

Вывод должен выглядеть так::


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

Любую распространенную игру с игровым полем можно моделировать похожим образом. Например, шахматы, "Сапер", "Морской бой" или Mastermind основаны на двумерной сетке. Для судоку естественно использовать числа для представления состояния игры, но для других игр могут лучше подходить другие методы.

Loading
Loading
Loading
Loading
Loading
Вы дошли до конца этого раздела! Перейти к следующему разделу:

Текущие баллы можно посмотреть в синем индикаторе в правом нижнем углу страницы.

В этой части:
  1. 1. Дополнительные списки

  2. 2. Ссылки

  3. 3. Словарь

  4. 4. Кортеж