Как разделить строку и присоединиться к ней, не создавая промежуточный список в Python? - PullRequest
9 голосов
/ 23 августа 2010

Скажите, у меня есть что-то вроде следующего:

dest = "\n".join( [line for line in src.split("\n") if line[:1]!="#"] )

(т.е. убрать любые строки, начинающиеся с # из многострочной строки src)

src очень большой, поэтому я предполагаю, что .split() создаст большой промежуточный список. Я могу изменить понимание списка на выражение генератора, но есть ли какой-то вид «xsplit», который я могу использовать для работы только с одной строкой за раз? Правильно ли мое предположение? Какой самый эффективный способ памяти?

Уточнение : Это произошло из-за того, что моему коду не хватает памяти. Я знаю, что есть способы полностью переписать мой код, чтобы обойти это, но вопрос о Python: существует ли версия split () (или эквивалентная идиома), которая ведет себя как генератор и, следовательно, не делает дополнительную работу копия src?

Ответы [ 5 ]

5 голосов
/ 23 августа 2010

Вот способ сделать общий тип разбиения, используя itertools

>>> import itertools as it
>>> src="hello\n#foo\n#bar\n#baz\nworld\n"
>>> line_gen = (''.join(j) for i,j in it.groupby(src, "\n".__ne__) if i)
>>> '\n'.join(s for s in line_gen if s[0]!="#")
'hello\nworld'

groupby обрабатывает каждый символ в src отдельно, поэтому производительность, вероятно, не является звездной, но при этом избегается создание промежуточных огромных структур данных

Наверное, лучше потратить несколько строк и сделать генератор

>>> src="hello\n#foo\n#bar\n#baz\nworld\n"
>>>
>>> def isplit(s, t): # iterator to split string s at character t
...     i=j=0
...     while True:
...         try:
...             j = s.index(t, i)
...         except ValueError:
...             if i<len(s):
...                 yield s[i:]
...             raise StopIteration
...         yield s[i:j]
...         i = j+1
...
>>> '\n'.join(x for x in isplit(src, '\n') if x[0]!='#')
'hello\nworld'

re имеет метод с именем finditer, который также может использоваться для этой цели

>>> import re
>>> src="hello\n#foo\n#bar\n#baz\nworld\n"
>>> line_gen = (m.group(1) for m in re.finditer("(.*?)(\n|$)",src))
>>> '\n'.join(s for s in line_gen if not s.startswith("#"))
'hello\nworld'

сравнение производительности - это упражнение для ОП, чтобы примерить реальные данные

5 голосов
/ 23 августа 2010
buffer = StringIO(src)
dest = "".join(line for line in buffer if line[:1]!="#")

Конечно, это действительно имеет смысл, если вы используете StringIO во всем.Он работает в основном так же, как файлы.Вы можете искать, читать, писать, повторять (как показано) и т. Д.

4 голосов
/ 23 августа 2010

В вашем существующем коде вы можете изменить список на выражение генератора:

dest = "\n".join(line for line in src.split("\n") if line[:1]!="#")

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

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

import re
regex = re.compile('^#.*\n?', re.M)
dest = regex.sub('', src)

Это позволит избежать не только создания временных списков, но и создания временных строк для каждой строки во входных данных. Вот некоторые измерения производительности предлагаемых решений:

init = r'''
import re, StringIO
regex = re.compile('^#.*\n?', re.M)
src = ''.join('foo bar baz\n' for _ in range(100000))
'''

method1 = r'"\n".join([line for line in src.split("\n") if line[:1] != "#"])'
method2 = r'"\n".join(line for line in src.split("\n") if line[:1] != "#")'
method3 = 'regex.sub("", src)'
method4 = '''
buffer = StringIO.StringIO(src)
dest = "".join(line for line in buffer if line[:1] != "#")
'''

import timeit

for method in [method1, method2, method3, method4]:
    print timeit.timeit(method, init, number = 100)

Результаты:

 9.38s   # Split then join with temporary list
 9.92s   # Split then join with generator
 8.60s   # Regular expression
64.56s   # StringIO

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

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

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

2 голосов
/ 23 августа 2010

Если я правильно понимаю ваш вопрос о «более общих вызовах split ()», вы можете использовать re.finditer, например, так:

output = ""

for i in re.finditer("^.*\n",input,re.M):
    i=i.group(0).strip()
    if i.startswith("#"):
        continue
    output += i + "\n"

Здесь вы можете заменить регулярное выражение чем-то более сложным.

1 голос
/ 23 августа 2010

Проблема в том, что строки в Python неизменны, поэтому будет очень сложно что-либо сделать вообще без промежуточного хранилища.

...