Фундаментальные структуры данных в Python

В стандартной библиотеке Python содержится обширный набор структур данных. Однако, из-за различий в именах часто неясно, насколько известные [[абстрактные типы данных]] соответствуют конкретной реализации в Python.

Другие языки, такие как Java, в большей степени привержены «компьютерным наукам», что отражается в прозрачном именовании своих встроенных структур данных. Например, список — это не просто «список»! В Java — это либо LinkedList, либо ArrayList, что облегчает оценку вычислительной сложности этих типов.

Python предпочитает более простую и «человечную» схему именования. Недостатком является то, что для Python неясно, что лежит в основе реализации встроенного типа list — [[связанный список]] или [[динамический массив]]?

Моя цель в этой серии статей — разъяснить, как наиболее распространенные абстрактные структуры данных сопоставляются со схемой именования Python и предоставить конспективное описание каждого. Эта информация также поможет вам при подготовке к интервью на вакантную должность pythonist-а.

Хорошо, давайте начнем. Эта статья — «аэродром» для отдельных руководств по структурам данных, ссылки на которые вы всегда здесь найдёте:

Фундаментальные структуры данных в Python

  1. Словари, карты и хэш-таблицы
  2. Наборы и мультимножества
  3. Массивы
  4. Записи, структуры и объекты передачи данных
  5. Стэк (stack)
  6. Очереди (deq)
  7. Очереди приоритетов
  8. Связанные списки

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

Source




Руководства по структурам данных Python: словари, карты и хэш-таблицы

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

В Python словари (или «dicts» — ”дикты», для краткости) являются центральной структурой данных. Дикты хранят произвольное количество объектов, каждый из которых идентифицируется уникальным ключом. Словари часто называют картами, хэш-картами, таблицами поиска или ассоциативными массивами. Они позволяют осуществлять эффективный поиск, вставку, обновление и удаление любого объекта, связанного с данным ключом.

Более практичное объяснение — телефонные книги, которые являются достойным аналогом словарей из реального мира:

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

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

Словари Python, хэш-карты и хэш-таблицы

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

В Python есть полезные синтаксические «плюшки» для работы со словарями в своих программах. Например, использование фигурных скобок ({}) в синтаксисе описания словарей даёт чёткое понимание и позволяют удобно создавать новые словари:

phonebook = {
    'bob': 7387,
    'alice': 3719,
    'jack': 7052,
}

squares = {x: x * x for x in range(10)}

В словарях Python ключи для индексирования могут хешироваться любыми алгоритмами. Хэшируемый объект имеет хэш‑значение, которое никогда не изменяется в течение своей жизни (см. __hash__). Кроме того, хеш можно сравнивать с другими объектами (см. __eq__).

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

Встроенный тип данных dict

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

Словари Python основаны на хорошо проверенной и точно настроенной реализации хэш-таблицы, которая обеспечивает ожидаемую производительность со средней временной сложностью O(1) для операций поиска, вставки, обновления и удаления.

По некоторым причинам иногда неудобно использовать стандартный тип dict, включенный в Python. для этого существуют специализированные структуры данных, например, пропуск списков или списки на основе B-дерева.

>>> phonebook = {'bob': 7387, 'alice': 3719, 'jack': 7052}
>>> phonebook['alice']
3719

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

collections.OrderedDict — запомнить порядок вставки ключей

Подкласс словаря, который запоминает порядок вставки ключей, добавленных в коллекцию.

В CPython 3.6+ стандартные экземпляры dict сохраняют порядок вставки ключей, что является просто побочным эффектом. В стандартной спецификации Python такая возможность не определена. Если порядок ключей важен для работы вашего алгоритма, то лучше всего четко сообщить об этом с помощью класса OrderDict.

Класс OrderedDict не встроен в основной язык и должен быть импортирован из модуля collections стандартной библиотеки.

>>> import collections
>>> d = collections.OrderedDict(one=1, two=2, three=3)

>>> d
OrderedDict([('one', 1), ('two', 2), ('three', 3)])

>>> d['four'] = 4
>>> d
OrderedDict([('one', 1), ('two', 2), ('three', 3), ('four', 4)])

>>> d.keys()
odict_keys(['one', 'two', 'three', 'four'])

collections.defaultdict — возвращает значения по умолчанию для отсутствующих ключей

Ещё один подкласс словаря, принимающий значение по умолчанию в конструкторе, которое будет возвращено, если запрошенный ключ не может быть обнаружен в экземпляре defaultdict. Это может частично сэкономить время при написании кода и сделать намерение программиста более ясным по сравнению с использованием методов get() или перехватом исключения KeyError в обычных словарях.

>>> from collections import defaultdict
>>> dd = defaultdict(list)

# Accessing a missing key creates it and initializes it
# using the default factory, i.e. list() in this example:
>>> dd['dogs'].append('Rufus')
>>> dd['dogs'].append('Kathrin')
>>> dd['dogs'].append('Mr Sniffles')

>>> dd['dogs']
['Rufus', 'Kathrin', 'Mr Sniffles']

collections.ChainMap — поиск нескольких словарей в одном представлении

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

>>> from collections import ChainMap
>>> dict1 = {'one': 1, 'two': 2}
>>> dict2 = {'three': 3, 'four': 4}
>>> chain = ChainMap(dict1, dict2)

>>> chain
ChainMap({'one': 1, 'two': 2}, {'three': 3, 'four': 4})

# ChainMap searches each collection in the chain
# from left to right until it finds the key (or fails):
>>> chain['three']
3
>>> chain['one']
1
>>> chain['missing']
KeyError: 'missing'

types.MappingProxyType — обертка для создания словарей, предназначенных только для чтения

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

>>> from types import MappingProxyType
>>> read_only = MappingProxyType({'one': 1, 'two': 2})

>>> read_only['one']
1
>>> read_only['one'] = 23
TypeError: "'mappingproxy' object does not support item assignment"

Использование словарей в Python: заключение

Все реализации хеш‑таблиц Python, перечисленные здесь допустимы и встроенны в стандартную библиотеку Python.

Если нужны общие указания, какой тип отображения следует использовать в своих программах Python, то я бы рекомендовал вам встроенный тип данных dict. Это универсальная и оптимизированная реализация, которая встроена непосредственно в основной язык.

Только если у вас есть специальные требования, которые выходят за рамки того, что предусмотрено dict, я бы рекомендовал вам использовать один из типов данных, которые перечислены здесь. Да, я по—прежнему считаю, что они являются допустимыми вариантами, но обычно ваш код будет более ясным и простым в обслуживании другими разработчиками, если он в большинстве случаев опирается на стандартные словари Python.

Ознакомьтесь со статьёй «Фундаментальные структуры данных в Python» и серией статьй из рубрики «Фундаментальные структуры данных в Python».

В этой статье чего-то не хватает или вы нашли ошибку? Помогите коллеге и оставьте комментарий ниже.

Source




Руководства по структурам данных Python: наборы и мультимножества

Как реализовать изменяемые и неизменяемые структуры данных set и multiset (bag) в Python, используя встроенные типы данных и классы из стандартной библиотеки?

Набор (set) — неупорядоченная коллекция объектов, которая не допускает дублирования элементов. Обычно наборы используются для быстрого тестирования значения на принадлежность к набору, для вставки или удаления новых значений из набора, а также для вычисления объединения или пересечения двух наборов.

В «правильной» реализации набора тесты на членство будут выполняться, как ожидается, в течение O(1) времени. Операции объединения, пересечения, вычитания и подмножества должны занимать в среднем O(n) времени. Реализация set, включенная в стандартную библиотеку Python, соответствует этой производительности.

Так же, как и словари, у наборов есть спецобработка в Python и синтаксические «плюшки», которые облегчают создание наборов. Например, синтаксис выражения set с фигурными скобками даёт понимание сущности и позволяют удобно определять новые экземпляры набора:

vowels = {'a', 'e', 'i', 'o', 'u'}
squares = {x * x for x in range(10)}

Внимание: для создания пустого набора, необходимо вызвать конструктор set(), так как использование пустых фигурных скобок ({}) неоднозначно и может привести к созданию словаря.

Python и стандартная библиотека предоставляют следующие реализации набора:

Встроенный тип данных set

Тип set в Python является изменяемым и позволяет делать динамические вставки и удаления элементов. Наборы Python поддерживаются типом данных dict и имеют одинаковые характеристики производительности. Любой хэшируемый объект может быть элементом набора.

>>> vowels = {'a', 'e', 'i', 'o', 'u'}
>>> 'e' in vowels
True

>>> letters = set('alice')
>>> letters.intersection(vowels)
{'a', 'e', 'i'}

>>> vowels.add('x')
>>> vowels
{'i', 'a', 'u', 'o', 'x', 'e'}

>>> len(vowels)
6

Встроенный frozenset

Неизменяемая версия set, которая не может быть изменена после создания. Frozenset статический и допускает только операции запроса (без вставки или удаления). Поскольку frozenset является статическими и хэшируемыми, то может использоваться как ключ словаря или как элемент другого набора.

>>> vowels = frozenset({'a', 'e', 'i', 'o', 'u'})
>>> vowels.add('p')
AttributeError: "'frozenset' object has no attribute 'add'"

Класс collections.Counter

Класс collections.Counter стандартной библиотеки Python реализует тип multiset (или bag — мешок, сумка), когда в наборе необходимо иметь не уникальные элементы.

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

>>> from collections import Counter
>>> inventory = Counter()

>>> loot = {'sword': 1, 'bread': 3}
>>> inventory.update(loot)
>>> inventory
Counter({'bread': 3, 'sword': 1})

>>> more_loot = {'sword': 1, 'apple': 1}
>>> inventory.update(more_loot)
>>> inventory
Counter({'bread': 3, 'sword': 2, 'apple': 1})

Будьте осторожны с подсчетом количества элементов в объекте Counter. Вызов len() возвращает количество уникальных элементов в multiset, в то время как общее количество элементов необходимо подсчитывать немного иначе:

>>> len(inventory)
3  # Unique elements
>>> sum(inventory.values())
6  # Total no. of elements

Ознакомьтесь с полной серией статей «Основные структуры данных в Python». В этой статье чего-то не хватает или вы нашли ошибку? Помогите коллеге и оставьте комментарий ниже.

Source




Руководства по структурам данных Python: массивы

Как реализовать массивы в Python, используя только встроенные типы данных и классы из стандартной библиотеки? Здесь есть примеры кода и рекомендации.

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

В этой статье мы рассмотрим реализации массива в Python с использованием только основных конструкций языка или возможности стандартной библиотеки Python.

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

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

Итак, как работаю массивы в Python и для чего они нужны?

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

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

Реальной аналогией для массива является парковка:

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

Но не все парковки одинаковы:

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

С точки зрения производительности, найти элемент в массиве по его номеру очень быстро. В этом случае правильная реализация массива гарантирует постоянное время доступа O(1).

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

Давайте обсудим возможные варианты:

Изменяемые одномерные динамические массивы — list

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

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

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

>>> arr = ['one', 'two', 'three']
>>> arr[0]
'one'

# Lists have a nice repr:
>>> arr
['one', 'two', 'three']

# Lists are mutable:
>>> arr[1] = 'hello'
>>> arr
['one', 'hello', 'three']

>>> del arr[1]
>>> arr
['one', 'three']

# Lists can hold arbitrary data types:
>>> arr.append(23)
>>> arr
['one', 'three', 23]

Неизменяемый контейнер, кортеж — tuple

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

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

>>> arr = 'one', 'two', 'three'
>>> arr[0]
'one'

# Tuples have a nice repr:
>>> arr
('one', 'two', 'three')

# Tuples are immutable:
>>> arr[1] = 'hello'
TypeError: "'tuple' object does not support item assignment"

>>> del arr[1]
TypeError: "'tuple' object doesn't support item deletion"

# Tuples can hold arbitrary data types:
# (Adding elements creates a copy of the tuple)
>>> arr + (23,)
('one', 'two', 'three', 23)

Базовый типизированный массив  — array.array

Модуль array Python обеспечивает эффективное хранение базовых типов данных в C-стиле, таких как байты, 32‑разрядные целые числа, числа с плавающей точкой и т. д.

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

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

Кроме того, массивы поддерживают большинство методов, характерных для обычных списков. Например, для добавления элемента в массив используется знакомый метод array.append().

В результате такого сходства между списками и массивами объектов, массив можно использовать в качестве «эквивалентной замены», что не требует серьезных изменений вашего кода.

>>> import array
>>> arr = array.array('f', (1.0, 1.5, 2.0, 2.5))
>>> arr[1]
1.5

# Arrays have a nice repr:
>>> arr
array('f', [1.0, 1.5, 2.0, 2.5])

# Arrays are mutable:
>>> arr[1] = 23.0
>>> arr
array('f', [1.0, 23.0, 2.0, 2.5])

>>> del arr[1]
>>> arr
array('f', [1.0, 2.0, 2.5])

>>> arr.append(42.0)
>>> arr
array('f', [1.0, 2.0, 2.5, 42.0])

# Arrays are "typed":
>>> arr[1] = 'hello'
TypeError: "must be real number, not str"

Неизменные массивы символов Юникода — str

Python 3.x использует объекты str для хранения текстовых данных в виде неизменных последовательностей символов Юникода. Собственно говоря, это означает, что str — это рекурсивная структура данных, где каждый символ сам является объектом str с длиной равной 1, кроме того, он не может быть изменён.

Строковые объекты эффективны с точки зрения использования памяти, поскольку плотно упакованы и все элементы относятся к одному типе данных. Если вы храните текст в Юникоде, то надо использовать все возможности обработки. Поскольку строки неизменны, то для модификации необходимо создать копию. Наиболее близким эквивалентом «изменяемой строки» является список, где хранятся отдельные символы.

>>> arr = 'abcd'
>>> arr[1]
'b'

>>> arr
'abcd'

# Strings are immutable:
>>> arr[1] = 'e'
TypeError: "'str' object does not support item assignment"

>>> del arr[1]
TypeError: "'str' object doesn't support item deletion"

# Strings can be unpacked into a list to
# get a mutable representation:
>>> list('abcd')
['a', 'b', 'c', 'd']
>>> ''.join(list('abcd'))
'abcd'

# Strings are recursive data structures:
>>> type('abc')
""
>>> type('abc'[0])
""

Неизменный массив байт — bytes

Байтовые объекты — это неизменные последовательности отдельных байтов (целые числа в диапазоне от 0 <= x <= 255). Концептуально они похожи на объекты str.

Как и строки, bytes имеют свой собственный синтаксис для создания компактных объектов. Байтовые объекты неизменны, но в отличие от строк есть специальный тип данных «изменяемый байтовый массив», называемый bytearray, куда они могут быть распакованы. Но об этом в следующем разделе.

>>> arr = bytes((0, 1, 2, 3))
>>> arr[1]
1

# Bytes literals have their own syntax:
>>> arr
b'\x00\x01\x02\x03'
>>> arr = b'\x00\x01\x02\x03'

# Only valid "bytes" are allowed:
>>> bytes((0, 300))
ValueError: "bytes must be in range(0, 256)"

# Bytes are immutable:
>>> arr[1] = 23
TypeError: "'bytes' object does not support item assignment"

>>> del arr[1]
TypeError: "'bytes' object doesn't support item deletion"

Изменяемый массив байт — bytearray

Тип bytearray представляет собой изменяемую последовательность целых чисел в диапазоне от 0 до 255. Он тесно похож на тип bytes, а главным отличием является то, что bytearrays можно свободно изменять — можно перезаписывать элементы, удалять существующие элементы или добавлять новые. Объект bytearray при этом будет расти или уменьшаться должным образом.

Объекты Bytearrays могут быть преобразованы обратно в неизменные объекты bytes, но при этом производится полное копированию сохраненных данных и операция занимает O(n) времени.

>>> arr = bytearray((0, 1, 2, 3))
>>> arr[1]
1

# The bytearray repr:
>>> arr
bytearray(b'\x00\x01\x02\x03')

# Bytearrays are mutable:
>>> arr[1] = 23
>>> arr
bytearray(b'\x00\x17\x02\x03')

>>> arr[1]
23

# Bytearrays can grow and shrink in size:
>>> del arr[1]
>>> arr
bytearray(b'\x00\x02\x03')

>>> arr.append(42)
>>> arr
bytearray(b'\x00\x02\x03*')

# Bytearrays can only hold "bytes"
# (integers in the range 0 <= x <= 255)
>>> arr[1] = 'hello'
TypeError: "an integer is required"

>>> arr[1] = 300
ValueError: "byte must be in range(0, 256)"

# Bytearrays can be converted back into bytes objects:
# (This will copy the data)
>>> bytes(arr)
b'\x00\x02\x03*'

Какой способ представления массивов надо использовать в Python?

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

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

Оставаясь в рамках естественных типов данных Python и стандартной библиотеки, вот к чему сводится наш выбор:

  • Нужно хранить произвольные объекты, потенциально, с различными типами данных? — Используйте list или tuple, в зависимости от необходимости проводить в них изменения.
  • Есть числовые данные (целочисленные/с плавающей данные), требуется плотная упаковка и важна производительность? — Попробуйте array.array и посмотрите, делает ли он все, что нужно. Рассмотрите возможность выхода за пределы стандартной библиотеки и попробуйте такой пакет, как [[NumPy]].
  • Есть текстовые данные, представленные в виде символов Юникода? — Используйте встроенный в Python str. Если нужна «изменяемая строка», используйте список символов list.
  • Хотите сохранить непрерывный блок байтов? — Используйте bytes (неизменяемый) или bytearray (изменяемый) массивы байтов.

Лично мне, в большинстве случаев, нравится начинать с простого list и только потом думать о повышении производительности или объёме памяти, если это становится проблемой.

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

Использование list в качестве базовой структуры реализации массива общего назначения ускоряет разработку и делает программирование комфортным.

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

Ознакомьтесь с полной серией статей «Основные структуры данных в Python». В этой статье чего-то не хватает или вы нашли ошибку? Помогите коллеге и оставьте комментарий ниже.

Source




Руководства по структурам данных Python: записи, структуры и объекты передачи данных в Python

Как реализовать записи, структуры и «привычные старые объекты данных» в Python, используя только встроенные типы данных и классы из стандартной библиотеки.

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

В этой статье я довольно «вольно» обращаюсь с понятием «запись». Например, здесь также собираюсь обсудить такие типы, как встроенный tuple, который в классическом понимании из‑за отсутствия именованных полей обычно не относят к “записям”.

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

Хорошо, начинаем:

Встроенный тип данных – словарь (dict)

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

В качестве записи или объекта данных в Python можно использовать словари (dict). Словари легко создавать и для этого имеются своеобразные синтаксические «плюшки», встроенный в язык в виде словарных литералов. Синтаксис словаря лаконичен и довольно удобен для ввода.

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

car1 = {
    'color': 'red',
    'mileage': 3812.4,
    'automatic': True,
}
car2 = {
    'color': 'blue',
    'mileage': 40231.0,
    'automatic': False,
}

# У Dict есть хорошее формальное строковое представление (repr):
>>> car2
{'color': 'blue', 'automatic': False, 'mileage': 40231.0}

# Получить значение поля mileage:
>>> car2['mileage']
40231.0

# Dict можно изменить:
>>> car2['mileage'] = 12
>>> car2['windshield'] = 'broken'
>>> car2
{'windshield': 'broken', 'color': 'blue',
 'automatic': False, 'mileage': 12}

# Нет защиты от неправильных имен полей,
# или пропущенных/дополнительных полей:
car3 = {
    'colr': 'green',
    'automatic': False,
    'windshield': 'broken',
}

Встроенный тип данных – кортеж (tuple)

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

Кортежи занимают намного меньше места в памяти, чем списки, они производительней, при создании экземпляра класса работают быстрее. Как можно видеть в приведенной ниже байт‑кодовой раскладке, построение константного кортежа занимает всего один LOAD_CONST, в то время как построение объекта list с тем же содержанием требует дополнительно нескольких операций:

>>> import dis
>>> dis.dis(compile("(23, 'a', 'b', 'c')", '', 'eval'))
  1       0 LOAD_CONST           4 ((23, 'a', 'b', 'c'))
          3 RETURN_VALUE

>>> dis.dis(compile("[23, 'a', 'b', 'c']", '', 'eval'))
  1       0 LOAD_CONST           0 (23)
          3 LOAD_CONST           1 ('a')
          6 LOAD_CONST           2 ('b')
          9 LOAD_CONST           3 ('c')
         12 BUILD_LIST           4
         15 RETURN_VALUE

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

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

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

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

# Подя: color, mileage, automatic
car1 = ('red', 3812.4, True)
car2 = ('blue', 40231.0, False)

# Есть хорошее формальное строковое представление (repr):
>>> car1
('red', 3812.4, True)
>>> car2
('blue', 40231.0, False)

# Получить mileage:
>>> car2[1]
40231.0

# Tuples не могут быть изменены:
>>> car2[1] = 12
TypeError: "'tuple' object does not support item assignment"

# Нет защиты от пропущенных/лишних полей
# или неправильного порядка следования полей:
>>> car3 = (3431.5, 'green', True, 'silver')

Создание собственного класса

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

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

Кроме того, формальное строковое представление (repr, по умолчанию) для объектов, созданных из пользовательских классов, не очень полезно. Чтобы исправить это, возможно, придется добавить свой собственный метод __repr__, так же обычно довольно многословен и должен обновляться каждый раз при добавлении нового поля.

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

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

class Car:
    def __init__(self, color, mileage, automatic):
        self.color = color
        self.mileage = mileage
        self.automatic = automatic

car1 = Car('red', 3812.4, True)
car2 = Car('blue', 40231.0, False)

# Получить mileage:
>>> car2.mileage
40231.0

# Изменение значений элементов в экземпляре класса:
>>> car2.mileage = 12
>>> car2.windshield = 'broken'

# Строковое представление не очень полезно
# (необходимо добавить метод __repr__, написанный вручную):
>>> car1
<Car object at 0x1081e69e8>

Класс collections.namedtuple

Класс namedtuple, доступный начиная с Python 2.6+, расширяет возможности встроенного типа tuple. Аналогично определению пользовательского класса, использование namedtuple позволяет повторно определить используемые «схемы элементов» для записей, что гарантируют использование правильных имен полей.

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

Кроме того, namedtuple — это, ну…, именованные кортежи. К каждому объекту, хранящемуся в них, можно получить доступ через уникальный идентификатор. Это освобождает от необходимости запоминать целочисленные индексы или прибегать к обходным путям, таким как определение целочисленных констант в качестве мнемоник для ваших индексов.

Объекты namedtuple реализованы как обычные классы Python. Когда речь заходит об использовании памяти, они также “лучше”, чем обычные классы, так же эффективней в памяти, чем обычные кортежи:

>>> from collections import namedtuple
>>> from sys import getsizeof

>>> p1 = namedtuple('Point', 'x y z')(1, 2, 3)
>>> p2 = (1, 2, 3)

>>> getsizeof(p1)
72
>>> getsizeof(p2)
72

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

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

Использование namedtuple поверх неструктурированных tuple и dict также может облегчить жизнь моих коллег, потому что namedtuple делают данные, передаваемые вокруг, “самодокументированными”, по крайней мере до некоторой степени.

Для получения дополнительной информации и примеров кода посмотрите туториал по namedtuple.

from collections import namedtuple

Car = namedtuple('Car' , 'color mileage automatic')

car1 = Car('red', 3812.4, True)

# Замечательный repr:
>>> car1
Car(color='red', mileage=3812.4, automatic=True)

# Доступ к полям:
>>> car1.mileage
3812.4

# Поля изменять нельзя:
>>> car1.mileage = 12
AttributeError: "can't set attribute"
>>> car1.windshield = 'broken'
AttributeError: "'Car' object has no attribute 'windshield'"

Класс typing.NamedTuple

Этот класс, добавленный в Python 3.6 и является младшим братом collections.namedtuple. Он очень похож на namedtuple, а главное отличие заключается в обновлении синтаксиса при определения новых типов записей, добавлена поддержка подсказок типа данных.

Обратите внимание, что аннотации типов не применяются без отдельного инструмента проверки типов, такого как type checking tool like mypy, но даже без поддержки инструментов они могут предоставлять полезные подсказки другим программистам (или быть ужасно запутанными, если подсказки типа устарели.)

from typing import NamedTuple

class Car(NamedTuple):
    color: str
    mileage: float
    automatic: bool

car1 = Car('red', 3812.4, True)

# Instances have a nice repr
>>> car1
Car(color='red', mileage=3812.4, automatic=True)

# Accessing fields
>>> car1.mileage
3812.4

# Fields are immutable
>>> car1.mileage = 12
AttributeError: "can't set attribute"
>>> car1.windshield = 'broken'
AttributeError: "'Car' object has no attribute 'windshield'"

# Type annotations are not enforced without
# a separate type checking tool like mypy:
>>> Car('red', 'NOT_A_FLOAT', 99)
Car(color='red', mileage='NOT_A_FLOAT', automatic=99)

Класс struct.Struct

Этот класс выполняет преобразования между значениями Python и C-структурами, сериализованными в объекты bytes Python. Например, он может быть использован для обработки двоичных данных, хранящихся в файлах или при сетевых соединениях.

Структуры определяются с помощью формата strings-like mini language, который позволяет определить расположение различных типов данных C, таких char, int, и long, а также их вариант unsigned.

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

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

from struct import Struct

MyStruct = Struct('i?f')

data = MyStruct.pack(23, False, 42.0)

# All you get is a blob of data:
>>> data
b'\x17\x00\x00\x00\x00\x00\x00\x00\x00\x00(B'

# Data blobs can be unpacked again:
>>> MyStruct.unpack(data)
(23, False, 42.0)

Класс types.SimpleNamespace

Вот еще один «эзотерический» выбор для реализации объектов данных в Python. Этот класс был добавлен в Python 3.3 и обеспечивает доступ к атрибутам пространства имен. Он также включает в себя значимый __repr__ по умолчанию.

Как следует из названия, SimpleNamespace — это простой именованный словарь с доступом по атрибутам, которые красиво печатаются. Атрибуты могут быть свободно добавлены, изменены и удалены.

from types import SimpleNamespace
car1 = SimpleNamespace(color='red', mileage=3812.4, automatic=True)

# repr по умолчанию:
>>> car1
namespace(automatic=True, color='red', mileage=3812.4)

# Можно изменять
>>> car1.mileage = 12
>>> car1.windshield = 'broken'
>>> del car1.automatic
>>> car1
namespace(color='red', mileage=12, windshield='broken')

Какую конструкцию надо использовать для объектов данных в Python?

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

  • У вас есть всего несколько (2-3) полей: использование простого кортежа может быть оправдано, потому что порядок полей легко запомнить или имена полей излишни. Например, описание точки в трёхмерном пространстве (x, y, z).
  • Вам нужны неизменяемые поля: в этом случае простые кортежи, collections.namedtuple, typing.NamedTuple были бы хорошими вариантами для реализации подобного объекта данных.
  • Вам нужно заблокировать имена полей, чтобы избежать опечаток: collections.namedtuple и typing.NamedTuple ваши друзья.
  • Вы хотите, чтобы все было просто: простой объект словаря может быть хорошим выбором из-за удобного синтаксиса, который близко напоминает JSON.
  • Вам нужен полный контроль над своей структурой данных: пришло время написать пользовательский класс с декоратором @property, сеттерами и геттерами.
  • Вам нужно добавить поведение (методы) к объекту: вы должны написать пользовательский класс. Либо с нуля, либо путем расширения collections.namedtuple или typing.NamedTuple.
  • Вам нужно плотно упаковать данные для сериализации их на диск или отправки по сети: самое время применить struct.Struct, который является отличным вариантом для этого случая.

Если ключевым моментом выбора является безопасность, то рекомендую для реализации простой записи, структуры или объекта данных в Python использовать:

  • collections.namedtuple в Python 2.x; и
  • его младшего брата typing.NamedTuple в Python 3.

Ознакомьтесь с полной серией статей «Основные структуры данных в Python». В этой статье чего-то не хватает или вы нашли ошибку? Помогите коллеге и оставьте комментарий ниже.

Source




Руководства по структурам данных Python: очередь

Очередь — это набор объектов, работающих при вставке и удалении по принципу first in, first out — «первым пришёл — первым ушёл» (англ. [[FIFO]]). Операции вставки и удаления иногда называются enqueue и dequeue. В отличие от списков или массивов , очереди обычно не допускают произвольного доступа к содержащимся в них объектам.

Вот реальная аналогия для очереди первого входа, первого выхода:

Представьте себе очередь питонистов, ожидающих, чтобы забрать свои значки конференции в первый день регистрации PyCon. Новые дополнения к очереди вносятся в заднюю часть очереди, когда новые люди входят в место проведения конференции и” выстраиваются в очередь», чтобы получить свои значки. Удаление (обслуживание) происходит в передней части очереди, так как разработчики получают свои значки и сумки для конференций swag и покидают очередь.

Еще один способ запомнить характеристики структуры данных очереди-это думать о ней как о канале:

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

Очереди похожи на стеки, и разница между ними заключается в удалении элементов:

С помощью очереди вы удаляете элемент, добавленный последним (первый вход, первый выход или [[FIFO]]); а с помощью стека вы удаляете элемент, добавленный последним ( последний вход, первый выход или [[LIFO]]).

С точки зрения производительности, правильная реализация очереди, как ожидается, займет O(1) времени для операций вставки и удаления. Это две основные операции, выполняемые в очереди, и они должны быть быстрыми в правильной реализации.

Очереди имеют широкий спектр применения в алгоритмах и для решения задач планирования, а также параллельного программирования. Короткий и красивый алгоритм, использующий очередь, — это широтно-первый поиск (BFS) в структуре данных дерева или графика.
Алгоритмы планирования часто используют внутренние очереди приоритетов. Это специализированные очереди: вместо получения следующего элемента по времени вставки приоритетная очередь получает элемент с наивысшим приоритетом. Приоритет отдельных элементов определяется очередью на основе порядка, применяемого к их ключам.

Обычная очередь, однако, не будет повторно заказывать элементы, которые она несет. Вы получаете то, что вы вкладываете, и именно в таком порядке (помните пример с трубой?)

Python поставляется с несколькими реализациями очереди, каждый из которых имеет немного разные характеристики. Давайте взглянем на них:

Встроенный список list

Можно использовать обычный listв качестве очереди, но это не идеально с точки зрения производительности . Списки для этой цели довольно медленны, потому что вставка или удаление элемента в начале требует сдвига всех других элементов на один, требуя O(n) времени.

Поэтому я бы не рекомендовал вам использовать a listв качестве временной очереди в Python (если вы не имеете дело только с небольшим количеством элементов).

# How to use Python's list as a FIFO queue:

q = []

q.append('eat')
q.append('sleep')
q.append('code')

>>> q
['eat', 'sleep', 'code']

# Careful: This is slow!
>>> q.pop(0)
'eat'

Класс collections.deque

Класс deque реализует двустороннюю очередь, которая поддерживает добавление и удаление элементов с любого конца за O (1) раз.

Объекты deque Python реализованы в виде двусвязных списков, что дает им отличную производительность для элементов очереди и удаления из очереди, но плохую производительность O(n) для случайного доступа к элементам в середине очереди.

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

collections.deque это отличный выбор по умолчанию, если вы ищете структуру данных очереди в стандартной библиотеке Python.

# How to use collections.deque as a FIFO queue:

from collections import deque
q = deque()

q.append('eat')
q.append('sleep')
q.append('code')

>>> q
deque(['eat', 'sleep', 'code'])

>>> q.popleft()
'eat'
>>> q.popleft()
'sleep'
>>> q.popleft()
'code'

>>> q.popleft()
IndexError: "pop from an empty deque"

Класс queue.Queue

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

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

В зависимости от вашего варианта использования семантика блокировки может быть полезной или просто нести ненужные накладные расходы. В этом случае вам было бы лучше использовать collections.dequeв качестве очереди общего назначения.

# How to use queue.Queue as a FIFO queue:

from queue import Queue
q = Queue()

q.put('eat')
q.put('sleep')
q.put('code')

>>> q


>>> q.get()
'eat'
>>> q.get()
'sleep'
>>> q.get()
'code'

>>> q.get_nowait()
queue.Empty

>>> q.get()
# Blocks / waits forever...

Класс multiprocessing.Queue

Это реализация общей очереди заданий, которая позволяет помещенным в очередь элементам обрабатываться параллельно несколькими параллельными работниками. Распараллеливание на основе процессов популярно в Python из-за глобальной блокировки интерпретатора (GIL) .

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

# How to use multiprocessing.Queue as a FIFO queue:

from multiprocessing import Queue
q = Queue()

q.put('eat')
q.put('sleep')
q.put('code')

>>> q


>>> q.get()
'eat'
>>> q.get()
'sleep'
>>> q.get()
'code'

>>> q.get()
# Blocks / waits forever...

Хороший выбор по умолчанию: collections.deque
Если вы не ищете поддержку параллельной обработки, предложенная реализация collections.deque является отличным выбором по умолчанию для реализации структуры данных очереди FIFO в Python.

I’D предоставляет характеристики производительности, которые вы ожидаете от хорошей реализации очереди, а также может использоваться в качестве стека (очередь LIFO).

Ознакомьтесь с полной серией статей «Основные структуры данных в Python».

В этой статье чего-то не хватает или вы нашли ошибку? Помогите коллеге и оставьте комментарий ниже.

Source




Руководства по структурам данных Python: стек

Как реализовать структуру данных стека ([[LIFO]]) в Python, используя только встроенные типы и классы из стандартной библиотеки?

Cтек — это набор объектов, который поддерживает правило last in, first out или «последний пришел, первый вышел» ([[LIFO]]) для вставок и удалений. В отличие от списков или массивов, стеки обычно не допускают произвольного доступа к содержащимся в них объектам. Операции вставки и удаления также часто называются push и pop.

Вот вам аналогия для структуры стековых данных из реальной жизни — стопка пластинок:

Новые пластинки складываются наверх стопки. А поскольку пластинки хрупкие и представляют великую ценность для меломанов, то можно снимать только самую верхнюю пластинку (последний пришел, первый вышел). Чтобы достать пластинку снизу, необходимо одну за другой снять самые верхние пластинки.

Стеки и очереди похожи. Они представляют собой линейные коллекции объектов, а разница заключается в порядке доступа к объектам: в очереди вы удаляете самые ранний добавленный объект (first-in, first-out или [[FIFO]]); а в стеке наоборот вы удаляете последний добавленный объект (last-in, first-out или [[LIFO]]),

С точки зрения производительности ожидается, что при правильной реализации стека потребуется O(1) времени для вставки и удаления.

Стек используется во многих алгоритмах, от, например, синтаксического анализа языка до управления памятью («стек вызовов»). Короткий и красивый алгоритм с использованием стека — [[поиск в глубину]] (DFS) в деревьях и графах.

В Python есть несколько реализаций стека, каждая из которых имеет свои особенности. Давайте посмотрим на них:

Cписок — list

Тип данных list вполне себе приличная структура для организации стека, имеет операции push и pop, выполняющиеся за время O(1).

По сути, списки являются динамическими массивами и им иногда необходимо изменить размер пространства для хранения элементов при добавлении или удалении. В списке происходит перераспределение памяти, так что не каждый push или pop требует изменения размера, отсюда и получается оценка O(1) для этих операций.

Недостатком является то, что производительность не стабильна и нельзя сказать, что при вставке и удалении она всегда O(1), как в реализации на основе связанного списка (например, collection.deque, см. ниже). С другой стороны, быстрый доступ к случайному элементу с производительностью O(1) может быть дополнительным преимуществом.

Важное предупреждение относительно производительности: при использовании списков в качестве стеков для стабильной производительности O(1) вставки и удаления необходимо добавлять новые элементы в конец списка с помощью метода append(), удаляя при этом хвост, используя pop(). Стеки, основанные на списках Python, растут вправо и сжимаются слева.

Добавление и удаление в начало происходит намного медленнее и занимает O(n) времени, так как существующие элементы должны быть смещены по кругу для освобождения место для нового элемента.

# How to use a Python list as a stack (LIFO):
s = []
s.append('eat')
s.append('sleep')
s.append('code')
>>> s
['eat', 'sleep', 'code']
>>> s.pop()
'code'
>>> s.pop()
'sleep'
>>> s.pop()
'eat'
>>> s.pop()
IndexError: "pop from empty list"

Класс collections.deque

Класс deque реализует двустороннюю очередь, которая поддерживает добавление и удаление элементов с любого конца за время O(1).

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

Объекты deque в Python реализованы в виде двусвязных списков, что дает им отличную и стабильную производительность для вставки и удаления элементов, но низкую производительность O(n) для случайного доступа к элементам в середине стека.

collection.deque — отличный выбор из стандартной библиотеки Python для реализации стека с характеристиками производительности связанного списка.

# How to use collections.deque as a stack (LIFO):
from collections import deque
q = deque()
q.append('eat')
q.append('sleep')
q.append('code')
>>> q
deque(['eat', 'sleep', 'code'])
>>> q.pop()
'code'
>>> q.pop()
'sleep'
>>> q.pop()
'eat'
>>> q.pop()
IndexError: "pop from an empty deque"

Класс queue.LifoQueue

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

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

В зависимости от варианта использования блокировки может оказаться полезной или просто повлечь за собой ненужные накладные расходы. В этом случае вам лучше использовать list или deque в качестве стека общего назначения.

# How to use queue.LifoQueue as a stack:
from queue import LifoQueue
s = LifoQueue()
s.put('eat')
s.put('sleep')
s.put('code')
>>> s
<queue.LifoQueue object at 0x108298dd8>
>>> s.get()
'code'
>>> s.get()
'sleep'
>>> s.get()
'eat'
>>> s.get_nowait()
queue.Empty
>>> s.get()
# Blocks / waits forever...

Лучший выбор по умолчанию: collections.deque

Если нет необходимости параллельной обработки (или ручной блокировки и разблокировки), то выбор сводится к встроенному типу list или collection.deque. Разница заключается в структуре данных «под капотом» и в простоте использования.

  • list есть динамический массив, который отлично подходит для быстрого произвольного доступа, но требует периодического изменения размера при добавлении или удалении элементов. Список перераспределяет свое пространство хранения, так что не каждый push или pop требует изменения размера, обеспечивая производительность O(1). Но нужно быть осторожным при вставке и удалении элементов только справа (append и pop), иначе произойдёт снижение производительности до O(n).
  • collection.deque является двусвязным списком, который оптимизирован для добавления и удаления с любой стороны и обеспечивает одинаковую производительность O(1) для этих операций. Производительность не только стабильна, но и сам класс deque проще, поскольку не нужно беспокоиться о добавлении или удалении элементов с «неправильного конца».

Исходя из этих соображений collection.deque является отличным выбором для реализации стека (очереди LIFO) в Python.

Ознакомьтесь с полной серией статей «Основные структуры данных в Python».

В этой статье чего-то не хватает или вы нашли ошибку? Помогите коллеге и оставьте комментарий ниже.

Source




Букварь разработки: полезные трюки Python от А до Z

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

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

Чтобы работать с ним стало еще удобнее, возьмите на вооружение несколько полезных советов. Некоторые из них найдены в документации стандартной библиотеки, другие — в PyPi. Четыре или пять обнаружились на awesome-python.com.

all or any: все или хоть что-нибудь

Python — удивительно простой и выразительный язык. Его даже иногда называют «выполняемым псевдокодом». И с этим трудно поспорить, когда вы можете себе позволить конструкции, подобные этим:

x = [True, True, False]

if any(x):
   print("По крайней мере один элемент True")
if all(x):
   print("Все элементы True")
if any(x) and not all(x):
  print("Хотя бы один элемент True и один False")

bashplotlib: графики в терминале

А вы знали, что можно строить графики прямо в командной строке? Теперь знаете. За одну из самых удобных возможностей языка отвечает модуль bashplotlib.

$ pip install bashplotlib

collections: коллекции на любой вкус

Встроенные типы данных в Python — высший класс, но иногда хочется чего-то большего. Что ж, если хочется, обратитесь к модулю collections и выбирайте дополнительную структуру на свой вкус.

from collections import OrderedDict, Counter

# Упорядоченный список сохранит последовательность элементов
x = OrderedDict(a=1, b=2, c=3)

# Счетчик рассортирует символы по частотам
y = Counter("Hello World!")

dir: что внутри?

Хотелось ли вам когда-нибудь заглянуть в объект и увидеть, какими свойствами он обладает? Разумеется, в Python вы можете это сделать.

>>> dir()
>>> dir("Привет, мир!")
>>> dir(dir)

Это одна из самых важных возможностей Python для интерактивного запуска и отладки кода. Подробнее функция dir() описана в документации.

emoji: Python — ?

Серьезно ?? Абсолютно серьезно ?.

$ pip install emoji

Не притворяйтесь, что не будете этого делать.

from emoji import emojize

print(emojize(":thumbs_up:"))

__future__: импорт из будущего

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

Модуль __future__  позволяет импортировать функциональность из будущего. Это практически путешествие во времени — настоящее волшебство.

from __future__ import print_function

print("Привет, мир!")

Попробуйте, например, использовать фигурные скобки.

geopy: где я нахожусь?

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

$ pip install geopy

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

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

from geopy import GoogleV3

place = "221b Baker Street, London"
location = GoogleV3().geocode(place)

print(location.address)
print(location.location)

howdoi: StackOverflow прямо в терминале

Застряли во время разработки и никак не можете поймать за хвост решение, которое уже точно где-то видели? Надо идти на StackOverflow, но не хочется выходить из консоли?

Тогда вам нужен это суперполезный CLI-инструмент.

$ pip install howdoi

Задавайте любой вопрос, howdoi найдет что вам посоветовать.

$ howdoi vertical align css
$ howdoi for loop in java
$ howdoi undo commits in git

Инструмент собирает самые популярные ответы со StackOverflow, хотя они не всегда могут помочь…

$ howdoi exit vim

inspect: что здесь происходит?

Модуль inspect — отличный помощник, когда нужно разобраться, что происходит в вашем коде. Он может инспектировать даже сам себя!

В примере метод getsource() применяется, чтобы вывести исходный код inspect. А getmodule() распечатает модуль, в котором он был определен.

Последняя строчка просто выводит номер строки.

import inspect

print(inspect.getsource(inspect.getsource))
print(inspect.getmodule(inspect.getmodule))
print(inspect.currentframe().f_lineno)

Конечно же, эти примитивные примеры не описывают все возможности модуля inspect. Это отличное решение для создания самодокументированного кода.

Jedi: будь джедаем

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

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

**kwargs: словарь аргументов

Понимание таинственного звездного синтаксиса — важный этап в изучении Python.

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

Нет необходимости использовать именно слово kwargs, это лишь общепринятый пример.

dictionary = {"a": 1, "b": 2}

def someFunction(a, b):
  print(a + b)
  return

# а это то же самое:
someFunction(**dictionary)
someFunction(a=1, b=2)

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

List comprehensions: Генераторы списков

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

numbers = [1,2,3,4,5,6,7]
evens = [x for x in numbers if x % 2 is 0]
odds = [y for y in numbers if y not in evens]


cities = ['Лондон', 'Дублин', 'Осло']

def visit(city):
   print("Добро пожаловать в " + city)

for city in cities:
   visit(city)

Если вы хотите узнать о генераторах больше, загляните сюда.

map: перебор коллекций

Существует много встроенных возможностей Python, предназначенных для программирования в функциональном стиле. Среди них метод map(), который в паре с лямбда-функциями творит чудеса.

x = [1, 2, 3]
y = map(lambda x : x + 1 , x)

# выведет [2,3,4]
print(list(y))

Для каждого элемента коллекции x выполняется простая функция. Результатом работы метода map() является специальный объект, который легко конвертировать в любую итерируемую структуру, например, в список.

newspaper3k: все новости мира

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

Так что если вы думали об использовании BeautifulSoup или какой-либо другой библиотеки вебскраппинга, не тратьте время и просто возьмите newspape.

$ pip install newspaper3k

Operator overloading: перегрузка операторов

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

Например, вы думали о том, почему с помощью оператора + можно и складывать числа, и конкатенировать строки? Это живой пример перегрузки.

Можно создать объекты, которые по-своему интерпретируют обычные операторы языка.

class Thing:
  def __init__(self, value):
    self.__value = value

    # перегрузка оператора >
    def __gt__(self, other):
      return self.__value > other.__value

    # перегрузка оператора <
    def __lt__(self, other):
      return self.__value < other.__value

something = Thing(100)
nothing = Thing(0)

# True
something > nothing

# False
something < nothing

# Error
something + nothing

pprint: красивая печать

Функция print отлично справляется со своей работой. Но если вы захотите вывести на печать какой-нибудь громоздкий многоуровневый объект, результат будет довольно уродливым.

На помощь спешит модуль pretty-print из стандартной библиотеки. Он предоставляет массу возможностей Python для тех, кто имеет дело с нетривиальными структурами и сложными объектами. Теперь все что угодно можно вывести в удобном для чтения формате.

import requests
import pprint

url = 'https://randomuser.me/api/?results=1'
users = requests.get(url).json()

pprint.pprint(users)

Queue: реализация очередей

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

Например, FIFO-очереди (первый на вход — первый на выход) отдают элементы в том порядке, в котором они были добавлены. LIFO-очереди (последний на вход — первый на выход), наоборот, дают доступ к элементу, добавленному последним. И наконец, в приоритетных очередях значение имеет порядок сортировки.

Взгляните, как применяются очереди для многопоточного программирования.

__repr__: вывод в виде строки

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

>>> file = open('file.txt', 'r')
>>> print(file)
<open file 'file.txt', mode 'r' at 0x10d30aaf0>

Прежде всего это нужно для удобной отладки, поэтому не ленитесь добавлять вашим классам метод __repr__. А если вам нужно красивое строковое представление для пользовательского интерфейса, пригодится метод __str__.

class someClass:
  def __repr__(self):
    return "<какое-то описание>"

someInstance = someClass()

# выведет <какое-то описание>
print(someInstance)

sh: команды терминала прямо из кода

Порой применение стандартных библиотек os и subprocess превращается в головную боль для разработчика. Но есть удобная альтернатива — библиотека sh.

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

from sh import *

sh.pwd()
sh.mkdir('new_folder')
sh.touch('new_file.txt')
sh.whoami()
sh.echo('This is great!')

Type hints: указания типов

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

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

В современном стандарте Python появилась возможность добавлять определение типа на стадии разработки.

def addTwo(x : Int) -> Int:
  return x + 2

Для типов можно даже создавать псевдонимы:

from typing import List

Vector = List[float]
Matrix = List[Vector]

def addMatrix(a : Matrix, b : Matrix) -> Matrix:
  result = []

  for i,row in enumerate(a):
    result_row =[]
    for j, col in enumerate(row):
      result_row += [a[i][j] + b[i][j]]
    result += [result_row]
  return result

x = [[1.0, 0.0], [0.0, 1.0]]
y = [[2.0, 1.0], [0.0, -2.0]]

z = addMatrix(x, y)

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

uuid

Одна из встроенных возможностей Python — генерация универсальных уникальных идентификаторов. За это отвечает модуль uuid.

import uuid

user_id = uuid.uuid4()

print(user_id)

Результат работы этого кода — одно из 2122 возможных 128-битных чисел. Вероятность дублирования меньше, чем одна миллиардная доля — это совсем неплохо.

Virtual environments: виртуальные среды

Одна из самых полезных возможностей Python.

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

Нет нужды делать сложный выбор, ведь Python поддерживает виртуальные среды.

python -m venv my-project
source my-project/bin/activate
pip install all-the-modules

На одном компьютере теперь может работать сразу несколько автономных версий языка.

wikipedia: мировые запасы информации к вашим услугам

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

import wikipedia

result = wikipedia.page('freeCodeCamp')

print(result.summary)

for link in result.links:
  print(link)

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

xkcd: комиксы

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

import antigravity

Ты прекрасен, Python!

YAML

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

Ряд возможностей для комбинации YAML и Python дает модуль PyYAML.

Просто инсталлируйте его:

$ pip install pyyaml

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

import yaml

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

zip: упаковка нескольких коллекций

Последняя, но не худшая, из букваря возможностей Python — функция zip(). Используйте ее, если необходимо объединить два списка в один словарь:

keys = ['a', 'b', 'c']
vals = [1, 2, 3]

zipped = dict(zip(keys, vals))

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

Вы также можете «распаковать» объекты, вызвав функцию zip(*list).

Надеемся, что в A-Z списке возможностей Python вы нашли что-нибудь полезное для своих проектов. Делитесь вашими любимыми трюками в комментариях.

Оригинал: An A-Z of useful Python tricks




RegEx — регулярные выражения в Python

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

Решил я давеча моим школьникам дать задачек на регулярные выражения для изучения. А к задачкам нужна какая-нибудь теория. И стал я искать хорошие тексты на русском. Пяток сносных нашёл, но всё не то. Что-то смято, что-то упущено. У этих текстов был не только фатальный недостаток. Мало картинок, мало примеров. И почти нет разумных задач. Ну неужели поиск IP-адреса — это самая частая задача для регулярных выражений? Вот и я думаю, что нет.
Про разницу (?:…) / (…) фиг найдёшь, а без этого знания в некоторых случаях можно только страдать.

Плюс в питоне есть немало регулярных плюшек. Например, re.split может добавлять тот кусок текста, по которому был разрез, в список частей. А в re.sub можно вместо шаблона для замены передать функцию. Это — реальные вещи, которые прямо очень нужны, но никто про это не пишет.
Так и родился этот достаточно многобуквенный материал с подробностями, тонкостями, картинками и задачами.

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

Содержание

Регулярные выражения в Python от простого к сложному;
Содержание;
    Примеры регулярных выражений;
    Сила и ответственность;
Документация и ссылки;
Основы синтаксиса;
    Шаблоны, соответствующие одному символу;
    Квантификаторы (указание количества повторений);
    Жадность в регулярках и границы найденного шаблона;
    Пересечение подстрок;
Эксперименты в песочнице;
Регулярки в питоне;
Пример использования всех основных функций;
    Тонкости экранирования в питоне (‘\\\\\\\\foo’);
    Использование дополнительных флагов в питоне;
Написание и тестирование регулярных выражений;
Задачи — 1;
Скобочные группы (?:…) и перечисления |;
    Перечисления (операция «ИЛИ»);
    Скобочные группы (группировка плюс квантификаторы);
    Скобки плюс перечисления;
    Ещё примеры;
Задачи — 2;
Группирующие скобки (…) и match-объекты в питоне;
    Match-объекты;
    Группирующие скобки (…);
    Тонкости со скобками и нумерацией групп.;
    Группы и re.findall;
    Группы и re.split;
Использование групп при заменах;
    Замена с обработкой шаблона функцией в питоне;
    Ссылки на группы при поиске;
Задачи — 3;
Шаблоны, соответствующие не конкретному тексту, а позиции;
    Простые шаблоны, соответствующие позиции;
    Сложные шаблоны, соответствующие позиции (lookaround и Co);
    lookaround на примере королей и императоров Франции;
Задачи — 4;
Post scriptum;

Регулярное выражение — это строка, задающая шаблон поиска подстрок в тексте. Одному шаблону может соответствовать много разных строчек. Термин «Регулярные выражения» является переводом английского словосочетания «Regular expressions». Перевод не очень точно отражает смысл, правильнее было бы «шаблонные выражения». Регулярное выражение, или коротко «регулярка», состоит из обычных символов и специальных командных последовательностей. Например, \d задаёт любую цифру, а \d+ — задает любую последовательность из одной или более цифр. Работа с регулярками реализована во всех современных языках программирования. Однако существует несколько «диалектов», поэтому функционал регулярных выражений может различаться от языка к языку. В некоторых языках программирования регулярками пользоваться очень удобно (например, в питоне), в некоторых — не слишком (например, в C++).

Примеры регулярных выражений

Регулярка Её смысл
simple text В точности текст «simple text»
\d{5} Последовательности из 5 цифр
\d означает любую цифру
{5} — ровно 5 раз
\d\d/\d\d/\d{4} Даты в формате ДД/ММ/ГГГГ
(и прочие куски, на них похожие, например, 98/76/5432)
\b\w{3}\b Слова в точности из трёх букв
\b означает границу слова
(с одной стороны буква, а с другой — нет)
\w — любая буква,
{3} — ровно три раза
[-+]?\d+ Целое число, например, 7, +17, -42, 0013 (возможны ведущие нули)
[-+]? — либо -, либо +, либо пусто
\d+ — последовательность из 1 или более цифр
[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)? Действительное число, возможно в экспоненциальной записи
Например, 0.2, +5.45, -.4, 6e23, -3.17E-14.
См. ниже картинку.


Сила и ответственность

Регулярные выражения, или коротко, регулярки — это очень мощный инструмент. Но использовать их следует с умом и осторожностью, и только там, где они действительно приносят пользу, а не вред. Во-первых, плохо написанные регулярные выражения работают медленно. Во-вторых, их зачастую очень сложно читать, особенно если регулярка написана не лично тобой пять минут назад. В-третьих, очень часто даже небольшое изменение задачи (того, что требуется найти) приводит к значительному изменению выражения. Поэтому про регулярки часто говорят, что это write only code (код, который только пишут с нуля, но не читают и не правят). А также шутят: Некоторые люди, когда сталкиваются с проблемой, думают «Я знаю, я решу её с помощью регулярных выражений.» Теперь у них две проблемы. Вот пример write-only регулярки (для проверки валидности e-mail адреса (не надо так делать!!!)):

(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|
2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])

А вот здесь более точная регулярка для проверки корректности email адреса стандарту RFC822. Если вдруг будете проверять email, то не делайте так! Если адрес вводит пользователь, то пусть вводит почти что угодно, лишь бы там была собака. Надёжнее всего отправить туда письмо и убедиться, что пользователь может его получить.

Документация и ссылки

  • Оригинальная документация: docs.python.org/3/library/re.html;
  • Очень подробный и обстоятельный материал: www.regular-expressions.info/;
  • Разные сложные трюки и тонкости с примерами: http://www.rexegg.com/;
  • Он-лайн отладка регулярок regex101.com (не забудьте поставить галочку Python в разделе FLAVOR слева);
  • Он-лайн визуализация регулярок www.debuggex.com/ (не забудьте выбрать Python);
  • Могущественный текстовый редактор Sublime text 3, в котором очень удобный поиск по регуляркам;

Основы синтаксиса

Любая строка (в которой нет символов .^$*+?{}[]\|()) сама по себе является регулярным выражением. Так, выражению Хаха будет соответствовать строка “Хаха” и только она. Регулярные выражения являются регистрозависимыми, поэтому строка “хаха” (с маленькой буквы) уже не будет соответствовать выражению выше. Подобно строкам в языке Python, регулярные выражения имеют спецсимволы .^$*+?{}[]\|(), которые в регулярках являются управляющими конструкциями. Для написания их просто как символов требуется их экранировать, для чего нужно поставить перед ними знак \. Так же, как и в питоне, в регулярных выражениях выражение \n соответствует концу строки, а \t — табуляции.

Шаблоны, соответствующие одному символу

Во всех примерах ниже соответствия регулярному выражению выделяются бирюзовым цветом с подчёркиванием.

Шаблон Описание Пример Применяем к тексту
. Один любой символ, кроме новой строки \n. м.л.ко молоко, малако,
Им0л0коИхлеб
\d Любая цифра СУ\d\d СУ35, СУ111, АЛСУ14
\D Любой символ, кроме цифры 926\D123 926)123, 1926-1234
\s Любой пробельный символ (пробел, табуляция, конец строки и т.п.) бор\sода бор ода, бор
ода
, борода
\S Любой непробельный символ \S123 X123, я123, !123456, 1 + 123456
\w Любая буква (то, что может быть частью слова), а также цифры и _ \w\w\w Год, f_3, qwert
\W Любая не-буква, не-цифра и не подчёркивание сом\W сом!, сом?
[..] Один из символов в скобках,
а также любой символ из диапазона a-b
[0-9][0-9A-Fa-f] 12, 1F, 4B
[^..] Любой символ, кроме перечисленных <[^>]> <1>, <a>, <>>
\d≈[0-9],
\D≈[^0-9],
\w≈[0-9a-zA-Z
а-яА-ЯёЁ],
\s≈[ \f\n\r\t\v]
Буква “ё” не включается в общий диапазон букв!
Вообще говоря, в \d включается всё, что в юникоде помечено как «цифра», а в \w — как буква. Ещё много всего!
[abc-], [-1] если нужен минус, его нужно указать последним или первым
[*[(+\\\]\t] внутри скобок нужно экранировать только ] и \
\b Начало или конец слова (слева пусто или не-буква, справа буква и наоборот).
В отличие от предыдущих соответствует позиции, а не символу
\bвал вал, перевал, Перевалка
\B Не граница слова: либо и слева, и справа буквы,
либо и слева, и справа НЕ буквы
\Bвал перевал, вал, Перевалка
\Bвал\B перевал, вал, Перевалка

Квантификаторы (указание количества повторений)

Шаблон Описание Пример Применяем к тексту
{n} Ровно n повторений \d{4} 1, 12, 123, 1234, 12345
{m,n} От m до n повторений включительно \d{2,4} 1, 12, 123, 1234, 12345
{m,} Не менее m повторений \d{3,} 1, 12, 123, 1234, 12345
{,n} Не более n повторений \d{,2} 1, 12, 123
? Ноль или одно вхождение, синоним {0,1} валы? вал, валы, валов
* Ноль или более, синоним {0,} СУ\d* СУ, СУ1, СУ12, …
+ Одно или более, синоним {1,} a\)+ a), a)), a))), ba)])
*?
+?
??
{m,n}?
{,n}?
{m,}?
По умолчанию квантификаторы жадные
захватывают максимально возможное число символов.
Добавление ? делает их ленивыми,
они захватывают минимально возможное число символов
\(.*\)
\(.*?\)
(a + b) * (c + d) * (e + f)
(a + b) * (c + d) * (e + f)

Жадность в регулярках и границы найденного шаблона

Как указано выше, по умолчанию квантификаторы жадные. Этот подход решает очень важную проблему — проблему границы шаблона. Скажем, шаблон \d+ захватывает максимально возможное количество цифр. Поэтому можно быть уверенным, что перед найденным шаблоном идёт не цифра, и после идёт не цифра. Однако если в шаблоне есть не жадные части (например, явный текст), то подстрока может быть найдена неудачно. Например, если мы хотим найти «слова», начинающиеся на СУ, после которой идут цифры, при помощи регулярки СУ\d*, то мы найдём и неправильные шаблоны:

ПАСУ13 СУ12, ЧТОБЫ СУ6ЕНИЕ УДАЛОСЬ.

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

Пересечение подстрок

В обычной ситуации регулярки позволяют найти только непересекающиеся шаблоны. Вместе с проблемой границы слова это делает их использование в некоторых случаях более сложным. Например, если мы решим искать e-mail адреса при помощи неправильной регулярки \w+@\w+ (или даже лучше, [\w'._+-]+@[\w'._+-]+), то в неудачном случае найдём вот что:

foo@boo@goo@moo@roo@zoo

То есть это с одной стороны и не e-mail, а с другой стороны это не все подстроки вида текст-собака-текст, так как boo@goo и moo@roo пропущены.

Эксперименты в песочнице

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

  1. Найдите все натуральные числа (возможно, окружённые буквами);
  2. Найдите все «слова», написанные капсом (то есть строго заглавными), возможно внутри настоящих слов (аааБББввв);
  3. Найдите слова, в которых есть русская буква, а когда-нибудь за ней цифра;
  4. Найдите все слова, начинающиеся с русской или латинской большой буквы (\b — граница слова);
  5. Найдите слова, которые начинаются на гласную (\b — граница слова);;
  6. Найдите все натуральные числа, не находящиеся внутри или на границе слова;
  7. Найдите строчки, в которых есть символ * (. — это точно не конец строки!);
  8. Найдите строчки, в которых есть открывающая и когда-нибудь потом закрывающая скобки;
  9. Выделите одним махом весь кусок оглавления (в конце примера, вместе с тегами);
  10. Выделите одним махом только текстовую часть оглавления, без тегов;
  11. Найдите пустые строчки;

Регулярки в питоне

Функции для работы с регулярками живут в модуле re. Основные функции:

Функция Её смысл
re.search(pattern, string) Найти в строке string первую строчку, подходящую под шаблон pattern;
re.fullmatch(pattern, string) Проверить, подходит ли строка string под шаблон pattern;
re.split(pattern, string, maxsplit=0) Аналог str.split(), только разделение происходит по подстрокам, подходящим под шаблон pattern;
re.findall(pattern, string) Найти в строке string все непересекающиеся шаблоны pattern;
re.finditer(pattern, string) Итератор всем непересекающимся шаблонам pattern в строке string (выдаются match-объекты);
re.sub(pattern, repl, string, count=0) Заменить в строке string все непересекающиеся шаблоны pattern на repl;

Пример использования всех основных функций

import re 

match = re.search(r'\d\d\D\d\d', r'Телефон 123-12-12') 
print(match[0] if match else 'Not found') 
# -> 23-12 
match = re.search(r'\d\d\D\d\d', r'Телефон 1231212') 
print(match[0] if match else 'Not found') 
# -> Not found 

match = re.fullmatch(r'\d\d\D\d\d', r'12-12') 
print('YES' if match else 'NO') 
# -> YES 
match = re.fullmatch(r'\d\d\D\d\d', r'Т. 12-12') 
print('YES' if match else 'NO') 
# -> NO 

print(re.split(r'\W+', 'Где, скажите мне, мои очки??!')) 
# -> ['Где', 'скажите', 'мне', 'мои', 'очки', ''] 

print(re.findall(r'\d\d\.\d\d\.\d{4}', 
                 r'Эта строка написана 19.01.2018, а могла бы и 01.09.2017')) 
# -> ['19.01.2018', '01.09.2017'] 

for m in re.finditer(r'\d\d\.\d\d\.\d{4}', r'Эта строка написана 19.01.2018, а могла бы и 01.09.2017'): 
    print('Дата', m[0], 'начинается с позиции', m.start()) 
# -> Дата 19.01.2018 начинается с позиции 20 
# -> Дата 01.09.2017 начинается с позиции 45 

print(re.sub(r'\d\d\.\d\d\.\d{4}', 
             r'DD.MM.YYYY', 
             r'Эта строка написана 19.01.2018, а могла бы и 01.09.2017')) 
# -> Эта строка написана DD.MM.YYYY, а могла бы и DD.MM.YYYY 

Тонкости экранирования в питоне ('\\\\\\\\foo')

Так как символ \ в питоновских строках также необходимо экранировать, то в результате в шаблонах могут возникать конструкции вида '\\\\par'. Первый слеш означает, что следующий за ним символ нужно оставить «как есть». Третий также. В результате с точки зрения питона '\\\\' означает просто два слеша \\. Теперь с точки зрения движка регулярных выражений, первый слеш экранирует второй. Тем самым как шаблон для регулярки '\\\\par' означает просто текст \par. Для того, чтобы не было таких нагромождений слешей, перед открывающей кавычкой нужно поставить символ r, что скажет питону «не рассматривай \ как экранирующий символ (кроме случаев экранирования открывающей кавычки)». Соответственно можно будет писать r'\\par'.

Использование дополнительных флагов в питоне

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

Константа Её смысл
re.ASCII По умолчанию \w, \W, \b, \B, \d, \D, \s, \S соответствуют
все юникодные символы с соответствующим качеством.
Например, \d соответствуют не только арабские цифры,
но и вот такие: ٠١٢٣٤٥٦٧٨٩.
re.ASCII ускоряет работу,
если все соответствия лежат внутри ASCII.
re.IGNORECASE Не различать заглавные и маленькие буквы.
Работает медленнее, но иногда удобно
re.MULTILINE Специальные символы ^ и $ соответствуют
началу и концу каждой строки
re.DOTALL По умолчанию символ \n конца строки не подходит под точку.
С этим флагом точка — вообще любой символ
import re 
print(re.findall(r'\d+', '12 + ٦٧')) 
# -> ['12', '٦٧'] 
print(re.findall(r'\w+', 'Hello, мир!')) 
# -> ['Hello', 'мир'] 
print(re.findall(r'\d+', '12 + ٦٧', flags=re.ASCII)) 
# -> ['12'] 
print(re.findall(r'\w+', 'Hello, мир!', flags=re.ASCII)) 
# -> ['Hello'] 
print(re.findall(r'[уеыаоэяию]+', 'ОООО ааааа ррррр ЫЫЫЫ яяяя')) 
# -> ['ааааа', 'яяяя'] 
print(re.findall(r'[уеыаоэяию]+', 'ОООО ааааа ррррр ЫЫЫЫ яяяя', flags=re.IGNORECASE)) 
# -> ['ОООО', 'ааааа', 'ЫЫЫЫ', 'яяяя'] 

text = r""" 
Торт 
с вишней1 
вишней2 
""" 
print(re.findall(r'Торт.с', text)) 
# -> [] 
print(re.findall(r'Торт.с', text, flags=re.DOTALL)) 
# -> ['Торт\nс'] 
print(re.findall(r'виш\w+', text, flags=re.MULTILINE)) 
# -> ['вишней1', 'вишней2'] 
print(re.findall(r'^виш\w+', text, flags=re.MULTILINE)) 
# -> ['вишней2'] 

Написание и тестирование регулярных выражений

Для написания и тестирования регулярных выражений удобно использовать сервис regex101.com (не забудьте поставить галочку Python в разделе FLAVOR слева) или текстовый редактор Sublime text 3.

Задачи — 1

Задача 01. Регистрационные знаки транспортных средств

В России применяются регистрационные знаки нескольких видов.
Общего в них то, что они состоят из цифр и букв. Причём используются только 12 букв кириллицы, имеющие графические аналоги в латинском алфавите — А, В, Е, К, М, Н, О, Р, С, Т, У и Х.

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

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

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

Ввод Вывод
С227НА777 
КУ22777 
Т22В7477 
М227К19У9 
 С227НА777 
Private 
Taxi 
Fail 
Fail 
Fail

Задача 02. Количество слов

Слово — это последовательность из букв (русских или английских), внутри которой могут быть дефисы.
На вход даётся текст, посчитайте, сколько в нём слов.
PS. Задача решается в одну строчку. Никакие хитрые техники, не упомянутые выше, не требуются.

Ввод Вывод
Он --- серо-буро-малиновая редиска!! 
>>>:-> 
А не кот. 
www.kot.ru
9

Задача 03. Поиск e-mailов

Допустимый формат e-mail адреса регулируется стандартом RFC 5322.
Если говорить вкратце, то e-mail состоит из одного символа @ (at-символ или собака), текста до собаки (Local-part) и текста после собаки (Domain part). Вообще в адресе может быть всякий беспредел (вкратце можно прочитать о нём в википедии). Довольно странные штуки могут быть валидным адресом, например:
"very.(),:;<>[]\".VERY.\"very@\\ \"very\".unusual"@[IPv6:2001:db8::1]
"()<>[]:,;@\\\"!#$%&'-/=?^_`{}| ~.a"@(comment)exa-mple
Но большинство почтовых сервисов такой ад и вакханалию не допускают. И мы тоже не будем 🙂

Будем рассматривать только адреса, имя которых состоит из не более, чем 64 латинских букв, цифр и символов '._+-, а домен — из не более, чем 255 латинских букв, цифр и символов .-. Ни Local-part, ни Domain part не может начинаться или заканчиваться на .+-, а ещё в адресе не может быть более одной точки подряд.
Кстати, полезно знать, что часть имени после символа + игнорируется, поэтому можно использовать синонимы своего адреса (например, shаshkоv+spam@179.ru и shаshkоv+vk@179.ru), для того, чтобы упростить себе сортировку почты. (Правда не все сайты позволяют использовать «+», увы)

На вход даётся текст. Необходимо вывести все e-mail адреса, которые в нём встречаются. В общем виде задача достаточно сложная, поэтому у нас будет 3 ограничения:
две точки внутри адреса не встречаются;
две собаки внутри адреса не встречаются;
считаем, что e-mail может быть частью «слова», то есть в boo@ya_ru мы видим адрес boo@ya, а в foo№boo@ya.ru видим boo@ya.ru.

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

Ввод Вывод
Иван Иванович! 
Нужен ответ на письмо от ivanoff@ivan-chai.ru. 
Не забудьте поставить в копию 
serge'o-lupin@mail.ru- это важно.
ivanoff@ivan-chai.ru 
serge'o-lupin@mail.ru
NO: foo.@ya.ru, foo@.ya.ru 
PARTLY: boo@ya_ru, -boo@ya.ru-, foo№boo@ya.ru
boo@ya 
boo@ya.ru 
boo@ya.ru 

Скобочные группы (?:...) и перечисления |

Перечисления (операция «ИЛИ»)

Чтобы проверить, удовлетворяет ли строка хотя бы одному из шаблонов, можно воспользоваться аналогом оператора or, который записывается с помощью символа |. Так, некоторая строка подходит к регулярному выражению A|B тогда и только тогда, когда она подходит хотя бы к одному из регулярных выражений A или B. Например, отдельные овощи в тексте можно искать при помощи шаблона морковк|св[её]кл|картошк|редиск.

Скобочные группы (группировка плюс квантификаторы)

Зачастую шаблон состоит из нескольких повторяющихся групп. Так, MAC-адрес сетевого устройства обычно записывается как шесть групп из двух шестнадцатиричных цифр, разделённых символами - или :. Например, 01:23:45:67:89:ab. Каждый отдельный символ можно задать как [0-9a-fA-F], и можно весь шаблон записать так:
[0-9a-fA-F]{2}[:-][0-9a-fA-F]{2}[:-][0-9a-fA-F]{2}[:-][0-9a-fA-F]{2}[:-][0-9a-fA-F]{2}[:-][0-9a-fA-F]{2}

Ситуация становится гораздо сложнее, когда количество групп заранее не зафиксировано.
Чтобы разрешить эту проблему в синтаксисе регулярных выражений есть группировка (?:...). Можно писать круглые скобки и без значков ?:, однако от этого у группировки значительно меняется смысл, регулярка начинает работать гораздо медленнее. Об этом будет написано ниже. Итак, если REGEXP — шаблон, то (?:REGEXP) — эквивалентный ему шаблон. Разница только в том, что теперь к (?:REGEXP) можно применять квантификаторы, указывая, сколько именно раз должна повториться группа. Например, шаблон для поиска MAC-адреса, можно записать так:
[0-9a-fA-F]{2}(?:[:-][0-9a-fA-F]{2}){5}

Скобки плюс перечисления

Также скобки (?:...) позволяют локализовать часть шаблона, внутри которой происходит перечисление. Например, шаблон (?:он|тот) (?:шёл|плыл) соответствует каждой из строк «он шёл», «он плыл», «тот шёл», «тот плыл», и является синонимом он шёл|он плыл|тот шёл|тот плыл.

Ещё примеры

Шаблон Применяем к тексту
(?:\w\w\d\d)+ Есть миг29а, ту154б. Некоторые делают даже миг29ту154ил86.
(?:\w+\d+)+ Есть миг29а, ту154б. Некоторые делают даже миг29ту154ил86.
(?:\+7|8)(?:-\d{2,3}){4} +7-926-123-12-12, 8-926-123-12-12
(?:[Хх][аоеи]+)+ Мухахахахехо, ну хааахооохе, да хахахехохииии! Хам трамвайный.
\b(?:[Хх][аоеи]+)+\b Муха — хахахехо, ну хааахооохе, да хахахехохииии! Хам трамвайный.

Задачи — 2

Задача 04. Замена времени

Вовочка подготовил одно очень важное письмо, но везде указал неправильное время.
Поэтому нужно заменить все вхождения времени на строку (TBD). Время — это строка вида HH:MM:SS или HH:MM, в которой HH — число от 00 до 23, а MM и SS — число от 00 до 59.

Ввод Вывод
Уважаемые! Если вы к 09:00 не вернёте 
чемодан, то уже в 09:00:01 я за себя не отвечаю. 
PS. С отношением 25:50 всё нормально!
Уважаемые! Если вы к (TBD) не вернёте 
чемодан, то уже в (TBD) я за себя не отвечаю. 
PS. С отношением 25:50 всё нормально!

Задача 05. Действительные числа в паскале

Паскаль требует, чтобы реальные константы имели либо десятичную точку, либо экспоненту (начиная с буквы e или E и официально называемую масштабным коэффициентом), либо обе, в дополнение к обычному набору десятичных цифр. Если десятичная точка включена, у нее должна быть хотя бы одна десятичная цифра с каждой стороны от нее. Как и ожидалось, знак (+ или -) может предшествовать целому числу или показателю степени, или обоим. Экспоненты могут не включать дробные цифры. Пробелы могут предшествовать или следовать за реальной константой, но они не могут быть встроены в нее. Обратите внимание, что синтаксические правила Паскаля для реальных констант не делают предположений о диапазоне действительных значений, как и эта проблема. Ваша задача в этой задаче состоит в том, чтобы определить действительные константы Паскаля.

Ввод Вывод
 
1.2 
  1. 
    1.0e-55  
      e-12   
  6.5E 
        1e-12  
  +4.1234567890E-99999           
  7.6e+12.5 
   99 
 
1.2 is legal. 
1. is illegal. 
1.0e-55 is legal. 
e-12 is illegal. 
6.5E is illegal. 
1e-12 is legal. 
+4.1234567890E-99999 is legal. 
7.6e+12.5 is illegal. 
99 is illegal. 

Задача 06. Аббревиатуры

Владимир устроился на работу в одно очень важное место. И в первом же документе он ничего не понял,
там были сплошные ФГУП НИЦ ГИДГЕО, ФГОУ ЧШУ АПК и т.п. Тогда он решил собрать все аббревиатуры, чтобы потом найти их расшифровки на http://sokr.ru/. Помогите ему.

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

Ввод Вывод
Это курс информатики соответствует ФГОС и ПООП, 
это подтверждено ФГУ ФНЦ НИИСИ РАН
ФГОС 
ПООП 
ФГУ ФНЦ НИИСИ РАН

Группирующие скобки (...) и match-объекты в питоне

Match-объекты

Если функции re.search, re.fullmatch не находят соответствие шаблону в строке, то они возвращают None, функция re.finditer не выдаёт ничего. Однако если соответствие найдено, то возвращается match-объект. Эта штука содержит в себе кучу полезной информации о соответствии шаблону. Полный набор атрибутов можно посмотреть в документации, а здесь приведём самое полезное.

Метод Описание Пример
match[0],
match.group()
Подстрока, соответствующая шаблону match = re.search(r'\w+', r'$$ What??')
match[0] # -> 'What'
match.start() Индекс в исходной строке, начиная с которого идёт найденная подстрока match = re.search(r'\w+', r'$$ What??')
match.start() # -> 3
match.end() Индекс в исходной строке, который следует сразу за найденной подстрока match = re.search(r'\w+', r'$$ What??')
match.end() # -> 7

Группирующие скобки (...)

Если в шаблоне регулярного выражения встречаются скобки (...) без ?:, то они становятся группирующими. В match-объекте, который возвращают re.search, re.fullmatch и re.finditer, по каждой такой группе можно получить ту же информацию, что и по всему шаблону. А именно часть подстроки, которая соответствует (...), а также индексы начала и окончания в исходной строке. Достаточно часто это бывает полезно.

import re 
pattern = r'\s*([А-Яа-яЁё]+)(\d+)\s*' 
string = r'---   Опять45   ---' 
match = re.search(pattern, string) 
print(f'Найдена подстрока >{match[0]}< с позиции {match.start(0)} до {match.end(0)}') 
print(f'Группа букв >{match[1]}< с позиции {match.start(1)} до {match.end(1)}') 
print(f'Группа цифр >{match[2]}< с позиции {match.start(2)} до {match.end(2)}') 
### 
# -> Найдена подстрока >   Опять45   < с позиции 3 до 16 
# -> Группа букв >Опять< с позиции 6 до 11 
# -> Группа цифр >45< с позиции 11 до 13 

«>

Тонкости со скобками и нумерацией групп.

Если к группирующим скобкам применён квантификатор (то есть указано число повторений), то подгруппа в match-объекте будет создана только для последнего соответствия. Например, если бы в примере выше квантификаторы были снаружи от скобок '\s*([А-Яа-яЁё])+(\d)+\s*', то вывод был бы таким:

# -> Найдена подстрока >   Опять45   < с позиции 3 до 16 
# -> Группа букв >ь< с позиции 10 до 11 
# -> Группа цифр >5< с позиции 12 до 13 

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

import re 
pattern = r'((\d)(\d))((\d)(\d))' 
string = r'123456789' 
match = re.search(pattern, string) 
print(f'Найдена подстрока >{match[0]}< с позиции {match.start(0)} до {match.end(0)}') 
for i in range(1, 7): 
    print(f'Группа №{i} >{match[i]}< с позиции {match.start(i)} до {match.end(i)}') 
### 
# -> Найдена подстрока >1234< с позиции 0 до 4 
# -> Группа №1 >12< с позиции 0 до 2 
# -> Группа №2 >1< с позиции 0 до 1 
# -> Группа №3 >2< с позиции 1 до 2 
# -> Группа №4 >34< с позиции 2 до 4 
# -> Группа №5 >3< с позиции 2 до 3 
# -> Группа №6 >4< с позиции 3 до 4 

Группы и re.findall

Если в шаблоне есть группирующие скобки, то вместо списка найденных подстрок будет возвращён список кортежей, в каждом из которых только соответствие каждой группе. Это не всегда происходит по плану, поэтому обычно нужно использовать негруппирующие скобки (?:...).

import re 
print(re.findall(r'([a-z]+)(\d*)', r'foo3, im12, go, 24buz42')) 
# -> [('foo', '3'), ('im', '12'), ('go', ''), ('buz', '42')] 

Группы и re.split

Если в шаблоне нет группирующих скобок, то re.split работает очень похожим образом на str.split. А вот если группирующие скобки в шаблоне есть, то между каждыми разрезанными строками будут все соответствия каждой из подгрупп.

import re 
print(re.split(r'(\s*)([+*/-])(\s*)', r'12  +  13*15   - 6')) 
# -> ['12', '  ', '+', '  ', '13', '', '*', '', '15', '   ', '-', ' ', '6'] 

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

import re 
print(re.split(r'\s*([+*/-])\s*', r'12  +  13*15   - 6')) 
# -> ['12', '+', '13', '*', '15', '-', '6'] 

Использование групп при заменах

Использование групп добавляет замене (re.sub, работает не только в питоне, а почти везде) очень удобную возможность: в шаблоне для замены можно ссылаться на соответствующую группу при помощи \1, \2, \3, .... Например, если нужно даты из неудобного формата ММ/ДД/ГГГГ перевести в удобный ДД.ММ.ГГГГ, то можно использовать такую регулярку:

import re 
text = "We arrive on 03/25/2018. So you are welcome after 04/01/2018." 
print(re.sub(r'(\d\d)/(\d\d)/(\d{4})', r'\2.\1.\3', text)) 
# -> We arrive on 25.03.2018. So you are welcome after 01.04.2018. 

Если групп больше 9, то можно ссылаться на них при помощи конструкции вида \g<12>.

Замена с обработкой шаблона функцией в питоне

Ещё одна питоновская фича для регулярных выражений: в функции re.sub вместо текста для замены можно передать функцию, которая будет получать на вход match-объект и должна возвращать строку, на которую и будет произведена замена. Это позволяет не писать ад в шаблоне для замены, а использовать удобную функцию. Например, «зацензурим» все слова, начинающиеся на букву «Х»:

import re 
def repl(m): 
    return '>censored(' + str(len(m[0])) + ')<' 
text = "Некоторые хорошие слова подозрительны: хор, хоровод, хороводоводовед." 
print(re.sub(r'\b[хХxX]\w*', repl, text)) 
# -> Некоторые >censored(7)< слова подозрительны: >censored(3)<, >censored(7)<, >censored(15)<. 

Ссылки на группы при поиске

При помощи \1, \2, \3, ... и \g<12> можно ссылаться на найденную группу и при поиске. Необходимость в этом встречается довольно редко, но это бывает полезно при обработке простых xml и html.

Только пообещайте, что не будете парсить сложный xml и тем более html при помощи регулярок! Регулярные выражения для этого не подходят. Используйте другие инструменты. Каждый раз, когда неопытный программист парсит html регулярками, в мире умирает котёнок. Если кажется «Да здесь очень простой html, напишу регулярку», то сразу вспоминайте шутку про две проблемы. Не нужно пытаться парсить html регулярками, даже Пётр Митричев не сможет это сделать в общем случае 🙂 Использование регулярных выражений при парсинге html подобно залатыванию резиновой лодки шилом. Закон Мёрфи для парсинга html и xml при помощи регулярок гласит: парсинг html и xml регулярками иногда работает, но в точности до того момента, когда правильность результата будет очень важна.

Используйте lxml и beautiful soup.

import re 
text = "SPAM <foo>Here we can <boo>find</boo> something interesting</foo> SPAM" 
print(re.search(r'<(\w+?)>.*?</\1>', text)[0]) 
# -> <foo>Here we can <boo>find</boo> something interesting</foo> 
text = "SPAM <foo>Here we can <foo>find</foo> OH, NO MATCH HERE!</foo> SPAM" 
print(re.search(r'<(\w+?)>.*?</\1>', text)[0]) 
# -> <foo>Here we can <foo>find</foo> 

Задачи — 3

Задача 07. Шифровка

Владимиру потребовалось срочно запутать финансовую документацию. Но так, чтобы это было обратимо.
Он не придумал ничего лучше, чем заменить каждое целое число (последовательность цифр) на его куб. Помогите ему.

Ввод Вывод
Было закуплено 12 единиц техники 
по 410.37 рублей.
Было закуплено 1728 единиц техники 
по 68921000.50653 рублей.

Задача 08. То ли акростих, то ли акроним, то ли апроним
Акростих — осмысленный текст, сложенный из начальных букв каждой строки стихотворения.
Акроним — вид аббревиатуры, образованной начальными звуками (напр. НАТО, вуз, НАСА, ТАСС), которое можно произнести слитно (в отличие от аббревиатуры, которую произносят «по буквам», например: КГБ — «ка-гэ-бэ»).
На вход даётся текст. Выведите слитно первые буквы каждого слова. Буквы необходимо выводить заглавными.
Эту задачу можно решить в одну строчку.

Ввод Вывод
Московский государственный институт международных отношений
МГИМО
микоян авиацию снабдил алкоголем, 
народ доволен работой авиаконструктора
МАСАНДРА

Задача 09. Хайку

Хайку — жанр традиционной японской лирической поэзии века, известный с XIV века.
Оригинальное японское хайку состоит из 17 слогов, составляющих один столбец иероглифов. Особыми разделительными словами — кирэдзи — текст хайку делится на части из 5, 7 и снова 5 слогов. При переводе хайку на западные языки традиционно вместо разделительного слова использую разрыв строки и, таким образом, хайку записываются как трёхстишия.

Перед вами трёхстишия, которые претендуют на то, чтобы быть хайку. В качестве разделителя строк используются символы / . Если разделители делят текст на строки, в которых 5/7/5 слогов, то выведите «Хайку!». Если число строк не равно 3, то выведите строку «Не хайку. Должно быть 3 строки.» Иначе выведите строку вида «Не хайку. В i строке слогов не s, а j.», где строка i — самая ранняя, в которой количество слогов неправильное.

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

Ввод Вывод
Вечер за окном. / Еще один день прожит. / Жизнь скоротечна… Хайку!
Просто текст Не хайку. Должно быть 3 строки.
Как вишня расцвела! / Она с коня согнала / И князя-гордеца. Не хайку. В 1 строке слогов не 5, а 6.
На голой ветке / Ворон сидит одиноко… / Осенний вечер! Не хайку. В 2 строке слогов не 7, а 8.
Тихо, тихо ползи, / Улитка, по склону Фудзи, / Вверх, до самых высот! Не хайку. В 1 строке слогов не 5, а 6.
Жизнь скоротечна… / Думает ли об этом / Маленький мальчик. Хайку!

Шаблоны, соответствующие не конкретному тексту, а позиции

Отдельные части регулярного выражения могут соответствовать не части текста, а позиции в этом тексте. То есть такому шаблону соответствует не подстрока, а некоторая позиция в тексте, как бы «между» буквами.

Простые шаблоны, соответствующие позиции

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

Шаблон Описание Пример Применяем к тексту
^ Начало всего текста или начало строчки текста,
если flag=re.MULTILINE
^Привет
$ Конец всего текста или конец строчки текста,
если flag=re.MULTILINE
Будь здоров!$
\A Строго начало всего текста
\Z Строго конец всего текста
\b Начало или конец слова (слева пусто или не-буква, справа буква и наоборот) \bвал вал, перевал, Перевалка
\B Не граница слова: либо и слева, и справа буквы,
либо и слева, и справа НЕ буквы
\Bвал перевал, вал, Перевалка
\Bвал\B перевал, вал, Перевалка


Сложные шаблоны, соответствующие позиции (lookaround и Co)

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

Шаблон Описание Пример Применяем к тексту
(?=...) lookahead assertion, соответствует каждой
позиции, сразу после которой начинается
соответствие шаблону …
Isaac (?=Asimov) Isaac Asimov, Isaac other
(?!...) negative lookahead assertion, соответствует
каждой позиции, сразу после которой
НЕ может начинаться шаблон …
Isaac (?!Asimov) Isaac Asimov, Isaac other
(?<=...) positive lookbehind assertion, соответствует
каждой позиции, которой может заканчиваться шаблон …
Длина шаблона должна быть фиксированной,
то есть abc и a|b — это ОК, а a* и a{2,3} — нет.
(?<=abc)def abcdef, bcdef
(?<!...) negative lookbehind assertion, соответствует
каждой позиции, которой НЕ может
заканчиваться шаблон …
(?<!abc)def abcdef, bcdef


На всякий случай ещё раз. Каждый их этих шаблонов проверяет лишь то, что идёт непосредственно перед позицией или непосредственно после позиции. Если пару таких шаблонов написать рядом, то проверки будут независимы (то есть будут соответствовать AND в каком-то смысле).

lookaround на примере королей и императоров Франции

Людовик(?=VI) — Людовик, за которым идёт VI

КарлIV, КарлIX, КарлV, КарлVI, КарлVII, КарлVIII,
ЛюдовикIX, ЛюдовикVI, ЛюдовикVII, ЛюдовикVIII, ЛюдовикX, …, ЛюдовикXVIII,
ФилиппI, ФилиппII, ФилиппIII, ФилиппIV, ФилиппV, ФилиппVI

Людовик(?!VI) — Людовик, за которым идёт не VI

КарлIV, КарлIX, КарлV, КарлVI, КарлVII, КарлVIII,
ЛюдовикIX, ЛюдовикVI, ЛюдовикVII, ЛюдовикVIII, ЛюдовикX, …, ЛюдовикXVIII,
ФилиппI, ФилиппII, ФилиппIII, ФилиппIV, ФилиппV, ФилиппVI

(?<=Людовик)VI — «шестой», но только если Людовик

КарлIV, КарлIX, КарлV, КарлVI, КарлVII, КарлVIII,
ЛюдовикIX, ЛюдовикVI, ЛюдовикVII, ЛюдовикVIII, ЛюдовикX, …, ЛюдовикXVIII,
ФилиппI, ФилиппII, ФилиппIII, ФилиппIV, ФилиппV, ФилиппVI

(?<!Людовик)VI — «шестой», но только если не Людовик

КарлIV, КарлIX, КарлV, КарлVI, КарлVII, КарлVIII,
ЛюдовикIX, ЛюдовикVI, ЛюдовикVII, ЛюдовикVIII, ЛюдовикX, …, ЛюдовикXVIII,
ФилиппI, ФилиппII, ФилиппIII, ФилиппIV, ФилиппV, ФилиппVI

Шаблон Комментарий Применяем к тексту
(?<!\d)\d(?!\d) Цифра, окружённая не-цифрами Text ABC 123 A1B2C3!
(?<=#START#).*?(?=#END#) Текст от #START# до #END# text from #START# till #END#
\d+(?=_(?!_)) Цифра, после которой идёт ровно одно подчёркивание 12_34__56
^(?:(?!boo).)*?$ Строка, в которой нет boo
(то есть нет такого символа,
перед которым есть boo)
a foo and
boo and zoo
and others
^(?:(?!boo)(?!foo).)*?$ Строка, в которой нет ни boo, ни foo a foo and
boo and zoo
and others


Прочие фичи

Конечно, здесь описано не всё, что умеют регулярные выражения, и даже не всё, что умеют регулярные выражения в питоне. За дальнейшим можно обращаться к этому разделу. Из полезного за кадром осталась компиляция регулярок для ускорения многократного использования одного шаблона, использование именных групп и разные хитрые трюки.
А уж какие извращения можно делать с регулярными выражениями в языке Perl — поручик Ржевский просто отдыхает 🙂

Задачи — 4

Задача 10. CamelCase -> under_score

Владимир написал свой открытый проект, именуя переменные в стиле «ВерблюжийРегистр».
И только после того, как написал о нём статью, он узнал, что в питоне для имён переменных принято использовать подчёркивания для разделения слов (under_score). Нужно срочно всё исправить, пока его не «закидали тапками».

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

Ввод Вывод
MyVar17 = OtherVar + YetAnother2Var 
TheAnswerToLifeTheUniverseAndEverything = 42
my_var17 = other_var + yet_another2_var 
the_answer_to_life_the_universe_and_everything = 42

Задача 11. Удаление повторов

Довольно распространённая ошибка ошибка — это повтор слова.
Вот в предыдущем предложении такая допущена. Необходимо исправить каждый такой повтор (слово, один или несколько пробельных символов, и снова то же слово).

Ввод Вывод
Довольно распространённая ошибка ошибка — это лишний повтор повтор слова слова. Смешно, не не правда ли? Не нужно портить хор хоровод. Довольно распространённая ошибка — это лишний повтор слова. Смешно, не правда ли? Не нужно портить хор хоровод.

Задача 12. Близкие слова

Для простоты будем считать словом любую последовательность букв, цифр и знаков _ (то есть символов \w).
Дан текст. Необходимо найти в нём любой фрагмент, где сначала идёт слово «олень», затем не более 5 слов, и после этого идёт слово «заяц».

Ввод Вывод
Да он олень, а не заяц!
олень, а не заяц

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

Ввод Вывод
12 мало 
лучше 123 
1234 почти 
12354 хорошо 
стало 123456 
супер 1234567
12 мало 
лучше 123 
1,234 почти 
12,354 хорошо 
стало 123,456 
супер 1,234,567

Задача 14. Разделить текст на предложения

Для простоты будем считать, что:

  • каждое предложение начинается с заглавной русской или латинской буквы;
  • каждое предложение заканчивается одним из знаков препинания .;!?;
  • между предложениями может быть любой непустой набор пробельных символов;
  • внутри предложений нет заглавных и точек (нет пакостей в духе «Мы любим творчество А. С. Пушкина)».

Разделите текст на предложения так, чтобы каждое предложение занимало одну строку.
Пустых строк в выводе быть не должно. Любые наборы из полее одного пробельного символа замените на один пробел.

Ввод Вывод
В        этом 
предложении разрывы строки... Но это 
не так важно! Совсем? Да, совсем! И это 

не    должно мешать.
В этом предложении разрывы строки... 
Но это не так важно! 
Совсем? 
Да, совсем! 
И это не должно мешать. 

Задача 15. Форматирование номера телефона

Если вы когда-нибудь пытались собирать номера мобильных телефонов, то наверняка знаете, что почти любые 10 человек используют как минимум пяток различных способов записать номер телефона. Кто-то начинает с +7, кто-то просто с 7 или 8, а некоторые вообще не пишут префикс. Трёхзначный код кто-то отделяет пробелами, кто-то при помощи дефиса, кто-то скобками (и после скобки ещё пробел некоторые добавляют). После следующих трёх цифр кто-то ставит пробел, кто-то дефис, кто-то ничего не ставит. И после следующих двух цифр — тоже. А некоторые начинают за здравие, а заканчивают… В общем очень неудобно!

На вход даётся номер телефона, как его мог бы ввести человек. Необходимо его переформатировать в формат +7 123 456-78-90. Если с номером что-то не так, то нужно вывести строчку Fail!.

Ввод Вывод
+7 123 456-78-90
+7 123 456-78-90
8(123)456-78-90
+7 123 456-78-90
7(123) 456-78-90
+7 123 456-78-90
1234567890
+7 123 456-78-90
123456789
Fail!
+9 123 456-78-90
Fail!
+7 123 456+78=90
Fail!
+7(123 45678-90
+7 123 456-78-90
8(123  456-78-90
Fail!

Задача 16. Поиск e-mail’ов — 2

В предыдущей задаче мы немного схалтурили.
Однако к этому моменту задача должна стать посильной!

На вход даётся текст. Необходимо вывести все e-mail адреса, которые в нём встречаются. При этом e-mail не может быть частью слова, то есть слева и справа от e-mail’а должен быть либо конец строки, либо не-буква и при этом не один из символов '._+-, допустимых в адресе.

Ввод Вывод
Иван Иванович! 
Нужен ответ на письмо от ivanoff@ivan-chai.ru. 
Не забудьте поставить в копию 
serge'o-lupin@mail.ru- это важно.
ivanoff@ivan-chai.ru 
serge'o-lupin@mail.ru
NO: foo.@ya.ru, foo@.ya.ru, foo@foo@foo
NO: +foo@ya.ru, foo@ya-ru
NO: foo@ya_ru, -foo@ya.ru-, foo@ya.ru+
NO: foo..foo@ya.ru 
YES: (boo1@ya.ru), boo2@ya.ru!, boo3@ya.ru
boo1@ya.ru 
boo2@ya.ru 
boo3@ya.ru 

Post scriptum

PS. Текст длинный, в нём наверняка есть опечатки и ошибки. Пишите о них скорее в личку, я тут же исправлю.
PSS. Ух и намаялся я нормальный html в хабра-html перегонять. Кажется, парсер хабра писан на регулярках, иначе как объяснить все те странности, которые приходилось вылавливать бинпоиском? 🙂

Регулярные выражения в Python от простого к сложному. Подробности, примеры, картинки, упражнения

DebuggexBeta




Введение в SQL-библиотеки Python

Смею вас заверить, что все программные приложения работают с данными и чаще всего это делает [[система управления базами данных]](СУБД), Многие языки программирования имеют встроенные средства взаимодействия с СУБД, однако, другие требуют сторонних пакетов. В этом руководстве мы изучим различные SQL‑библиотеки Python, которые для этого можно использовать. Мы разработаем простое приложение для взаимодействия с базами данных [[SQLite]], [[MySQL]] и [[PostgreSQL]].
Здесь мы узнаем, как:

  • Подключиться к различным системам управления базами данных с помощью [[SQL]]‑библиотек Python;
  • Взаимодействовать с базами данных [[SQLite]], [[MySQL]] и [[PostgreSQL]];
  • Выполнять общие запросы к базе данных с помощью приложений Python;
  • Разрабатывать приложения для разных баз данных с использованием скриптов Python.

Чтобы получить максимальную отдачу от этого учебного пособия, надо знать основы [[Python]], [[SQL]] и иметь некоторый опыт работы с системами управления базами данных. Вы также должны иметь возможность загружать и импортировать пакеты в Python и знать, как устанавливать и запускать (локально или удаленно) различные серверы баз данных.

Содержание

Что такое схема базы данных

В этом руководстве мы разработаем очень маленькое приложения базы данных для социальных сетей. База данных будет состоять из четырех таблиц: 1) users, 2) posts, 3) comments и 4) likes.

Схема базы данных показана ниже:

Схема базы данных нашего приложения
Схема базы данных нашего приложения

Таблицы users и posts имеют отношение один ко многим, так как у одного пользователя может быть несколько постов. Кроме того, один пользователь может оставлять множество комментариев к посту и один пост может иметь несколько различных комментариев. Таким образом, и users, и posts имеют отношения один ко многим с таблицей comments. Та же ситуация с таблицей likes, с которой и users, и posts так-же имеют отношения один ко многим.

Использование SQL‑библиотек Python для связи с базами данных

Прежде чем начать работу с какой-либо базой данных через SQL‑библиотеку Python необходимо к ней подключиться. В этом разделе мы узнаем, что это такое и как это сделать с базами данных SQLite, MySQL и PostgreSQL из приложения Python.

Примечание:

Вам понадобятся серверы баз данных MySQL и PostgreSQL. Перед выполнением своих сценариев они должны быть установлены и запущены. Для быстрого ознакомления с тем, как запустить сервер MySQL, ознакомьтесь с разделом MySQL Запуск проекта Django. Чтобы узнать, как создать базу данных в PostgreSQL, ознакомьтесь с разделом «Настройка базы данных» в Предотвращение атак SQL‑инъекций с помощью Python.

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

SQLite

SQLite, вероятно, самая простая база данных, к которой можно подключиться из приложения Python и для этого нет необходимости подключать внешние модули. По умолчанию установка Python содержит SQL‑библиотеку с именем sqlite3 , которую мы можем использовать для работы с SQLite.

Более того, базы данных SQLite являются безсерверными и автономными, поскольку они читают и записывают данные в файл. Это означает, что, в отличие от MySQL и PostgreSQL, вам даже не нужно устанавливать и запускать сервер для выполнения операций с базой данных SQLite!

Вот как мы используете sqlite3 для подключения к базе данных SQLite в Python:

import sqlite3
from sqlite3 import Error
def create_connection(path):
 connection = None
 try:
 connection = sqlite3.connect(path)
 print("Connection to SQLite DB successful")
 except Error as e:
 print(f"The error '{e}' occurred")
 return connection

Вот как работает этот код:

  • Строки 1 и 2 импортируют модуль sqlite3 и класс Error.
  • Строка 4 определяет функцию .create_connection(), которая принимает путь к базе данных SQLite.
  • Строка 7 использует .connect() из модуля sqlite3 и принимает путь к базе данных SQLite в качестве параметра. Если база данных существует в указанном месте, то соединение с базой данных установлено. В противном случае создается новая база данных в указанном месте и устанавливается соединение.
  • Строка 8 печатает статус успешного подключения к базе данных.
  • Строка 9 ловит любое исключение, которое может быть выдано, если .connect() не удается установить соединение.
  • Строка 10 отображает сообщение об ошибке в консоли.

Функция sqlite3.connect(path) возвращает объект connection, который, в свою очередь, возвращается функцией create_connection(). Этот объект connection можно использовать для выполнения запросов к базе данных SQLite. Следующий скрипт создает соединение с базой данных SQLite:

connection = create_connection("E:\\db_python_app.sqlite")

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

MySQL

В отличие от SQLite для подключения к базе данных MySQL требуется дополнительный модуль и нужно установить драйвер Python SQL. Одним из таких драйверов является mysql-connector-python. Вы можете загрузить этот модуль Python SQL с помощью pip:

$ pip install mysql-connector-python

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

  1. Установите соединение с сервером MySQL.
  2. Выполните отдельный запрос, чтобы создать базу данных.

Определите функцию, которая подключается к серверу базы данных MySQL и возвращает объект подключения:

import mysql.connector
from mysql.connector import Error
def create_connection(host_name, user_name, user_password):
 connection = None
 try:
 connection = mysql.connector.connect(
  host=host_name,
  user=user_name,
  passwd=user_password
 )
 print("Connection to MySQL DB successful")
 except Error as e:
 print(f"The error '{e}' occurred")
 return connection
connection = create_connection("localhost", "root", "")

В этом скрипте мы определили функцию create_connection(), которая имеет следующие параметры:

  1. host_name
  2. user_name
  3. user_password

Модуль SQL Python mysql.connector содержит метод .connect(), который мы используете в строке 7 для подключения к серверу базы данных MySQL. Как только соединение установлено, объект connection возвращается вызывающей функции. Наконец, в строке 18 мы вызываете create_connection() с именем хоста, именем пользователя и паролем.

Пока мы только установили соединение. База данных еще не создана. Для этого мы запишите другую функцию create_database(), которая принимает два параметра:

  1. connection является объектом connection с сервером базы данных, с которым мы хотите взаимодействовать
  2. query  — запрос, который создает базу данных.

Вот как выглядит эта функция:

def create_database(connection, query):
 cursor = connection.cursor()
 try:
 cursor.execute(query)
 print("Database created successfully")
 except Error as e:
 print(f"The error '{e}' occurred")

Для выполнения запросов мы используем объект cursor. Запрос query, который должен быть выполнен, передается в cursor.execute() в формате string
Создайте базу данных с именем db_python_app для своего приложения социальных сетей на сервере базы данных MySQL:

create_database_query = "CREATE DATABASE db_python_app"
create_database(connection, create_database_query)

Теперь мы создали базу данных db_python_app на сервере базы данных. Однако объект connection, возвращаемый функцией create_connection(), подключен к серверу базы данных MySQL. Вам необходимо подключиться к базе данных db_python_app. Для этого мы можем изменить create_connection() следующим образом:

def create_connection(host_name, user_name, user_password, db_name):
 connection = None
 try:
 connection = mysql.connector.connect(
  host=host_name,
  user=user_name,
  passwd=user_password,
  database=db_name
 )
 print("Connection to MySQL DB successful")
 except Error as e:
 print(f"The error '{e}' occurred")
 return connection

В строке 8 видно, что create_connection() теперь принимает дополнительный параметр с именем db_name. Этот параметр указывает имя базы данных, к которой мы хотим подключиться. Мы можем передать имя базы данных, к которой хотим подключиться при вызове этой функции:

connection = create_connection("localhost", "root", "", "db_python_app")

Приведенный выше скрипт успешно вызывает create_connection() и подключается к базе данных db_python_app.

PostgreSQL

В библиотеке SQL Python, как и для MySQL нет функций, которые можно использовать для работы с базой данных PostgreSQL. Вместо этого вам нужно установить сторонний драйвер SQL для взаимодействия с PostgreSQL. Одним из таких драйверов SQL для PostgreSQL является psycopg2. Выполните следующую команду на своем терминале, чтобы установить SQL‑модуль Python psycopg2 :

$ pip install psycopg2

Как и в случае SQLite и MySQL, мы определим функцию create_connection() для подключения к нашей базе данных PostgreSQL:

import psycopg2
from psycopg2 import OperationalError
def create_connection(db_name, db_user, db_password, db_host, db_port):
 connection = None
 try:
 connection = psycopg2.connect(
  database=db_name,
  user=db_user,
  password=db_password,
  host=db_host,
  port=db_port,
 )
 print("Connection to PostgreSQL DB successful")
 except OperationalError as e:
 print(f"The error '{e}' occurred")
 return connection

Мы используем psycopg2.connect() для подключения к серверу PostgreSQL из своего приложения на Python.

Кроме того, create_connection() создаёт соединение с базой данных PostgreSQLю. Для начала мы создаём подключение к базе данных по‑умолчанию postgres, используя следующую строку:

connection = create_connection(
 "postgres", "postgres", "abc123", "127.0.0.1", "5432"
)

Затем мы должны создать базу данных db_python_app внутри базы данных по‑умолчанию postgres. Вы можете определить функцию для выполнения любого запроса SQL в PostgreSQL. Ниже мы определяете create_database() для создания новой базы данных на сервере базы данных PostgreSQL:

def create_database(connection, query):
 connection.autocommit = True
 cursor = connection.cursor()
 try:
 cursor.execute(query)
 print("Query executed successfully")
 except OperationalError as e:
 print(f"The error '{e}' occurred")
create_database_query = "CREATE DATABASE db_python_app"
create_database(connection, create_database_query)

Запустив приведенный мыше скрипт, на своем сервере баз данных PostgreSQL мы увидите базу данных db_python_app.
Прежде чем выполнять запросы к базе данных db_python_app, необходимо подключиться к ней:

connection = create_connection(
 "db_python_app", "postgres", "abc123", "127.0.0.1", "5432"
)

После выполнения мышеописанного сценария будет установлено соединение с базой данных db_python_app, расположенной на сервере базы данных postgres. Здесь 127.0.0.1 есть IP-адрес хоста сервера базы данных, а 5432  — номер порта сервера баз данных.

Создание таблиц

В предыдущем разделе мы увидели, как подключаться к серверам баз данных SQLite, MySQL и PostgreSQL, используя разные SQL‑библиотеки Python. Вы создали базу данных db_python_app на всех трех серверах баз данных. В этом разделе мы узнаем, как создавать таблицы в этих трех базах данных.
Как уже говорилось, мы создадим четыре таблицы:

  1. users
  2. posts
  3. comments
  4. likes

Начнем с SQLite.

SQLite

Для выполнения запросов в SQLite используйте cursor.execute(). Определите функцию execute_query(), которая использует этот метод. Наша функция примет объект connection и строку запроса, которую мы передадим в cursor.execute().

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

Примечание:

Этот сценарий должен быть записан в том же файле, где мы создали соединение для своей базы данных SQLite.

Вот наша функция:

def execute_query(connection, query):
 cursor = connection.cursor()
 try:
 cursor.execute(query)
 connection.commit()
 print("Query executed successfully")
 except Error as e:
 print(f"The error '{e}' occurred")

Этот код пытается выполнить запрос query и при необходимости печатает сообщение об ошибке.

Далее, запишите свой запрос query:

create_users_table = """
CREATE TABLE IF NOT EXISTS users(
 id INTEGER PRIMARY KEY AUTOINCREMENT,
 name TEXT NOT NULL,
 age INTEGER,
 gender TEXT,
 nationality TEXT
);
"""

Это говорит о создании таблицы users со следующими пятью столбцами:

  1. id
  2. name
  3. age
  4. gender
  5. nationality

Наконец, вызовем execute_query() для создания таблицы. Передадим объект connection, созданный в предыдущем разделе, вместе со строкой create_users_table, содержащей запрос создания таблицы:

execute_query(connection, create_users_table)

Следующий запрос используется для создания таблицы posts :

create_post_table = """CREATE TABLE IF NOT EXISTS posts(
 id INTEGER PRIMARY KEY AUTOINCREMENT, 
 title TEXT NOT NULL, 
 description TEXT NOT NULL, 
 user_id INTEGER NOT NULL, 
 FOREIGN KEY(user_id) REFERENCES users(id)
);
"""

Поскольку между таблицами users и posts существует отношение один ко многим, существует внешний ключ user_id в таблице posts, который ссылается на столбец id в таблице users. Выполните следующий скрипт для создания таблицы posts :

execute_query(connection, create_posts_table)

Наконец, мы можем создать таблицы comments и likes с помощью следующего сценария:

create_comments_table = """
CREATE TABLE IF NOT EXISTS comments(
 id INTEGER PRIMARY KEY AUTOINCREMENT, 
 text TEXT NOT NULL, 
 user_id INTEGER NOT NULL, 
 post_id INTEGER NOT NULL, 
 FOREIGN KEY(user_id) REFERENCES users(id) FOREIGN KEY(post_id) REFERENCES posts(id)
);
"""
create_likes_table = """
CREATE TABLE IF NOT EXISTS likes(
 id INTEGER PRIMARY KEY AUTOINCREMENT, 
 user_id INTEGER NOT NULL, 
 post_id integer NOT NULL, 
 FOREIGN KEY(user_id) REFERENCES users(id) FOREIGN KEY(post_id) REFERENCES posts(id)
);
"""
execute_query(connection, create_comments_table) 
execute_query(connection, create_likes_table)

Вы можете видеть, что creating tables в SQLite очень похоже на использование необработанного SQL. Все, что нужно сделать, это сохранить запрос в строковой переменной и затем передать эту переменную в cursor.execute().

MySQL

Мы будем использовать SQL‑модуль Python mysql‑connector‑python для создания таблиц в MySQL. Как и в SQLite, нам нужно передать запрос в cursor.execute(), который возвращается путем вызова .cursor() в объект connection. Мы можем создать другую функцию execute_query(), которая принимает строку connection и query :

def execute_query(connection, query):
 cursor = connection.cursor()
 try:
 cursor.execute(query)
 connection.commit()
 print("Query executed successfully")
 except Error as e:
 print(f"The error '{e}' occurred")

В строке 4 мы передаём запрос query в cursor.execute().
Для создания таблицы users используем следующую функцию:

create_users_table = """
CREATE TABLE IF NOT EXISTS users(
 id INT AUTO_INCREMENT, 
 name TEXT NOT NULL, 
 age INT, 
 gender TEXT, 
 nationality TEXT, 
 PRIMARY KEY(id)
) ENGINE = InnoDB
"""
execute_query(connection, create_users_table)

Запрос для реализации отношения внешнего ключа в MySQL немного отличается по сравнению с SQLite. Более того, MySQL использует ключевое слово AUTO_INCREMENT(вместо ключевого слова AUTOINCREMENT в SQLite) для создания столбцов, значения которых автоматически увеличиваются при создании или вставке новых записей.

Следующий скрипт создает таблицу posts с внешним ключом user_id, который ссылается на столбец id в таблице users:

create_posts_table = """
CREATE TABLE IF NOT EXISTS posts(
 id INT AUTO_INCREMENT, 
 title TEXT NOT NULL, 
 description TEXT NOT NULL, 
 user_id INTEGER NOT NULL, 
 FOREIGN KEY fk_user_id(user_id) REFERENCES users(id), 
 PRIMARY KEY(id)
) ENGINE = InnoDB
"""

execute_query(connection, create_posts_table)

Аналогично, для создания таблиц comments и likes можно передать соответствующие запросы CREATE в execute_query().

PostgreSQL

Как и в случае базами данных SQLite и MySQL, объект connection, возвращаемый функцией psycopg2.connect(), содержит объект cursor. Можно использовать cursor.execute() для выполнения запросов к вашей базе данных PostgreSQL.

Запишем функцию execute_query():

def execute_query(connection, query):
 connection.autocommit = True
 cursor = connection.cursor()
 try:
 cursor.execute(query)
 print("Query executed successfully")
 except OperationalError as e:
 print(f"The error '{e}' occurred")

Эту функцию можно использовать для создания таблиц, вставки, изменения и удаления записей в базе данных PostgreSQL.

Теперь создадим таблицу users внутри базы данных db_python_app:

create_users_table = """
CREATE TABLE IF NOT EXISTS users(
 id SERIAL PRIMARY KEY,
 name TEXT NOT NULL, 
 age INTEGER,
 gender TEXT,
 nationality TEXT
)
"""

execute_query(connection, create_users_table)

Посмотрите, запрос на создание таблицы users в PostgreSQL немного отличается от SQLite и MySQL. Здесь есть ключевое слово SERIAL, которое используется для создания столбцов, где значения увеличиваются автоматически. Напомним, что в MySQL используется ключевое слово AUTO_INCREMENT.

Кроме того, ссылки на внешние ключи также указываются по-разному так, как показано в следующем сценарии для создания таблицы posts:

create_posts_table = """
CREATE TABLE IF NOT EXISTS posts(
 id SERIAL PRIMARY KEY, 
 title TEXT NOT NULL, 
 description TEXT NOT NULL, 
 user_id INTEGER REFERENCES users(id)
)
"""

execute_query(connection, create_posts_table)
 

Чтобы создать таблицу comments, вам нужно написать запрос CREATE для таблицы comments и передать его в execute_query(). Процесс создания таблицы likes такой же. Вам нужно только изменить запрос CREATE, чтобы создать таблицу likes вместо таблицы comments.

Вставка записей

В предыдущем разделе мы показали, как создавать таблицы в базах данных SQLite, MySQL и PostgreSQL с использованием различных SQL‑модулей Python. В этом разделе вы узнаете, как вставить записи в свои таблицы.

SQLite

Для вставки записи в базу данных SQLite можно использовать ту же функцию execute_query(), которую мы уже использовали для создания таблиц. Во-первых, мы должны сохранить запрос INSERT INTO в строке, затем передать объект connection и строку query в execute_query().

Давайте вставим пять записей в таблицу users :

create_users = """
INSERT INTO
 users(name, age, gender, nationality)
VALUES
('James', 25, 'male', 'USA'),
('Leila', 32, 'female', 'France'),
('Brigitte', 35, 'female', 'England'),
('Mike', 40, 'male', 'Denmark'),
('Elizabeth', 21, 'female', 'Canada');
"""

execute_query(connection, create_users) 

Поскольку для столбца id установлено автоматическое увеличение, нам не нужно указывать значение столбца id для этих users. Таблица users автоматически заполнит эти пять записей значениями id от 1 до 5, если до сих пор таблица была пустая.

Теперь вставим шесть записей в таблицу posts :

create_posts = """
INSERT INTO
 posts(title, description, user_id)
VALUES
("Happy", "I am feeling very happy today", 1),
("Hot Weather", "The weather is very hot today", 2),
("Help", "I need some help with my work", 2),
("Great News", "I am getting married", 1),
("Interesting Game", "It was a fantastic game of tennis", 5),
("Party", "Anyone up for a late-night party today?", 3);
"""

execute_query(connection, create_posts)

Важно отметить, что столбец user_id таблицы posts является внешним ключом, который ссылается на столбец id таблица users. Это означает, что столбец user_id должен содержать значение, которое уже существует в столбце id таблицы users. Если его не существует, то это ошибка.

Аналогично, следующий скрипт вставляет записи в таблицы comments и likes:

create_comments = """
INSERT INTO
 comments(text, user_id, post_id)
VALUES
('Count me in', 1, 6),
('What sort of help?', 5, 3),
('Congrats buddy', 2, 4),
('I was rooting for Nadal though', 4, 5),
('Help with your thesis?', 2, 3),
('Many congratulations', 5, 4);
"""

create_likes = """
INSERT INTO
 likes(user_id, post_id)
VALUES
(1, 6),
(2, 3),
(1, 5),
(5, 4),
(2, 4),
(4, 2),
(3, 6);
"""

execute_query(connection, create_comments)
execute_query(connection, create_likes)

В обоих случаях запрос INSERT INTO сохраняется в строке и выполняется с помощью execute_query().

MySQL

Есть два способа вставить записи в базы данных MySQL из приложения Python. Первый подход похож на SQLite. Можно сохранить запрос INSERT INTO в строке, а затем использовать cursor.execute() для вставки записей.

Ранее мы определили функцию-обертку execute_query(), которую использовали для вставки записей. Можно повторно использовать эту же функцию сейчас для выполнения вставки записи в нашу таблицу MySQL. Следующий скрипт вставляет записи в таблицу users с помощью execute_query():

create_users = """
INSERT INTO
 `users`(`name`, `age`, `gender`, `nationality`)
VALUES
('James', 25, 'male', 'USA'),
('Leila', 32, 'female', 'France'),
('Brigitte', 35, 'female', 'England'),
('Mike', 40, 'male', 'Denmark'),
('Elizabeth', 21, 'female', 'Canada');
"""

execute_query(connection, create_users)

Второй подход использует cursor.executemany(), который принимает два параметра:

  1. Строка запроса с заполнителями для вставляемой новой строки;
  2. Список записей, которые мы хотим вставить.

Посмотрите на следующий пример, который вставляет две записи в таблицу likes:

sql = "INSERT INTO likes( user_id, post_id) VALUES( %s, %s)"
val = [(4, 5),(3, 4)]

cursor = connection.cursor()
cursor.executemany(sql, val)
connection.commit()

От нас зависит, какой подход выбрать для вставки записей в таблицу MySQL. Эксперт SQL может смело использовать .execute(). А вот если слово с [[SQL]] повергает в лёгкое недоумение, то будет проще использовать .executemany(). При любом из этих двух подходов можно успешно добиться своего и вставить нужные записи в таблицы posts, comments и likes.

PostgreSQL

В предыдущем разделе мы показали два подхода для вставки записей в таблицы базы данных SQLite. Первый использует запрос строки SQL, а второй использует .executemany(). psycopg2 следует этому второму подходу, хотя .execute() используется для выполнения запроса на основе заполнителя.

Вы передаете SQL‑запрос с заполнителями и списком записей в .execute(). Каждая запись в списке будет кортежем, где значения кортежей соответствуют значениям столбцов в таблице базы данных. Вот как вы можете вставить пользовательские записи в таблицу users в базе данных PostgreSQL:

users = [
 ("James", 25, "male", "USA"),
 ("Leila", 32, "female", "France"),
 ("Brigitte", 35, "female", "England"),
 ("Mike", 40, "male", "Denmark"),
 ("Elizabeth", 21, "female", "Canada"),
]

user_records = ", ".join(["%s"] * len(users))

insert_query =(
  f"INSERT INTO users(name, age, gender, nationality) VALUES {user_records}"
)

connection.autocommit = True
cursor = connection.cursor()
cursor.execute(insert_query, users)

Приведенный выше сценарий создает список users, который содержит пять записей пользователей в виде кортежей. Затем вы создаете строку-заполнитель с пятью элементами-заполнителями(% s), которые соответствуют пяти пользовательским записям. Строка-заполнитель объединяется с запросом, который вставляет записи в таблицу users. Наконец, строка запроса и пользовательские записи передаются в .execute(). Приведенный выше скрипт успешно вставляет пять записей в таблицу users.

Взгляните на другой пример вставки записей в таблицу PostgreSQL. Следующий скрипт вставляет записи в таблицу posts :

posts = [
 ("Happy", "I am feeling very happy today", 1),
 ("Hot Weather", "The weather is very hot today", 2),
 ("Help", "I need some help with my work", 2),
 ("Great News", "I am getting married", 1),
 ("Interesting Game", "It was a fantastic game of tennis", 5),
 ("Party", "Anyone up for a late-night party today?", 3),
]

post_records = ", ".join(["%s"] * len(posts))

insert_query =(
  f"INSERT INTO posts(title, description, user_id) VALUES {post_records}"
)

connection.autocommit = True
cursor = connection.cursor()
cursor.execute(insert_query, posts)

You can insert records into the comments and likes tables with the same approach.

Отбор записей

In this section, you’ll see how to select records from database tables using the different Python SQL modules. In particular, you’ll see how to perform SELECT queries on your SQLite, MySQL, and PostgreSQL databases.

SQLite

Чтобы выбрать записи с использованием SQLite, вы снова можете использовать cursor.execute(). Однако после того, как вы это сделаете, вам нужно будет вызвать .fetchall(). Этот метод возвращает список кортежей, где каждый кортеж сопоставлен с соответствующей строкой в извлеченных записях.

Чтобы упростить процесс, вы можете создать функцию execute_read_query() :

def execute_read_query(connection, query):
  cursor = connection.cursor()
  result = None
  try:
    cursor.execute(query)
    result = cursor.fetchall()
    return result
  except Error as e:
    print(f"The error '{e}' occurred")

Эта функция принимает объект connection и запрос SELECT и возвращает выбранную запись.

SELECT

Теперь давайте выберем все записи из таблицы users:

select_users = "SELECT * from users"
users = execute_read_query(connection, select_users)

for user in users:
  print(user)

В приведенном выше сценарии запрос SELECT выбирает всех пользователей из таблицы users. Это передается в execute_read_query(), который возвращает все записи из таблицы users. Затем записи просматриваются и печатаются на консоль.

Примечание:

Не рекомендуется использовать SELECT * для больших таблиц, поскольку это может привести к большому количеству операций ввода-вывода, которые увеличивают сетевой трафик.

Результат вышеприведенного запроса выглядит следующим образом:

(1, 'James', 25, 'male', 'USA')
(2, 'Leila', 32, 'female', 'France')
(3, 'Brigitte', 35, 'female', 'England')
(4, 'Mike', 40, 'male', 'Denmark')
(5, 'Elizabeth', 21, 'female', 'Canada')

Таким же образом вы можете извлечь все записи из таблицы posts с помощью приведенного ниже сценария:

select_posts = "SELECT * FROM posts"
posts = execute_read_query(connection, select_posts)

for post in posts:
  print(post)

Вывод выглядит так:

(1, 'Happy', 'I am feeling very happy today', 1)
(2, 'Hot Weather', 'The weather is very hot today', 2)
(3, 'Help', 'I need some help with my work', 2)
(4, 'Great News', 'I am getting married', 1)
(5, 'Interesting Game', 'It was a fantastic game of tennis', 5)
(6, 'Party', 'Anyone up for a late-night party today?', 3)

В результате отображаются все записи в таблице posts.

JOIN

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

select_users_posts = """
SELECT
 users.id,
 users.name,
 posts.description
FROM
 posts
 INNER JOIN users ON users.id = posts.user_id
"""

users_posts = execute_read_query(connection, select_users_posts)

for users_post in users_posts:
  print(users_post)

Вот результат:

(1, 'James', 'I am feeling very happy today')
(2, 'Leila', 'The weather is very hot today')
(2, 'Leila', 'I need some help with my work')
(1, 'James', 'I am getting married')
(5, 'Elizabeth', 'It was a fantastic game of tennis')
(3, 'Brigitte', 'Anyone up for a late night party today?')

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

select_posts_comments_users = """
SELECT
 posts.description as post,
 text as comment,
 name
FROM
 posts
 INNER JOIN comments ON posts.id = comments.post_id
 INNER JOIN users ON users.id = comments.user_id
"""

posts_comments_users = execute_read_query(
  connection, select_posts_comments_users
)

for posts_comments_user in posts_comments_users:
  print(posts_comments_user)

Смотрим результат:

('Anyone up for a late night party today?', 'Count me in', 'James')
('I need some help with my work', 'What sort of help?', 'Elizabeth')
('I am getting married', 'Congrats buddy', 'Leila')
('It was a fantastic game of tennis', 'I was rooting for Nadal though', 'Mike')
('I need some help with my work', 'Help with your thesis?', 'Leila')
('I am getting married', 'Many congratulations', 'Elizabeth')

Результат показывает, что имена столбцов не возвращаются функцией .fetchall(). Чтобы получить имена столбцов нужно использовать атрибут .description объекта cursor. Например, следующий скрипт возвращает имена вех столбцов для вышеуказанного запроса:

cursor = connection.cursor()
cursor.execute(select_posts_comments_users)
cursor.fetchall()

column_names = [description[0] for description in cursor.description]
print(column_names)

Вот эти имена:

['post', 'comment', 'name'] 

Вы можете увидеть имена столбцов для данного запроса.

WHERE

Теперь мы выполним запрос SELECT, который возвращает сообщение вместе с общим количеством лайков, полученных этим сообщением:

select_post_likes = """
SELECT
 description as Post,
 COUNT(likes.id) as Likes
FROM
 likes,
 posts
WHERE
 posts.id = likes.post_id
GROUP BY
 likes.post_id
"""

post_likes = execute_read_query(connection, select_post_likes)

for post_like in post_likes:
  print(post_like)

Результат выглядит следующим образом:

('The weather is very hot today', 1)
('I need some help with my work', 1)
('I am getting married', 2)
('It was a fantastic game of tennis', 1)
('Anyone up for a late night party today?', 2)

Используя предложение WHERE можно получать более детальнык результаты.

MySQL

Процесс выбора записей в MySQL абсолютно идентичен процессу выбора записей в SQLite. Вы можете использовать cursor.execute(), а затем .fetchall(). Следующий скрипт описывает функцию-обёртку execute_read_query(), которую можно использовать для отбора записей:

def execute_read_query(connection, query):
  cursor = connection.cursor()
  result = None
  try:
    cursor.execute(query)
    result = cursor.fetchall()
    return result
  except Error as e:
    print(f"The error '{e}' occurred")

Теперь получим все записи из таблицы users:

select_users = "SELECT * FROM users"
users = execute_read_query(connection, select_users)

for user in users:
  print(user)

Результат будет похож на то, что мы уже видели с SQLite.

PostgreSQL

Процесс выбора записей из таблицы PostgreSQL с помощью модуля psycopg2 аналогичен тому, что мы делали с SQLite и MySQL. Опять же, мы будем использовать cursor.execute(), а затем .fetchall() для выбора записей из таблиц PostgreSQL. Следующий скрипт выбирает все записи из таблицы users и выводит их на консоль:

def execute_read_query(connection, query):
  cursor = connection.cursor()
  result = None
  try:
    cursor.execute(query)
    result = cursor.fetchall()
    return result
  except OperationalError as e:
    print(f"The error '{e}' occurred")

select_users = "SELECT * FROM users"
users = execute_read_query(connection, select_users)

for user in users:
  print(user)

Опять же, результат будет похож на то, что мы видели раньше.

Обновление записей в таблицах

В последнем разделе вы увидели, как получить записи из баз данных SQLite, MySQL и PostgreSQL. В этом разделе вы узнаете о процессе обновления записей в SQLite, PostgresSQL и MySQL.

SQLite

Обновление записей в SQLite довольно просто. Мы снова можем использовать execute_query(), например, обновим пост с id, равным 2. Сначала используем SELECT и прочитаем это пост:

select_post_description = "SELECT description FROM posts WHERE id = 2"

post_description = execute_read_query(connection, select_post_description)

for description in post_description:
  print(description)

Мы должны увидеть следующий вывод:

('The weather is very hot today',) 

Следующий скрипт обновляет описание:

update_post_description = """
UPDATE
 posts
SET
 description = "The weather has become pleasant now"
WHERE
 id = 2
"""

execute_query(connection, update_post_description)

Теперь, если выполнить запрос SELECT еще раз, то должны увидеть следующий результат:

('The weather has become pleasant now',) 

Запись была обновлена.

MySQL

Процесс обновления записей в MySQL с помощью mysql‑connector‑python является полной копией SQL-модуля sqlite3. Нам необходимо передать строковый запрос в cursor.execute(). Например, следующий скрипт обновляет описание поста с id равным 2:

update_post_description = """
UPDATE
 posts
SET
 description = "The weather has become pleasant now"
WHERE
 id = 2
"""

execute_query(connection, update_post_description)

Опять же, мы использовали нашу функцию-обертку execute_query()для обновления описания публикации.

PostgreSQL

Запрос на обновление PostgreSQL подобен тому, что мы видели в SQLite и MySQL. Можно использовать вышеупомянутые сценарии для обновления записей в таблице PostgreSQL.

Удаление записей в таблицах

В этом разделе вы увидите, как удалять записи таблиц в базах данных SQLite, MySQL и PostgreSQL. Процесс удаления записей одинаков для всех трех СУБД и запрос DELETE для трех баз данных одинаков.

SQLite

Опять же воспользуемся execute_query() для удаления записи из нашей базы данных SQLite. Все, что нужно сделать, это передать объект connection и строку запроса для записи, которую хотим удалить, в execute_query(). Затем execute_query() создаст объект cursor и, используя connection, передаст строку запроса в cursor.execute(), который удалит записи.

Например, попробуйте удалить комментарий с id, равным 5 :

delete_comment = "DELETE FROM comments WHERE id = 5"
execute_query(connection, delete_comment)

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

MySQL

Процесс удаления в MySQL также похож на SQLite и показан в следующем примере:

delete_comment = "DELETE FROM comments WHERE id = 2"
execute_query(connection, delete_comment)

Здесь мы удаляем второй комментарий из таблицы comments базы данных db_python_app на своем сервере MySQL.

PostgreSQL

Запрос на удаление для PostgreSQL также похож на SQLite и MySQL. Вы можете написать строку запроса на удаление, используя ключевое слово DELETE, а затем передав запрос и объект connection в execute_query(). Это удалит указанные записи из вашей базы данных PostgreSQL.

Заключение

Из этого руководства мы узнали, как использовать три распространенные SQL‑библиотеки Python. sqlite3, mysql-connector-python и psycopg2 позволяют подключать приложение Python к базам данных SQLite, MySQL и PostgreSQL соответственно.
Теперь мы можем:

  • Взаимодействовать с базами данных SQLite, MySQL или PostgreSQL
  • Использовать три разных модуля Python SQL
  • Выполннять SQL‑запросов к различным базам данных из приложения Python

Однако это только вершина айсберга! Существуют также SQL‑библиотеки Python дляобъектно-реляционного отображения, такие как SQLAlchemy и Django ORM, которые автоматизируют задачу взаимодействия с базой данных в Python. Подробнее об этих библиотеках мы узнаем в других руководствах в нашем базах данных Python.

По мотивам: Introduction to Python SQL Libraries