Глава 8. Функции на языке Python
Главы
- Введение в документы
- Базовые возможности JSON
- JSON в таблицах
- Индексирование JSON
- Ограничения в документах
- Язык путей JSONPath
- Отчеты, функции, расписание
- Функции на языке Python
- Версионирование и архивация документов
- Релевантный поиск
Содержание
- Главы
- Предварительные шаги
- Простые функции на Python
- Сторонние пакеты
- Работа с json(b). Трансформации
- Функции для работы с заявками
- Прочие сведения
Обсудим тему, которая, надеемся, разожжет в читателе интерес: как подключить к Postgres другие языки, например Python? Техника предлагает интересные возможности, особенно для работы с документами. Читатель узнает, как связать интерпретатор Python с базой данных и что можно сделать с его помощью. Считайте эту главу факультативом к предыдущей – дополнением, которое оказалось слишком длинным для параграфа и публикуется отдельно.
В прошлой главе мы познакомились с основами функций. В числе прочего мы упомянули, что функции можно писать на разных языках. По умолчанию это SQL, который не отличается от обычных запросов. Диалект plpgsql предлагает переменные, циклы, исключения и все то, что свойственно императивным языкам.
Оба диалекта встроены в Postgres и доступны по умолчанию. Чтобы использовать другие языки, прибегают к следующей схеме. На сервере, где запущен Postgres, устанавливают интерпретатор языка – это могут быть Perl, Python, Tcl и другие технологии. Далее включают расширение Postgres, которое связывает сервер с интерпретатором. Теперь можно писать функции на новом языке. Свойство language подсказывает Postgres, какой именно используется, например language plperl. Если все настроено без ошибок, управление передается интерпретатору, а результат – обратно пользователю. Схематично процесс выглядит так:
┌────────────────────────────────────────────────────────────────────┐
│ SQL invoke │
│┌─────────────┐ ┌─────────────┐ ┌────────────────┐│
││ │ │ │ │ ││
││ ├──────────▶ ├──────────▶ ││
││ user │ │ Postgres │ │ Python, Perl ││
││ ◀──────────┤ ◀──────────┤ ││
││ │ │ │ │ ││
│└─────────────┘ response └─────────────┘ response └────────────────┘│
└────────────────────────────────────────────────────────────────────┘
Схема требует ряда условий. Так, интерпретатор должен находиться в определенной директории – той, где ожидает его Postgres. Возможна ситуация, когда интерпретатор установлен и работает, однако Postgres ищет его по другому пути.
Разные версии Postgres требуют разных версий интерпретатора. В случае с Python соответствие выражено таблицей (взято с сайта postgresapp.com):
| PostgreSQL Version | Python Version |
|---|---|
| PostgreSQL 18 | Python >= 3.9.x from python.org |
| PostgreSQL 17 | Python 3.13.x from python.org |
| PostgreSQL 16 | Python 3.12.x from python.org |
| PostgreSQL 15 | Python 3.11.x from python.org |
| PostgreSQL 14 | Python 3.9.x from python.org |
| PostgreSQL 13 | Python 3.8.x from python.org |
| PostgreSQL 12 and earlier | Python 2.7 (included with macOS) |
Postgres можно связать с разными языками: Perl, Python, Ruby, Node.js, Tcl. Для дальнейшей работы мы выберем Python. Среди кандидатов он – пожалуй, самый популярный язык. Python отличается простым синтаксисом и богатыми возможностями. Он подходит для новичков в программировании, для многих он – первый язык. Знание Python желательно, если вы работаете в финансах, аналитике и вообще любой сфере, связанной с компьютерами.
Возможно, читатель задался вопросом: зачем добавлять сторонний язык в Postgres? На это могут быть разные причины. Главная заключается в том, что данные нужны многим людям – не только программистам, но и менеджерам, аналитикам, руководству. У людей разные навыки: одни знают SQL, другие владеют скриптовыми языками. Порой сотрудников, которые знают Python, достаточно много для того, чтобы установить ради них расширение. При этом принимают меры, которые мы оговорим позже.
Другая причина в том, что иногда сторонний язык быстрее и лаконичней. По сравнению с Python диалект plpgsql крайне многословен. Он труден в работе со вложенными документами – предметом разговора нашей книги. Ниже мы увидим, что одна и та же логика, выраженная на двух языках, читается по-разному.
Предварительные шаги
Коротко опишем, как установить Python и связать его с базой данных. Предупредим читателя, что материал в этом разделе неполный. Установка зависит от многих параметров: операционной системы, ее версии, пакетного менеджера, версии Postgres, системных библиотек. Их сочетание дает матрицу, заполнить которую – сложная задача. Ограничимся лишь простыми случаями и ссылками на документацию.
Перед установкой сверьтесь с таблицей версий, которую мы привели выше. Из нее следует, что Postgres младше версии 18 требует конкретных версий интерпретатора. Скажем, для Postgres 17 это Python 3.13.x. У Postgres 18 требования лояльнее: подойдет любой интерпретатор старше версии 3.9.x. Текст ниже предполагает, что мы работаем с Postgres 17, и нам понадобится Python версии 3.13.
Требование конкретной версии усложняет установку. Дело в том, что если выполнить команду ниже, вы установите последнюю версию Python (3.14.6 на момент написания книги), а нам необходима 3.13.x.
sudo apt install python3 python3-pip python3-dev -y
Если ваша операционная система – Linux семейства Debian подключите сторонний PPA-репозиторий deadsnakes. В нем находятся разные версии языка, в том числе устаревшие:
sudo apt update
sudo apt install software-properties-common -y
sudo add-apt-repository ppa:deadsnakes/ppa -y
sudo apt update
После этого установите Python конкретной версии, в том числе утилиту pip и
библиотеки разработчика:
sudo apt install python3.13 python3.13-pip python3.13-dev -y
Следующая команда установит расширение plpython3, которое служит посредником
между базой и интерпретатором.
sudo apt install postgresql-contrib-17 postgresql-plpython3-17 -y
Теперь проверьте установку, выполнив:
python3 --version
# Python 3.13.9
Убедитесь, что версия равна 3.13 с точностью до двух старших компонент, например 3.13.0 или 3.13.9, но не 3.14.0.
Пользователи MacOS, как правило, пользуются менеджером пакетов
Homebrew. Интерпретатор Python, установленный из него, находится в директории
/opt/homebrew/bin, однако Postgres ожидает его в другом месте:
/Library/Frameworks/Python.framework/Versions/3.13/bin/python3
Чтобы удовлетворить это требование, скачайте графический установщик 3.13 c сайта
python.org и запустите его – Python окажется в нужной директории. Другой способ
– создать мягкую ссылку из homebrew в /Library/Frameworks при помощи ln:
sudo ln -s /opt/homebrew/Frameworks/Python.framework /Library/Frameworks/
Команда sudo необходима, потому что каталог /Library – системный. Как
отмечает документация, мягкая ссылка не рекомендуется к
использованию, потому что при загрузке библиотек следование ссылкам не
гарантируется.
В случае Windows установка проста: скачайте дистрибутив Python 3.13 с официального сайта и запустите его.
Когда вы закончили с установкой, перейдите в консоль psql. Подключитесь к базе
под суперпользователем и установите расширение командой:
CREATE EXTENSION plpython3u;
Буква “u” на конце означает untrusted, то есть недоверенное расширение. Устанавливать подобные расширения может только владелец базы. Python имеет доступ к файлам, сети и системе в целом, поэтому код на нем нельзя считать безопасным.
Если расширение не установилось, вы получите сообщение об ошибке. Наиболее вероятно, что Postgres ищет интерпретатор не в том месте, где он установлен, а в каком-то другом. В этом случае сообщение довольно подробно:
ERROR: could not load library "/Applications/Postgres.app/Contents/Versions/17/lib/postgresql/plpython3.dylib": dlopen(/Applications/Postgres.app/Contents/Versions/17/lib/postgresql/plpython3.dylib, 0x000A): Library not loaded: /Library/Frameworks/Python.framework/Versions/3.13/Python
Referenced from: <B2141B7A-14B3-3F4E-86A7-96D338EA4385> /Applications/Postgres.app/Contents/Versions/17/lib/postgresql/plpython3.dylib
Reason: tried: '/Library/Frameworks/Python.framework/Versions/3.13/Python' (no such file), '/System/Volumes/Preboot/Cryptexes/OS/Library/Frameworks/Python.framework/Versions/3.13/Python' (no such file), '/Library/Frameworks/Python.framework/Versions/3.13/Python' (no such file)
Читать его следует так: не удалось загрузить динамическую библиотеку plpython3.dylib. Причина в том, что ни по одному из путей не найден интерпретатор Python.
Проблему решают установкой интерпретатора в нужное место. Также можно скопировать файлы или создать мягкую ссылку командой ln. Другая возможная ошибка – несовпадение версий. Postgres требует одну версию интерпретатора, а установлена другая. Эта проблема тоже снимается переустановкой.
Надеемся, команда create extension прошла без ошибок, и вы готовы следовать дальше. Если же нет, предлагаем читателю ссылки, которые, возможно, помогут довести установку до конца:
- PostgreSQL: how to install plpythonu extension
- How to install Postgres 16 with plpython3u: Recipes for macOS, Ubuntu, Debian, CentOS, Docker
Простые функции на Python
Когда расширение установлено, опробуем его на простой функции. Обратите
внимание, что свойство language равно plpython3u:
create or replace function my_python_func(a integer, b text, c boolean)
returns text
language plpython3u as $$
items = [a, b, c]
return ', '.join(map(str, items))
$$;
Функция вызывается как обычно:
select my_python_func(20, 'hello', true) as x;
┌─────────────────┐
│ x │
├─────────────────┤
│ 20, hello, True │
└─────────────────┘
Обратите внимание: элемент True начинается с заглавной буквы. Это доказывает,
что булево значение было приведено к строке на стороне Python: там они
начинаются с заглавной (True, False).
Пробы прошли удачно, и теперь мы погрузимся в детали. Первое, что мы обсудим, будут типы.
При работе с интерпретатором важно понимать, как сопоставляются типы Postgres и стороннего языка, в нашем случае – Python. Очевидно, сперва они преобразуются в одну сторону: из Postgres в Python, а затем обратно: результат Python преобразуется в Postgres. Знание этих принципов поможет избежать недоразумений.
Между Postgres и Python действуют следующие соглашения:
- Целочисленные типы
int2,int4,int8становятся типомIntegerв Python; - Числа с плавающей точкой
float4иfloat8становятсяFloat; - Строковые типы
text,varcharи другие приводятся к строкеunicode; - Массивы приводятся к спискам (тип List);
- Байтовый массив становится типом
bytes; - Логические значения
trueиfalse–Boolean; Nullпреобразуется кNone.
Остальные типы передаются строками, и код на Python должен распарсить их. Ниже мы узнаем, как автоматизировать приведение некоторых типов, в частности самого нужного – json(b).
При работе с Python часто используют отладочную печать – функцию print. Если
вызвать ее в теле функции, мы не увидим результата ни в консоли psql, ни в логах
Postgres. Вместо print используйте глобальный объект plpy и его метод info.
Давайте проверим, что получит на вход функция, принимающая тип
timestamptz. Для этого залогируем тип аргумента:
create or replace function py_datetime_test(dt timestamptz)
returns text
language plpython3u as $$
plpy.info("argument: %s, type: %s" % (dt, type(dt)), hint="debug")
return str(dt)
$$;
Вот что мы увидим в psql при вызове функции:
INFO: argument: 2026-06-20 16:34:02.298144+03, type: <class 'str'>
HINT: debug
Выражение type: <class 'str'> говорит о том, что на стороне Python аргумент
оказался строкой. Чтобы привести его к привычному datetime, распарсим его
методом fromisoformat. Сама функция вернет год полученной даты:
create or replace function py_datetime_test2(dt_iso timestamptz)
returns int8
language plpython3u as $$
from datetime import datetime
dt = datetime.fromisoformat(dt_iso)
plpy.info("argument: %s, type: %s" % (dt, type(dt)), hint="debug")
return dt.year
$$;
select py_datetime_test2(now()) as x;
INFO: argument: 2026-06-20 16:39:41.228606+03:00, type: <class 'datetime.datetime'>
HINT: debug
┌──────┐
│ x │
├──────┤
│ 2026 │
└──────┘
Когда код отлажен, вызовы plpy.info удаляют.
Сторонние пакеты
Функции на Python могут импортировать модули. Выше мы использовали стандартный
модуль datetime; чуть позже мы воспользуемся json. К модулям применяются
стандартные правила работы с ними. Они загружаются лишь однажды, поэтому
повторный вызов import datetime ничего не делает. При загрузке модулей
просматриваются пути относительно того места, где установлен
интерпретатор. Обычно это директория site-packages и некоторые другие.
Чтобы использовать сторонний пакет, установите его в тот интерпретатор, который ищет Postgres. Приведем пример с пакетом numpy. Выполните в системе:
pip3 install numpy
Напишем функцию, которая перемножает матрицы средствами numpy:
create or replace function py_test3()
returns text
language plpython3u as $$
import numpy as np
A = np.array([[1, 2],
[3, 4]])
B = np.array([[5, 6],
[7, 8]])
C = A * B
plpy.info("matrix mul result: %s" % C, hint="debug")
return str(C)
$$;
Проверка:
select py_test3() as x;
INFO: matrix mul result: [[ 5 12] [21 32]]
HINT: debug
┌───────────┐
│ x │
├───────────┤
│ [[ 5 12] ↵│
│ [21 32]] │
└───────────┘
Типичная ошибка с установкой пакетов – они ставятся в директорию другого интерпретатора и поэтому недоступны в Postgres. Здесь как нельзя лучше подходит картинка Python Environment из комикса xkcd: она иллюстрируют весь хаос с путями Python. Убедитесь, что pip3 ссылается на нужный вам интерпретатор. Возможно, вам понадобится утилита venv для изолированного окружения Python.
Работа с json(b). Трансформации
Опробуем Python в работе с JSON-документами. Пусть это будет функция, которая принимает документ и дополняет его полями:
create or replace function py_test_doc(doc jsonb)
returns jsonb
language plpython3u as $$
doc2 = doc.copy()
doc2["foo"] = 1
doc2["bar"] = 2
return doc2
$$;
Если ее вызвать, получим ошибку:
select py_test_doc('{"a": "hello"}');
ERROR: AttributeError: 'str' object has no attribute 'copy'
CONTEXT: Traceback (most recent call last):
PL/Python function "py_test_doc", line 2, in <module>
doc2 = doc.copy()
PL/Python function "py_test_doc"
Оказывается, переменная doc – строка. Вспомним, что мы говорили о
преобразовании типов: jsonb не попадает ни под одно правило, поэтому
передается строкой – ровно как и другие сложные типы вроде hstore, timestamp
и других.
Из положения выходят двумя способами. Первый – распарсить параметр модулем json, а результат, напротив, привести к JSON-строке. В этом случае все пройдет без ошибок:
create or replace function py_test_doc(doc jsonb)
returns jsonb
language plpython3u as $$
import json
doc2 = json.loads(doc)
doc2["foo"] = 1
doc2["bar"] = 2
return json.dumps(doc2)
$$;
select py_test_doc('{"a": "hello"}');
┌────────────────────────────────────┐
│ py_test_doc │
├────────────────────────────────────┤
│ {"a": "hello", "bar": 2, "foo": 1} │
└────────────────────────────────────┘
Недостаток этого подхода в ручной работе. Когда у вас много функций для работы с
JSON, каждая начинается и заканчивается вызовами json.loads() и
json.dumps(). Это неудобно и засоряет код.
Второй способ – зарегистрировать трансформацию типов. Трансформация – это запись в системной таблице Postgres, которая объединяет язык, тип и две функции: кодирования и декодирования. Функции пишут на языке Си, чтобы обеспечить быстродействие. Создание трансформации для Python выглядит так:
CREATE TRANSFORM FOR <type> LANGUAGE plpython3u (
FROM SQL WITH FUNCTION <pg_to_python>(internal),
TO SQL WITH FUNCTION <python_to_pg>(internal)
)
В примере выше <type> означает тип, который нас интересует, а <pg_to_python>
и <python_to_pg> – функции преобразования в одну сторону и обратно. Детали
этой команды описаны в документации.
Разумеется, писать функции на Си, отлаживать и регистрировать их – непростая
задача. Расширение jsonb_plpython3u берет ее на себя: при включении оно выполнит
CREATE TRANSFORM для типов json и jsonb с нужными функциями. Выполните:
create extension jsonb_plpython3u;
Когда расширение установлено, дополните функцию выражением (до тела):
TRANSFORM FOR TYPE jsonb
С ним вызовы json.loads() и json.dumps() можно удалить, ровно как и импорт
модуля json. Параметр doc попадает функцию, будучи приведенным к структуре
Python, а результат автоматически приводится к json(b). Вот как выглядит новая
функция:
create or replace function py_test_doc(doc jsonb)
returns jsonb
transform for type jsonb
language plpython3u as $$
doc2 = doc.copy()
doc2["foo"] = 1
doc2["bar"] = 2
return doc2
$$;
Функции для работы с заявками
Рассмотрим, как использовать мощь Python в работе с документами. В прошлой главе мы написали несколько функций для заявок. Перепишем их на новом языке, чтобы почувствовать разницу.
Первая функция – получить сотрудников, которые работают над заявкой, в виде строки через запятую. Имена должны быть уникальными и следовать по алфавиту.
Функция сводится к двум циклам for: внешнему по полю departments и
внутреннему – по users. Найденные элементы добавляем в множество, чтобы
исключить повторы. Далее приводим его к списку и сортируем. Метод .join()
соединяет элементы списка разделителем.
create or replace function py_app_users(doc jsonb)
returns text
transform for type jsonb
immutable strict parallel safe
language plpython3u as $$
user_set = set()
for dep in doc["departments"]:
for user in dep["users"]:
user_set.add(user["name"])
user_list = list(user_set)
user_list.sort()
return ", ".join(user_list)
$$;
Проверим функцию:
select
doc->>'application_id' as app_id,
py_app_users(doc) as users
from
applications
limit
100;
Результат:
┌────────┬────────────────────────────────────┐
│ app_id │ users │
├────────┼────────────────────────────────────┤
│ 1 │ User 1, User 11, User 21, User 31 │
│ 2 │ User 12, User 2, User 22, User 32 │
│ 3 │ User 13, User 23, User 3, User 33 │
│ 4 │ User 14, User 24, User 34, User 4 │
│ 5 │ User 15, User 25, User 35, User 5 │
│ 6 │ User 16, User 26, User 36, User 6 │
│ 7 │ User 17, User 27, User 37, User 7 │
│ 8 │ User 18, User 28, User 38, User 8 │
Другой пример – получить пользователя, который работал над заявкой
последним. Поле journal заявки хранит историю статусов: событие, время и код
пользователя. Снача найдем последний по времени статус: для этого упорядочим
список events по полю datetime. Затем возьмем элемент с конца, а из него –
поле user_id. Учтем, что список может быть пустым и оператор events[-1]
спровоцирует IndexError. В этом случае вернем None (в терминах Postgres –
null).
create or replace function py_app_last_event_user_id(doc jsonb)
returns uuid
transform for type jsonb
immutable strict parallel safe
language plpython3u as $$
events = doc["journal"]
events.sort(key=lambda event: event['datetime'])
try:
return events[-1]["user_id"]
except IndexError:
return None
$$;
Вызов функции:
select
doc->>'application_id' as app_id,
py_app_last_event_user_id(doc) as last_user_id
from
applications
limit
10;
Результат:
┌────────┬──────────────────────────────────────┐
│ app_id │ last_user_id │
├────────┼──────────────────────────────────────┤
│ 129 │ 1f4c535f-2cbb-4d54-be0a-05e3cbbc7734 │
│ 130 │ 4d17e4ae-b8b2-4dbf-acaf-d42924aa8963 │
│ 131 │ 0506442e-449e-47e6-9584-e6cf35c95259 │
│ 132 │ dcffbf96-a2ee-4a5d-9209-b371f52c46d4 │
│ 133 │ 92c2e2e8-fa21-4fa8-8675-b12433af61b8 │
│ 134 │ ed2b40ee-b897-41fe-8a3a-bc81390a610f │
│ 135 │ dd3986e8-6ece-4f7e-911f-fff5bcbbb276 │
│ 136 │ 3555410a-5db8-4870-acd9-884d264768ac │
│ 137 │ 2309d179-6f8f-43ff-9a8c-892d42d50594 │
│ 138 │ fa7c1304-a3fa-4f88-aeea-391cbc1fdcaa │
└────────┴──────────────────────────────────────┘
Приведем еще одну функцию, на этот раз сложнее двух предыдущих. Ее задача – отобразить финансовые показатели заявки в виде короткой строки. Напомним, что заявка на кредит содержит несколько вариантов, в каждом из которых свои сумма, валюта и срок. Приведем фрагмент этих данных:
"amounts": [
{
"amount": 42278084,
"period": {
"d": 6,
"m": 1,
"w": 10,
"y": 8
},
"currency": "RUB"
},
{
"amount": 92662590,
"period": {
"d": 3,
"m": 2,
"w": 10,
"y": 8
},
"currency": "EUR"
}
]
Требуется следующее:
- Для каждого варианта подсчитать сумму в рублях. Если валюта отлична от рублей, привести сумму к рублям согласно курсу обмена (передается словарем).
- Рассчитать дату окончания относительно текущей даты.
- Построить строку вида миллионы рублей -> срок окончания.
Пример того, что от нас ожидают:
┌────────┬──────────────────────────────────────────────────────────────┐
│ app_id │ amount_summary │
├────────┼──────────────────────────────────────────────────────────────┤
│ 129 │ 1.110 mln RUB => 2030-01-10; 6.971 mln RUB => 2030-05-30 │
│ 130 │ 82.978 mln RUB => 2029-05-21; 551.047 mln RUB => 2033-05-25 │
│ 131 │ 6.084 mln RUB => 2028-11-13; 761.777 mln RUB => 2029-11-09 │
Курсы обмена передаются словарем валюта -> коэффициент. Последний означает, на сколько нужно умножить сумму в валюте, отличной от рубля, чтобы получить рубли. Для самого рубля коэффициент равен единице:
{
"RUB": 1.0,
"EUR": 82.9,
"USD": 73.4
}
Изучите код функции, а ниже мы его прокомментируем:
create or replace function py_app_amount_summary(doc jsonb, rates jsonb)
returns text
transform for type jsonb
immutable strict parallel safe
language plpython3u as $$
from datetime import date
from dateutil.relativedelta import relativedelta
now = date.today()
parts = []
amounts = doc["amounts"]
for amount in amounts:
currency = amount["currency"]
rate = rates[currency]
value = float(amount["amount"] * rate)
y = int(amount["period"]["y"])
m = int(amount["period"]["m"])
w = int(amount["period"]["w"])
d = int(amount["period"]["d"])
days = d + w * 7
td = relativedelta(years=y, months=m, days=days)
limit = now + td
parts.append("%.3f mln RUB => %s" % (value / 10e6, limit))
return "; ".join(parts)
$$;
Список parts накапливает фрагменты, которые позже объединятся в
строку. Обходим элементы списка amounts: на каждом шаге находим валюту и курс
обмена из словаря rates. Умножив курс на сумму, мы получим сумму в рублях –
переменную value. Мы воспользовались числами с плавающей запятой, хотя для
денег это не лучшее решение из-за потери точности. В промышленном коде мы бы при
применили класс Decimal.
Чтобы получить срок окончания, к текущей дате прибавляем число лет, месяцы,
недель и дней. Здесь тоже кроется тонкость: встроенный класс timedelta не
поддерживает сложение с типами date и datetime. Понадобится сторонний пакет
relativedelta. Установите его командой:
pip3 install relativedelta
Перезагружать сервер Postgres не требуется: интерпретатор подхватит новый пакет при вызове функции.
Когда сумма и срок окончания найдены, в список заносится строка с их форматированным представлением. На последнем этапе список объединяется точкой с запятой.
Составим запрос с новой функцией для десяти случайных заявок. Курсы обмена
поместим в секцию CTE под именем rates. Выражение from applications, rates
означает, что каждая строка rates будет соединена со строкой applications. В
функцию py_app_amount_summary передается заявка и словарь rates:
with
rates(rates) as (
select * from (values ($$
{
"RUB": 1.0,
"EUR": 82.9,
"USD": 73.4
}
$$::jsonb))
)
select
doc->>'application_id' as app_id,
py_app_amount_summary(doc, rates) as amount_summary
from
applications, rates
limit
10;
Результат:
┌────────┬──────────────────────────────────────────────────────────────┐
│ app_id │ amount_summary │
├────────┼──────────────────────────────────────────────────────────────┤
│ 129 │ 1.110 mln RUB => 2030-01-10; 6.971 mln RUB => 2030-05-30 │
│ 130 │ 82.978 mln RUB => 2029-05-21; 551.047 mln RUB => 2033-05-25 │
│ 131 │ 6.084 mln RUB => 2028-11-13; 761.777 mln RUB => 2029-11-09 │
│ 132 │ 67.613 mln RUB => 2029-10-11; 639.206 mln RUB => 2031-05-08 │
│ 133 │ 222.791 mln RUB => 2035-02-01; 420.098 mln RUB => 2033-01-05 │
│ 134 │ 193.476 mln RUB => 2029-12-08; 240.862 mln RUB => 2028-12-26 │
│ 135 │ 280.998 mln RUB => 2030-09-20; 4.927 mln RUB => 2035-02-20 │
│ 136 │ 1.351 mln RUB => 2027-11-08; 0.230 mln RUB => 2028-02-09 │
│ 137 │ 3.701 mln RUB => 2028-05-31; 455.712 mln RUB => 2036-11-25 │
│ 138 │ 4.096 mln RUB => 2030-10-31; 1.517 mln RUB => 2030-05-31 │
└────────┴──────────────────────────────────────────────────────────────┘
Автор согласен, что последний пример сложен и требует концентрации. Однако он повторяет одну из задач, которую приходилось решать на практике. Возможно, такая же задача однажды встанет перед читателем.
На этом месте мы приостановим обзор plpython3u. Цель данной главы не в том,
чтобы выучить новый язык – о Python написаны сотни книг и статей, и будет лучше
обратиться к ним. Автор надеялся разжечь в читателе интерес, показать интересную
возможность: подключить сторонний язык, чтобы упростить работу с
данными. Полагаем, читатель понял принцип, а технические детали найдутся в
документации.
Прочие сведения
Перечислим положения, которых следует придерживаться при работе с Python.
При работе с коллекциями мы использовали квадратные скобки, чтобы взять элемент по индексу (для списков) или значение по ключу (для словарей):
id = doc["attrs"]["journal"][-1]["user_id"]
Если нужного индекса или ключа нет, возникнет исключение: IndexError или
KeyError. Квадратные скобки небезопасны: используя их, мы полагаем, что искомые
поля существуют, что не всегда так. Иные документы отличаются, даже если
описывают одну сущность. Для словарей вместо квадратных скобок лучше
использовать мягкий доступ методом get. Последний возвращает либо None, либо то,
что передано вторым параметром:
doc = {...}
id = doc.get("attrs", {}).get("organization", {}).get("id")
Подобные конструкции, однако, многословны. Поможет функция getin, которая
принимает документ и произвольное число ключей. На первом KeyError функция
прерывается и возвращает None:
def getin (obj, *paths):
result = obj
for path in paths:
try:
result = result[path]
except KeyError:
return None
return result
Пример:
data = {"a": {"b": {"c": 5}}}
print(getin(data, "a", "b", "c"))
# 5
print(getin(data, "a", "dunno", "c"))
# None
К сожалению, список не предлагает метода get, так что придется либо проверять
его длину, либо ловить исключение IndexError. Расширьте функцию getin так,
чтобы она проверяла тип очередного элемента (словарь или список) и применяла к
нему метод .get или индексный оператор.
Коротко мы обмолвились о глобальном объекте plpy. С его помощью не только
логируют данные: plpy выполняет запросы к базе (plpy.execute), управляет
транзакциями (plpy.commit/rollback) и многое другое. Изучите его методы в
документации.
Если функция на Python возвращает множество элементов (setof <type>),
результатом может быть даже не список, а итератор – объект с методами __iter__
и __next__. Подобный объект производит элементы по требованию, чаще всего на
базе прежних значений. Примером итератора могут быть факториал или числа
Фибоначчи. Также им обходят сложные структуры, например граф маршрутов или
дерево связей.
Поскольку Python имеет неограниченный доступ к системе, примите хотя бы
минимальные меры предосторожности. В идеале plpython3u устанавливают на
реплике – копии базы данных, доступной только для чтения. Код на Python должен
быть тщательно вычитан, опробован локально и покрыт тестами.
Если сотрудники знают Python, но установить plpython3u нет возможности,
рассмотрите концепцию плейбуков. Плейбук – это веб-сервер, который вызывает
удаленный интерпретатор. Для запуска кода сотруднику не требуется локальное
окружение: все выполняется на сервере. Веб-интерфейс показывает результаты в
удобном виде: матрицы и таблицы можно свернуть и развернуть. Экземпляр класса
PIL.Image.Image выводится картинкой, а не байтовым массивом.
Плейбуки популярны в финансах и науке. Они поддерживают многие языки, однако Python, как правило, доступен изначально. Наиболее известный проект называется Jupiter; его можно развернуть локально. Существуют и облачные плейбуки, например Splunk.
Итак, мы рассказали все, что планировали о языке Python и его связи с Postgres. Надеемся, читателю было интересно. Переходим к следующей главе – как версионировать документы.
Нашли ошибку? Выделите мышкой и нажмите Ctrl/⌘+Enter