§6.3. Работаем с данными

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

Массивы

В программах часто приходится работать с большим количеством однородных данных, например, с большим количеством чисел. Такие данные хранят в массивах. Задать массив можно, перечислив через запятую его элементы в квадратных скобках. Можно создать и пустой массив, не содержащий элементов: [].

In [1]: numbers = [20, 8, 3, 16]

Массивы, как и строки, можно конкатенировать (складывать).

In [2]: numbers = numbers + [40, 39]

In [3]: numbers
Out[3]: [20, 8, 3, 16, 40, 39]

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

In [4]: something = [20, "abc", 4.5, False]

К элементам массива можно обратиться по индексу — порядковому номеру (нумерация начинается с нуля). Таким же образом элемент массива можно изменить.

In [5]: numbers[0]
Out[5]: 20

In [6]: numbers[1] = numbers[1] + 345

Можно указать два номера через двоеточие. Значением такого выражения будет подмассив, причём начальный элемент (с номером слева от двоеточия) будет включён в него, а конечный (с номером справа от двоеточия) не будет. Если поставить двоеточие, но не указать номер, то подмассив будет начинаться от начала массива либо продолжаться до конца.

In [7]: numbers[2:4]
Out[7]: [3, 16]

In [8]: numbers[3:]
Out[8]: [16, 40, 39]

In [9]: numbers[:2]
Out[9]: [20, 353]

Объекты

Значение любого выражения в языке Python является объектом. Число, строка, массив, даже функция — всё это примеры объектов. Объект может быть сохранён в переменную, может быть передан аргументом в функцию, может быть элементом массива.

У объекта есть определённые методы. Это функции, которые не просто существуют, а относятся к конкретному объекту и вызываются для операций непосредственно с ним.

Строки и байты

Строки похожи на массивы в том, что к символам строки можно обращаться по индексам, как к элементам массива — и получать тоже строку. Но есть и различие: присвоить символ в строку по номеру не удастся.

In [10]: s = "abcdef"

In [11]: s[2]
Out[11]: 'c'

In [12]: s[3:5]
Out[12]: 'de'

In [13]: s[1] = "B"
# TypeError: 'str' object does not support item assignment

Помимо строк, состоящих из символов, существуют байтовые строки. Они ведут себя почти так же, как и обычные, однако при обращении по одиночному индексу результатом является не байтовая строка из одного символа, а число от 0 до 255. Байтовые строки задаются и обозначаются так же, как обычные, только перед кавычками вплотную ставится символ b: например, b"bytes". Если требуется задать байт, который не может быть представлен символами в коде, записывают его в шестнадцатеричном виде, прибавляя спереди \x: например, b"uvw\xffxyz".

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

Преобразовать байтовую строку в обычную можно методом .decode(), а обычную в байтовую — методом .encode(). Аргумент метода — строка, указывающая кодировку. Будем использовать кодировку "utf-8".

In [14]: s = "Луна"

In [15]: s.encode("utf-8")
Out[15]: b'\xd0\x9b\xd1\x83\xd0\xbd\xd0\xb0'

In [16]: bs = b'\xd0\xa1\xd0\xbe\xd0\xbb\xd0\xbd\xd1\x86\xd0\xb5'

In [17]: bs.decode("utf-8")
Out[17]: 'Солнце'

Форматные строки

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

In [18]: apples = 3

In [19]: oranges = 44

In [20]: f"Яблок: {apples}, апельсинов: {oranges}, итого: {apples + oranges}"
Out[20]: 'Яблок: 3, апельсинов: 44, итого: 47'

В фигурных скобках можно через двоеточие указать формат значения. Пригодятся форматы d — число, x — шестнадцатеричное число, X — шестнадцатеричное число с большими буквами. Перед числовым форматом можно указывать количество цифр, которое должно занять значение. Если перед этим количеством поставить ещё 0, то недостающие цифры будут заполнены нулями, иначе — пробелами.

In [21]: f"Код заказа: {apples:06d}-{oranges:06d}"
Out[21]: 'Код заказа: 000003-000044'

In [22]: f"Робот, смотри, апельсинов всего 0x{oranges:x}"
Out[22]: 'Робот, смотри, апельсинов всего 0x2c'

Циклы и условия

Вся мощь компьютеров в том, что они могут много раз повторять одинаковые действия, не ошибаясь (или, по крайней мере, ошибаясь всегда одинаково). Как правило, написание своей программы как раз преследует цель не решить какую-то сложную задачу, а повторить что-нибудь простое много раз. Конструкция, обеспечивающая выполнение одинаковых действий много раз, называется циклом; сами же повторяемые действия образуют тело цикла.

Самый простой способ создать цикл в языке Python — воспользоваться оператором for. В нём указывается название переменной цикла и объект, задающий значения, которые она будет принимать при каждом повторении тела цикла. Если это строка или массив, для переменной будут по очереди взяты все значения, которые там содержатся.

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

Вот пример: для каждого символа строки s — этот символ мы будем называть i — выведем слова Выводим символ и непосредственно сам символ i. Для четырёхсимвольной строки "test" тело цикла повторится четырежды, каждый раз для нового символа.

In [23]: s = "test"

In [24]: for i in s:
    ...:     print(f"Выводим символ {i}")
    ...:
Выводим символ t
Выводим символ e
Выводим символ s
Выводим символ t

Если нам требуется просто повторить какое-то действие известное количество раз, нужный объект можно создать функцией range(). Например:

In [25]: for i in range(3):
    ...:     print("Строка номер", i)
    ...:
Строка номер 0
Строка номер 1
Строка номер 2

Как видим, range(3) снабжает цикл числами 0, 1 и 2. Чтобы получить значения от 0 до какого-то n включительно, потребуется вызвать range(n + 1). Если мы хотим начинать не с нуля, функции нужно передать два аргумента, начальное число и конечное (которое, как и в случае с одним аргументом, в цикл не попадёт): например, range(5, 9) даст значения 5, 6, 7 и 8. Наконец, передача трёх аргументов позволит пройти запрошенный диапазон чисел с шагом, отличным от 1: например, range(30, 50, 5) даст числа 30, 35, 40 и 45.

В цикле мы можем захотеть по-разному обрабатывать разные значения, скажем, чётные и нечётные, или положительные и отрицательные. В таких случаях вводят условное ветвление операторами if — elif — else. После оператора if пишется условие — выражение, которое может быть истинным или ложным, например, i < 0. Дальше пишут двоеточие, а под ним, как и в случае цикла for, с отступом пишут действия, которые нужно исполнить при истинности условия. Если условие оказалось ложным, проверяется следующий блок elif и условие в нём; если не выполнилось и оно, то следующий, и так далее. Если ни одно условие не оказалось истинным, выполняются операторы блока else — в нём условие уже не указывается. Блоков elif может быть один или несколько или не быть вообще, блок else может присутствовать или отсутствовать.

Суть понятна, если посмотреть на пример:

In [26]: for i in range(-1, 3):
    ...:     print("Здравствуйте")
    ...:     if i < 0:
    ...:         print(f"{i} отрицательное")
    ...:     elif i == 0:
    ...:         print(f"{i} ноль")
    ...:         print("Смотрите, правда ноль")
    ...:     else:
    ...:         print(f"{i} положительное")
    ...:     print("До свидания")
    ...:     print("")
    ...:
Здравствуйте
-1 отрицательное
До свидания

Здравствуйте
0 ноль
Смотрите, правда ноль
До свидания

Здравствуйте
1 положительное
До свидания

Здравствуйте
2 положительное
До свидания

Условие не обязательно должно быть арифметическим. Так, с помощью оператора in можно проверять наличие элемента в массиве или подстроки в строке. Например, истинными будут выражения "human" in "humankind" и 7 in [1, 3, 5, 7, 9].

Несколько условий можно объединять с помощью операторов and и or, подобно тому, как мы это делаем в языке. Например, сразу понятно, что значит условие i > 0 and j < 0.

Файлы

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

Открыть файл можно функцией open(). Ей передаётся имя файла и режим — определённая строка, определяющая, что мы делаем с файлом. Файл можно открыть на чтение и на запись, причём последняя подразделяется на перезапись (когда существовавшее содержимое файла затирается) и дозапись (при которой новые данные добавляются к существовавшему содержимому). Открытие на чтение — режим "r" (англ. read — читать), на перезапись — "w" (англ. write — писать), на дозапись — "a" (англ. append — дополнять).

С содержимым файла можно работать как со строками или как с байтами, открывая его, соответственно, в текстовом или в бинарном режиме. Текстовым режимам "r", "w" и "a" соответствуют бинарные режимы "rb", "wb" и "ab". Режим можно не указывать — в таком случае будет подразумеваться текстовый режим чтения.

Открываемый на чтение файл должен существовать. При открытии на запись несуществующего файла он будет создан.

Объект, соответствующий открытому файлу, следует присвоить в переменную для дальнейшей работы с ним.

In [27]: f1 = open("/etc/passwd", "r")

In [28]: f2 = open("result.bin", "wb")

In [29]: f3 = open("download.bmp", "rb")

У объектов файлов можно вызывать методы .read() (в простейшем случае без аргументов) и .write() (с единственным аргументом — данными, которые мы намерены записать).

In [30]: bitmap = f3.read()

In [31]: f2.write(bitmap[:14])
Out[31]: 14

Метод .write() вернул число — количество байт, которые удалось записать в файл. Пока его можно смело игнорировать.

Открытый на чтение файл можно использовать напрямую в цикле for. Так файл будет обработан построчно: в переменную цикла будет попадать очередная строка файла, включая завершающий её символ перевода строки.

In [32]: f = open("/etc/passwd")

In [33]: for line in f:
   ...:     print(line)
   ...:
root:x:0:0:root:/root:/bin/bash

daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin

bin:x:2:2:bin:/bin:/usr/sbin/nologin

…

Пример: URL-кодирование

В §3.3 мы рассмотрели URL-кодирование строк. Напомним: английские буквы, цифры, а также символы -, _, . и ~ кодируются как есть, а остальные представляются в виде символа % и двух шестнадцатеричных цифр. Например, строка two+two=four превратится в two%2Btwo%3Dfour. Давайте напишем программу, кодирующую строку этим методом.

Пусть у нас есть строка.

s = "two+two=four, ура!"

Следующим действием нужно преобразовать её в байты, то есть закодировать. Явно укажем кодировку UTF-8.

s_bytes = s.encode("utf-8")

Заведём переменную для результата. Это строка, к которой мы будем добавлять закодированные символы. Поначалу эта строка пустая.

result = ""

Теперь нам нужно по очереди обработать каждый байт. Если в нём один из разрешённых символов, то добавим его к результату. Иначе же к результату нужно добавить символ % и шестандцатеричную запись байта. Буквы в такой записи должны быть большими, сама она должна быть длиной 2 символа, и при необходимости первым из них должен быть ноль — из этих требований составляем форматную строку f"%{b:02X}".

for b in s_bytes:
    if b in b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_.~":
        result = result + chr(b)
    else:
        result = result + f"%{b:02X}"

С полученной строкой result можно что-то сделать — например, вывести функцией print().

Вот так может выглядеть программа целиком:

s = "two+two=four, ура!"
s_bytes = s.encode("utf-8")
result = ""
for b in s_bytes:
    if b in b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_.~":
        result = result + chr(b)
    else:
        result = result + f"%{b:02X}"
print(result)

Поскольку URL-кодирование строки — операция, в принципе, полезная, которая может пригодиться не раз, мы можем захотеть оформить наш код в функцию. Тогда переменная s будет её аргументом, а result — возвращаемым значением:

def url_encode(s):
    s_bytes = s.encode("utf-8")
    result = ""
    for b in s_bytes:
        if b in b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_.~":
            result = result + chr(b)
        else:
            result = result + f"%{b:02X}"
    return result

Теперь мы можем ей пользоваться:

In [34]: url_encode("two+two=four, ура!")
Out[34]: 'two%2Btwo%3Dfour%2C%20%D1%83%D1%80%D0%B0%21'

В процессе написания такой функции нам могла прийти мысль, что URL-кодирование известно не первый день, в мире широко используется, и, наверное, такую функцию уже написали опытные программисты, другие за ними проверили, выявили и исправили ошибки, и проще и надёжнее взять эту готовую функцию, а не пытаться писать свою. Мысль совершенно правильная.

Язык Python обладает обширным набором встроенной функциональности. Если у вас возникает типовая подзадача, такая, как URL-кодирование строки, нужно поискать стандартную функцию. Наличие большого количества таких функций — важное преимущество языка и популярная причина выбирать именно его.

Поиском в интернете выясняем, что нужная нам функция содержится в библиотеке urllib.parse и называется quote(). Чтобы пользоваться функциями библиотеки, её следует импортировать, написав слово import и название библиотеки: import urllib.parse. Дальше мы можем пользоваться функцией из библиотеки так же, как и обычной или своей собственной. Однако перед названием функции придётся указывать название библиотеки, отделив его точкой.

In [35]: import urllib.parse

In [36]: urllib.parse.quote("two+two=four, ура!")
Out[36]: 'two%2Btwo%3Dfour%2C%20%D1%83%D1%80%D0%B0%21'

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

Бывает так, что нужной вам функциональности нет в Python по умолчанию, однако существует библиотека, которая делает то, что нужно. Чтобы воспользоваться такой библиотекой, её нужно установить. Например, так, если вам понадобилась библиотека requests:

pip3 install requests

После этого import этой библиотеки станет работать так же, как и в случае стандартных.

In [37]: import requests

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

Выводы

  1. Существуют массивы, состоящие из нумерованных элементов.

  2. Различаются строки, состоящие из символов, и байтовые строки из байт. Преобразовывать их друг в друга можно методами .encode() и .decode().

  3. Все хранимые в переменных сущности являются объектами. У объектов есть методы, которые можно вызывать как функции.

  4. Можно открывать файлы, читать из них данные и записывать их туда.

  5. Циклы нужны для многократного выполнения однотипных действий. Цикл for выполняет указанные действия для каждого значения из указанного набора (массива, строки, файла или объекта range).

  6. Python содержит большое количество библиотек, решающих множество типовых задач. Через pip можно установить ещё больше библиотек.

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

§6.4. Скачиваем интернет ⟶