Неправильное использование списков и генераторов выражений в Python

задняя часть Python алгоритм Программа перевода самородков

Понимание списков — одна из моих любимых функций Python. Мне так нравятся списковые включения, что я написал о них статью.статья, сделал один раз против нихречь, также провел один на PyCon 2018Трехчасовой урок понимания.

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

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

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

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

Написание переполненных понятий

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

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

def get_factors(dividend):
    """返回所给数值的所有因子作为一个列表。"""
    return [n for n in range(1, dividend+1) if dividend % n == 0]

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

def get_factors(dividend):
    """返回所给数值的所有因子作为一个列表。"""
    return [
        n
        for n in range(1, dividend+1)
        if dividend % n == 0
    ]

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

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

вывод слишком некрасивый

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

Взгляните на этот вывод:

fizzbuzz = [
    f'fizzbuzz {n}' if n % 3 == 0 and n % 5 == 0
    else f'fizz {n}' if n % 3 == 0
    else f'buzz {n}' if n % 5 == 0
    else n
    for n in range(100)
]

Этот вывод эквивалентенforцикл:

fizzbuzz = []
for n in range(100):
    fizzbuzz.append(
        f'fizzbuzz {n}' if n % 3 == 0 and n % 5 == 0
        else f'fizz {n}' if n % 3 == 0
        else f'buzz {n}' if n % 5 == 0
        else n
    )

вывод иforВсе циклы используют три уровня вложенности.Встроенный оператор if(ПитонаТернарный оператор)

Вот более читаемый способ, используяif-elif-elseструктура:

fizzbuzz = []
for n in range(100):
    if n % 3 == 0 and n % 5 == 0:
        fizzbuzz.append(f'fizzbuzz {n}')
    elif n % 3 == 0:
        fizzbuzz.append(f'fizz {n}')
    elif n % 5 == 0:
        fizzbuzz.append(f'buzz {n}')
    else:
        fizzbuzz.append(n)

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

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

number_things = [
    n // 2 if n % 2 == 0 else n * 3
    for n in numbers
]

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

number_things = [
    (n // 2 if n % 2 == 0 else n * 3)
    for n in numbers
]

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

number_things = [
    even_odd_number_switch(n)
    for n in numbers
]

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

Замаскированный под деривационную петлю

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

Например, этот код кажется пониманием:

[print(n) for n in range(1, 11)]

Но это не похоже на выводбегать. То, что мы достигаем с пониманием, не то, что должно было быть.

Если мы сделаем это понимание в Python, вы поймете, что я имею в виду:

>>> [print(n) for n in range(1, 11)]

[None, None, None, None, None, None, None, None, None, None]

Мы хотели вывести все числа от 1 до 10, что мы и сделали. Но это дедуктивное утверждение возвращает всеNoneСписок ценностей для нас ничего не значит.

Какой список вы даете пониманию, какой список он построит. мы начинаем сprintфункция, чтобы получить значение для построения списка, иprintВозвращаемое значение функции равноNone.

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

Мы можем заменить предыдущий код следующим кодом:

for n in range(1, 11):
    print(n)

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

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

Если вы не создаете новый список, не используйте включения.

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

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

Я видел и написал кучу такого кода:

import csv

with open('populations.csv') as csv_file:
    lines = [
        row
        for row in csv.reader(csv_file)
    ]

Этот вывод будетуникальностьзначения сортируются. Его цель — перебрать предоставленный нами итератор (csv.reader(csv_file)) и создайте список.

Однако в Python мы предоставляем более конкретный инструмент для этой задачи:listконструктор. питонlistКонструктор выполняет за нас работу по созданию цикла и созданию списка.

import csv

with open('populations.csv') as csv_file:
    lines = list(csv.reader(csv_file))

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

Если вам не нужно фильтровать элементы или сопоставлять их с новыми элементами при построении списка,Вам не нужно использовать понимание, вам просто нужно использоватьlistКонструктор.

Это производное преобразуется изzipполучено вrowкортеж и поместить в список:

def transpose(matrix):
    """返回给定列表的转置版本。"""
    return [
        [n for n in row]
        for row in zip(*matrix)
    ]

Мы также можем использоватьlistКонструктор:

def transpose(matrix):
    """返回给定列表的转置版本。"""
    return [
        list(row)
        for row in zip(*matrix)
    ]

Всякий раз, когда вы видите вывод, подобный этому:

my_list = [x for x in some_iterable]

Вместо этого вы можете написать это:

my_list = list(some_iterable)

То же самое относится кdictа такжеsetвывод.

Вот что я часто писал:

states = [
    ('AL', 'Alabama'),
    ('AK', 'Alaska'),
    ('AZ', 'Arizona'),
    ('AR', 'Arkansas'),
    ('CA', 'California'),
    # ...
]

abbreviations_to_names = {
    abbreviation: name
    for abbreviation, name in states
}

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

Эта задача действительно былаdictКонструктор делается с помощью:

abbreviations_to_names = dict(states)

listа такжеdictКонструктор of - не единственная дедуктивная альтернатива. Стандартная библиотека и сторонние библиотеки содержат множество инструментов, и иногда они лучше подходят для ваших требований к циклам, чем включения.

Вот выражение генератора, которое суммирует вложенные итераторы:

def sum_all(number_lists):
    """返回二维列表中所有元素的和。"""
    return sum(
        n
        for numbers in number_lists
        for n in numbers
    )

использоватьitertools.chainТакого же можно добиться:

from itertools import chain

def sum_all(number_lists):
    """返回二维列表中所有元素的和。"""
    return sum(chain.from_iterable(number_lists))

Когда использовать понимание и когда использовать замену, не так однозначно.

Я часто борюсь с использованиемitertools.chainЕще вывод. Я обычно записываю оба и использую более четкий.

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

недействительная работа

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

Этот код открывает файл слов (по одному слову в строке), сохраняет файл и подсчитывает количество вхождений каждого слова:

from collections import Counter

word_counts = Counter(
    word
    for word in open('word_list.txt').read().splitlines()
)

Мы используем генераторное выражение, но оно нам не нужно. Вы можете написать это прямо так:

from collections import Counter

word_counts = Counter(open('word_list.txt').read().splitlines())

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

Вот еще одно неверное понимание:

with open('word_list.txt') as words_file:
    lines = [line for line in words_file]
    for line in lines:
        if 'z' in line:
            print('z word', line, end='')

мы прошлиwords_file, преобразованный в списокlines, а затем пройтиlinesоднажды. Все преобразование в списки не требуется.

Мы можем напрямую пройтиwords_file:

with open('word_list.txt') as words_file:
    for line in words_file:
        if 'z' in line:
            print('z word', line, end='')

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

В Python нас больше интересуетэто итераторвместоэто список.

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

Когда следует использовать понимание?

Итак, когда действительно следует использовать понимания?

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

new_things = []
for ITEM in old_things:
    if condition_based_on(ITEM):
        new_things.append(some_operation_on(ITEM))

Цикл можно переписать с таким пониманием:

new_things = [
    some_operation_on(ITEM)
    for ITEM in old_things
    if condition_based_on(ITEM)
]

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

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

def is_prime(candidate):
    for n in range(2, candidate):
        if candidate % n == 0:
            return False
    return True

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

def is_prime(candidate):
    return all(
        candidate % n != 0
        for n in range(2, candidate)
    )

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

Также есть код для аналогичного сценария:

def sum_of_squares(numbers):
    total = 0
    for n in numbers:
        total += n**2
    return total

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

def sum_of_squares(numbers):
    return sum(n**2 for n in numbers)

Итак, в дополнение к рассмотрению проверки «Могу ли я скопировать-вставить из цикла в понимание», нам также необходимо рассмотреть: Можем ли мы улучшить наш код, комбинируя выражения генератора с функциями или классами, которые принимают итераторы?

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

Используйте понимание списка после обсуждения

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

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

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

картинаany,allа такжеsumтакие функции иCounterа такжеchainТакие классы являются инструментами, которые принимают итераторы, онис выводомочень подходит, иногдаПолностью заменяет вывод.

Помните, что у понимания есть только одна цель:Создать новый итератор из старого итератора, слегка корректируя значения и/или фильтруя значения, которые не соответствуют критериям в процессе. Понимание — прекрасный инструмент, ноОни не единственные ваши инструменты. Когда ваш вывод не работает, не забывайтеlistа такжеdictконструктор иforцикл.

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


Программа перевода самородковэто сообщество, которое переводит высококачественные технические статьи из Интернета сНаггетсДелитесь статьями на английском языке на . Охват контентаAndroid,iOS,внешний интерфейс,задняя часть,блокчейн,продукт,дизайн,искусственный интеллектЕсли вы хотите видеть более качественные переводы, пожалуйста, продолжайте обращать вниманиеПрограмма перевода самородков,официальный Вейбо,Знай колонку.