Как избежать создания ненужных списков?

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

def insight_pull(file):
    with open(file) as in_f:
        lines = in_f.readlines()

        dirty = [line.split('    ') for line in lines]
        clean = [i[1] for i in dirty]
        cleaner = [[clean[i],clean[i + 1]] for i in range(0, len(clean),2)]
        cleanest = [i[0].split() + i[1].split() for i in cleaner]


        with open("Output_File.txt", "w") as out_f:
            out_f.writelines(' '.join(i) + '\n' for i in cleanest)

Согласно примеру выше:

    # Pull raw data from file splitting on '   '.
    dirty = [line.split('    ') for line in lines]

    # Select every 2nd element from each nested list.
    clean = [i[1] for i in dirty]

    # Couple every 2nd element with it's predecessor into a new list.
    cleaner = [[clean[i],clean[i + 1]] for i in range(0, len(clean),2)]

    # Split each entry in cleaner into the final formatted list.
    cleanest = [i[0].split() + i[1].split() for i in cleaner]

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

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

4 ответа

Решение

Генератор выражений

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

Идея @VPfB об использовании георераторов - хорошее решение, если в вашем коде есть другие места для повторного использования генераторов. Если вам не нужно повторно использовать генераторы, используйте выражения генератора.

Выражения генератора являются ленивыми, как генераторы, поэтому при объединении в цепочку, как здесь, цикл будет вычисляться один раз в конце, когда вызывается writelines.

def insight_pull(file):
    with open(file) as in_f:
        dirty = (line.split('    ') for line in in_f)    # Combine with next
        clean = (i[1] for i in dirty)
        cleaner = (pair for pair in zip(clean,clean))    # Redundantly silly
        cleanest = (i[0].split() + i[1].split() for i in cleaner)

        # Don't build a single (possibily huge) string with join
        with open("Output_File.txt", "w") as out_f:
            out_f.writelines(' '.join(i) + '\n' for i in cleanest)

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

def insight_pull(file):
    with open(file) as in_f:
        clean = (line.split('    ')[0] for line in in_f)
        cleaner = zip(clean,clean)
        cleanest = (i[0].split() + i[1].split() for i in cleaner)

        with open("Output_File.txt", "w") as out_f:
            for line in cleanest:
                out_f.write(line + '\n')

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

# All even values from 2-18
even = (i*2 for i in range(1, 10))

# Only those divisible by 3
multiples_of_3 = (val for val in even if val % 3 == 0)

# And finally, we want to evaluate the remaining values as hex
hexes = [hex(val) for val in multiples_of_3]
# output: ['0x6', '0xc', '0x12']

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

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

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

def insight_pull(file):
    with open(file) as in_f:
        my_list = in_f.readlines()

        my_list = [line.split('    ') for line in my_list]
        my_list = [i[1] for i in my_list]
        my_list = [[my_list[i],my_list[i + 1]] for i in range(0, len(my_list),2)]
        my_list = [i[0].split() + i[1].split() for i in my_list]


    with open("Output_File.txt", "w") as out_f:
        out_f.writelines(' '.join(i) + '\n' for i in my_list)

Для достижения цели я бы порекомендовал конвейерную обработку. Я нашел статью, которая раскрывает технику: генераторы трубопроводов.

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

Ведущий f в именах функций обозначает фильтр.

def fromfile(name):
    # see coments
    with open(name) as in_f:
        for line in in_f:
            yield line

def fsplit(pp):
    for line in pp: 
        yield line.split('    ')

def fitem1(pp):
    for item in pp: 
        yield item[1]

def fpairs(pp):
    # edited
    for x in pp:
        try:
            yield [x, next(pp)]
        except StopIteration:
            break

def fcleanup(pp):
    for i in pp: 
        yield i[0].split() + i[1].split()

pipeline = fcleanup(fpairs(fitem1(fsplit(fromfile(NAME)))))

output = list(pipeline)

Для реального использования я бы собрал первые 3 фильтра, а также следующие 2.

Другие вопросы по тегам