Python: cтруктуры данных и функции, работа с файлами, генераторы и декораторы

Структуры данных, функции и работа с файлами

Коллекции

  1. Списки и кортежи
  2. Списки. Пример программы
  3. Словари
  4. Словари. Пример программы
  5. Множества
  6. Множества. Пример программы

Функции

  1. Файлы
  2. Функциональное программирование
  3. Декораторы
  4. Генераторы

Коллекции

    Списки и кортежи

Коллекция — это переменная-контейнер, в которой может содержаться какое-то количество объектов, где объекты могут быть одного типа или разного. В случае списков это упорядоченные наборы элементов, которые могут быть разных типов. Сами списки определяются с помощью квадратных скобочек или с помощью вызова литерала list. Вы также можете создать список из одинаковых значений с помощью умножения. Несмотря на вышесказанное, чаще всего списки содержат переменные одного типа. Также списки могут содержать другие коллекции, как, например, user_data. Однако для таких данных чаще всего используются кортежи, о которых будет сказано позже.

Для получения длины списка вызывают встроенную функцию len(). В Python не нужно явно указывать размер списка или вручную выделять на него память. Размер списка хранится в структуре, с помощью которой реализован тип список, поэтому длина вычисляется за константное время.

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

Можно использовать доступ по индексу для присваивания (изменения элементов):

Обращение к несуществующему индексу приводит к ошибке IndexError: list index out of range.

С помощью оператора in можно проверить, существует ли какой-то объект в списке:

Срезы в списках работают точно так же, как и в строках. Создадим список из 10 элементов с помощью встроенной функции range и поэкспериментируем на нём со срезами:

Важно знать, что при получении среза создаётся новый объект — новый список:

Как и все коллекции, списки поддерживают протокол итерации — мы можем итерироваться по элементам списка, использую цикл for.

Обратите внимание, что итерация производится именно по элементам списка, а не по индексам, как во многих других языках.

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

Так как списки являются изменяемой структурой данных, мы можем добавлять и удалять элементы. Например, мы можем добавить в наш список collections элемент ‘OrderedDict’.

Если вам нужно расширить список другим списком, вы можете использовать метод extend, который добавляет переданный список в конец вашего списка.

Также можно использовать перегруженный оператор +, который также добавляет переменную в конец вашего списка:

Для удаление элемента из списка можно использовать ключевое слово del.

Часто нам нужно найти минимальный/максимальный элемент в массиве или посчитать сумму всех элементов. Вы можете это сделать при помощи встроенных функций min, max, sum.

Часто бывает полезно преобразовать список в строку, для этого можно использовать метод str.join():

Ещё одна часто встречающаяся операция со списками — это сортировка. В Python существует несколько методов сортировки.

Для начала создадим случайный список с помощью функции модуля random:

Для сортировки списка в Python есть два способа: стандартная функция sorted, которая возвращает новый список, полученный сортировкой исходного, и метод списка .sort(), который сортирует in-place. Для сортировки используется алгоритм TimSort.

Если нужно отсортировать список в обратном порядке:

Для той же цели можно использовать встроенную функцию reversed, которая возвращает так называемый reverse iterator. Об итераторах будет сказано позднее, пока достаточно понимать, что это объект, который поддерживает протокол итерации. Данный объект можно преобразовать в список, и получится список с обратным порядком элементов.

Кроме методов, которые мы обсудили выше, существует также много других, о которых можно прочесть в документации:

•   append

•   clear

•   copy

•   count

•   extend

•   index

•   insert

•   pop

•   remove

•   reverse

•   sort

Перейдём к кортежам. Кортежи — это неизменяемые списки (мы не можем ни добавлять, ни удалять элементы из кортежа). Кортежи определяются с помощью круглых скобок или литерала tuple.

Например, мы можем создать кортеж immutables и поместить туда неизменяемые типы.

Если попробовать заменить нулевой элемент на float, Python выдаст ошибку, потому что кортежи неизменяемы.

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

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

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

    Списки. Пример программы

Разберём задачу на применение списков — поиск медианы случайного списка. Медиана — это значение в отсортированном списке, которое лежит ровно посередине, таким образом, половина значений — слева от него, и половина значений — справа.

Сначала создадим случайный список со случайным (чтобы было интереснее) количеством элементов.

Отсортируем наш список:

По определению медианы, она равна среднему элементу в отсортированном списке, если количество элементов нечётное. Если число элементов чётное, то медиана — это среднее арифметическое от двух средних элементов. Мы заведем переменную half_size, в которую положим значение, равное половине длины списка. Также заведём переменную median, сначала имеющую значение None.

Теперь запишем условие на чётность элементов и найдём медиану по определению для каждого случая:

Посмотрим, что получилось в итоге:

Чтобы проверить наш результат, можно воспользоваться встроенным модулем statistics.

    Словари

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

Если пытаться получить доступ по ключу, которого не существует, Python выдаст ошибку KeyError. Однако, часто бывает полезно попытаться достать значение по ключу из словаря, а в случае отсутствия ключа вернуть какое-то стандартное значение. Для этого есть встроенный метод get.Доступ к значению по ключу осуществляется за константное время, то есть не зависит от размера словаря. Это достигается с помощью алгоритма хеширования.

Проверка на вхождения ключа в словарь так же осуществляется за константное время и выполняется с помощью ключевого слова in:

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

Также, чтобы добавить какой-то ключ-значение в словарь, можно использовать встроенный метод update, который принимает словарь и дополняет им (а также обновляет в случае одинаковых ключей) исходный словарь.

Чтобы удалить ключ-значение из словаря и одновременно вернуть значение, используют метод pop:

Часто бывает необходимо не только попробовать проверить, существует ли ключ в словаре, но и в случае неудачи добавить эту новую пару ключ-значение. Для этого есть метод setdefault:

Если вызвать setdefault и в качестве дефолтного значения передать new_default, вернётся значение, которое уже лежит в словаре — значение default:

Словари, как и все коллекции, поддерживают протокол итерации. С помощью цикла for можно итерироваться по ключам словаря:

Если нам нужно итерироваться не по ключам, а по ключам и значениям сразу, можно использовать метод словаря items, который возвращает ключи и значения.

Если нужно итерироваться по значениям, используйте логично метод values, который возвращает именно значения. Также существует симметричный метод keys, который возвращает итератор ключей.

Важная особенность словарей в Python-е: они содержат ключи и значения в неупорядоченном виде. Однако, в Python-е существует тип OrderedDict (содержится в модуле collections), который гарантирует вам, что ключи хранятся именно в том порядке, в каком вы их добавили в словарь.

    Словари. Пример программы

Разберём следующую задачу на словари: найти 3 самых часто встречающихся слова в Zen of Python.

Скопируем этот текст и поместим его в переменную zen. После этого заведём переменную zen_map, в которой будем хранить слова, которые уже нашли, и то, сколько раз их уже нашли. Будем итерироваться с помошью метода split(), который разобьёт нашу строку по пробельным символам. Очищать слова от знаков препинания и пробельных символов будем с помощью метода strip().

На выходе имеем словарь, в котором ключами являются слова, а значениями — сколько раз слова встретились в тексте. Теперь найдём самые частотные слова. В переменную zen_items поместим список кортежей (ключ, значение) с помощью метода items(). Затем отсортируем список по вторым элементам в кортеже, используя модуль operator. В метод sorted() в качестве аргумента key передадим operator.itemgetter(1) (т.к. мы сортируем по элементам с индексом 1).

Как это часто бывает в Python-е, существует встроенный модуль, который поможет вам решить эту задачу намного быстрее. Импортируем Counter из модуля collections. Теперь осталось только «очистить» слова и передать их в Counter.

     Множества

Множество в питоне — это неупорядоченный набор уникальных объектов. Множества изменяемы и чаще всего используются для удаления дубликатов и всевозможных проверок на вхождение.

Чтобы объявить пустое множество, можно воспользоваться литералом set или использовать фигурные скобки, чтобы объявить множество и одновременно добавить туда какие-то элементы.

Чтобы проверить, содержится ли объект в множестве, используется уже знакомое нам ключевое слово in. Проверка выполняется за константное время, время выполнения операции не зависит от размера множества. Это достигается за счёт хэширования каждого элемента структуры по аналогии со словарями. По полученному от хэш-функции ключу и происходит поиск объекта. Таким образом, во множествах могут содержаться только хэшируемые объекты.

Чтобы добавить элемент в множество, используется метод add. Также множества в Python поддерживают стандартные операции над множествами — такие как объединение, разность, пересечение и симметрическая разность.

Создадим два множества с чётными и нечётными числами до десяти:

Теперь найдём объединение и пересечение этих множеств:

Найдём разность двух множеств:

Или симметрическую разность:

Множества — изменяемая структура данных, поэтому можно как добавлять туда элементы, так и удалять. Для удаления конкретного элемента существует метод remove, для удаления любого элемента можно использовать pop. Остальные методы можно посмотреть в help или документации.

Также в питоне существует неизменяемый аналог типа set — тип frozenset.

 

    Множества. Пример программы

Задача на множества: через сколько итераций функция random.randint(1, 10) выдаст повтор?

Будем добавлять неповторяющиеся случайные числа в множество random_set. Если очередное число уже есть в random_set — выйдем из цикла. Затем посчитаем длину множества (и прибавим 1, т.к. не учли последнее число).

Таким образом, мы получили повтор через 6 итераций.

Функции

    Функции

Функция — это блок кода, который можно использовать несколько раз в разных местах программы. Мы можем передавать функции аргументы и получать возвращаемые значения. Чтобы определить функцию в языке Python, нужно использовать литерал def и с помощью отступа определить блок кода функции. По PEP8 функции называют snake_case-ом.

Объявим функцию, которая возвращает секундную часть текущего времени.

Чтобы получить документационную строку, можно обратиться к атрибуту doc, а имя функции получается с помощью атрибута name.

Чаще всего функция определяется с параметрами, т.к. зачастую функции каким-то образом обрабатывают переданные им значения. Определим функцию split_tags, которая принимает параметр tag_string (например, равный строке с тегами текущего курса). Пусть функция разобьёт эту строку по запятым и вернёт список тегов.

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

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

Во многих других языках программирования значения параметра передаются в функцию либо по ссылке, либо по значению (и между двумя этими случаями проводится строгая граница). В Python-е каждая переменная является связью имени с объектом в памяти, и именно эта ссылка на объект передается в функцию. Таким образом, если мы передадим в функцию список и в ходе выполнения функции изменим его, этот список измениться глобально:

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

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

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

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

В Python-е всё же есть возможность изменять глобальные переменные с помощью global или non local, но использовать эти особенности не рекомендуется.

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

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

Что произойдёт, если мы вызовем эту функцию дважды:

Чтобы разобраться, проверим, каковы дефолтные значения параметров функции:

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

Чтобы исправить предыдущий пример, возьмём в качестве значения по умолчанию None:

Довольно красивой особенностью Python-а является возможность определения функции, которая принимает разные количества аргументов. Определим функцию printer, которая принимает любое количество аргументов — все аргументы записываются в tuple args. Затем функция печатает по порядку все аргументы:

Также в аргументах можно развернуть список значений:

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

Точно так же мы можем разыменовывать (разворачивать) словари, используя **:

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

    Файлы

Для открытия файлов используется встроенный метод open, которой передаётся путь к файлу — например, filename. Функция open возвращает файловый объект, с которым мы потом можем работать, для того чтобы записывать данные или читать данные из файлов.

Файлы можно открывать по-разному — на запись, на чтение, на чтение и запись, на дозапись. Делается это с помощью модов, которые также передаются в функцию open. Например, a — это дозапись, w — это, очевидно, запись, r — это прочтение, r+ — это запись и чтение одновременно. Точно так же можно открывать файл в бинарном виде, то есть работать с бинарными данными — для этого к моду добавляют букву b.

Чтобы записать в файл, применяем к соответствующему файловому объекту метод write, передавая ему строку. Метод write возвращает количество символов, которые мы записали (или количество байт в случае байтовой строки).

Закрывают файлы так:

В Python принято закрывать файлы, т.к. в противном случае может произойти нечто неприятное — например, в операционной системе могут закончиться файловые дескрипторы.

Итак, чтобы открыть файл на чтение и запись, нам нужно использовать r+. Мы можем читать данные из файла с помощью метода read, который по умолчанию читает столько, сколько сможет (если файл слишком большой, он может не поместиться в памяти). Вы также можете указать в методе read конкретное количество информации, которое вы хотите прочитать, передав size.

Когда мы прочитали весь файл, указатель того, где мы сейчас находимся в файле — в самом конце (в примере выше — на 47-ом символе). Если мы попробуем прочитать еще раз, то мы ничего не найдем. Для того чтобы прочитать файл заново, нужно использовать метод seek и перенести указатель на начало файла.

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

Если закрыть файл, вызов функции read() приведёт к ошибке — закрытый файл нельзя прочитать.

Рекомендуется открывать файлы несколько по-другому — с помощью контекстного менеджера, который позволяет не заботиться о закрытии файлов. Вы можете открыть файл с помощью оператора with, записать файловый объект в переменную f и потом работать с файлом внутри этого контекстного блока. После выхода из блока интерпретатор Python закроет файл.

 

    Функциональное программирование

Функции в Python — это такие же объекты, как и, например, строки, списки или классы. Их можно передавать в другие функции, возвращать из функций, создавать на лету — то есть это объекты первого класса

Итак, функции можно передавать в функции. Также их можно создавать внутри других функций.

Т.к. мы вернули другую функцию, в переменной multiplier теперь хранится функция inner:

Давайте попробуем определить функцию inner, которая будет принимать один аргумент и умножать его всегда на то самое число, которое мы передали в get_multiplier. Например, мы передаем get_multiplier двойку и получаем функцию, которая всегда умножает переданный ей аргумент на двойку. Эта концепция называется «замыканием».

Этот приём очень важен и в дальнейшем будет использоваться в декораторах.

Иногда бывает необходимо применить какую-то функцию к набору элементов. Для этих целей существует несколько стандартных функций. Одна из таких функций — это map, которая принимает функцию и какой-то итерабельный объект (например, список) и применяет полученную функцию ко всем элементам объекта.

Обратите внимание на вызов функции list вокруг map’а, потому что map по умолчанию возвращает map object (некий итерабельный объект)

То же самое можно сделать и без функции map, но более длинно:

Ещё одна функция, которая часто используется в контексте функционального программирования, это функция filter. Функция filter позволяет фильтровать по какому-то предикату итерабельный объект. Она принимает на вход функцию-условие и сам итерабельный объект.

Заметим, что несмотря на то, что map и filter очень мощны, не стоит злоупотреблять ими, т.к. это ухудшает читаемость кода.

Если мы хотим передать в map небольшую функцию, которая нам больше не понадобится, можно использовать анонимные функции (или lambda-функции). Lambda позволяет вам определить функцию in place, то есть без литерала def. Сделаем то же самое, что и в предыдущем примере, c помощью lambda:

Лямбда-функция — это как обычная функция, но без имени:

Lambda можно применять с filter:

Упражнение: написать функцию, которая превращает список чисел в список строк.

Модуль functools позволяет использовать функциональные особенности Python-а ещё лучше. Например, в functools в последних версиях языка принесли функцию reduce, которая позволяет сжимать данные, применяя последовательно функцию и запоминая результат:

То же самое можно сделать с помощью анонимной функции:

Метод partial из functools который позволяет немного модифицировать поведение функций, а именно задать функцию с частью параметров исходной функции, а остальные параметры заменить на некоторые дефолтные значения. Например:

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

Лучше использовать списочные выражения (list comprehensions), то есть писать цикл прямо в квадратных скобках:

Со списочными выражениями код работает немного быстрее. Точно так же можно написать списочное выражение с некоторым условием:

С помощью list comprehensions можно определять словари таким образом:

Если применять list comprehensions с фигурными скобками, но без двоеточий, мы получим set:

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

Без скобок списочное выражение возвращает генератор — объект, по которому можно итерироваться (подробнее про генераторы будет рассказано позже).

Ещё одна важная функция — функция zip — позволяет вам склеить два итерабельных объекта. В следующем примере мы по порядку соединяем объекты из numList и squaredList в кортежи:

 

    Декораторы

Декоратор — это функция, которая принимает функцию и возвращает функцию. И ничего более. Например, простейший декоратор принимает функцию и возвращает её же:

 

Выражение с @ — всего лишь синтаксический сахар. Мы можем написать то же самое без него: decorated = decorator(decorated)

Чуть более сложный декоратор, который меняет функцию на другую:

Декоратор измеряющий время выполнения функции:

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

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

Из-за того, что с помощью декоратора мы подменили функцию, её имя поменялось.

Этот факт иногда мешает при отладке. Чтобы такого не происходило, можно использовать декоратор wraps из модуля functools. Он подменяет определённые аргументы, docstring-и и названия так, что функция не меняется:

Более сложная задача: написать декоратор с параметром, который записывает лог в указанный файл. Для этого logger должен принимать имя файла и возвращать декоратор, который принимает функцию и подменяет её функцией wrapped, как мы делали до этого. Всё просто:

Посмотрим, что будет, если применить сразу несколько декораторов:

Видим, что сначала вызвался сначала первый декоратор, потом второй. Разберём это подробнее. Функция second_decorator возвращает новую функцию wrapped, таким образом, функция подменяется на wrapped внутри second_decorator-а. После этого вызывается first_decorator, который принимает функцию полученную из second_decorator-а wrapped и возвращает ещё одну функцию wrapped заменяя decorated на неё. Таким образом, итоговая функция decorated — это функция wrapped из first_decorator вызывающая функцию из second_decorator-а.

Ещё один пример на применение декораторов. Обратите внимание, что сначала теги идут в том же порядке, что и декораторы, а затем в обратном. Это происходит потому, что декораторы вызываются один внутри другого.

 

    Генераторы

Простейший генератор — это функция в которой есть оператор yield. Этот оператор возвращает результат, но не прерывает функцию. Пример:

Генератор even_range прибавляет к числу двойку и делает с ним операцию yield, пока current < end. Каждый раз, когда выполняется yield, возвращается значение current, и каждый раз, когда мы просим следующий элемент, выполнение функции возвращается к последнему моменту, после чего она продолжает исполняться. Чтобы посмотреть, как это происходит на самом деле, можно воспользоваться функцией next, которая действительно применяется каждый раз при итерации.

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

Можем проверить, что функция действительно прерывается каждый раз после выполнения yield:

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

Приведём классический пример про числа Фибоначчи:

С таким генератором нам не нужно помнить много чисел Фибоначчи, которые быстро растут — достаточно помнить два последних числа.

Еще одна важная особенность генераторов — это возможность передавать генератору какие-то значения. Эта особенность активно используется в асинхронном программировании, о котором будет речь позднее. Пока определим генератор accumulator, который хранит общее количество данных и в бесконечном цикле получает с помощью оператора yield значение. На первой итерации генератор возвращает начально значение total. После этого мы можем послать данные в генератор с помощью метода генератора send. Поскольку генератор остановил исполнение в некоторой точке, мы можем послать в эту точку значение, которое запишется в value. Далее, если value не было передано, генератор выходит из цикла, иначе прибавляем его к total.

 

Оставить комментарий

Ваш адрес email не будет опубликован.