Ваш пост VoAGI – почему не стоит злоупотреблять формированием списков в Python.

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

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

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

  • Красивое лучше, чем уродливое.
  • Простое лучше, чем сложное.
  • Читаемость имеет значение.

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

Итак, давайте начнем кодировать!

Компрехензии списков в Python: краткий обзор

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

new_list = []for item in iterable: new_list.append(output)

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

new_list = [output for item in iterable]

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

Следующий фрагмент кода:

new_list = []for item in iterable: if condition: new_list.append(output)

Может быть заменен этой компрехензией списка:

new_list = [output for item in iterable if condition]

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

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

Пример 1: Генерация простых чисел

Проблема: Дано число upper_limit, сгенерировать список всех простых чисел до этого числа.

Эту проблему можно разбить на две ключевые идеи:

  • Проверка, является ли число простым
  • Заполнение списка всеми простыми числами

Выражение компрехензии списка для этого выглядит следующим образом:

import mathupper_limit = 50 primes = [x for x in range(2, upper_limit + 1) if x>1 and all(x % i != 0 for i in range(2, int(math.sqrt(x)) + 1))]print(primes)

А вот результат:

Output >>>[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]

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

Возможно, лучше?

import mathupper_limit = 50 primes = [ x for x in range(2, upper_limit + 1) if x>1 and all(x % i != 0 for i in range(2, int(math.sqrt(x)) + 1))]print(primes)

Гораздо легче читать, безусловно. Теперь давайте напишем действительно лучшую версию.

 

Лучшая версия

 

Хотя использование comprehension-списков является хорошей идеей для решения этой проблемы, логика проверки простых чисел в comprehension-списке делает его громоздким.

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

import mathdef is_prime(num):    return num > 1 and all(num % i != 0 for i in range(2, int(math.sqrt(num)) + 1))upper_limit = 50  primes = [    x   for x in range(2, upper_limit + 1)  if is_prime(x)]print(primes)

 

Результат >>>[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]

 

Будет ли лучшая версия достаточно хорошей? Это делает выражение comprehension намного легче для понимания. Теперь ясно, что выражение собирает все числа до upper_limit, которые являются простыми (т.е. is_prime() возвращает True).

 

Пример 2: Работа с матрицами

 

Проблема: Дана матрица, найдите следующее:

  • Все простые числа
  • Индексы простых чисел
  • Сумму простых чисел
  • Простые числа, отсортированные в порядке убывания

  

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

Однако, чтобы найти индексы, нам понадобится еще одно сложное выражение comprehension (я отформатировал код так, чтобы его легко читать).

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

Вот код:

import mathfrom pprint import pprintmy_matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]def is_prime(num):    return num > 1 and all(num % i != 0 for i in range(2, int(math.sqrt(num)) + 1))# Выровнять матрицу и отфильтровать только простые числаprimes = [    x   for row in my_matrix    for x in row    if is_prime(x)]# Найти индексы простых чисел в исходной матрицеprime_indices = [  (i, j)  for i, row in enumerate(my_matrix)  for j, x in enumerate(row)  if x in primes]# Вычислить сумму простых чиселsum_of_primes = sum(primes)# Отсортировать простые числа в порядке убыванияsorted_primes = sorted(primes, reverse=True)# Создать словарь с результатамиresult = {  "primes": primes,   "prime_indices": prime_indices, "sum_of_primes": sum_of_primes, "sorted_primes": sorted_primes}pprint(result)

 

И соответствующий вывод:

Результат >>>{'primes': [2, 3, 5, 7], 'prime_indices': [(0, 1), (0, 2), (1, 1), (2, 0)], 'sum_of_primes': 17, 'sorted_primes': [7, 5, 3, 2]}

 

Что же представляет собой лучшая версия?

 

Лучшая версия

 

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

import mathfrom pprint import pprintdef is_prime(num):    return num > 1 and all(n % i != 0 for i in range(2, int(math.sqrt(num)) + 1))def flatten_matrix(matrix):    flattened_matrix = []    for row in matrix:        for x in row:            if is_prime(x):                flattened_matrix.append(x)    return flattened_matrixdef find_prime_indices(matrix, flattened_matrix):    prime_indices = []    for i, row in enumerate(matrix):        for j, x in enumerate(row):            if x in flattened_matrix:                prime_indices.append((i, j))    return prime_indicesdef calculate_sum_of_primes(flattened_matrix):    return sum(flattened_matrix)def sort_primes(flattened_matrix):    return sorted(flattened_matrix, reverse=True)my_matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]primes = flatten_matrix(my_matrix)prime_indices = find_prime_indices(my_matrix, primes)sum_of_primes = calculate_sum_of_primes(primes)sorted_primes = sort_primes(primes)result = {  "primes": primes,   "prime_indices": prime_indices, "sum_of_primes": sum_of_primes, "sorted_primes": sorted_primes}pprint(result)

 

Этот код также дает тот же вывод, что и раньше.

Output >>>{'primes': [2, 3, 5, 7], 'prime_indices': [(0, 1), (0, 2), (1, 1), (2, 0)], 'sum_of_primes': 17, 'sorted_primes': [7, 5, 3, 2]}

 

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

 

Пример 3: Разбор вложенных JSON-строк

 

Проблема: Разобрать заданную вложенную JSON-строку на основе условий и получить список нужных значений.

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

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

  • Целыми числами или списком целых чисел
  • Строками или списком строк

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

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

  • Если значение не является словарем и ключ начинается с ‘inner_key’, используется [inner_item].
  • Если значение является словарем с ‘sub_key’, используется [inner_item['sub_key']].
  • Если значение является строкой или целым числом, используется [inner_item].
  • Если значение является словарем, используется list(inner_item.values()).

Взгляните на следующий фрагмент кода:

import jsonjson_string = '{"key1": {"inner_key1": [1, 2, 3], "inner_key2": {"sub_key": "value"}}, "key2": {"inner_key3": "text"}}'# Разбираем строку JSON в словарь Pythondata = json.loads(json_string)flattened_data = [   value   if isinstance(value, (int, str))    else value  if isinstance(value, list)  else list(value)    for inner_dict in data.values() for key, inner_item in inner_dict.items()   for value in (      [inner_item]        if not isinstance(inner_item, dict) and key.startswith("inner_key")     else [inner_item["sub_key"]]        if isinstance(inner_item, dict) and "sub_key" in inner_item     else [inner_item]       if isinstance(inner_item, (int, str))       else list(inner_item.values())  )]print(f"Значения: {flattened_data}")

 

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

Вывод >>> Значения: [[1, 2, 3], 'значение', 'текст']

 

Как видно, генератор списка очень трудно понять.

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

 

Лучшая версия

 

Я считаю, что следующий отрывок с использованием вложенных циклов for и условной конструкции if-elif лучше. Потому что понять, что происходит, гораздо проще.

flattened_data = []for inner_dict in data.values():    for key, inner_item in inner_dict.items():        if not isinstance(inner_item, dict) and key.startswith("inner_key"):            flattened_data.append(inner_item)        elif isinstance(inner_item, dict) and "sub_key" in inner_item:            flattened_data.append(inner_item["sub_key"])        elif isinstance(inner_item, (int, str)):            flattened_data.append(inner_item)        elif isinstance(inner_item, list):            flattened_data.extend(inner_item)        elif isinstance(inner_item, dict):            flattened_data.extend(inner_item.values())print(f"Значения: {flattened_data}")

 

Это также дает ожидаемый результат:

Вывод >>> Значения: [[1, 2, 3], 'значение', 'текст']

 

Хороша ли лучшая версия? Ну, на самом деле нет.

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

В этом примере, однако, версия с условными конструкциями if-elif и вложенными циклами проще понять, чем генератор списка.

 

Заключение

 

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

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

[Bala Priya C](https://twitter.com/balawc27) – разработчик и технический писатель из Индии. Она любит работать в области математики, программирования, науки о данных и создания контента. Ее интересы и компетенции включают DevOps, науку о данных и обработку естественного языка. Она любит читать, писать, кодировать и пить кофе! В настоящее время она работает над обучением и делится своими знаниями с сообществом разработчиков, создавая учебники, руководства, мнения и многое другое.