Введение в Python и обработку данных

Спецкурс кафедры Теоретической информатики мехмата МГУ

Подробная информация по курсу: http://rmbk.me/mm_python/

Лекция 3: контейнеры данных

Модель данных в Python

По ноутбуку PythonCourse_First.ipynb.

Список / List

Во многих задачах появляется необходимость работать не с единичными переменными, а с их набором.

Список в Python это массив, который может содержать элементы любого типа, его размер может динамически меняться.

https://docs.python.org/3/tutorial/datastructures.html#more-on-lists

In [ ]:
# help(list)

# Две эквивалентные записи для создания пустого списка
a = list()
a = []

# Можно создавать сразу заполненный список
a = [0, 1, 2]

# Можно добавлять элементы в конец списка; О(1)
a.append(3)

# Можно добавлять элементы другого списка в конец списка; О(k), где k - длина второго списка
a.extend([3, 5])

# Можно обращаться к любому элементу списка и менять его (!); О(1)
a[4] = 4
print(f'a[2] = {a[2]}')
print(f'a[-1] = {a[-1]}')
print(f'Наш список выглядит так: {a}')

# Можно посчитать количество вхождений (как у строки); O(n), где n - длина списка
a.count(5)
print(f'Количество чисел 5 в нем: {a.count(5)}')

# Можно узнать индекс элемента; O(n)
a.index(4)
print(f'Число 4 стоит на {a.index(4)} месте')

# Можно на указанное место вставлять новый элемент; O(n)
a.insert(5, 33)

# Можно отсортировать список; O(n * log n)
a.sort()

# Можно отсортировать список в обратном порядке
a.sort(reverse=True)
print(f'Наш список в обратном порядке: {a}')

# Длина списка высчитывается через len
print(f'Длина списка: {len(a)}')

# Списки можно складывать
print(f'Сложенные списки дают один список: {[3, 4, 5] + [6, 7, 8]}')

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

Увеличение размера выделенной памяти происходит блоками, чтобы резервировать место на будущее, по следующей формуле: new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6);. И получается такой шаблон 0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ..., т.е. в первый раз выделяется память на 4 элемента, потом на 8 и т.д.

Кортеж / Tuple

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

https://docs.python.org/3/tutorial/datastructures.html#tuples-and-sequences

In [ ]:
# help(tuple)

# Пустой кортеж
t = tuple()
t = ()

# Заполненный кортеж
t = (0, 1, 2, 3, 4)

# Можно обращаться к любому элементу списка, но не менять (!)
print(f't[2] = {t[2]}')
print(f't[-1] = {t[-1]}')

# Имеет два метода, аналогичных тем, что у списков
print(f'Количество троек в кортеже: {t.count(3)}')
print(f'Двойка стоит на {t.index(2)} месте')

# Длина кортежа высчитывается через len
print(f'Длина кортежа: {len(t)}')

# Кортежи можно тоже складывать, результатом будет новый кортеж
print(f'Сложение кортежей: {(1, 2) + (3, 4, 5)}')

# Кортеж из одного элемента должен всё равно записываться с запятой
# Иначе интерпретатор примет его как обычные скобки
t = (1)    # Это обычный int в скобочках
t = (1, )  # Это кортеж из одного int'а

Кортежи в функциях

Часто кортежи используются для возвращения нескольких элементов из функции

In [ ]:
# Задача: найти середину отрезка AB на плоскости
def center(a, b):
    x = (a[0] + b[0]) / 2
    y = (a[1] + b[1]) / 2
    # Обратите внимание, здесь возвращаем два элемента сразу
    # Скобки в данном случае можно не ставить
    return x, y

point_a = (0, 0)
point_b = (4, 2)

point_c = center(point_a, point_b)
print(f'Тип ответа функции: {type(point_c)}')
print(f'Точка С: {point_c}')

# Кортежи можно разбирать на части, если мы гарантированно знаем количество элементов в нем
x_part, y_part = center(point_a, point_b)
print(f'Координата Х: {x_part}, координата Y: {y_part}')

Циклы по контейнерам

Рассмотрим цикл for подробнее. В нём указывается переменная и множество значений, которые она должна принять. Это множество значений может быть задано и генераторами, и контейнерами, к примеру строками, списками, кортежами.

Вспомним синтаксис такого цикла:

for переменная in последовательность:
    тело_цикла
In [ ]:
# Цикл по кортежу
# Скобки кортежа в первом случае можно не указывать, интерпретатор поймёт по запятым

for i in 1, 2, 3:
    print(i)
    
a = (4, 5, 6)

for i in a:
    print(i)
In [ ]:
# Цикл по списку: всё ровно то же
for i in [1, 2, 3]:
    print(i)

a = [4, 5, 6]

for i in a:
    print(i)
In [ ]:
# Цикл по строке: строка также является контейнером
for symbol in 'Hello':
    print(symbol)

Настройка print

К слову, функцию print можно настраивать через аргументы:

  • sep — это сепаратор, разделитель; ставится, если аргументов несколько, по умолчанию равен пробелу
  • end — это окончание; ставится в конце выполнения функции print, по умолчанию равен \n — перевод на новую строку
In [ ]:
for i in range(5):
    print(i, i * i, sep=' --- ')

print()  # Пустой print просто добавляет пустую строку

for symbol in 'Hello':
    print(symbol, end=', ')

Enumerate

Чтобы двигаться по контейнеру с автоматическим счётчиком используется генератор enumerate. Он итеративно возвращает tuple из двух элементов: счетчик и элемент контейнера.

In [ ]:
a = 'Hello'
for i, symbol in enumerate(a):
    print(f'{symbol} стоит на {i} месте')

print()
    
for i, symbol in enumerate(a, start=1):
    print(f'{symbol} стоит на {i} месте, если считать с 1')

Методы split и join

Методы split и join вызываются от строк и тесно связаны со списками:

  • split разрезает строку по указанному символу (по умолчанию по пробелу) и возвращает список "кусочков"
  • join, наоборот, позволяет собрать список в одну строку с указанным разделителем

Лучше сразу на примерах:

In [ ]:
a = 'London is the capital of Great Britain'
print(a.split())

b = '127.0.0.1'
print(b.split('.'))
In [ ]:
a = ['Moscow', 'State', 'University']

print(''.join(a))
print()
print(' '.join(a))
print()
print('\n'.join(a))  # `\n` -- это символ перевода на новую строку
print()
print(' ~ '.join(a))

Генератор списка / List comprehension

Генератор списка, как понятно из названия, создает список при этом позволяет в одну строку делать простые операции.

Синтаксис этой операции читается почти как на английском языке и легко запоминается:

[выражение for переменная in последовательность]
# или с условием
[выражение for переменная in последовательность if условие]

И сразу к примерам:

In [ ]:
# Читается как: Создай список из i, где i бери от 0 до 9
a = [i for i in range(10)]  
print(a)

# Читается как: Создай список из квадратов i, где i бери от 0 до 9
a = [i * i for i in range(10)]
print(a)

# Читается как: Создай список из кубов i, где i бери от 0 до 9, и i должен быть нечетным
a = [i * i * i for i in range(10) if i % 2 == 1]
print(a)

string = '1 2 3 4 5'
# Читается как: Создай список из строк с числами с приведением их к целому числу
a = [int(symbol) for symbol in string.split()]
print(a)

Срез / Slice

К последовательным контейнерам (строки, списки и кортежи, к примеру) можно применять т.н. срезы.

Они позволяют получить последовательную часть объекта.

a наш контейнер, а i, j, k — некоторые числа:

  • a[i:j] — срез из j - i элементов: a[i], a[i + 1], ..., a[j - 1]
  • a[i:j:k] — срез с шагом k: a[i], a[i + k], a[i + 2 * k], ...
  • число k может быть отрицательным, тогда элементы будут идти в обратном порядке
  • числа i и j могут быть опущены, тогда на их места будут подставлены 0 и -1 соответственно, т.е. начало и конец

Полезное знание: с помощью среза можно развернуть контейнер в обратном порядке — a[::-1].

Посмотрим на примеры:

In [ ]:
def slices(a):
    print(f'Дан контейнер: {a}')
    print(f'Элементы с 1 по 3: {a[1:3]}')
    print(f'Элементы с 1 по 5 с шагом 2: {a[1:6:2]}')
    print(f'Элементы с 5 по последний: {a[5:]}')
    print(f'Обратный порядок: {a[::-1]}')
    print(f'Обратный порядок c шагом 2: {a[::-2]}')
    print()
    
x = [0, 1, 2, 3, 4, 5, 6, 7]
slices(x)

y = 'Hello World!'
slices(y)

Домашнее задание №3

  • (*) C1: Пользователь задаёт список чисел, выведите все элементы списка, которые больше предыдущего элемента
  • (*) C2: Посчитать сумму и произведение цифр положительного числа, заданного пользователем
  • (*) C3: Определить длину самого короткого слова в строке, заданной пользователем
  • (*) C4: Пользователь задаёт числа a, b, c из уравнения $ax^2 + bx + c = 0$, вывести корни уравнения или сообщить, что их нет
  • (**) C5: Задача FIB на Rosalind
  • (**) C6: Задача SUBS на Rosalind