Русский
Русский
English
Статистика
Реклама

Python 3

Перевод Как вернуть сразу несколько значений из функции в Python 3

28.08.2020 14:05:29 | Автор: admin
Сегодня мы делимся с вами переводом статьи, которую нашли на сайте medium.com. Автор, Vivek Coder, рассказывает о способах возврата значений из функции в Python и объясняет, как можно отличить друг от друга разные структуры данных.


Фото с сайта Unsplash. Автор: Vipul Jha

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

def hours_to_write(happy_hours):   week1 = happy_hours + 2   week2 = happy_hours + 4   week3 = happy_hours + 6   return [week1, week2, week3] print(hours_to_write(4))# [6, 8, 10]

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

Способ 1: возврат значений с помощью словарей


Словари содержат комбинации элементов, которые представляют собой пары ключ значение (key:value), заключенные в фигурные скобки ({}).

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

people={  'Robin': 24,  'Odin': 26,  'David': 25}

А теперь перейдем к функции, которая возвращает словарь с парами ключ значение.

# A Python program to return multiple values using dictionary# This function returns a dictionary def people_age():     d = dict();     d['Jack'] = 30    d['Kim'] = 28    d['Bob'] = 27    return dd = people_age() print(d)# {'Bob': 27, 'Jack': 30, 'Kim': 28}

Способ 2: возврат значений с помощью списков


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

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

# A Python program to return multiple values using list def test():     str1 = "Happy"    str2 = "Coding"    return [str1, str2]; list = test() print(list)# ['Happy', 'Coding']

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

def natural_numbers(numbers = []):      for i in range(1, 16):       numbers.append(i)   return numbers print(natural_numbers())# [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]

Способ 3: возврат значений с помощью кортежей


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

Кортежи напоминают списки, однако их нельзя изменить после того, как они были объявлены. А еще, как правило, кортежи быстрее в работе, чем списки. Кортеж можно создать, отделив элементы запятыми: x, y, z или (x, y, z).

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

Bob = ("Bob", 7, "Google")

А вот пример написания функции для возврата кортежа.

# A Python program to return multiple values using tuple# This function returns a tuple def fun():     str1 = "Happy"    str2 = "Coding"    return str1, str2; # we could also write (str1, str2)str1, str2= fun() print(str1) print(str2)# Happy  Coding

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

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

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

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

def student(name, class):   return (name, class)print(student("Brayan", 10))# ('Brayan', 10)

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

Способ 4: возврат значений с помощью объектов


Тут все так же, как в C/C++ или в Java. Можно просто сформировать класс (в C он называется структурой) для сохранения нескольких признаков и возврата объекта класса.

# A Python program to return multiple values using class class Intro:  def __init__(self):   self.str1 = "hello"  self.str2 = "world"# This function returns an object of Introdef message():  return Intro()  x = message() print(x.str1) print(x.str2)# hello  world

Способ 5: возврат значений с помощью классов данных (Python 3.7+)


Классы данных в Python 3.7+ как раз помогают вернуть класс с автоматически добавленными уникальными методами, модулем typing и другими полезными инструментами.

from dataclasses import dataclass@dataclassclass Item_list:    name: str    perunit_cost: float    quantity_available: int = 0    def total_cost(self) -> float:        return self.perunit_cost * self.quantity_available    book = Item_list("better programming.", 50, 2)x = book.total_cost()print(x)print(book)# 100  Item_list(name='better programming.', perunit_cost=50,   quantity_available=2)

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

Вывод


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

Учите матчасть и постоянно развивайте свои навыки программирования. Спасибо за внимание!
Подробнее..

Перевод Как строить красивые графики на Python с Seaborn

02.02.2021 18:09:11 | Автор: admin

Будущих студентов курса Python Developer. Professional и всех желающих приглашаем принять участие в открытом вебинаре на тему Фреймворкирование и метаклассы.

А сейчас делимся традиционным переводом полезного материала.


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

Есть множество инструментов для визуализации данных, таких как Tableau, Power BI, ChartBlocks и других, которые являются no-code инструментами. Они очень мощные, и у каждого своя аудитория. Однако для работы с сырыми данными, требующими обработки, а также в качестве песочницы, Python подойдет лучше всего.

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

Python лучший инструмент для data science и этому много причин, но самая важная это его экосистема библиотек. Для работы с данными в Python есть много замечательных библиотек, таких как numpy, pandas, matplotlib, tensorflow.

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

Разработчики создали новую библиотеку на основе matplotlib, которая называется seaborn. Seaborn такая же мощная, как и matplotlib, но в то же время предоставляет большую абстракцию для упрощения графиков и привносит некоторые уникальные функции.

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

Что такое Seaborn?

Seaborn это библиотека для создания статистических графиков на Python. Она основывается на matplotlib и тесно взаимодействует со структурами данных pandas.

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

Она абстрагирует сложность, позволяя вам проектировать графики в соответствии с вашими нуждами.

Установка Seaborn

Установить seaborn так же просто, как и любую другую библиотеку, для этого вам понадобится ваш любимый менеджер пакетов Python. Во время установки seaborn библиотека установит все зависимости, включая matplotlib, pandas, numpy и scipy.

Давайте уже установим seaborn и, конечно же, также пакет notebook, чтобы получить доступ к песочнице с данными.

pipenv install seaborn notebook

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

import seaborn as snsimport pandas as pdimport numpy as npimport matplotlib

Строим первые графики

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

Давайте посмотрим, как это работает на наборе данных о рейсах самолетов.

flights_data = sns.load_dataset("flights")flights_data.head()

year

month

passengers

0

1949

Jan

112

1

1949

Feb

118

2

1949

Mar

132

3

1949

Apr

129

4

1949

May

121

Вся магия происходит при вызове функции load_dataset, которая ожидает имя загружаемых данных и возвращает dataframe. Все эти наборы данных доступны в репозитории на Github.

Диаграмма рассеяния Scatter Plot

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

sns.scatterplot(data=flights_data, x="year", y="passengers")

Легко, не правда ли? Функция scatterplot принимает в себя набор данных, который нужно визуализировать, и столбцы, которые будут выступать как оси x и y.

Линейный график Line Plot

Этот график рисует линию, которая представляет собой развитие непрерывных или категориальных данных. Этот вид графиков популярен и известен, и его легко создать. Как и раньше, мы воспользуемся функцией lineplot с набором данных и столбцами, представляющими оси x и y. Остальное за нас сделает seaborn.

sns.lineplot(data=flights_data, x="year", y="passengers")

Столбчатая диаграмма Bar Plot

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

sns.barplot(data=flights_data, x="year", y="passengers")

Она очень красочная, знаю. Позже мы научимся кастомизировать ее.

Расширение функционала с matplotlib

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

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

diamonds_data = sns.load_dataset('diamonds')plt.subplot(1, 2, 1)sns.countplot(x='carat', data=diamonds_data)plt.subplot(1, 2, 2)sns.countplot(x='depth', data=diamonds_data)

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

Мы визуализируем по одной диаграмме seaborn на каждой части графика, смешивая функционал matplotlib и seaborn.

Seaborn и Pandas

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

drinks_df = pd.read_csv("data/drinks.csv")sns.barplot(x="country", y="beer_servings", data=drinks_df)

Создание красивых графиков с помощью стилей

Seaborn дает возможность менять интерфейс ваших графиков. Для этого из коробки у нас в распоряжении есть пять стилей: darkgrid, whitegrid, dark, white и ticks.

sns.set_style("darkgrid")sns.lineplot(data = data, x = "year", y = "passengers")

А вот другой пример.

sns.set_style("whitegrid")sns.lineplot(data=flights_data, x="year", y="passengers")

Крутые варианты использования

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

Сначала загрузим набор данных.

tips_df = sns.load_dataset('tips')tips_df.head()

total_bill

tip

sex

smoker

day

time

size

0

16.99

1.01

Female

No

Sun

Dinner

2

1

10.34

1.66

Male

No

Sun

Dinner

3

2

21.01

3.50

Male

No

Sun

Dinner

3

3

23.68

3.31

Male

No

Sun

Dinner

2

4

24.59

3.61

Female

No

Sun

Dinner

4

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

Давайте добавим еще один столбец, в котором будет процентное отношение чаевых ко всему счету.

tips_df["tip_percentage"] = tips_df["tip"] / tips_df["total_bill"]tips_df.head()

Теперь данные выглядят так:

total_bill

tip

sex

smoker

day

time

size

tip_percentage

0

16.99

1.01

Female

No

Sun

Dinner

2

0.059447

1

10.34

1.66

Male

No

Sun

Dinner

3

0.160542

2

21.01

3.50

Male

No

Sun

Dinner

3

0.166587

3

23.68

3.31

Male

No

Sun

Dinner

2

0.139780

4

24.59

3.61

Female

No

Sun

Dinner

4

0.146808

А теперь мы начнем строить графики.

Процент чаевых

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

sns.histplot(tips_df["tip_percentage"], binwidth=0.05)

Чтобы все хорошо читалось, нам пришлось настроить свойство binwidth, зато теперь мы быстрее можем понять и оценить данные. Большинство клиентов оставляют чаевые в размере от 15 до 20% от счета, но есть несколько случаев, когда чаевые превышают 70%. Эти значения называются аномалиями или выбросами, и на них всегда стоит обращать внимание, чтобы понять являются ли эти значения ошибочными.

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

sns.histplot(data=tips_df, x="tip_percentage", binwidth=0.05, hue="time")

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

Общее количество чаевых за определенный день недели

Еще одна интересная метрика это количество чаевых, которые получает персонал в зависимости от дня недели.

sns.barplot(data=tips_df, x="day", y="tip", estimator=np.sum)

Кажется, пятница хороший день, чтобы остаться дома.

Влияние размера столика и дня недели на чаевые

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

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

pivot = tips_df.pivot_table(    index=["day"],    columns=["size"],    values="tip_percentage",    aggfunc=np.average)sns.heatmap(pivot)

Заключение

Конечно, есть еще множество штук, которые мы можем сделать с seaborn, про них вы можете узнать в официальной документации.

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


Узнать подробнее о курсе Python Developer. Professional.

Зарегистрироваться на вебинар по теме Фреймворкирование и метаклассы.

Подробнее..

Перевод Режим мачете теги для фреймов

30.04.2021 16:23:36 | Автор: admin

Перевод подготовлен в рамках онлайн-курса "Python Developer. Professional".

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


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

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

Что ж, странно: два теста вызываются, но вызываются еще и четыре вызова моей функции настройки теста и четыре для прерывания. Я только недавно преобразовал этот набор из unittest.TestCase, и у меня есть несколько странных прокладок для уменьшения оттока кода. Думая о повторной настройке, я подумал о том, что либо мои прокладки были какими-то не такими, либо я наткнулся на краевой сценарий того, как pytest запускает тесты.

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

def setup_test(self):    import inspect    project_home = "/Users/ned/coverage"    site_packages = ".tox/py39/lib/python3.9/site-packages/"    with open("/tmp/foo.txt", "a") as foo:        print("setup_test", file=foo)        for t in inspect.stack()[::-1]:            # t is (frame, filename, lineno, function, code_context, index)            frame, filename, lineno, function = t[:4]            filename = os.path.relpath(filename, project_home)            filename = filename.replace(site_packages, "")            show = "%30s : %s:%d" % (function, filename, lineno)            print(show, file=foo)

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

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

setup_test                      <module> : igor.py:424                          main : igor.py:416           do_test_with_tracer : igor.py:216                     run_tests : igor.py:133                          main : _pytest/config/__init__.py:84                      __call__ : pluggy/hooks.py:286                     _hookexec : pluggy/manager.py:93                      <lambda> : pluggy/manager.py:84                    _multicall : pluggy/callers.py:187           pytest_cmdline_main : _pytest/main.py:243                  wrap_session : _pytest/main.py:206                         _main : _pytest/main.py:250                      __call__ : pluggy/hooks.py:286                     _hookexec : pluggy/manager.py:93                      <lambda> : pluggy/manager.py:84                    _multicall : pluggy/callers.py:187            pytest_runtestloop : _pytest/main.py:271                      __call__ : pluggy/hooks.py:286                     _hookexec : pluggy/manager.py:93                      <lambda> : pluggy/manager.py:84                    _multicall : pluggy/callers.py:187       pytest_runtest_protocol : flaky/flaky_pytest_plugin.py:94       pytest_runtest_protocol : _pytest/runner.py:78               runtestprotocol : _pytest/runner.py:87               call_and_report : flaky/flaky_pytest_plugin.py:138             call_runtest_hook : _pytest/runner.py:197                     from_call : _pytest/runner.py:226                      <lambda> : _pytest/runner.py:198                      __call__ : pluggy/hooks.py:286                     _hookexec : pluggy/manager.py:93                      <lambda> : pluggy/manager.py:84                    _multicall : pluggy/callers.py:187          pytest_runtest_setup : _pytest/runner.py:116                       prepare : _pytest/runner.py:362                         setup : _pytest/python.py:1468                  fillfixtures : _pytest/fixtures.py:296                 _fillfixtures : _pytest/fixtures.py:469               getfixturevalue : _pytest/fixtures.py:479        _get_active_fixturedef : _pytest/fixtures.py:502        _compute_fixture_value : _pytest/fixtures.py:587                       execute : _pytest/fixtures.py:894                      __call__ : pluggy/hooks.py:286                     _hookexec : pluggy/manager.py:93                      <lambda> : pluggy/manager.py:84                    _multicall : pluggy/callers.py:187          pytest_fixture_setup : _pytest/fixtures.py:936             call_fixture_func : _pytest/fixtures.py:795             connect_to_pytest : tests/mixins.py:33                    setup_test : tests/test_process.py:1651

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

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

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

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

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

def setup_test(self):    import inspect    project_home = "/Users/ned/coverage"    site_packages = ".tox/py39/lib/python3.9/site-packages/"    with open("/tmp/foo.txt", "a") as foo:        print("setup_test", file=foo)        for t in inspect.stack()[::-1]:            # t is (frame, filename, lineno, function, code_context, index)            frame, filename, lineno, function = t[:4]            visits = frame.f_locals.get("$visits", 0)       ## new            frame.f_locals["$visits"] = visits + 1          ## new            filename = os.path.relpath(filename, project_home)            filename = filename.replace(site_packages, "")            show = "%30s :  %d  %s:%d" % (function, visits, filename, lineno)            print(show, file=foo)

Теперь стеки все еще одинаковые, но количество посещений различается. Вот стек второго вызова настройки теста:

setup_test                      <module> :  1  igor.py:424                          main :  1  igor.py:416           do_test_with_tracer :  1  igor.py:216                     run_tests :  1  igor.py:133                          main :  1  _pytest/config/__init__.py:84                      __call__ :  1  pluggy/hooks.py:286                     _hookexec :  1  pluggy/manager.py:93                      <lambda> :  1  pluggy/manager.py:84                    _multicall :  1  pluggy/callers.py:187           pytest_cmdline_main :  1  _pytest/main.py:243                  wrap_session :  1  _pytest/main.py:206                         _main :  1  _pytest/main.py:250                      __call__ :  1  pluggy/hooks.py:286                     _hookexec :  1  pluggy/manager.py:93                      <lambda> :  1  pluggy/manager.py:84                    _multicall :  1  pluggy/callers.py:187            pytest_runtestloop :  1  _pytest/main.py:271                      __call__ :  1  pluggy/hooks.py:286                     _hookexec :  1  pluggy/manager.py:93                      <lambda> :  1  pluggy/manager.py:84                    _multicall :  1  pluggy/callers.py:187       pytest_runtest_protocol :  1  flaky/flaky_pytest_plugin.py:94       pytest_runtest_protocol :  0  _pytest/runner.py:78               runtestprotocol :  0  _pytest/runner.py:87               call_and_report :  0  flaky/flaky_pytest_plugin.py:138             call_runtest_hook :  0  _pytest/runner.py:197                     from_call :  0  _pytest/runner.py:226                      <lambda> :  0  _pytest/runner.py:198                      __call__ :  0  pluggy/hooks.py:286                     _hookexec :  0  pluggy/manager.py:93                      <lambda> :  0  pluggy/manager.py:84                    _multicall :  0  pluggy/callers.py:187          pytest_runtest_setup :  0  _pytest/runner.py:116                       prepare :  0  _pytest/runner.py:362                         setup :  0  _pytest/python.py:1468                  fillfixtures :  0  _pytest/fixtures.py:296                 _fillfixtures :  0  _pytest/fixtures.py:469               getfixturevalue :  0  _pytest/fixtures.py:479        _get_active_fixturedef :  0  _pytest/fixtures.py:502        _compute_fixture_value :  0  _pytest/fixtures.py:587                       execute :  0  _pytest/fixtures.py:894                      __call__ :  0  pluggy/hooks.py:286                     _hookexec :  0  pluggy/manager.py:93                      <lambda> :  0  pluggy/manager.py:84                    _multicall :  0  pluggy/callers.py:187          pytest_fixture_setup :  0  _pytest/fixtures.py:936             call_fixture_func :  0  _pytest/fixtures.py:795             connect_to_pytest :  0  tests/mixins.py:33                    setup_test :  0  tests/test_process.py:1651

Единицы это кадры, которые не менялись от первого ко второму вызову, а нули новые кадры. И теперь мы видим, что в flaky_pytest_plugin.py есть цикл, который второй раз вызывает настройку.

Стандартная ситуация: как только вы находите ответ, все становится очевидно. Я использую плагин pytest-flaky для автоматического повторения тестов, которые завершаются неудачно. Мой новый медленный тест не просто медленный, он еще и не проходит (на текущий момент), поэтому pytest-flaky запускает его еще раз.

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

Когда тест завершился успешно, выполнение по два раза исчезло, поскольку pytest-flaky не запускал его заново.

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

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

setup_test                      <module> :  3  igor.py:424                          main :  3  igor.py:416           do_test_with_tracer :  3  igor.py:216                     run_tests :  3  igor.py:133                          main :  3  _pytest/config/__init__.py:84                      __call__ :  3  pluggy/hooks.py:286                     _hookexec :  3  pluggy/manager.py:93                      <lambda> :  3  pluggy/manager.py:84                    _multicall :  3  pluggy/callers.py:187           pytest_cmdline_main :  3  _pytest/main.py:243                  wrap_session :  3  _pytest/main.py:206                         _main :  3  _pytest/main.py:250                      __call__ :  3  pluggy/hooks.py:286                     _hookexec :  3  pluggy/manager.py:93                      <lambda> :  3  pluggy/manager.py:84                    _multicall :  3  pluggy/callers.py:187            pytest_runtestloop :  3  _pytest/main.py:271                      __call__ :  1  pluggy/hooks.py:286                     _hookexec :  1  pluggy/manager.py:93                      <lambda> :  1  pluggy/manager.py:84                    _multicall :  1  pluggy/callers.py:187       pytest_runtest_protocol :  1  flaky/flaky_pytest_plugin.py:94       pytest_runtest_protocol :  0  _pytest/runner.py:78               runtestprotocol :  0  _pytest/runner.py:87               call_and_report :  0  flaky/flaky_pytest_plugin.py:138             call_runtest_hook :  0  _pytest/runner.py:197                     from_call :  0  _pytest/runner.py:226                      <lambda> :  0  _pytest/runner.py:198                      __call__ :  0  pluggy/hooks.py:286                     _hookexec :  0  pluggy/manager.py:93                      <lambda> :  0  pluggy/manager.py:84                    _multicall :  0  pluggy/callers.py:187          pytest_runtest_setup :  0  _pytest/runner.py:116                       prepare :  0  _pytest/runner.py:362                         setup :  0  _pytest/python.py:1468                  fillfixtures :  0  _pytest/fixtures.py:296                 _fillfixtures :  0  _pytest/fixtures.py:469               getfixturevalue :  0  _pytest/fixtures.py:479        _get_active_fixturedef :  0  _pytest/fixtures.py:502        _compute_fixture_value :  0  _pytest/fixtures.py:587                       execute :  0  _pytest/fixtures.py:894                      __call__ :  0  pluggy/hooks.py:286                     _hookexec :  0  pluggy/manager.py:93                      <lambda> :  0  pluggy/manager.py:84                    _multicall :  0  pluggy/callers.py:187          pytest_fixture_setup :  0  _pytest/fixtures.py:936             call_fixture_func :  0  _pytest/fixtures.py:795             connect_to_pytest :  0  tests/mixins.py:33                    setup_test :  0  tests/test_process.py:1651

Тройки меняются на единицы в _pytest/main.py:271, то есть в цикле выполнения тестов, и это круто!


Узнать подробнее о курсе "Python Developer. Professional"

Смотреть вебинар Что нового в Python 3.10

Подробнее..

Python и статистический вывод часть 2

11.05.2021 18:04:27 | Автор: admin

Предыдущий пост см. здесь.

Выборки и популяции

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

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

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

Статистики это атрибуты, которые мы можем измерить на основе выборок. Параметры это атрибуты популяции, которые мы пытаемся вывести статистически.

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

Мера

Выборочная статистика

Популяционный параметр

Объем

n

N

Среднее значение

x

x

Стандартное отклонение

Sx

x

Стандартная ошибка

Sx

Если вы вернетесь к уравнению стандартной ошибки, то заметите, что она вычисляется не из выборочного стандартного отклонения Sx, а из популяционного стандартного отклонения x. Это создает парадоксальную ситуацию мы не можем вычислить выборочную статистику, используя для этого популяционные параметры, которые мы пытаемся вывести. На практике, однако, предполагается, что выборочное и популяционное стандартные отклонения одинаковы при размере выборки порядка n 30.

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

def ex_2_8():  '''Вычислить стандартную ошибку  средних значений за определенный день''' may_1 = '2015-05-01' df = with_parsed_date( load_data('dwell-times.tsv') )  filtered = df.set_index( ['date'] )[may_1] se = standard_error( filtered['dwell-time'] ) print('Стандартная ошибка:', se)
Стандартная ошибка: 3.627340273094217

Хотя мы взяли выборку всего из одного дня, вычисляемая нами стандартная ошибка очень близка к стандартному отклонению всех выборочных средних 3.6 сек. против 3.7 сек. Это, как если бы, подобно клетке, содержащей ДНК, в каждой выборке была закодирована информация обо всей находящейся внутри нее популяции.

Интервалы уверенности

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

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

Примечание. В большинстве языков под термином confidence в контексте инференциальной статистики понимается именно уверенность в отличие от отечественной статистики, где принято говорить о доверительном интервале. На самом деле речь идет не о доверии (trust), а об уверенности исследователя в полученных результатах. Это яркий пример мягкой подмены понятия. Подобного рода ошибки вполне объяснимы - первые переводы появились еще в доколумбову доинтернетовскую эпоху, когда источников было мало, и приходилось домысливать в силу своего понимания. Сегодня же, когда существует масса профильных глоссариев, словарей и источников, такого рода искажения не оправданы. ИМХО.

При установлении интервалов уверенности обычной практикой является задание интервала размером 95% мы на 95% уверены, что популяционный параметр находится внутри интервала. Разумеется, еще остается 5%-я возможность, что он там не находится.

Какой бы ни была стандартная ошибка, 95% популяционного среднего значения будет находиться между -1.96 и 1.96 стандартных отклонений от выборочного среднего. И, следовательно, число 1.96 является критическим значением для 95%-ого интервала уверенности. Это критическое значение носит название z-значения.

Название z-значение вызвано тем, что нормальное распределение называется z-распределением. Впрочем, иногда z-значение так и называют гауссовым значением.

Число 1.96 используется так широко, что его стоит запомнить. Впрочем, критическое значение мы можем вычислить сами, воспользовавшись функцией scipy stats.norm.ppf. Приведенная ниже функция confidence_interval ожидает значение для p между 0 и 1. Для нашего 95%-ого интервала уверенности оно будет равно 0.95. В целях вычисления положения каждого из двух хвостов нам нужно вычесть это число из единицы и разделить на 2 (2.5% для интервала уверенности шириной 95%):

def confidence_interval(p, xs): '''Интервал уверенности''' mu = xs.mean() se = standard_error(xs) z_crit = stats.norm.ppf(1 - (1-p) / 2)  return [mu - z_crit * se, mu + z_crit * se]def ex_2_9(): '''Вычислить интервал уверенности для данных за определенный день''' may_1 = '2015-05-01' df = with_parsed_date( load_data('dwell-times.tsv') )  filtered = df.set_index( ['date'] )[may_1] ci = confidence_interval(0.95, filtered['dwell-time']) print('Интервал уверенности: ', ci)
Интервал уверенности: [83.53415272762004, 97.753065317492741]

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

Сравнение выборок

После вирусной маркетинговой кампании веб-команда в AcmeContent извлекает для нас однодневную выборку времени пребывания посетителей на веб-сайте для проведения анализа. Они хотели бы узнать, не привлекла ли их недавняя кампания более активных посетителей веб-сайта. Интервалы уверенности предоставляют нам интуитивно понятный подход к сравнению двух выборок.

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

def ex_2_10():    '''Сводные статистики данных, полученных        в результате вирусной кампании'''    ts = load_data('campaign-sample.tsv')['dwell-time']         print('n:                      ', ts.count())    print('Среднее:                ', ts.mean())    print('Медиана:                ', ts.median())    print('Стандартное отклонение: ', ts.std())    print('Стандартная ошибка:     ', standard_error(ts))    ex_2_10()
n:                       300Среднее:                 130.22Медиана:                 84.0Стандартное отклонение:  136.13370714388034Стандартная ошибка:      7.846572839994115

Среднее значение выглядит намного больше, чем то, которое мы видели ранее 130 сек. по сравнению с 90 сек. Вполне возможно, здесь имеется некое значимое расхождение, хотя стандартная ошибка более чем в 2 раза больше той, которая была в предыдущей однодневной выборке, в силу меньшего размера выборки и большего стандартного отклонения. Основываясь на этих данных, можно вычислить 95%-й интервал уверенности для популяционного среднего, воспользовавшись для этого той же самой функцией confidence_interval, что и прежде:

def ex_2_11(): '''Интервал уверенности для данных, полученных в результате вирусной кампании''' ts = load_data('campaign-sample.tsv')['dwell-time']  print('Интервал уверенности:', confidence_interval(0.95, ts))
Интервал уверенности: [114.84099983154137, 145.59900016845864]

95%-ый интервал уверенности для популяционного среднего лежит между 114.8 и 145.6 сек. Он вообще не пересекается с вычисленным нами ранее популяционным средним в размере 90 сек. Похоже, имеется какое-то крупное расхождение с опорной популяцией, которое едва бы произошло по причине одной лишь ошибки выборочного обследования. Наша задача теперь состоит в том, чтобы выяснить почему это происходит.

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

Искаженность

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

Широко известным примером искажения при взятии выборки является опрос населения, проведенный в США еженедельным журналом Литературный Дайджест (Literary Digest) по поводу президентских выборов 1936 г. Это был один из самых больших и самых дорогостоящих когда-либо проводившихся опросов: тогда по почте было опрошено 2.4 млн. человек. Результаты были однозначными губернатор-республиканец от шт. Канзас Альфред Лэндон должен был победить Франклина Д. Рузвельта с 57% голосов. Как известно, в конечном счете на выборах победил Рузвельт с 62% голосов.

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

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

Если мы откроем файл campaign_sample.tsv, то обнаружим, что наша выборка приходится исключительно на 6 июня 2015 года. Это был выходной день, и этот факт мы можем легко подтвердить при помощи функции pandas:

'''Проверка даты''' d = pd.to_datetime('2015 6 6') d.weekday() in [5,6]
True

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

Визуализация разных популяций

Теперь снимем фильтр для рабочих дней и построим график среднесуточного времени пребывания для всех дней недели рабочих и выходных:

def ex_2_12():  '''Построить график времени ожидания  по всем дням, без фильтра''' df = load_data('dwell-times.tsv') means = mean_dwell_times_by_date(df)['dwell-time'] means.hist(bins=20) plt.xlabel('Ежедневное время ожидания неотфильтрованное, сек.') plt.ylabel('Частота') plt.show()

Этот пример сгенерирует следующую ниже гистограмму:

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

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

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

def ex_2_13():    '''Сводные статистики данных,       отфильтрованных только по выходным дням'''    df = with_parsed_date( load_data('dwell-times.tsv') )    df.index = df['date']    df = df[df['date'].index.dayofweek > 4]   # суббота-воскресенье    weekend_times = df['dwell-time']      print('n:                      ', weekend_times.count())    print('Среднее:                ', weekend_times.mean())    print('Медиана:                ', weekend_times.median())    print('Стандартное отклонение: ', weekend_times.std())    print('Стандартная ошибка:     ', standard_error(weekend_times))        
n:                       5860Среднее:                 117.78686006825939Медиана:                 81.0Стандартное отклонение:  120.65234077179436Стандартная ошибка:      1.5759770362547678

Итоговое среднее значение в выходные дни (на основе 6-ти месячных данных) составляет 117.8 сек. и попадает в пределы 95%-ого интервала уверенности для маркетинговой выборки. Другими словами, хотя среднее значение времени пребывания в размере 130 сек. является высоким даже для выходных, расхождение не настолько большое, что его нельзя было бы приписать простой случайной изменчивости в выборке.

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

Это и будет темой следующего поста, поста 3.

Примеры исходного кода для этого поста находятся в моемрепона Github. Все исходные данные взяты врепозиторииавтора книги.

Подробнее..

Управляем звуком ПК от активности пользователя с помощью Python

17.06.2021 14:15:17 | Автор: admin

Настройка программного обеспечения

Без промедления начнём. Нам нужно установить следующее ПО:

  • Windows 10

  • Anaconda 3 (Python 3.8)

  • Visual Studio 2019 (Community) - объясню позже, зачем она понадобится.

Открываем Anaconda Prompt (Anaconda3) и устанавливаем следующие пакеты:

pip install opencv-pythonpip install dlibpip install face_recognition

И уже на этом моменте начнутся проблемы с dlib.

Решаем проблему с dlib

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

Итак, первая же ошибка говорит о том, что у нас не установлен cmake.

ERROR: CMake must be installed to build dlib
ERROR: CMake must be installed to build dlibERROR: CMake must be installed to build dlib

Не закрывая консоль, вводим следующую команду:

pip install cmake
Проблем при установке быть не должно

Пробуем установить пакет той же командой (pip install dlib), но на этот раз получаем новую ошибку:

Отсутствуют элементы Visual Studio

Ошибка явно указывает, что у меня, скорее всего, стоит студия с элементами только для C# - и она оказывается права. Открываем Visual Studio Installer, выбираем "Изменить", в вкладке "Рабочие нагрузки" в разделе "Классические и мобильные приложения" выбираем пункт "Разработка классических приложений на С++":

Пошагово
"Изменить""Изменить"Разработка классических приложений на С++Разработка классических приложений на С++Ждем окончания установкиЖдем окончания установки

Почему важно оставить все галочки, которые предлагает Visual Studio. У меня с интернетом плоховато, поэтому я решил не скачивать пакет SDK для Windows, на что получил следующую ошибку:

Не нашли компилятор

Я начал искать решение этой ошибки, пробовать менять тип компилятора (cmake -G " Visual Studio 16 2019"), но только стоило установить SDK, как все проблемы ушли.

Я пробовал данный метод на двух ПК и отмечу ещё пару подводных камней. Самое главное - Visual Studio должна быть 2019 года. У меня под рукой был офлайн установщик только 2017 - я мигом его поставил, делаю команду на установку пакета и получаю ошибку, что нужна свежая Microsoft Visual C++ версии 14.0. Вторая проблема была связана с тем, что даже установленная студия не могла скомпилировать проект. Помогла дополнительная установка Visual C++ 2015 Build Tools и Microsoft Build Tools 2015.

Открываем вновь Anaconda Prompt, используем ту же самую команду и ждём, когда соберется проект (около 5 минут):

Сборка
Всё прошло успешноВсё прошло успешно

Управляем громкостью

Вариантов оказалось несколько (ссылка), но чем проще - тем лучше. На русском язычном StackOverflow предложили использовать простую библиотеку от Paradoxis - ей и воспользуемся. Чтобы установить её, нам нужно скачать архив, пройти по пути C:\ProgramData\Anaconda3\Lib и перенести файлы keyboard.py, sound.py из архива. Проблем с использованием не возникало, поэтому идём дальше

Собираем события мыши

Самым популярным модулем для автоматизации управления мышью/клавиатурой оказался pynput. Устанавливаем так же через (pip install dlib). У модуля в целом неплохое описание - https://pynput.readthedocs.io/en/latest/mouse.html . Но у меня возникли сложности при получении событий. Я написал простую функцию:

from pynput import mousedef func_mouse():        with mouse.Events() as events:            for event in events:                if event == mouse.Events.Scroll or mouse.Events.Click:                    #print('Переместил мышку/нажал кнопку/скролл колесиком: {}\n'.format(event))                    print('Делаю половину громкости: ', time.ctime())                    Sound.volume_set(volum_half)                    break

Самое интересное, что если раскомментировать самую первую строчку и посмотреть на событие, которое привело выходу из цикла, то там можно увидеть Move. Если вы заметили, в условии if про него не слово. Без разницы, делал я только скролл колесиком или только нажатие любой клавиши мыши - все равно просто движение мыши приводит к выходу из цикла. В целом, мне нужно все действия (Scroll, Click, Move), но такое поведение я объяснить не могу. Возможно я где-то ошибаюсь, поэтому можете поправить.

А что в итоге?

Adam Geitgey, автор библиотеки face recognition, в своём репозитории имеет очень хороший набор примеров, которые многие используют при написании статей: https://github.com/ageitgey/face_recognition/tree/master/examples

Воспользуемся одним из них и получим следующий код, который можно скачать по ссылке: Activity.ipynb, Activity.py

Код
# Подключаем нужные библиотекиimport cv2import face_recognition # Получаем данные с устройства (веб камера у меня всего одна, поэтому в аргументах 0)video_capture = cv2.VideoCapture(0) # Инициализируем переменныеface_locations = []from sound import SoundSound.volume_up() # увеличим громкость на 2 единицыcurrent = Sound.current_volume() # текущая громкость, если кому-то нужноvolum_half=50  # 50% громкостьvolum_full=100 # 100% громкостьSound.volume_max() # выставляем сразу по максимуму# Работа со временем# Подключаем модуль для работы со временемimport time# Подключаем потокиfrom threading import Threadimport threading# Функция для работы с активностью мышиfrom pynput import mousedef func_mouse():        with mouse.Events() as events:            for event in events:                if event == mouse.Events.Scroll or mouse.Events.Click:                    #print('Переместил мышку/нажал кнопку/скролл колесиком: {}\n'.format(event))                    print('Делаю половину громкости: ', time.ctime())                    Sound.volume_set(volum_half)                    break# Делаем отдельную функцию с напоминаниемdef not_find():    #print("Cкрипт на 15 секунд начинается ", time.ctime())    print('Делаю 100% громкости: ', time.ctime())    #Sound.volume_set(volum_full)    Sound.volume_max()        # Секунды на выполнение    #local_time = 15    # Ждём нужное количество секунд, цикл в это время ничего не делает    #time.sleep(local_time)        # Вызываю функцию поиска действий по мышке    func_mouse()    #print("Cкрипт на 15 сек прошел")# А тут уже основная часть кодаwhile True:    ret, frame = video_capture.read()        '''    # Resize frame of video to 1/2 size for faster face recognition processing    small_frame = cv2.resize(frame, (0, 0), fx=0.50, fy=0.50)    rgb_frame = small_frame[:, :, ::-1]    '''    rgb_frame = frame[:, :, ::-1]        face_locations = face_recognition.face_locations(rgb_frame)        number_of_face = len(face_locations)        '''    #print("Я нашел {} лицо(лица) в данном окне".format(number_of_face))    #print("Я нашел {} лицо(лица) в данном окне".format(len(face_locations)))    '''        if number_of_face < 1:        print("Я не нашел лицо/лица в данном окне, начинаю работу:", time.ctime())        '''        th = Thread(target=not_find, args=()) # Создаём новый поток        th.start() # И запускаем его        # Пока работает поток, выведем на экран через 10 секунд, что основной цикл в работе        '''        #time.sleep(5)        print("Поток мыши заработал в основном цикле: ", time.ctime())                #thread = threading.Timer(60, not_find)        #thread.start()                not_find()        '''        thread = threading.Timer(60, func_mouse)        thread.start()        print("Поток мыши заработал.\n")        # Пока работает поток, выведем на экран через 10 секунд, что основной цикл в работе        '''        #time.sleep(10)        print("Пока поток работает, основной цикл поиска лица в работе.\n")    else:        #все хорошо, за ПК кто-то есть        print("Я нашел лицо/лица в данном окне в", time.ctime())        Sound.volume_set(volum_half)            for top, right, bottom, left in face_locations:        cv2.rectangle(frame, (left, top), (right, bottom), (0, 0, 255), 2)        cv2.imshow('Video', frame)        if cv2.waitKey(1) & 0xFF == ord('q'):        breakvideo_capture.release()cv2.destroyAllWindows()

Суть кода предельно проста: бегаем в цикле, как только появилось хотя бы одно лицо (а точнее координаты), то звук делаем 50%. Если не нашёл никого поблизости, то запускаем цикл с мышкой.

Тестирование в бою

Ожидание и реальность

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

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

Так же возникает закономерный вопрос - а если вместо живого человека поставить перед монитором картинку? Да, она распознает, что, скорее всего, не совсем верно. Мне попался очень хороший материал по поводу определения живого лица в реальном времени - https://www.machinelearningmastery.ru/real-time-face-liveness-detection-with-python-keras-and-opencv-c35dc70dafd3/ , но это уже немного другой уровень и думаю новичкам это будет посложнее. Но эксперименты с нейронными сетями я чуть позже повторю, чтобы тоже проверить верность и повторяемость данного руководства.

Немаловажным фактором на качество распознавания оказывает получаемое изображение с веб-камеры. Предложение использовать 1/4 изображения (сжатие его) приводит только к ухудшению - моё лицо алгоритм распознать так и не смог. Для повышения качества предлагают использовать MTCNN face detector (пример использования), либо что-нибудь посложнее из абзаца выше.

Другая интересная особенность - таймеры в Питоне. Я, опять же, признаю, что ни разу до этого не было нужды в них, но все статьях сводится к тому, чтобы ставить поток в sleep(кол-во секунд). А если мне нужно сделать так, чтобы основной поток был в работе, а по истечению n-ое количества секунд не было активности, то выполнялась моя функция? Использовать демонов (daemon)? Так это не совсем то, что нужно. Писать отдельную программу, которая взаимодействует с другой? Возможно, но единство программы пропадает.

Заключение

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

P.S. Предлагаю вам, читатели, обсудить в комментариях статью - ваши идеи, замечания, уточнения.

Подробнее..

Перевод Какие возможности появились у утилиты rdiff-backup благодаря миграции на Python 3

22.09.2020 10:23:41 | Автор: admin
В процессе миграции на Python 3 разработчики утилиты rdiff-backup усовершенствовали её, добавив много новых фич.



В марте 2020 года вышел второй крупный релиз утилиты rdiff-backup. Второй за 11 лет. Во многом, это объясняется прекращением поддержки Python 2. Разработчики решили совместить приятное с полезным и доработали функционал утилиты.

Около 20 лет она верой и правдой служит Linux-сообществу помогает делать бэкапы на локальных и удалённых машинах, скажем так без лишней головной боли. Секрет прост: утилита позволяет делать бэкап только тех файлов, которые изменились с прошлого резервного копирования. Для более краткого обозначения этого процесса существует термин инкрементальное резервное копирование.

Второе рождение rdiff-backup пережила благодаря команде энтузиастов, которую возглавили Эрик Зольф и Патрик Дюфресне из IKUS Software, а также Отто Кекяляйнен из Seravo.


Новые фичи


Проект переехал в новый репозиторий и предлагает всем желающим стать контрибьюторами. Команда внесла в новый релиз все полезные доработки, которые появлялись в течение прошедших 11 лет. Среди них поддержка разрежённых файлов и баг-фиксы для жёстких ссылок.

Автоматизация на базе Travis CI


Другое важнейшее улучшение это конвейер CI/CD на базе распределённого веб-сервиса Travis CI. Теперь пользователи смогут запускать rdiff-backup в различных тестовых средах без риска сломать работающий проект. Конвейер CI/CD позволит выполнять автоматизированную сборку и доставку для всех основных платформ.

Простая установка с помощью yum и apt


Новая версия работает на большинстве ОС семейства Linux Fedora, Red Hat, Elementary, Debian и многих других. Разработчики постарались подготовить все необходимые открытые репозитории для лёгкого доступа к утилите. Установить rdiff-backup можно с помощью менеджера пакетов или пошаговой инструкции на GitHub-странице проекта.

Новый дом


Сайт проекта переехал с Savannah на GitHub Pages (rdiff-backup.net), разработчики обновили контент и дизайн сайта.

Как работать с rdiff-backup


Если вы познакомились с rdiff-backup только сейчас, вы удивитесь, насколько он прост в использовании. Разработчики позаботились о том, чтобы вы чувствовали себя комфортно: по их мнению, подобные утилиты не должны отвлекать своей сложностью от таких важных процессов, как подготовка бэкапа или планирование восстановления данных.

Бэкап


Чтобы запустить бэкап на локальном диске (например, USB), введите команду rdiff-backup, затем имя источника (откуда будете копировать файлы) и путь к каталогу, в который вы планируете сохранить их.

Например, чтобы сделать бэкап на локальном диске с именем my_backup_drive, введите:

$ rdiff-backup /home/tux/ /run/media/tux/my_backup_drive/

Для сохранения файлов во внешнем хранилище введите путь к удалённому серверу вместе со знаком ::

$ rdiff-backup /home/tux/ tux@example.com::/my_backup_drive/

Вероятно, ещё вам потребуются SSH ключи для доступа на сервер.

Восстановление файлов из бэкапа


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

Тут нам на помощь придут команды копирования cp для локального диска и scp для удалённого сервера.

Для локального диска нужно написать, например, такое:

$ cp _run_media/tux/my_backup_drive/Documents/example.txt \ ~/Documents

Для удалённого сервера:

$ scp tux@example.com::/my_backup_drive/Documents/example.txt \ ~/Documents

У команды rdiff-backup есть опции, которые позволяют настроить параметры копирования. Например, --restore-as-of даёт возможность указать, какую версию файла нужно восстановить.

Допустим, вы хотите восстановить файл в том состоянии, в котором он был 4 дня назад:

$ rdiff-backup --restore-as-of 4D \ /run/media/tux/foo.txt ~/foo_4D.txt

Или, может быть, вам нужна последняя версия:

$ rdiff-backup --restore-as-of now \ /run/media/tux/foo.txt ~/foo_4D.txt

Вы видите, что работать с rdiff-backup достаточно легко. У этой утилиты много настроек и возможностей. Например, вы можете исключать отдельные файлы из списка для бэкапа, делать резервное копирование с одного удалённого сервера на другой и так далее. С описанием всех её возможностей вы можете ознакомиться на странице с документацией.



На правах рекламы


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

Подробнее..

Мир без корутин. Итераторы-генераторы

24.07.2020 14:12:14 | Автор: admin

1. Введение


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

Начнем с терминологии. Уж сколько раз твердили миру, но до сих пор мир все еще задаются вопросами отличия асинхронного программирования от параллельного (см. дискуссию по теме асинхронности в [1]). Суть проблем понимания асинхронности в его сравнении с параллелизмом начинается с определения самого параллелизма. Его попросту нет. Есть некое интуитивное понимание, которое часто трактуется по-разному, но нет научного определения, которое снимало бы все вопросы столь же конструктивно, как дискуссию на тему результата операции дважды два.

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

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

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

Но спустимся с глобальных и в какой-то степени философских вершин науки программирования (computer since) на нашу грешную землю. Здесь, ничуть не умаляя достоинств популярного по нынешним временам языка Kotlin, хочется признаться в своей увлеченности языком Python. Возможно, когда-нибудь и в какой-то иной ситуации мои предпочтения изменятся, но по факту пока все именно так.

Этому способствуют несколько причин. В их числе свободный доступ к Python. Это не самый сильный аргумент, т.к. пример с тем же Qt говорит, что ситуация может в любой момент измениться. Но пока Python, в отличие от Kotlin, свободен, хотя бы в форме той же среды PyCharm фирмы JetBrains (за что им отдельное спасибо), то мои симпатии на его стороне. Привлекает и то, что существует масса русскоязычной литературы, примеров на Python в Интернете, как обучающих, так и вполне реальных. На Kotlin они не в таком количестве и не столь велико их разнообразие.

Может быть несколько опережая события, я решил представить результаты освоения Python в контексте вопросов определения и реализации программного параллелизма и асинхронности. Начало этому было положено статьей [2]. Нынче же будет рассмотрена тема генераторов-корутин. Мой интерес к ним подпитан необходимостью быть в курсе специфических, интересных, но не очень известных мне на текущий момент возможностей современных языков/языка программирования.

Поскольку я фактически чистый программист на С++, то это многое объясняет. Например, если в Python корутины и генераторы присутствуют достаточно давно, то в С++ им еще предстоит завоевать свое место. Но нужно ли это С++? На мой взгляд язык программирования нужно расширять обоснованно. Такое впечатление, что С++ тянул насколько это было возможно, а теперь спешно пытается наверстать упущенное. Но аналогичные проблемы параллелизма можно реализовать, используя другие понятия и модели, которые более фундаментальны, чем корутины/сопрограммы. А то, что за этим утверждением стоят не просто слова, далее и будет продемонстрировано.

Если уж признаваться во всем, то признаюсь и в том, что в отношении С++ я достаточно консервативен. Конечно, его объекты и возможности ООП для меня наше все, но я, скажем так, критически настроен к шаблонам. Ну, очень уж не глянулся мне когда-то их своеобразный птичий язык, который, как показалось, сильно затрудняет восприятие кода и понимание алгоритма. Хотя изредка я даже прибегал к их помощи, но на все это хватит пальцев одной руки. Библиотеку STL я уважаю и без нее не обхожусь :) Поэтому, исходя даже из этого факта, меня иногда посещают сомнения по поводу шаблонов. Поэтому по-прежнему избегаю их насколько могу. Вот и теперь с содроганием жду шаблонные корутины в С++ ;)

Иное дело Python. Шаблонов в нем пока не заметил и это успокаивает. Но, с другой стороны, это же, как ни странно, настораживает. Однако, когда я смотрю на код Kotlin и, тем более, на его подкапотный код, то тревога быстро проходит ;) Правда, думаю, это все же дело привычки и мои предубеждений. Надеюсь, что со временем я натренирую себя на адекватное их (шаблонов) восприятие.

Но вернемся к сопрограммам. Оказывается, что ныне они проходят под именем корутин. Что же нового произошло со сменой названия? Да, собственно ничего. Как и ранее рассматривается множество поочередно исполняемых функций. Так же, как и ранее, перед выходом из функции, но до завершения ее работы, фиксируется точка возврата, с которой в последствии работа возобновляется. Поскольку последовательность переключения не оговаривается, то программист сам управляет этим процессом, создавая свой планировщик (scheduler). Часто это просто циклический перебор функций. Такой, как, например, событийный цикл Round Robin в видео Олега Молчанова[3].

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

2. Генераторы списков данных


Итак, генераторы (generators). С ними часто ассоциируется асинхронное программирование и корутины. Обо всем этом доступно повествует серия видео от Олега Молчанова. Так, к ключевой особенности генераторов он относит их возможность ставить выполнение функции на паузу, чтобы затем продолжить ее выполнение с того же самого места, в котором она остановилась в прошлый раз (подробнее см. [3]). И в этом, учитывая выше сказанное по поводу достаточно уже древнего определения сопрограмм, ничего нового нет.

Но, как оказывается, генераторы нашли достаточно специфическое применение для создания списков данных. Введению в эту тему посвящено уже видео от Егорова Артема [4]. Но, как представляется, таким их применением мы смешиваем качественно разные понятия операции и процессы. Расширяя описательные возможности языка, мы во многом затушевываем проблемы, которые при этом могут возникнуть. Тут как бы, как говорится, не заиграться. Использование генераторов-корутин для описания данных способствует, как мне кажется, именно этому. Отметим, что Олег Молчанов также предупреждает, что не следует ассоциировать генераторы со структурами данных, подчеркивая, что генераторы это функции [3].

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

Еще одна проблема скорость генерации списка. Сейчас мы формируем единственный элемент списка на каждом переключении корутины, а это увеличивает время генерации данных. Процесс можно серьезно ускорить, если генерировать элементы пачками. Но, скорее всего, с этим будут проблемы. А как остановить уже запущенный процесс? Или другое. Список может быть весьма большим, в котором используются лишь отдельные элементы. В такой ситуации для эффективного доступа часто используют мемоизацию (memorization) данных. Кстати, почти сразу нашел статью на эту тему для Python см. [5] (дополнительно о мемоизации в терминах автоматов см. статью [6]). А как быть в этом случае?

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

Кстати, по теме списков и генераторов об их достоинствах и недостатках, пересекающихся со сделанными выше замечаниями, можно посмотреть еще одно видео Олега Молчанова [7].

3. Генераторы-корутины


В следующем видео Олега Молчанова [8] рассматривается применение генераторов для согласования работы сопрограмм. Собственно для этого они и предназначены. Обращает на себя внимание выбор моментов переключения сопрограмм. Их расстановка следует простому правилу ставим оператор yield перед блокирующими функциями. Под последними понимаются функции, время возврата из которых столь велико по сравнению с другими операциями, что вычисления ассоциируются с их остановкой. Из-за этого их и назвали блокирующими.

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

В рассматриваемом видео, как и в общем случае для корутин, продолжение работы сопрограммы определяет внешняя среда, представляющая собой событийный планировщик. В данном случае он представлен функцией с именем event_loop. И, вроде бы, все логично: планировщик выполнит анализ и продолжит работу сопрограммы, вызвав оператор next(), ровно тогда, когда необходимо. Проблема подстерегает там, где ее не ждали: планировщик может быть достаточно сложным. В предыдущем видео Молчанова (см. [3]) было все просто, т.к. выполнялась простая попеременная передача управления, при которой блокировок не было, т.к. не было соответствующих вызовов. Тем не менее, подчеркнем, что в любом случае хотя бы простой планировщик, но необходим.

Проблема 1. Интересно было убедиться, что в любой момент оператор next() может легко превратиться в блокирующий (см. event_loop). Дело в том, что он не возвращает управления до тех пор, пока генератор не достигнет очередного yield. Если что-то этому помешает, то функция, вызвавшая next(), будет заблокирована.

Проблема 2. Как это ни удивительно, но блокирующим может стать даже оператор select, если не указать параметр величину таймаута. В видео он приведен именно в блокирующем варианте.

Но дело даже не в необходимости планировщика, а в том, что он берет на себя несвойственные ему функции. Ситуация осложняется еще тем, что нужно реализовать алгоритм совместной работы множества сопрограмм. Сравнение планировщиков, рассмотренных в упомянутых двух видео Олега Молчанова подобную проблему отражает достаточно наглядно: алгоритм планировщика работы с сокетами в [8] заметно сложнее алгоритма карусели в [3].

3. К миру без корутин


Раз мы уверены, что мир без корутин возможен, противопоставляя им при этом автоматы, то необходимо показать, как аналогичные задачи решаются уже ими. Продемонстрируем это на том же на примере работы с сокетами. Заметим, что его исходная реализация получилась не столь тривиальной, чтобы в ней можно было разобраться сходу. Это подчеркивает неоднократно и сам автор видео. С подобными проблемами в контексте сопрограмм сталкиваются и другие. Так, недостатки корутин, связанные со сложностью их восприятия, понимания, отладки и т.п. обсуждаются в видео [10].

Сначала несколько слов о сложности рассматриваемого алгоритма. Причиной этому динамический и множественный характер процессов обслуживания клиентов. Для этого создается сервер, который слушает заданный порт и по мере появления запросов порождает множество функций обслуживания клиентов, достучавшихся до него. Поскольку клиентов может быть много, появляются они непредсказуемо, то создается динамический список из процессов обслуживания сокетов и обмена информации с ними. Код решения на генераторах Python, рассмотренный в видео [8], представлен в листинге 1.

Листинг 1. Сокеты на генераторах
import socketfrom select import selecttasks = []to_read = {}to_write = {}def server():    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)    server_socket.bind(('localhost', 5001))    server_socket.listen()    while True:        yield ('read', server_socket)        client_socket, addr = server_socket.accept()            print('Connection from', addr)        tasks.append(client(client_socket, addr))           print('exit server')def client(client_socket, addr):    while True:        yield ('read', client_socket)        request = client_socket.recv(4096)                      if not request:            break        else:            response = 'Hello World\n'.encode()            yield ('write', client_socket)            client_socket.send(response)                    client_socket.close()                                   print('Stop client', addr)def event_loop():    while any([tasks, to_read, to_write]):        while not tasks:            ready_to_read, ready_to_write, _ = select(to_read, to_write, [])            for sock in ready_to_read:                tasks.append(to_read.pop(sock))            for sock in ready_to_write:                tasks.append(to_write.pop(sock))        try:            task = tasks.pop(0)            reason, sock = next(task)               if reason == 'read':                to_read[sock] = task            if reason == 'write':                to_write[sock] = task        except StopIteration:            print('Done!')tasks.append(server())event_loop()


Алгоритмы сервера и клиента достаточно элементарны. Но должно насторожить, что сервер кладет функцию клиента в список задач. Дальше больше: трудно понять алгоритм работы событийного цикла event_loop. Вплоть до того, как может быть пустым список task, если в нем, как минимум, должен всегда присутствовать процесс сервера?..

Далее вводятся словари to_read и to_write. Сама работа со словарями требует отдельного пояснения, т.к. она сложнее, чем работа с обычными списками. Из-за этого под них подстроена информация, возвращаемая операторами yield. Далее начинаются пляски с бубном вокруг словарей и все становится похожим на некое бурление: что-то появляется, чтобы быть помещенным в словари, откуда поступает в список task и т.д. и т.п. Можно сломать голову, разбираясь во всем этом :)

А как будет выглядеть решение поставленной задачи на автоматах? Для автоматов логичным будет создать модели, эквивалентные уже рассмотренным в видео процессам работы с сокетами. В модели сервера, похоже, ничего менять не надо. Это будет автомат, работающий подобно функции server(). Его граф приведен на рис. 1а. Действие автомата y1() создает серверный сокет и подключает его к заданному порту. Предикат x1() определяет подключение клиента, а при его наличии действие y2() создает процесс обслуживания клиентского сокета, помещая последний в список процессов classes, включающий классы активных объектов.

На рис. 1б приведен граф модели для отдельного клиента. Находясь в состоянии 0, автомат определяет готовность клиента передать информацию (предикат x1() true) и принимает ответ в рамках действия y1() на переходе в состояние 1. Далее, когда клиент готов к приему информации (уже x2() должно быть в true), действие y2() реализует операцию посылки сообщения клиенту на переходе в начальное состояние 0. Если же клиент разрывает связь с сервером (в этом случае x3() false), то автомат переходит в состояние 4, закрывая клиентский сокет в действии y3(). Процесс остается в состоянии 4 до тех пор, пока не будет исключен из списка активных классов classes (о формировании списка см. выше описание модели сервера).

На рис. 1в приведен автомат, реализующий запуск процессов подобно функции event_loop() листинга 1. Только в данном случае алгоритм его работы много проще. Все сводится к тому, что автомат проходит по элементам списка активных классов и для каждого из них вызывает метод loop(). Реализует это действие y2(). Действие y4() исключает из списка классы, которые попали в состоянии 4. Остальные действия работают с индексом списка объектов: действие y3() наращивает индекс, действие y1() его сбрасывает.

Возможности объектного программирования на Python отличны от объектного программирования на С++. Поэтому за основу будет взята самая простая реализация автоматной модели (если быть точным, то это имитация автомата). В ее основе объектный принцип представления процессов, в рамках которого каждому процессу соответствует отдельный активный класс (их часто называют также агентами). Класс содержит необходимые свойства и методы (см. подробнее о специфических автоматных методах предикатах и действиях в [9]), а логика работы автомата (его функции переходов и выходов) сосредоточена в рамках метода, названного loop(). Для реализации логики поведения автомата будем использовать конструкцию if-elif-else.

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

Рис. 1. Графы автоматных процессов работы с сокетами
image


Автоматный объектный код на Python для работы с сокетами представлен на листинге 2. Это и есть наш своеобразный мир без корутин. Это мир с другими принципами проектирования программных процессов. Он характеризуется наличием алгоритмической модели параллельных вычислений (подробнее о ней см. [9], что главное и качественное отличие технологии автоматного программирования (АП) от корутинной технологии.

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

Листинг 2. Сокеты на автоматах
import socketfrom select import selecttimeout = 0.0; classes = []class Server:    def __init__(self): self.nState = 0;    def x1(self):        self.ready_client, _, _ = select([self.server_socket], [self.server_socket], [], timeout)        return self.ready_client    def y1(self):        self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)        self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)        self.server_socket.bind(('localhost', 5001))        self.server_socket.listen()    def y2(self):        self.client_socket, self.addr = self.server_socket.accept()        print('Connection from', self.addr)        classes.append(Client(self.client_socket, self.addr))    def loop(self):        if (self.nState == 0):      self.y1();      self.nState = 1        elif (self.nState == 1):            if (self.x1()):         self.y2();      self.nState = 0class Client:    def __init__(self, soc, adr): self.client_socket = soc; self.addr = adr; self.nState = 0    def x1(self):        self.ready_client, _, _ = select([self.client_socket], [], [], timeout)        return self.ready_client    def x2(self):        _, self.write_client, _ = select([], [self.client_socket], [], timeout)        return self.write_client    def x3(self): return self.request    def y1(self): self.request = self.client_socket.recv(4096);    def y2(self): self.response = 'Hello World\n'.encode(); self.client_socket.send(self.response)    def y3(self): self.client_socket.close(); print('close Client', self.addr)    def loop(self):        if (self.nState == 0):            if (self.x1()):                     self.y1(); self.nState = 1        elif (self.nState == 1):            if (not self.x3()):                 self.y3(); self.nState = 4            elif (self.x2() and self.x3()):     self.y2(); self.nState = 0class EventLoop:    def __init__(self): self.nState = 0; self.i = 0    def x1(self): return self.i < len(classes)    def y1(self): self.i = 0    def y2(self): classes[self.i].loop()    def y3(self): self.i += 1    def y4(self):        if (classes[self.i].nState == 4):            classes.pop(self.i)            self.i -= self.i    def loop(self):        if (self.nState == 0):            if (not self.x1()): self.y1();            if (self.x1()):     self.y2(); self.y4(); self.y3();namSrv = Server(); namEv = EventLoop()while True:    namSrv.loop(); namEv.loop()



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

4. О принципах SRP и DRY


Принципы единой ответственности SRP (The Single Responsibility Principle) и не повторяйся DRY (don't repeat yourself) озвучены в контексте еще одного видео Олега Молчанова [11]. Соответствуя им, функция должна содержать только целевой код, чтобы не нарушать принцип SRY, и не содействовать повторению лишнего кода, чтобы не нарушать принцип DRY. В этих целях предлагается использовать декораторы. Но есть и другое решение автоматное.

В предыдущей статье [2], еще не подозревая о наличии подобных принципов, был приведен пример, использующий декораторы. Рассмотрен счетчик, который, кстати, при желании мог бы порождать и списки. Упомянут при этом объект-секундомер, измеряющий время работы счетчика. Если объекты соответствуют принципам SRP и DRY, то их функциональность не столь важна, как протокол взаимодействия. В реализации код счетчика никак не связан с кодом секундомера, а изменение любого из объектов не затронет другого. Их связывает только протокол, о котором объекты договариваются на берегу и далее ему строго следуют.

Таким образом параллельная автоматная модель по сути перекрывает возможности декораторов. Она гибче и проще реализует их возможности, т.к. не окружает (не декорирует) собой код функции. В целях объективной оценки и сравнения автоматной и обычной технологии на листинге 3 представлен объектный аналог счетчика, рассмотренного в предыдущей статье [2], где за комментариями представлены упрощенные варианты с временами их исполнения и исходный вариант счетчика.

Листинг 3. Автоматная реализация счетчика
import time# 1) 110.66 secclass PCount:    def __init__(self, cnt ): self.n = cnt; self.nState = 0    def x1(self): return self.n > 0    def y1(self): self.n -=1    def loop(self):        if (self.nState == 0 and self.x1()):            self.y1();        elif (self.nState == 0 and not self.x1()):  self.nState = 4;class PTimer:    def __init__(self): self.st_time = time.time(); self.nState = 0    def x1(self): return cnt.nState == 4    def y1(self):        t = time.time() - self.st_time        print ("speed CPU------%s---" % t)    def loop(self):       if (self.nState == 0 and self.x1()): self.y1(); self.nState = 1       elif (self.nState == 1): exit()cnt = PCount(100000000)tmr = PTimer()# event loopwhile True:    cnt.loop();    tmr.loop()# # 2) 73.38 sec# class PCount:#     def __init__(self, cnt ): self.n = cnt; self.nState = 0#     def loop(self):#         if (self.nState == 0 and self.n > 0): self.n -= 1;#         elif (self.nState == 0 and not self.n > 0):  self.nState = 4;# # class PTimer:#     def __init__(self): self.st_time = time.time(); self.nState = 0#     def loop(self):#        if (self.nState == 0 and cnt.nState == 4):#            t = time.time() - self.st_time#            print("speed CPU------%s---" % t)#            self.nState = 1#        elif (self.nState == 1): exit()# # cnt = PCount(100000000)# tmr = PTimer()# while True:#     cnt.loop();#     tmr.loop()# # 3) 35.14 sec# class PCount:#     def __init__(self, cnt ): self.n = cnt; self.nState = 0#     def loop(self):#         if (self.nState == 0 and self.n > 0):#             self.n -= 1;#             return True#         elif (self.nState == 0 and not self.n > 0):  return False;## cnt = PCount(100000000)# st_time = time.time()# while cnt.loop():#     pass# t = time.time() - st_time# print("speed CPU------%s---" % t)# # 4) 30.53 sec# class PCount:#     def __init__(self, cnt ): self.n = cnt; self.nState = 0#     def loop(self):#         while self.n > 0:#             self.n -= 1;#             return True#         return False## cnt = PCount(100000000)# st_time = time.time()# while cnt.loop():#     pass# t = time.time() - st_time# print("speed CPU------%s---" % t)# # 5) 18.27 sec# class PCount:#     def __init__(self, cnt ): self.n = cnt; self.nState = 0#     def loop(self):#         while self.n > 0:#             self.n -= 1;#         return False# # cnt = PCount(100000000)# st_time = time.time()# while cnt.loop():#     pass# t = time.time() - st_time# print("speed CPU------%s---" % t)# # 6) 6.96 sec# def count(n):#   st_time = time.time()#   while n > 0:#     n -= 1#   t = time.time() - st_time#   print("speed CPU------%s---" % t)#   return t## def TestTime(fn, n):#   def wrapper(*args):#     tsum=0#     st = time.time()#     i=1#     while (i<=n):#       t = fn(*args)#       tsum +=t#       i +=1#     return tsum#   return wrapper## test1 = TestTime(count, 2)# tt = test1(100000000)# print("Total ---%s seconds ---" % tt)



Сведем в таблицу времена работы различных вариантов и прокомментируем результаты работы.

1. Классическая автоматная реализация 110.66 сек
2. Автоматная реализация без автоматных методов 73.38 сек
3. Без автомата-секундомера 35.14
4. Счетчик в форме while с выходом на каждой итерации 30.53
5. Счетчик с блокирующим циклом 18.27
6. Исходный счетчик с декоратором 6.96

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

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

5. Выводы


Использовать ли автоматную технологию или довериться корутинам решение целиком и полностью лежит на программисте. Нам здесь важно, чтобы он знал, что есть иной, чем корутины, подход/технология к проектированию программ. Можно даже представить следующий экзотический вариант. Сначала, на этапе проектирования модели, создается автоматная модель решения. Она строго научна, доказательна и хорошо документируется. Затем ее, например, с целью повышения быстродействия, уродуют до обычного варианта кода так, как это демонстрирует листинг 3. Можно представить даже обратный рефакторинг кода, т.е. переход от 7-го варианта к 1-му, но это, хотя и возможный, но наименее вероятный ход событий :)

На рис. 2 представлены слайды из видео на тему асинхронщины [10]. И плохое, похоже, перевешивает хорошее. И если на мой взгляд автоматы это всегда хорошо, то в случае асинхронного программирования выбирай, как говорится, на свой вкус. Но, похоже, вариант плохо будет наиболее вероятен. И об этом, проектируя программу, программисту должен знать заранее.

Рис. 2. Характеристики асинхронного программирования
image


Безусловно, и автоматный код в чем-то не без греха. У него будет несколько больший объем кода. Но, во-первых, он лучше структурирован и потому в нем легче разобраться и его проще сопровождать. А, во-вторых, не всегда он будет больше, т.к. с повышением сложности, скорее всего, будет даже выигрыш (за счет, например, повторного использования автоматных методов) Он проще и понятнее в отладке. Да, в конце он концов, он полностью соответствует принципам SRP и DRY. А это, порой, перевешивает многое.

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

Листинг 4. Чтение символов с клавиатуры
/*int main(int argc, char *argv[]){    QCoreApplication a(argc, argv);    int C=0;    while (C != 'e')    {        C = getch();        putchar (C);    }    return a.exec();}*///*int main(int argc, char *argv[]){    QCoreApplication a(argc, argv);    int C=0;    while (C != 'e')    {        if (kbhit()) {            C = getch();            putch(C);        }    }    return a.exec();}//*/



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

Если же функция тяжела сама по себе, т.е. требует значительного времени работы, а периодический выход из нее по типу работы сопрограмм (это можно сделать и не используя механизм тех же корутин, чтобы к нему не привязываться) сложно сделать или не имеет особого смысла, то остается помещать подобные функции в отдельный поток и затем контролировать завершение их работы (см. реализацию класса QCount в [2]).

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

Литература


1. Python Junior подкаст. Про асинхронность в питоне. [Электронный ресурс], Режим доступа: www.youtube.com/watch?v=Q2r76grtNeg, свободный. Яз. рус. (дата обращения 13.07.2020).
2. Параллелизм и эффективность: Python vs FSM. [Электронный ресурс], Режим доступа: habr.com/ru/post/506604, свободный. Яз. рус. (дата обращения 13.07.2020).
3. Молчанов О. Основы асинхронности в Python #4: Генераторы и событийный цикл Round Robin. [Электронный ресурс], Режим доступа: www.youtube.com/watch?v=PjZUSSkGLE8], свободный. Яз. рус. (дата обращения 13.07.2020).
4. 48 Генераторы и итераторы. Выражения -генераторы в Python. [Электронный ресурс], Режим доступа: www.youtube.com/watch?v=vn6bV6BYm7w, свободный. Яз. рус. (дата обращения 13.07.2020).
5. Мемоизация и каррирование (Python). [Электронный ресурс], Режим доступа: habr.com/ru/post/335866, свободный. Яз. рус. (дата обращения 13.07.2020).
6. Любченко В.С. О борьбе с рекурсией. Мир ПК, 11/02. www.osp.ru/pcworld/2002/11/164417
7. Молчанов О. Уроки Python cast #10 Что такое yield. [Электронный ресурс], Режим доступа: www.youtube.com/watch?v=ZjaVrzOkpZk, свободный. Яз. рус. (дата обращения 18.07.2020).
8. Молчанов О. Основы асинхронности в Python #5: Асинхронность на генераторах. [Электронный ресурс], Режим доступа: www.youtube.com/watch?v=hOP9bKeDOHs, свободный. Яз. рус. (дата обращения 13.07.2020).
9. Модель параллельных вычислений. [Электронный ресурс], Режим доступа: habr.com/ru/post/486622, свободный. Яз. рус. (дата обращения 20.07.2020).
10. Полищук А. Асинхронщина в Python. [Электронный ресурс], Режим доступа: www.youtube.com/watch?v=lIkA0TDX8tE, свободный. Яз. рус. (дата обращения 13.07.2020).
11. Молчанов О. Уроки Python cast #6 Декораторы. [Электронный ресурс], Режим доступа: www.youtube.com/watch?v=Ss1M32pp5Ew, свободный. Яз. рус. (дата обращения 13.07.2020).
Подробнее..

Мир без корутин. Костыли для программиста asyncio

02.08.2020 20:17:48 | Автор: admin

1. Введение


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

Мы программисты. Может, в разной степени, но некоторые уж точно. Это я к тому, что мы разные и мыслить можем тоже по-разному. Утверждение, что программист мыслит только последовательно, столь же однобоко, вредно и даже кощунственно, как и то, что человек только бегает. Он иногда и летает. Кто-то, как летчики, делает это довольно регулярно, а некоторые, как космонавты, даже месяцами и непрерывно. Идея последовательного мышления принижает способности человека. В какой-то момент и на какое-то время в это можно даже поверить, но " все-таки она вертится" это про то, что рано или поздно жизнь возьмет свое.

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

Попробуем отбросить навязываемые программные костыли и воспарить над программной обыденностью. И пусть это будет не прыжок, а, может, не такой уж высокий и длительный, но все же, особенно в сравнении с костылями, полет. Ведь когда-то и Можайский Александр Федорович (не путать с Можайским городским судом Московской области ;) или те же братья Райт преодолели по воздуху впервые несколько сот метров. Да и испытания современных самолетов начинают с пробежки и кратковременного отрыва от взлетной полосы.

2. Совсем простой пример с asyncio


А начнем мы с полетов на Python. Программа полетов простая. Есть самолеты (которые, правда, в исходном варианте образности пауки см. [1]) с именами на фюзеляжах Blog, News, Forum. Вылетают они одновременно. Каждый за определенное время должен пролететь отрезок пути и выбросить, предположим, флажок с номером преодоленного отрезка. Так нужно поступить три раза. И только после этого приземлиться.

На языке Python модель подобного поведения описывает, а затем и моделирует, код листинга 1.
Листинг 1. Код самолетов-пауков на Python
import asyncioimport timeasync def spider(site_name): for page in range(1, 4):     await asyncio.sleep(1)     print(site_name, page)spiders = [ asyncio.ensure_future(spider("Blog")), asyncio.ensure_future(spider("News")), asyncio.ensure_future(spider("Forum"))]start = time.time()event_loop = asyncio.get_event_loop()event_loop.run_until_complete(asyncio.gather(*spiders))event_loop.close()print("{:.2F}".format(time.time() - start))



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

Blog 1
News 1
Forum 1
Blog 2
News 2
Forum 2
Blog 3
News 3
Forum 3
3.00

Почему это так подробно разъясняет видео [1]. Но мы-то ребята с фантазией и одновременный (по сценарию асинхронный) полет наших трех самолетов без использования asyncio представим по-другому на базе автоматных моделей. Так, листинг 2 приводит код автоматной задержки аналога асинхронной задержки из модуля asyncio, представленной строкой await asyncio.sleep(1) в листинге 1.
Листинг 2. Код автоматной задержки на Python
import timeclass PSleep:    def __init__(self, t, p_FSM): self.SetTime = t; self.nState = 0; self.bIfLoop = False; self.p_mainFSM = p_FSM    def x1(self): return time.time() - self.t0 <= self.SetTime    def y1(self): self.t0 = time.time()    def loop(self):        if (self.nState == 0): self.y1(); self.nState = 1        elif (self.nState == 1):            if (not self.x1()): self.nState = 4


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

На листинге 3 показан автоматный аналог асинхронного самолета-паука (см. также листинг 1). Весьма вероятно, что ассу программирования на Python подобное не приснится даже в самом кошмарном сне! Исходный код из четырех строчек увеличился в 15 раз! Это ли не повод восхищения типичным кодом Python вообще и asycio в частности или, как минимум, доказательство преимущества корутинной технологии перед автоматным программированием?
Листинг 3. Код автоматного паука на Python
# "паук" для страницы "Blog"class PBSpider:    def __init__(self, name):        self.nState = 0; self.bIfLoop = True; self.site_name = name; self.page = 1;        self.p_mainFSM = b_sleep;    def x1(self): return self.page < 4    def y1(self):        self.bIfLoop = False; automaton.append(b_sleep);        b_sleep.p_mainFSM = blog        automaton[-1].bIfLoop = True;        automaton[-1].nState = 0    def y2(self): print(self.site_name, self.page)    def y3(self): self.page += 1    def y4(self): self.page = 1    def loop(self):        if (self.x1() and self.nState == 0):  self.y1(); self.nState = 1        elif (not self.x1()  and self.nState == 0): self.y1(); self.y4(); self.nState = 33        elif (self.nState == 1): self.y2(); self.y3(); self.nState = 0# "паук" для страницы "News"class PNSpider:    def __init__(self, name):        self.nState = 0; self.bIfLoop = True; self.site_name = name; self.page = 1;        self.p_mainFSM = n_sleep;    def x1(self): return self.page < 4    def y1(self):        self.bIfLoop = False; automaton.append(n_sleep);        n_sleep.p_mainFSM = news        automaton[-1].bIfLoop = True;        automaton[-1].nState = 0    def y2(self): print(self.site_name, self.page)    def y3(self): self.page += 1    def y4(self): self.page = 1    def loop(self):        if (self.x1() and self.nState == 0):  self.y1(); self.nState = 1        elif (not self.x1()  and self.nState == 0): self.y1(); self.y4(); self.nState = 33        elif (self.nState == 1): self.y2(); self.y3(); self.nState = 0# паук для страницы "Forum"class PFSpider:    def __init__(self, name):        self.nState = 0; self.bIfLoop = True; self.site_name = name; self.page = 1;        self.p_mainFSM = f_sleep;    def x1(self): return self.page < 4    def y1(self):        self.bIfLoop = False; automaton.append(f_sleep);        f_sleep.p_mainFSM = forum        automaton[-1].bIfLoop = True;        automaton[-1].nState = 0    def y2(self): print(self.site_name, self.page)    def y3(self): self.page += 1    def y4(self): self.page = 1    def loop(self):        if (self.x1() and self.nState == 0):  self.y1(); self.nState = 1        elif (not self.x1()  and self.nState == 0): self.y1(); self.y4(); self.nState = 33        elif (self.nState == 1): self.y2(); self.y3(); self.nState = 0# задержкиb_sleep = PSleep(1, 0)n_sleep = PSleep(1, 0)f_sleep = PSleep(1, 0)# "пауки"blog = PBSpider("Blog")news = PNSpider("News")forum = PFSpider("Forum")# формирование исходного списка процессовautomaton = []automaton.append(blog);automaton.append(news);automaton.append(forum);start = time.time()# управление процессами (аналог event_loop)while True:    ind = 0;    while True:        while ind < len(automaton):            if automaton[ind].nState == 4:                automaton[ind].p_mainFSM.bIfLoop = True                automaton.pop(ind)                ind -=1            elif automaton[ind].bIfLoop:                automaton[ind].loop()            elif automaton[ind].nState == 33:                print("{:.2F}".format(time.time() - start))                exit()            ind += 1        ind = 0


А вот и результат автоматных полетов:

News 1
Forum 1
Blog 1
Blog 2
News 2
Forum 2
News 3
Forum 3
Blog 3
3.00

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

Но основная причина все же не в указателях. Код на С++, показанный далее, при полной свободе работы с указателями, имеет на один класс даже большее число строк. Причина в используемой вычислительной модели, языке ее описания и подходов к реализации алгоритмов на ее базе. Рис. 1 демонстрирует обычную модель самолета-паука в форме блок-схемы и модель в форме автомата. Можно видеть, что внешне и по качеству это разные модели, хотя и допускающие эквивалентные преобразования. У автоматов есть состояния, у блок-схем их нет и в помине. Автоматы по определению работают в дискретном времени, а блок-схемы об этом и не мечтают. Все это накладывает на реализацию модели определенные обязательства.

Отсутствие понятия дискретного времени суть проблем существующей блок-схемной модели программирования, которая должна реализовать, строго говоря, для нее нереализуемое, т.е. параллельные процессы. Напомним, что для автоматов автоматная сеть, как модель параллельных процессов (а также асинхронных), это естественное их состояние.
Рис. 1. Автоматная и блок-схемная модели самолета-паука
image

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

Для блок-схемы в одном квадратике можно без проблем перечислить действия и это не изменит последовательный характер их работы. Если же у автомата заменить переходы в состояниях s2, s3 на цикл при состоянии s1, пометив дугу этими же действиями, то смысл алгоритма изменится, т.к. введет параллелизм в его работу. Поскольку упомянутые действия должны исполнятся строго последовательно, то это и предопределило вид модели автомата (см. рис.1). Конечный автомат модель, не допускающая двоемыслия.

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

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

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

3. О проблемах реализации автоматов в Python


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

Тем не менее первая проблема связана с языком описания автоматной модели. В С++ она решена средствами языка. В Python я таких возможностей не вижу. К сожалению, как сейчас иногда выражаются, от слова совсем. Поэтому за основу был взят метод реализации автоматов на базе управляющих операторов языка if-elif-else. Кроме того, напомним, в ВКП(а), кроме собственно автоматов, для полноценной реализации параллелизма введена теневая память и автоматные пространства. Без этого возможности автоматного программирования весьма ограничены и во многом неполноценны.

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

Далее представлен код эквивалентных рассматриваемому примеру автоматных классов на С++. Код задержки на листинге 4 эквивалентен строке await asyncio.sleep(1) на листинге 1. В графической форме ей соответствует модель автомата FAwaitSleep на рис. 1. Такой и только такой автомат можно считать асинхронным и он не будет тормозить вычислительный поток. Автомат FSleep на этом же рисунке соответствует обычному оператору sleep(). Он проще, но гарантированно разрушит модель дискретного времени из-за действия y1, вызывающего обычную последовательную задержку. А это уже ни куда не годится.
Листинг 4. Код асинхронной задержки
// Задержка (в дискретном времени)#include "lfsaappl.h"#include <QTime>class FAwaitSleep :    public LFsaAppl{public:    FAwaitSleep(int n);protected:    int x1();    QTime time;    int nAwaitSleep;};#include "stdafx.h"#include "FAwaitSleep.h"static LArc TBL_AwaitSleep[] = {    LArc("s1","s1","x1",  "--"),//    LArc("s1","00","^x1","--"),//    LArc()};FAwaitSleep::FAwaitSleep(int n):    LFsaAppl(TBL_AwaitSleep, "FAwaitSleep"){    nAwaitSleep = n; time.start();}int FAwaitSleep::x1() { return time.elapsed() < nAwaitSleep; }


Код самолета-паука на С++ демонстрирует листинг 5. Данный код в гораздо большей степени адекватен своей модели, чем блок-схема коду на Python. Особенно, если сравнить таблицу переходов автомата и внешний вид автоматного графа. Это просто разные формы описания одного и того же абстрактного понятия автомата. Здесь же показано, как передается указатель на родительский класс при создании задержки (см. вызов метода FCall в действии y1)
Листинг 5. Код самолета-паука, имитирующего чтение страниц сайта
// "Паук". Имитация страниц сайта#include "lfsaappl.h"class FAwaitSleep;class FSpider :    public LFsaAppl{public:    LFsaAppl* Create(CVarFSA *pCVF) { Q_UNUSED(pCVF)return new FSpider(nameFsa); }    bool FCreationOfLinksForVariables() override;    FSpider(string strNam);    virtual ~FSpider(void);    CVar *pVarStrSiteName;// имя сайта    FAwaitSleep *pFAwaitSleep{nullptr};protected:    int x1(); void y1(); void y2(); void y3(); void y4();    int page{1};};#include "stdafx.h"#include "FSpider.h"#include "FSleep.h"#include "FAwaitSleep.h"#include <QDebug>static LArc TBL_Spider[] = {    LArc("st","s1","--","--"),    LArc("s1","s2","x1","y1"),  // x1- номер<макс.числа страниц; y1-задержка;    LArc("s2","s3","--","y2"),  // y2- печатать номера страницы;    LArc("s3","s1","--","y3"),  // y3- увеличить номер страницы    LArc("s1","st","^x1","y4"), // y4- сбросит номера страницы    LArc()};FSpider::FSpider(string strNam):    LFsaAppl(TBL_Spider, strNam){ }FSpider::~FSpider(void) { if (pFAwaitSleep) delete pFAwaitSleep; }bool FSpider::FCreationOfLinksForVariables() {    pVarStrSiteName = CreateLocVar("strSiteName", CLocVar::vtString, "name of site");    return true;}// счетчик страниц меньше заданного числа страниц?int FSpider::x1() { return page < 4; }// create delay - pure sleep (synchronous function) or await sleep (asynchronous function)void FSpider::y1() {    //sleep(1000);    // await sleep (asynchronous function)    if (pFAwaitSleep) delete pFAwaitSleep;    pFAwaitSleep = new FAwaitSleep(1000);    pFAwaitSleep->FCall(this);}void FSpider::y2() {#ifdef QT_DEBUG    string str = pVarStrSiteName->strGetDataSrc();    printf("%s%d", str.c_str(), page);    qDebug()<<str.c_str()<<page;#endif}void FSpider::y3() { page++; }void FSpider::y4() { page = 1; }



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

4. Выводы


Краткость не всегда сестра таланта, а иногда еще и признак косноязычия. Правда, отличить одно от другого сразу сложно. Код на Python часто будет короче кода на С++. Но это характерно для простых случаев. Чем сложнее решение, тем эта разница будет меньше. В конце концов даже сложность решения определяется возможностями модели. Автоматная модель гораздо мощнее блок-схемной.

Автоматы и распараллеливание это в первую очередь весьма эффективные средства решения проблем сложности, борьбы с нею, а не столько средства увеличения скорости работы программы. Поскольку все это автоматная модель, параллелизм сложно реализуемо на Python, то, несмотря на все его фишки, батарейки и много еще чего, меня сложно склонить в его сторону. Я бы больше уделял внимание окружению С++, а не очень оправданному внедрению в него тех же корутин. Модель эта временная и причина ее внедрения во многом вынужденная. А что мы будем делать с этим костылем, когда будет решена проблема выбора параллельной модели?

Поэтому, извините, но мои предпочтения все еще на стороне С++. А если учесть область моих профессиональных интересов промышленные системы так называемого жуткого жесткого реального времени, то выбора как такового у меня пока что нет. Да, какое-то окружение, какой-то сервис можно создать, используя Python. Это удобно, это быстро, есть много прототипов и т.д. и т.п. Но ядро решения, его параллельную модель, логику самих процессов однозначно С++, однозначно автоматы. Здесь автоматы, конечно, главнее и рулят. Но никак не корутины :)

В дополнение Посмотрите видео [2], обратив внимание на реализацию модели ракеты. О ней, начиная примерно с 12-й минуты, и повествует видео. Респект лектору за использование автоматов :) А на сладкое предлагается еще одно решение из [3]. Оно в духе асинхронного программирования и asyncio. Собственно с этого примера все и началось реализация вложенных автоматов в Python. Здесь глубина вложения даже больше, чем в примере, подробно рассмотренном выше. На листинге 6 приведен исходный код и его автоматный аналог на Python. На рис. 2 автоматная модель чаепития, а на листинге 7 эквивалентная реализация на С++ для ВКП(а). Сравнивайте, анализируйте, делайте выводы, критикуйте
Листинг 6. Читаем и пьем чай асинхронно на Python
import asyncioimport time# # Easy Python. Asyncio в python 3.7 https://www.youtube.com/watch?v=PaY-hiuE5iE# # 10:10# async def teatime():#     await asyncio.sleep(1)#     print('take a cap of tea')#     await asyncio.sleep(1)## async def read():#     print('Reading for 1 hour...')#     await teatime()#     print('...reading for 1 hour...')## if __name__ == '__main__':#     asyncio.run(read())class PSleep:    def __init__(self, t, p_FSM): self.SetTime = t; self.nState = 0; self.bIfLoop = False; self.p_mainFSM = p_FSM    def x1(self): return time.time() - self.t0 <= self.SetTime    def y1(self): self.t0 = time.time()    def loop(self):        if (self.nState == 0): self.y1(); self.nState = 1        elif (self.nState == 1):            if (not self.x1()): self.nState = 4class PTeaTime:    def __init__(self, p_FSM): self.nState = 0; self.bIfLoop = False; self.p_mainFSM = p_FSM;    def y1(self): self.bIfLoop = False; automaton.append(sl); automaton[-1].bIfLoop = True; automaton[-1].nState = 0    def y2(self): print('take a cap of tea')    def loop(self):        if (self.nState == 0):  self.y1(); self.nState = 1        elif (self.nState == 1): self.y2(); self.nState = 2        elif (self.nState == 2): self.y1(); self.nState = 3        elif (self.nState == 3): self.nState = 4class PRead:    def __init__(self): self.nState = 0; self.bIfLoop = False;    def y1(self): print('Reading for 1 hour...')    def y2(self): self.bIfLoop = False; automaton.append(rt); automaton[-1].bIfLoop = True; automaton[-1].nState = 0    def loop(self):        if (self.nState == 0): self.y1(); self.nState = 1        elif (self.nState == 1): self.y2(); self.nState = 2        elif (self.nState == 2): self.y1(); self.nState = 33; self.bIfLoop = Falseread = PRead()rt = PTeaTime(read)sl = PSleep(5, rt)automaton = []automaton.append(read); automaton[-1].bIfLoop = Truewhile True:    ind = 0;    while True:        while ind < len(automaton):            if automaton[ind].nState == 4:                automaton[ind].p_mainFSM.bIfLoop = True                automaton.pop(ind)                ind -=1            elif automaton[ind].bIfLoop:                automaton[ind].loop()            elif automaton[ind].nState == 33:                exit()            ind += 1        ind = 0


Рис. 2. Автоматная модель чаепития
image

Листинг 7. Читаем и пьем чай асинхронно на С++
#include "lfsaappl.h"class FRead :    public LFsaAppl{public:    LFsaAppl* Create(CVarFSA *pCVF) { Q_UNUSED(pCVF)return new FRead(nameFsa); }    FRead(string strNam);    virtual ~FRead(void);protected:    void y1(); void y2();  void y3();    LFsaAppl *pFRealTime{nullptr};};#include "stdafx.h"#include "FRead.h"#include "FTeaTime.h"#include <QDebug>static LArc TBL_Read[] = {    LArc("s1","s2","--","y1"),// Reading for 1 hour...    LArc("s2","s3","--","y2"),// Call(TeaTime)    LArc("s3","s4","--","y1"),// Reading for 1 hour...    LArc("s4","s5","--","y3"),// sleep(5)    LArc("s5","s1","--","--"),//    LArc()};FRead::FRead(string strNam):    LFsaAppl(TBL_Read, strNam){ }FRead::~FRead(void) { if (pFRealTime) delete pFRealTime; }void FRead::y1() {#ifdef QT_DEBUG    qDebug()<<"Reading for 1 hour...";#endif}void FRead::y2() {    if (pFRealTime) delete pFRealTime;    pFRealTime = new FTeaTime("TeaTime");    pFRealTime->FCall(this);}void FRead::y3() { FCreateDelay(5000); }#include "lfsaappl.h"class FTeaTime :    public LFsaAppl{public:    FTeaTime(string strNam);protected:    void y1(); void y2();};#include "stdafx.h"#include "FTeaTime.h"#include <QDebug>#include "./LSYSLIB/FDelay.h"static LArc TBL_TeaTime[] = {    LArc("s1","s2","--","y1"),// sleep(1)    LArc("s2","s3","--","y2"),// take a cap of tea    LArc("s3","s4","--","y1"),// sleep(1)    LArc("s4","00","--","--"),//    LArc()};FTeaTime::FTeaTime(string strNam):    LFsaAppl(TBL_TeaTime, strNam){ }void FTeaTime::y1() { FCreateDelay(2000); }void FTeaTime::y2() {#ifdef QT_DEBUG    qDebug()<<"take a cap of tea";#endif}



PS
Уже после написания статьи после чтения перевода статьи Йерайна Диаза[4], познакомился с еще одним достаточно интересным и довольно восхищенным взглядом на корутины вообще и asyncio в частности. Несмотря на этот факт и ему подобные, мы все же пойдем иным путем :) Согласен только в одном с Робом Пайком (Rob Pike), что Concurrency Is Not Parallelesm. Конкурентность, можно сказать даже жестче, не имеет вообще ни чего общего с параллелизмом. И примечательно, что Google-переводчик переводит эту фразу как Параллелизм это не параллелизм. Мужчина по имени Google, конечно, не прав. Но кто-то же его в этом убедил? :)

Литература


1. Shultais Education. 1. Введение в асинхронное программирование. [Электронный ресурс], Режим доступа: www.youtube.com/watch?v=BmOjeVM0w1U&list=PLJcqk6mrJtxCo_KqHV2rM2_a3Z8qoE5Gk, свободный. Яз. рус. (дата обращения 01.08.2020).
2. Computer Science Center. Лекция 9. async / await (Программирование на Python). [Электронный ресурс], Режим доступа: www.youtube.com/watch?v=x6JZmBK2I8Y, свободный. Яз. рус. (дата обращения 13.07.2020).
3. Easy Python. Asyncio в python 3.7. [Электронный ресурс], Режим доступа: www.youtube.com/watch?v=PaY-hiuE5iE, свободный. Яз. рус. (дата обращения 01.08.2020).
4. Йерай Диаз (Yeray Diaz). Asyncio для практикующего python-разработчика. [Электронный ресурс], Режим доступа: www.youtube.com/watch?v=PaY-hiuE5iE, свободный. Яз. рус. (дата обращения 01.08.2020).
Подробнее..

Автоматический переводчик на PythonGTK3. Альтернатива Яндексу

06.09.2020 06:06:16 | Автор: admin
Ну вот и пришел долгожданный конец халяве(статья).

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

Я, разумеется, начал искать выход для себя и друзей. И нашел.

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

Убрано было все, что касается Яндекс, добавлены библиотеки langdetect и translators. Первая для определения языка, ибо без доступа к API пришлось бы делать это вручную. Вторая, соответственно, модуль доступа к гугло-переводчику посредством urllib и requests.
Вот, собственно, все новшества:
................from langdetect import detectimport translators as ts................indetect = detect(clip())def definition():if indetect == 'ru':        langout = 'en'    else:        langout = 'ru'    return langoutdef translate():    output = []    output = ts.google(clip(), to_language=definition(), if_use_cn_host=True)    return output................


Так же был изменен файл ~/.local/lib/python3.8/site-packages/translators/apis.py
53 #logger.add(sys.stdout, format='[{time:HH:mm:ss}] <lvl>{message}</lvl>', level='INFO')120 #sys.stderr.write(f'Using {data.get("country")} server backend.\n')144 self.cn_host_url = 'https://translate.google.ru'151 self.output_zh = 'ru-RU'

Ну вот совсем мне не нужен вывод отладочной информации, строки 53 и 120, а так же умолчальный сервер и вывод в 144 и 151 изменен с китайского на русский.

Проект на github.

P.S: Переводчик от Google, как оказалось, справляется со своим предназначением лучше яндексовского.
Подробнее..
Категории: Python , Яндекс , Python 3 , Gtk+ , Google translate

Aio api crawler

31.10.2020 06:11:16 | Автор: admin
Всем пример. Я начал работать над библиотекой для выдергивания данных из разных json api. Также она может использоваться для тестирования api.

Апишки описываются в виде классов, например

class Categories(JsonEndpoint):    url = "http://127.0.0.1:8888/categories"    params = {"page": range(100), "language": "en"}    headers = {"User-Agent": get_user_agent}    results_key = "*.slug"categories = Categories()class Posts(JsonEndpoint):    url = "http://127.0.0.1:8888/categories/{category}/posts"    params = {"page": range(100), "language": "en"}    url_params = {"category": categories.iter_results()}    results_key = "posts"    async def comments(self, post):        comments = Comments(            self.session,            url_params={"category": post.url.params["category"], "id": post["id"]},        )        return [comment async for comment in comments]posts = Posts()


В params и url_params могут быть функции(как здесь get_user_agent возвращает случайный useragent), range, итераторы, awaitable и асинхронные итераторы(таким образом можно увязать их между собой).

В параметрах headers и cookies тоже могут быть функции и awaitable.

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

А репозитории есть пример aiohttp сервера для этих классов чтобы всё можно было протестировать.

Помимо get параметров можно передавать их как data или json и задать другой method.

results_key разбивается по точке и будет пытаться выдергивать ключи из результатов. Например comments.*.text вернет текст каждого комментария из массива внутри comments.

Результаты оборачиваются во wrapper у которого есть свойства url и params. url это производное строки, у которой тоже есть params. Таким образом можно узнать какие параметры использовались для получения данного результата Это демонстрируется в методе comments.

Также там есть базовый класс Sink для обработки результатов. Например, складывания их в mq или базу данных. Он работает в отдельных тасках и получает данные через asyncio.Queue.

class LoggingSink(Sink):    def transform(self, obj):        return repr(obj)    async def init(self):        from loguru import logger        self.logger = logger    async def process(self, obj):        self.logger.info(obj)        return Truesink = LoggingSink(num_tasks=1)


Пример простейшего Sink. Метод transform позволяет провести какие-то манипуляции с объектом и вернуть None, если он нам не подходит. т.е. в тем также можно сделать валидацию.

Sink это асинхронный contextmanager, который при выходе по-идее будет ждать пока все объекты в очереди будут обработаны, потом отменит свои таски.

Ну и, наконец, для связки этого всего вместе я сделал класс Worker. Он принимает один endpoint и несколько sink`ов. Например,

worker = Worker(endpoint=posts, sinks=[loggingsink, mongosink])worker.run()


run запустит asyncio.run_until_complete для pipeline`а worker`а. У него также есть метод transform.

Ещё есть класс WorkerGroup который позволяет создать сразу несколько воркеров и сделать asyncio.gather для них.

В коде есть пример сервера, который генерит данные через faker и обработчиков для его endpoint`ов. Думаю это нагляднее всего.

Всё это на ранней стадии развития и я пока что часто менял api. Но сейчас вроде пришел к тому как это должно выглядеть. Буду раз merge request`ам и комментариям к моему коду.
Подробнее..
Категории: Python , Json , Python 3 , Aiohttp

Немного SQL алхимии

06.12.2020 02:07:34 | Автор: admin
О популярной библиотеке SQLAlchemy для работы с разными СУБД из Python было написано довольно много статей. Предлагаю вашему вниманию обзор и сравнение запросов с использованием ORM и SQL подходов. Данное руководство будет интересно прежде всего начинающим разработчикам, поскольку позволяет быстро окунуться в создание и работу с SQLAlchemy, поскольку документация от разработчика SQLAlchemy на мой скромный взгляд тяжела для чтения.
image

Немного о себе: я также начинающий разработчик, прохожу обучение по курсу Python разработчик. Данный материал был составлен не в результате ДЗ, а в порядке саморазвития. Мой код может быть достаточно наивным, в связи с чем прошу не стесняться и свои замечания оставлять в комментариях. Если я вас еще не напугал, прошу под кат :)

Мы с вами разберем практический пример нормализации плоской таблицы, содержащей дублирующиеся данные, до состояния 3НФ (третьей нормальной формы).
Из вот такой таблицы:
Таблица с данными
image

сделаем вот такую БД:
Схема связей БД
image

Для нетерпеливых: код, готовый к запуску находится в этом репозитории. Интерактивная схема БД здесь. Шпаргалка по составлению ORM запросов находится в конце статьи.

Договоримся, что в тексте статьи мы будем использовать слово Таблица вместо Отношение, и слово Поле вместо Аттрибута. По заданию нам надо таблицу с музыкальными файлами поместить в БД, при этом устранив избыточность данных. В исходной таблице (формат CSV) имеются следующие поля (track, genre, musician, album, length, album_year, collection, collection_year). Связи между ними такие:
каждый музыкант может петь в нескольких жанрах, как и в одном жанре могут выступать несколько музыкантов (отношение многие ко многим).
в создании альбома могут участвовать один или несколько музыкантов (отношение многие ко многим).
трек принадлежит только одному альбому (отношение один ко многим)
треки могут в ходить в состав нескольких сборников (отношение многие ко многим)
трек может не входить ни в одну в коллекцию.

Для упрощения предположим что названия жанров, имена музыкантов, названия альбомов и коллекций не повторяются. Названия треков могут повторяться. В БД мы запроектировали 8 таблиц:
genres (жанры)
genres_musicians (промежуточная таблица)
musicians (музыканты)
albums_musicians (промежуточная таблица)
albums (альбомы)
tracks (треки)
collections_tracks (промежуточная таблица)
collections (коллекции)
* данная схема тестовая, взята из одного из ДЗ, в ней есть некоторые недостатки например нет связи треков с музыкантом, а также трека с жанром. Но для обучения это несущественно, и мы опустим этот недостаток.

Для теста я создал две БД на локальном Postgres: TestSQL и TestORM, доступ к ним: логин и пароль test. Давайте наконец писать код!

Создаем подключения и таблицы


Создаем подключения к БД
* код функций read_data и clear_db есть в репозитории.
DSN_SQL = 'postgresql://test:test@localhost:5432/TestSQL'    DSN_ORM = 'postgresql://test:test@localhost:5432/TestORM'    # Прочитаем данные из CSV в память в виде словаря.    DATA = read_data('data/demo-data.csv')    print('Connecting to DB\'s...')    # Мы будем работать с сессиями, поэтому создадим их раздельными для каждой БД.    engine_orm = sa.create_engine(DSN_ORM)    Session_ORM = sessionmaker(bind=engine_orm)    session_orm = Session_ORM()    engine_sql = sa.create_engine(DSN_SQL)    Session_SQL = sessionmaker(bind=engine_sql)    session_sql = Session_SQL()    print('Clearing the bases...')    # Удаляем все таблицы из БД перед заливкой содержимого. Используем только для учебы.    clear_db(sa, engine_sql)    clear_db(sa, engine_orm)


Создаем таблицы классическим путем через SQL
* код функции read_query есть в репозитории. Тексты запросов также есть в репозитории.
print('\nPreparing data for SQL job...')    print('Creating empty tables...')    session_sql.execute(read_query('queries/create-tables.sql'))    session_sql.commit()    print('\nAdding musicians...')    query = read_query('queries/insert-musicians.sql')    res = session_sql.execute(query.format(','.join({f"('{x['musician']}')" for x in DATA})))    print(f'Inserted {res.rowcount} musicians.')    print('\nAdding genres...')    query = read_query('queries/insert-genres.sql')    res = session_sql.execute(query.format(','.join({f"('{x['genre']}')" for x in DATA})))    print(f'Inserted {res.rowcount} genres.')    print('\nLinking musicians with genres...')    # assume that musician + genre has to be unique    genres_musicians = {x['musician'] + x['genre']: [x['musician'], x['genre']] for x in DATA}    query = read_query('queries/insert-genre-musician.sql')    # this query can't be run in batch, so execute one by one    res = 0    for key, value in genres_musicians.items():        res += session_sql.execute(query.format(value[1], value[0])).rowcount    print(f'Inserted {res} connections.')    print('\nAdding albums...')    # assume that albums has to be unique    albums = {x['album']: x['album_year'] for x in DATA}    query = read_query('queries/insert-albums.sql')    res = session_sql.execute(query.format(','.join({f"('{x}', '{y}')" for x, y in albums.items()})))    print(f'Inserted {res.rowcount} albums.')    print('\nLinking musicians with albums...')    # assume that musicians + album has to be unique    albums_musicians = {x['musician'] + x['album']: [x['musician'], x['album']] for x in DATA}    query = read_query('queries/insert-album-musician.sql')    # this query can't be run in batch, so execute one by one    res = 0    for key, values in albums_musicians.items():        res += session_sql.execute(query.format(values[1], values[0])).rowcount    print(f'Inserted {res} connections.')    print('\nAdding tracks...')    query = read_query('queries/insert-track.sql')    # this query can't be run in batch, so execute one by one    res = 0    for item in DATA:        res += session_sql.execute(query.format(item['track'], item['length'], item['album'])).rowcount    print(f'Inserted {res} tracks.')    print('\nAdding collections...')    query = read_query('queries/insert-collections.sql')    res = session_sql.execute(query.format(','.join({f"('{x['collection']}', {x['collection_year']})" for x in DATA if x['collection'] and x['collection_year']})))    print(f'Inserted {res.rowcount} collections.')    print('\nLinking collections with tracks...')    query = read_query('queries/insert-collection-track.sql')    # this query can't be run in batch, so execute one by one    res = 0    for item in DATA:        res += session_sql.execute(query.format(item['collection'], item['track'])).rowcount    print(f'Inserted {res} connections.')    session_sql.commit()



По сути мы создаем пакетами справочники (жанры, музыкантов, альбомы, коллекции), а затем в цикле связываем остальные данные и строим вручную промежуточные таблицы. Запускаем код и видим что БД создалась. Главное не забыть вызывать commit() у сессии.

Теперь пробуем сделать тоже самое, но с применением ORM подхода. Для того чтобы работать с ORM нам надо описать классы данных. Для этого мы создадим 8 классов (по одному на кажую таблицу).
Заголовок спойлера
Код скрипта объявления классов.
Base = declarative_base()class Genre(Base):    __tablename__ = 'genres'    id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)    name = sa.Column(sa.String(20), unique=True)    # Объявляется отношение многие ко многим к Musician через промежуточную таблицу genres_musicians    musicians = relationship("Musician", secondary='genres_musicians')class Musician(Base):    __tablename__ = 'musicians'    id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)    name = sa.Column(sa.String(50), unique=True)    # Объявляется отношение многие ко многим к Genre через промежуточную таблицу genres_musicians    genres = relationship("Genre", secondary='genres_musicians')    # Объявляется отношение многие ко многим к Album через промежуточную таблицу albums_musicians    albums = relationship("Album", secondary='albums_musicians')class GenreMusician(Base):    __tablename__ = 'genres_musicians'    # здесь мы объявляем составной ключ, состоящий из двух полей    __table_args__ = (PrimaryKeyConstraint('genre_id', 'musician_id'),)    # В промежуточной таблице явно указываются что следующие поля являются внешними ключами    genre_id = sa.Column(sa.Integer, sa.ForeignKey('genres.id'))    musician_id = sa.Column(sa.Integer, sa.ForeignKey('musicians.id'))class Album(Base):    __tablename__ = 'albums'    id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)    name = sa.Column(sa.String(50), unique=True)    year = sa.Column(sa.Integer)    # Объявляется отношение многие ко многим к Musician через промежуточную таблицу albums_musicians    musicians = relationship("Musician", secondary='albums_musicians')class AlbumMusician(Base):    __tablename__ = 'albums_musicians'    # здесь мы объявляем составной ключ, состоящий из двух полей    __table_args__ = (PrimaryKeyConstraint('album_id', 'musician_id'),)    # В промежуточной таблице явно указываются что следующие поля являются внешними ключами    album_id = sa.Column(sa.Integer, sa.ForeignKey('albums.id'))    musician_id = sa.Column(sa.Integer, sa.ForeignKey('musicians.id'))class Track(Base):    __tablename__ = 'tracks'    id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)    name = sa.Column(sa.String(100))    length = sa.Column(sa.Integer)    # Поскольку по полю album_id идет связь один ко многим, достаточно указать чей это внешний ключ    album_id = sa.Column(sa.Integer, ForeignKey('albums.id'))    # Объявляется отношение многие ко многим к Collection через промежуточную таблицу collections_tracks    collections = relationship("Collection", secondary='collections_tracks')class Collection(Base):    __tablename__ = 'collections'    id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)    name = sa.Column(sa.String(50))    year = sa.Column(sa.Integer)    # Объявляется отношение многие ко многим к Track через промежуточную таблицу collections_tracks    tracks = relationship("Track", secondary='collections_tracks')class CollectionTrack(Base):    __tablename__ = 'collections_tracks'    # здесь мы объявляем составной ключ, состоящий из двух полей    __table_args__ = (PrimaryKeyConstraint('collection_id', 'track_id'),)    # В промежуточной таблице явно указываются что следующие поля являются внешними ключами    collection_id = sa.Column(sa.Integer, sa.ForeignKey('collections.id'))    track_id = sa.Column(sa.Integer, sa.ForeignKey('tracks.id'))


Нам достаточно создать базовый класс Base для декларативного стиля описания таблиц и унаследоваться от него. Вся магия отношений между таблицами заключается в правильном использовании relationship и ForeignKey. В коде указано в каком случае мы создаем какое отношение. Главное не забыть прописать relationship с обеих сторон связи многие ко многим.

Непосредственно создание таблиц с использованием ORM подхода происходит путем вызова:
Base.metadata.create_all(engine_orm)

И вот тут включается магия, буквально все классы, объявленные в коде через наследование от Base становятся таблицами. Сходу я не увидел как указать экземпляры каких классов надо создать сейчас, а какие отложить для создания позже (например в другой БД). Наверняка такой способ есть, но в нашем коде все классы-наследники Base инстанцируются одномоментно, имейте это ввиду.


Наполнение таблиц при использовании ORM подхода выглядит так:
Заполнение таблиц данными через ORM
    print('\nPreparing data for ORM job...')    for item in DATA:        # создаем жанры        genre = session_orm.query(Genre).filter_by(name=item['genre']).scalar()        if not genre:            genre = Genre(name=item['genre'])        session_orm.add(genre)        # создаем музыкантов        musician = session_orm.query(Musician).filter_by(name=item['musician']).scalar()        if not musician:            musician = Musician(name=item['musician'])        musician.genres.append(genre)        session_orm.add(musician)        # создаем альбомы        album = session_orm.query(Album).filter_by(name=item['album']).scalar()        if not album:            album = Album(name=item['album'], year=item['album_year'])        album.musicians.append(musician)        session_orm.add(album)        # создаем треки        # проверяем на существование трек не только по имени но и по альбому, так как имя трека по условию может        # быть не уникально        track = session_orm.query(Track).join(Album).filter(and_(Track.name == item['track'],                                                                 Album.name == item['album'])).scalar()        if not track:            track = Track(name=item['track'], length=item['length'])        track.album_id = album.id        session_orm.add(track)        # создаем коллекции, учитываем что трек может не входить ни в одну в коллекцию        if item['collection']:            collection = session_orm.query(Collection).filter_by(name=item['collection']).scalar()            if not collection:                collection = Collection(name=item['collection'], year=item['collection_year'])            collection.tracks.append(track)            session_orm.add(collection)        session_orm.commit()


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

Запросы к базам



По заданию нам надо написать 15 запросов используя обе техники SQL и ORM. Вот список поставленных вопросов в порядке возрастания сложности:
1. название и год выхода альбомов, вышедших в 2018 году;
2. название и продолжительность самого длительного трека;
3. название треков, продолжительность которых не менее 3,5 минуты;
4. названия сборников, вышедших в период с 2018 по 2020 год включительно;
5. исполнители, чье имя состоит из 1 слова;
6. название треков, которые содержат слово мой/my.
7. количество исполнителей в каждом жанре;
8. количество треков, вошедших в альбомы 2019-2020 годов;
9. средняя продолжительность треков по каждому альбому;
10. все исполнители, которые не выпустили альбомы в 2020 году;
11. названия сборников, в которых присутствует конкретный исполнитель;
12. название альбомов, в которых присутствуют исполнители более 1 жанра;
13. наименование треков, которые не входят в сборники;
14. исполнителя(-ей), написавшего самый короткий по продолжительности трек (теоретически таких треков может быть несколько);
15. название альбомов, содержащих наименьшее количество треков.
Как видите, вышеизложенные вопросы подразумевают как простую выборку так и с объединением таблиц, а также использование агрегатных функций.

Ниже предоставлены решения по каждому из 15 запросов в двух вариантах (используя SQL и ORM). В коде запросы идут парами, чтобы показать идентичность результатов на выводе в консоль.
Запросы и их краткое описание
    print('\n1. All albums from 2018:')    query = read_query('queries/select-album-by-year.sql').format(2018)    print(f'############################\n{query}\n############################')    print('----SQL way---')    res = session_sql.execute(query)    print(*res, sep='\n')    print('----ORM way----')    for item in session_orm.query(Album).filter_by(year=2018):        print(item.name)    print('\n2. Longest track:')    query = read_query('queries/select-longest-track.sql')    print(f'############################\n{query}\n############################')    print('----SQL way---')    res = session_sql.execute(query)    print(*res, sep='\n')    print('----ORM way----')    for item in session_orm.query(Track).order_by(Track.length.desc()).slice(0, 1):        print(f'{item.name}, {item.length}')    print('\n3. Tracks with length not less 3.5min:')    query = read_query('queries/select-tracks-over-length.sql').format(310)    print(f'############################\n{query}\n############################')    print('----SQL way---')    res = session_sql.execute(query)    print(*res, sep='\n')    print('----ORM way----')    for item in session_orm.query(Track).filter(310 <= Track.length).order_by(Track.length.desc()):        print(f'{item.name}, {item.length}')    print('\n4. Collections between 2018 and 2020 years (inclusive):')    query = read_query('queries/select-collections-by-year.sql').format(2018, 2020)    print(f'############################\n{query}\n############################')    print('----SQL way---')    res = session_sql.execute(query)    print(*res, sep='\n')    print('----ORM way----')    for item in session_orm.query(Collection).filter(2018 <= Collection.year,                                                     Collection.year <= 2020):        print(item.name)    print('\n5. Musicians with name that contains not more 1 word:')    query = read_query('queries/select-musicians-by-name.sql')    print(f'############################\n{query}\n############################')    print('----SQL way---')    res = session_sql.execute(query)    print(*res, sep='\n')    print('----ORM way----')    for item in session_orm.query(Musician).filter(Musician.name.notlike('%% %%')):        print(item.name)    print('\n6. Tracks that contains word "me" in name:')    query = read_query('queries/select-tracks-by-name.sql').format('me')    print(f'############################\n{query}\n############################')    print('----SQL way---')    res = session_sql.execute(query)    print(*res, sep='\n')    print('----ORM way----')    for item in session_orm.query(Track).filter(Track.name.like('%%me%%')):        print(item.name)    print('Ok, let\'s start serious work')    print('\n7. How many musicians plays in each genres:')    query = read_query('queries/count-musicians-by-genres.sql')    print(f'############################\n{query}\n############################')    print('----SQL way---')    res = session_sql.execute(query)    print(*res, sep='\n')    print('----ORM way----')    for item in session_orm.query(Genre).join(Genre.musicians).order_by(func.count(Musician.id).desc()).group_by(            Genre.id):        print(f'{item.name}, {len(item.musicians)}')    print('\n8. How many tracks in all albums 2019-2020:')    query = read_query('queries/count-tracks-in-albums-by-year.sql').format(2019, 2020)    print(f'############################\n{query}\n############################')    print('----SQL way---')    res = session_sql.execute(query)    print(*res, sep='\n')    print('----ORM way----')    for item in session_orm.query(Track, Album).join(Album).filter(2019 <= Album.year, Album.year <= 2020):        print(f'{item[0].name}, {item[1].year}')    print('\n9. Average track length in each album:')    query = read_query('queries/count-average-tracks-by-album.sql')    print(f'############################\n{query}\n############################')    print('----SQL way---')    res = session_sql.execute(query)    print(*res, sep='\n')    print('----ORM way----')    for item in session_orm.query(Album, func.avg(Track.length)).join(Track).order_by(func.avg(Track.length)).group_by(            Album.id):        print(f'{item[0].name}, {item[1]}')    print('\n10. All musicians that have no albums in 2020:')    query = read_query('queries/select-musicians-by-album-year.sql').format(2020)    print(f'############################\n{query}\n############################')    print('----SQL way---')    res = session_sql.execute(query)    print(*res, sep='\n')    print('----ORM way----')    for item in session_orm.query(Musician).join(Musician.albums).filter(Album.year != 2020).order_by(            Musician.name.asc()):        print(f'{item.name}')    print('\n11. All collections with musician Steve:')    query = read_query('queries/select-collection-by-musician.sql').format('Steve')    print(f'############################\n{query}\n############################')    print('----SQL way---')    res = session_sql.execute(query)    print(*res, sep='\n')    print('----ORM way----')    for item in session_orm.query(Collection).join(Collection.tracks).join(Album).join(Album.musicians).filter(            Musician.name == 'Steve').order_by(Collection.name):        print(f'{item.name}')    print('\n12. Albums with musicians that play in more than 1 genre:')    query = read_query('queries/select-albums-by-genres.sql').format(1)    print(f'############################\n{query}\n############################')    print('----SQL way---')    res = session_sql.execute(query)    print(*res, sep='\n')    print('----ORM way----')    for item in session_orm.query(Album).join(Album.musicians).join(Musician.genres).having(func.count(distinct(            Genre.name)) > 1).group_by(Album.id).order_by(Album.name):        print(f'{item.name}')    print('\n13. Tracks that not included in any collections:')    query = read_query('queries/select-absence-tracks-in-collections.sql')    print(f'############################\n{query}\n############################')    print('----SQL way---')    res = session_sql.execute(query)    print(*res, sep='\n')    print('----ORM way----')    # Important! Despite the warning, following expression does not work: "Collection.id is None"    for item in session_orm.query(Track).outerjoin(Track.collections).filter(Collection.id == None):        print(f'{item.name}')    print('\n14. Musicians with shortest track length:')    query = read_query('queries/select-musicians-min-track-length.sql')    print(f'############################\n{query}\n############################')    print('----SQL way---')    res = session_sql.execute(query)    print(*res, sep='\n')    print('----ORM way----')    subquery = session_orm.query(func.min(Track.length))    for item in session_orm.query(Musician, Track.length).join(Musician.albums).join(Track).group_by(            Musician.id, Track.length).having(Track.length == subquery).order_by(Musician.name):        print(f'{item[0].name}, {item[1]}')    print('\n15. Albums with minimum number of tracks:')    query = read_query('queries/select-albums-with-minimum-tracks.sql')    print(f'############################\n{query}\n############################')    print('----SQL way---')    res = session_sql.execute(query)    print(*res, sep='\n')    print('----ORM way----')    subquery1 = session_orm.query(func.count(Track.id)).group_by(Track.album_id).order_by(func.count(Track.id)).limit(1)    subquery2 = session_orm.query(Track.album_id).group_by(Track.album_id).having(func.count(Track.id) == subquery1)    for item in session_orm.query(Album).join(Track).filter(Track.album_id.in_(subquery2)).order_by(Album.name):        print(f'{item.name}')


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

Шпаргалка по сопоставлению SQL запросов и ORM выражений



1. название и год выхода альбомов, вышедших в 2018 году:
SQL
select namefrom albumswhere year=2018

ORM
session_orm.query(Album).filter_by(year=2018)


2. название и продолжительность самого длительного трека:
SQL
select name, lengthfrom tracksorder by length DESClimit 1

ORM
session_orm.query(Track).order_by(Track.length.desc()).slice(0, 1)


3. название треков, продолжительность которых не менее 3,5 минуты:
SQL
select name, lengthfrom trackswhere length >= 310order by length DESC

ORM
session_orm.query(Track).filter(310 <= Track.length).order_by(Track.length.desc())


4. названия сборников, вышедших в период с 2018 по 2020 год включительно:
SQL
select namefrom collectionswhere (year >= 2018) and (year <= 2020)

ORM
session_orm.query(Collection).filter(2018 <= Collection.year, Collection.year <= 2020)

* обратите внимание что здесь и далее фильтрация задается уже с использованием filter, а не с использованием filter_by.

5. исполнители, чье имя состоит из 1 слова:
SQL
select namefrom musicianswhere not name like '%% %%'

ORM
session_orm.query(Musician).filter(Musician.name.notlike('%% %%'))


6. название треков, которые содержат слово мой/my:
SQL
select namefrom trackswhere name like '%%me%%'

ORM
session_orm.query(Track).filter(Track.name.like('%%me%%'))


7. количество исполнителей в каждом жанре:
SQL
select g.name, count(m.name)from genres as gleft join genres_musicians as gm on g.id = gm.genre_idleft join musicians as m on gm.musician_id = m.idgroup by g.nameorder by count(m.name) DESC

ORM
session_orm.query(Genre).join(Genre.musicians).order_by(func.count(Musician.id).desc()).group_by(Genre.id)


8. количество треков, вошедших в альбомы 2019-2020 годов:
SQL
select t.name, a.yearfrom albums as aleft join tracks as t on t.album_id = a.idwhere (a.year >= 2019) and (a.year <= 2020)

ORM
session_orm.query(Track, Album).join(Album).filter(2019 <= Album.year, Album.year <= 2020)


9. средняя продолжительность треков по каждому альбому:
SQL
select a.name, AVG(t.length)from albums as aleft join tracks as t on t.album_id = a.idgroup by a.nameorder by AVG(t.length)

ORM
session_orm.query(Album, func.avg(Track.length)).join(Track).order_by(func.avg(Track.length)).group_by(Album.id)


10. все исполнители, которые не выпустили альбомы в 2020 году:
SQL
select distinct m.namefrom musicians as mleft join albums_musicians as am on m.id = am.musician_idleft join albums as a on a.id = am.album_idwhere not a.year = 2020order by m.name

ORM
session_orm.query(Musician).join(Musician.albums).filter(Album.year != 2020).order_by(Musician.name.asc())


11. названия сборников, в которых присутствует конкретный исполнитель (Steve):
SQL
select distinct c.namefrom collections as cleft join collections_tracks as ct on c.id = ct.collection_idleft join tracks as t on t.id = ct.track_idleft join albums as a on a.id = t.album_idleft join albums_musicians as am on am.album_id = a.idleft join musicians as m on m.id = am.musician_idwhere m.name like '%%Steve%%'order by c.name

ORM
session_orm.query(Collection).join(Collection.tracks).join(Album).join(Album.musicians).filter(Musician.name == 'Steve').order_by(Collection.name)


12. название альбомов, в которых присутствуют исполнители более 1 жанра:
SQL
select a.namefrom albums as aleft join albums_musicians as am on a.id = am.album_idleft join musicians as m on m.id = am.musician_idleft join genres_musicians as gm on m.id = gm.musician_idleft join genres as g on g.id = gm.genre_idgroup by a.namehaving count(distinct g.name) > 1order by a.name

ORM
session_orm.query(Album).join(Album.musicians).join(Musician.genres).having(func.count(distinct(Genre.name)) > 1).group_by(Album.id).order_by(Album.name)


13. наименование треков, которые не входят в сборники:
SQL
select t.namefrom tracks as tleft join collections_tracks as ct on t.id = ct.track_idwhere ct.track_id is null

ORM
session_orm.query(Track).outerjoin(Track.collections).filter(Collection.id == None)

* обратите внимание что несмотря на предупреждение в PyCharm надо именно так составлять условие фильтрации, если написать как предлагает IDE (Collection.id is None) то оно работать не будет.

14. исполнителя(-ей), написавшего самый короткий по продолжительности трек (теоретически таких треков может быть несколько):
SQL
select m.name, t.lengthfrom tracks as tleft join albums as a on a.id = t.album_idleft join albums_musicians as am on am.album_id = a.idleft join musicians as m on m.id = am.musician_idgroup by m.name, t.lengthhaving t.length = (select min(length) from tracks)order by m.name

ORM
subquery = session_orm.query(func.min(Track.length))session_orm.query(Musician, Track.length).join(Musician.albums).join(Track).group_by(Musician.id, Track.length).having(Track.length == subquery).order_by(Musician.name)


15. название альбомов, содержащих наименьшее количество треков:
SQL
select distinct a.namefrom albums as aleft join tracks as t on t.album_id = a.idwhere t.album_id in (    select album_id    from tracks    group by album_id    having count(id) = (        select count(id)        from tracks        group by album_id        order by count        limit 1    ))order by a.name

ORM
subquery1 = session_orm.query(func.count(Track.id)).group_by(Track.album_id).order_by(func.count(Track.id)).limit(1)subquery2 = session_orm.query(Track.album_id).group_by(Track.album_id).having(func.count(Track.id) == subquery1)session_orm.query(Album).join(Track).filter(Track.album_id.in_(subquery2)).order_by(Album.name)


Как видите, вышеизложенные вопросы подразумевают как простую выборку так и с объединением таблиц, а также использование агрегатных функций и подзапросов. Все это реально сделать с SQLAlchemy как в режиме SQL так и в режиме ORM. Разноообразие операторов и методов позволяет выполнить запрос наверное любой сложности.

Надеюсь данный материал поможет избавиться начинающим быстро и эффективно начать составлять запросы.
Подробнее..
Категории: Python , Sql , Python 3 , Sqlalchemy , Orm

Создаем схему базы данных на SQLAlchemy

07.02.2021 18:17:06 | Автор: admin

Много уже говорилось о том, что SQLAlchemy - одна из самых популярных библиотек для создания схем баз данных. Сегодня рассмотрим несложный пример по созданию небольшой схемы данных для приложения по поиску цитат. В качестве СУБД будем использовать PostgreSQL.

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

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

Построим схему в SQLAlchemy в соответствии с диаграммой. Для удобства запросов к базе и манипуляций с моделями включим в таблицу Quote relationship.

from sqlalchemy import Column, ForeignKey, Integer, String, Text, Date, DateTimefrom sqlalchemy.orm import relationshipfrom sqlalchemy.ext.declarative import declarative_baseBase = declarative_base()class Topic(Base):    __tablename__ = 'topic'    __tableargs__ = {        'comment': 'Темы цитат'    }    topic_id = Column(        Integer,        nullable=False,        unique=True,        primary_key=True,        autoincrement=True    )    name = Column(String(128), comment='Наименование темы')    description = Column(Text, comment='Описание темы')    def __repr__(self):        return f'{self.topic_id}{self.name}{self.description}'class Author(Base):    __tablename__ = 'author'    __tableargs__ = {        'comment': 'Авторы цитат'    }    author_id = Column(        Integer,        nullable=False,        unique=True,        primary_key=True,        autoincrement=True    )    name = Column(String(128), comment='Имя автора')    birth_date = Column(Date, comment='Дата рождения автора')    country = Column(String(128), comment='Страна рождения автора')    def __repr__(self):        return f'{self.author_id}{self.name}{self.birth_date}{self.country}'class Quote(Base):    __tablename__ = 'quote'    __tableargs__ = {        'comment': 'Цитаты'    }    quote_id = Column(        Integer,        nullable=False,        unique=True,        primary_key=True,        autoincrement=True    )    text = Column(Text, comment='Текст цитаты')    created_at = Column(DateTime, comment='Дата и время создания цитаты')    author_id = Column(Integer,  ForeignKey('author.author_id'), comment='Автор цитаты')    topic_id = Column(Integer, ForeignKey('topic.topic_id'), comment='Тема цитаты')    author = relationship('Author', backref='quote_author', lazy='subquery')    topic = relationship('Topic', backref='quote_topic', lazy='subquery')    def __repr__(self):        return f'{self.text}{self.created_at}{self.author_id}{self.topic_id}'

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

Для каждого из столбцов таблицы есть возможность задать различные параметры, как и в различных СУБД. Внешние ключи задаются через название таблицы и поле, которое будет являться внешним ключом. Для удобства операций с данными используется relationship. Он позволяет связать объекты таблиц, а не только отдельные поля, как происходит при объявлении только внешних ключей. Параметр lazy определяет, как связанные объекты загружаются при запросе через отношения. Значения joined и subquery фактически делают одно и то же: объединяют таблицы и возвращают результат, но под капотом устроены по-разному, поэтому могут быть различия в производительности, поскольку они по-разному объединяются в таблицы.

Магический метод __repr__ фактически определяет, что будет выведено на экран при распечатке таблицы.

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

engine = create_engine('postgresql://user:password@host/tablename')Base.metadata.create_all(engine)

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

Подробнее..
Категории: Python , Sql , Python 3 , Sqlalchemy , Database design

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru