Пересечение нескольких множеств в одну строчку
Сегодня на паре по программированию для журналистики данных мы столкнулись с такой задачей: есть список множеств, нужно найти пересечение их всех. Как это проще всего сделать в Python? Оказывается, у этой задачи есть решение в одну строчку.
Собственно, дело было так. Мы смотрели на данные по госзакупкам через API сайта clearspending.ru. Там у каждого контракта куча разных атрибутов. Как видно из следующих примеров, наборы атрибутов (в данном случае, ключей в словаре) у разных контрактов различаются. Мы хотели найти такие атрибуты, которые есть у всех контрактов.
import requests
# пример из документации
# https://github.com/idalab/clearspending-examples/wiki/Описание-API-Контракты
entrypoint = "http://openapi.clearspending.ru/restapi/v3/contracts/search/"
r = requests.get(entrypoint, {'customerregion': '05', 'sort':'-price'})
response = r.json()
contracts = response['contracts']['data']
contracts[0].keys()
contracts[1].keys()
По такому поводу я рассказал про множества в Python (давно откладывал это — к слову не приходилось) и написал такой код.
fields = set(contracts[0])
# сделали множество из списка ключей первого контракта
# здесь словарь contracts[0] рассматривается как iterable
# а в этом случае он итерирует свои ключи
# поэтому .keys() дописывать не нужно
for contract in contracts[1:]:
fields.intersection_update(contract)
# пересечь множество fields с множеством, полученным из списка ключей
# очередного контракта и результат записать в fields
# иными словами, выкинуть из fields те элементы, которых нет в
# списке ключей очередного контракта
fields
Он мне перестал нравиться ещё до того, как я закончил его писать. Ну, право дело, ведь когда нам нужно сложить числа в списке, мы не пишем цикл — мы просто вызываем функцию sum()
. Должно, наверное, и для множеств быть что-то похожее? На паре тратить время на поиски не хотелось, но, придя домой, я всё же решил найти ответ на этот вопрос. Оказывается, есть очень просто решение!
fields = set.intersection(*[set(contract) for contract in contracts])
fields
Дело в том, что set.intersection()
принимает на вход любое количество аргументов! С помощью спискового включения мы делаем список множеств, составленных из ключей каждого контракта, затем звёздочкой «распаковываем» этот список в набор аргументов set.intersection()
— вжух — и всё готово!
Пожалуй, по сравнению с моим исходным подходом есть только один недостаток: по памяти это решение более требовательное, потому что сначала создаётся список, потом он передаётся функции. Причём из-за необходимости распаковывать элементы здесь бессмысленно заменять список на генератор — всё равно память придётся тратить. Впрочем, с практической точки зрения это скорее всего не принципиально.
UPD. После подсказки @rusorrow о том, что .keys()
не нужен при создании множества, я подумал, что можно было бы сделать ещё короче: с помощью map
— мне кажется, что так даже лучше — это, пожалуй, тот случай, когда map
упрощает код по сравнению со списочными включениями.
set.intersection(*map(set, contracts))
Комментарии