Использование списков ч.2

Генерация строки в цикле

Генерация строк в циклах — задача, часто возникающая на практике. Типичный пример — функция, помогающая генерировать HTML-списки. Она принимает на вход коллекцию элементов и возвращает HTML-список из них:

coll = ['milk', 'butter']

build_HTML_list(coll)
# <ul><li>milk</li><li>butter</li></ul>

Как можно решить эту задачу в лоб:

  1. Создать переменную result и записать в нее <ul>.
  2. Пройтись циклом по элементам коллекции и дописать в результирующую строку очередной элемент <li>.
  3. Добавить в конце </ul> и вернуть result из функции.
def build_HTML_list(coll):
  result = '<ul>'
  for item in coll:
    result = f'{result}<li>{item}</li>'
    # либо так: result += '<li>{item}</li>'
  result = f'{result}</ul>'

  return result

coll = ['milk', 'butter']

print(build_HTML_list(coll))
# => <ul><li>milk</li><li>butter</li></ul>

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

def build_HTML_list(coll):
  parts = []
  for item in coll:
    parts.append(f'<li>{item}</li>')

  # Метод join объединяет элементы списка в строку
  # В качестве разделителя между значениями
  # используется значение строки
  inner_value = ''.join(parts)
  result = f'<ul>{inner_value}</ul>'
  return result

Размер кода практически не изменился, но способ формирования результата стал другим. Вместо строки, сначала собирается список, который затем превращается в строку с помощью метода .join(). У такого подхода есть и дополнительные плюсы:2222

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

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

parts = ['python', 'PHP', 'Python']
output = ', '.join(parts)

print(output) # => python, PHP, Python

Если каждое слово надо вывести на новой строчке, то в качестве разделителя используем символ перевода строки '\n':

parts = ['python', 'PHP', 'Python']

# Теперь каждое слово будет начинаться с новой строки
output = '\n'.join(parts)

print(output)
# => python
# => PHP
# => Python

Последний пример особенно важен. Новички часто допускают ошибку и добавляют перевод строки в момент формирования списка, а не в join(). Посмотрите на пример с нашей функцией build_HTML_list().

Правильно:

def build_HTML_list(coll):
  parts = []
  for item in coll:
    parts.append(f'<li>{item}</li>')
  inner_value = '\n'.join(parts) # перевод строки
  result = f'<ul>{inner_value}</ul>'
  return result

coll = ['milk', 'butter']

print(build_HTML_list(coll))
# <ul><li>milk</li>
# <li>butter</li></ul>

Неправильно:

def build_HTML_list(coll):
  parts = []
  for item in coll:
    parts.append(f'\n<li>{item}</li>')
  inner_value = ''.join(parts) # разделителя нет
  result = f'<ul>{inner_value}</ul>'
  return result

coll = ['milk', 'butter']

print(build_HTML_list(coll))
# <ul>
# <li>milk</li>
# <li>butter</li></ul>

Обработка строк через преобразование в список

На собеседованиях часто задают подобные задачки:

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

text = 'hello python'
capitalize_words(text) # 'Hello Python'

Решить ее можно многими способами. Чем больше называет человек — тем лучше. К ним относятся:

  1. Посимвольный перебор строки
  2. Через преобразование в список
  3. Регулярные выражения

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

def capitalize_words(sentence):
  # Определяем разделитель — пробел
  separator = ' '
  # split разделяет строку по указанному разделителю
  words = sentence.split(separator)
  # ...

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

def capitalize_words(sentence):
  separator = ' '
  words = sentence.split(separator)
  # Формируем список обработанных слов
  capitalized_words = []
  for word in words:
    capitalized_words.append(word.capitalize())

  # Соединяем обработанные слова обратно в предложение
  return separator.join(capitalized_words)

text = 'hello python'
print(capitalize_words(text)) # => Hello Python

Вложенные циклы

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

def flatten(coll):
    result = []
    for item in coll:
        # Функция isinstance проверяет является ли item типом данных список
        if isinstance(item, list):
            for sub_item in item:
                result.append(sub_item)
        else:
            result.append(item)
    return result

print(flatten([3, 2, [], [3, 4, 2], 3, [123, 3]]))
# => [ 3, 2, 3, 4, 2, 3, 123, 3 ]

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

Пример выноса в отдельную функцию кода на flatten:

# Изменяет первый список напрямую
# В данном случае такая реализация оправдана
def append(data1, data2):
    for item in data2:
        data1.append(item)

def flatten(coll):
    result = []
    for item in coll:
        if isinstance(item, list):
            append(result, item)
        else:
            result.append(item)

    return result

print(flatten([3, 2, [], [3, 4, 2], 3, [123, 3]]))

Попробуйте сами запустить код в окне ниже с интерпретатором Python и повторите примеры из статьи чтобы самим увидеть и понять как всё это работает. Для этого в ячейке с кодом нажмите клавиши на клавиатуре Shift+Enter или запустите код через кнопку Run по значку ▶.