Манипуляции со строками Python - проблемы с производительностью - PullRequest
6 голосов
/ 02 сентября 2011

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

try:
    data = []
    start = 0
    end = 0
    for info in self.Columns():
        end = start + (info.columnLength)
        slice = line[start:end]
        if slice == '' or len(slice) != info.columnLength:
            raise 'Wrong Input'
        if info.hasSignage:
            if(slice[0:1].strip() != '+' and slice[0:1].strip() != '-'):
                raise 'Wrong Input'
        if not info.skipColumn:
            data.append(slice)
        start = end 
    parsedLine = data
except:
    parsedLine = False

Ответы [ 6 ]

3 голосов
/ 03 сентября 2011
def fubarise(data):
    try:
        if nasty(data):
            raise ValueError("Look, Ma, I'm doing a big fat GOTO ...") # sheesh #1
        more_of_the_same()
        parsed_line = data
    except ValueError:
        parsed_line = False
        # so it can be a "data" or False -- sheesh #2
    return parsed_line

Нет смысла иметь разные сообщения об ошибках в операторе raise; их никогда не видели. Sheesh # 3.

Обновление: Вот предлагаемое улучшение, которое использует struct.unpack для быстрого разделения строк ввода. Это также иллюстрирует лучшую обработку исключений в предположении, что разработчик кода также выполняет его, и остановка на первой ошибке приемлема. Надежная реализация, которая регистрирует все ошибки во всех столбцах всех строк для пользовательской аудитории, является другим вопросом. Обратите внимание, что обычно проверка ошибок для каждого столбца будет гораздо более обширной, например, проверка на наличие лидирующего знака, но не проверка того, содержит ли столбец действительное число, выглядит немного странно.

import struct

def unpacked_records(self):
    cols = self.Columns()
    unpack_fmt = ""
    sign_checks = []
    start = 0
    for colx, info in enumerate(cols, 1):
        clen = info.columnLength
        if clen < 1:
            raise ValueError("Column %d: Bad columnLength %r" % (colx, clen))
        if info.skipColumn:
            unpack_fmt += str(clen) + "x"
        else:
            unpack_fmt += str(clen) + "s"
            if info.hasSignage:
                sign_checks.append(start)
        start += clen
    expected_len = start
    unpack = struct.Struct(unpack_fmt).unpack

    for linex, line in enumerate(self.whatever_the_list_of_lines_is, 1):
        if len(line) != expected_len:
            raise ValueError(
                "Line %d: Actual length %d, expected %d"
                % (linex, len(line), expected_len))
        if not all(line[i] in '+-' for i in sign_checks):
            raise ValueError("Line %d: At least one column fails sign check" % linex)
        yield unpack(line) # a tuple
2 голосов
/ 02 сентября 2011

как насчет (используя некоторые классы, чтобы иметь исполняемый пример):

class Info(object):
    columnLength = 5
    hasSignage = True
    skipColumn = False

class Something(object):

    def Columns(self):
        return [Info()]*4

    def bottleneck(self):
        try:
            data = []
            start = 0
            end = 0
            line = '+this-is just a line for testing'
            for info in self.Columns():
                start = end
                collength = info.columnLength
                end = start + collength
                if info.skipColumn:  # start with this
                    continue

                elif collength == 0: 
                    raise ValueError('Wrong Input')

                slice = line[start:end] # only now slicing, because it
                                        # is probably most expensive part

                if len(slice) != collength: 
                    raise ValueError('Wrong Input')

                elif info.hasSignage and slice[0] not in '+-': # bit more compact
                    raise ValueError('Wrong Input')

                else:
                    data.append(slice)

            parsedLine = data
        except:
            parsedLine = False

Something().bottleneck()

edit: когда длина среза равна 0, срез [0] не существует, поэтому необходимо проверить if collength == 0для первого

edit2: этот бит кода используется для множества строк, но информация о столбце не меняется, верно?Это позволяет вам

  • предварительно рассчитать список начальных точек каждого столбца (больше не нужно вычислять начало, конец)
  • , зная начало и конец заранее, .Columns() нужно только возвращать столбцы, которые не пропущены и имеют длину столбца> 0 (или вам действительно нужно увеличить ввод для длины == 0 в каждой строке ??)
  • длина обязательной каждой строкиизвестен и равен или каждой строке и может быть проверен перед циклом по столбцу. Информация

edit3: Интересно, как вы узнаете, к какому столбцу относится индекс данных, если вы используете 'skipColumn' ...

1 голос
/ 02 сентября 2011

РЕДАКТИРОВАТЬ: я немного изменить этот ответ. Я оставлю оригинальный ответ ниже.

В своем другом ответе я прокомментировал, что лучше всего будет найти встроенный модуль Python, который будет выполнять распаковку. Я не мог придумать ни одного, но, возможно, мне следовало искать в Google. @ Джон Мачин дал ответ, который показал, как это сделать: использовать модуль Python struct. Поскольку это написано на C, оно должно быть быстрее, чем мое чистое решение на Python. (На самом деле я ничего не измерял, так что это предположение.)

Я согласен, что логика в исходном коде "непифонова". Возвращать дозорное значение не лучше; лучше либо вернуть действительное значение, либо вызвать исключение. Другой способ сделать это - вернуть список допустимых значений, а также другой список недопустимых значений. Поскольку @John Machin предлагал код для получения допустимых значений, я подумал, что напишу здесь версию, которая возвращает два списка.

ПРИМЕЧАНИЕ. Возможно, наилучшим из возможных ответов было бы взять ответ @John Machin и изменить его, чтобы сохранить недопустимые значения в файле для возможного последующего просмотра. Его ответ дает ответы по одному, поэтому нет необходимости составлять большой список проанализированных записей; и сохранение плохих строк на диск означает, что нет необходимости создавать возможно большой список плохих строк.

import struct

def parse_records(self):
    """
    returns a tuple: (good, bad)
    good is a list of valid records (as tuples)
    bad is a list of tuples: (line_num, line, err)
    """

    cols = self.Columns()
    unpack_fmt = ""
    sign_checks = []
    start = 0
    for colx, info in enumerate(cols, 1):
        clen = info.columnLength
        if clen < 1:
            raise ValueError("Column %d: Bad columnLength %r" % (colx, clen))
        if info.skipColumn:
            unpack_fmt += str(clen) + "x"
        else:
            unpack_fmt += str(clen) + "s"
            if info.hasSignage:
                sign_checks.append(start)
        start += clen
    expected_len = start
    unpack = struct.Struct(unpack_fmt).unpack

    good = []
    bad = []
    for line_num, line in enumerate(self.whatever_the_list_of_lines_is, 1):
        if len(line) != expected_len:
            bad.append((line_num, line, "bad length"))
            continue
        if not all(line[i] in '+-' for i in sign_checks):
            bad.append((line_num, line, "sign check failed"))
            continue
        good.append(unpack(line))

    return good, bad

ОРИГИНАЛЬНЫЙ ТЕКСТ ОТВЕТА: Этот ответ должен быть намного быстрее, если информация self.Columns() одинакова для всех записей. Мы делаем обработку информации self.Columns() один раз и создаем пару списков, которые содержат именно то, что нам нужно для обработки записи.

Этот код показывает, как вычислить parsedList, но на самом деле не выдает его, не возвращает и не делает с ним ничего. Очевидно, вам нужно это изменить.

def parse_records(self):
    cols = self.Columns()

    slices = []
    sign_checks = []
    start = 0
    for info in cols:
        if info.columnLength < 1:
            raise ValueError, "bad columnLength"
        end = start + info.columnLength
        if not info.skipColumn:
            tup = (start, end)
            slices.append(tup)   
            if info.hasSignage:
                sign_checks.append(start)

    expected_len = end # or use (end - 1) to not count a newline

    try:
        for line in self.whatever_the_list_of_lines_is:
            if len(line) != expected_len:
                raise ValueError, "wrong length"
            if not all(line[i] in '+-' for i in sign_checks):
                raise ValueError, "wrong input"
            parsedLine = [line[s:e] for s, e in slices]

    except ValueError:
        parsedLine = False
1 голос
/ 02 сентября 2011

Первое, что я бы рассмотрел, это slice = line[start:end]. Нарезка создает новые экземпляры; Вы можете попытаться избежать явного построения line [start:end] и проверить его содержимое вручную.

Почему ты делаешь slice[0:1]? Это должно привести к подпоследовательности, содержащей один элемент slice (не так ли?), Таким образом, это может быть проверено более эффективно.

1 голос
/ 02 сентября 2011

Не вычисляйте start и end каждый раз в этом цикле.

Вычислять их ровно один раз перед использованием self.Columns() (Что бы это ни было. Если 'Columns` является классом со статическими значениями, это глупо. Если это функция с именем, начинающимся с заглавной буквы, это сбивает с толку.)

if slice == '' or len(slice) != info.columnLength может произойти, только если строка слишком короткая по сравнению с общим размером, требуемым для Columns. Проверьте один раз за пределами цикла.

slice[0:1].strip() != '+' конечно, выглядит как .startswith().

if not info.skipColumn. Примените этот фильтр еще до начала цикла. Уберите их из self.Columns().

0 голосов
/ 02 сентября 2011

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

Когда мы закончим, end должен указывать на конец строки;если это так, то все значения .columnLength должны быть в порядке.(Если только один не был отрицательным или что-то в этом роде!)

Так как это имеет ссылку на self, это должен быть фрагмент из функции-члена.Таким образом, вместо возбуждения исключений, вы можете просто return False выйти из функции раньше и вернуть флаг ошибки.Но мне нравится возможность отладки изменения условия except, чтобы он больше не перехватывал исключение, и получение трассировки стека, позволяющей определить, откуда возникла проблема.

@ Remi использовал slice[0] in '+-' там, где я использовал slice.startswith(('+', '-)).Я думаю, что мне больше нравится код @ Remi, но я оставил свой без изменений, чтобы показать вам другой путь.Способ .startswith() будет работать для строк длиннее, чем длина 1, но поскольку это только строка длиной 1, краткое решение работает.

try:
    line = line.strip('\n')
    data = []
    start = 0
    for info in self.Columns():
        end = start + info.columnLength
        slice = line[start:end]
        if info.hasSignage and not slice.startswith(('+', '-')):
            raise ValueError, "wrong input"
        if not info.skipColumn:
            data.append(slice)
        start = end

    if end - 1 != len(line):
        raise ValueError, "bad .columnLength"

    parsedLine = data

except ValueError:
    parsedLine = False
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...