Есть ли способ вернуться на шаг назад в цикле Python for? - PullRequest
0 голосов
/ 27 марта 2019

Я хочу найти способ эффективно имитировать такое же возможное поведение от циклов while до циклов for, чтобы сделать цикл "остановленным" или вернуться на шаг назад, если условие выполнено. Причина этого в том, что я пытаюсь реализовать вычисления с большим количеством итераций, и цикл for в 4 раза быстрее, чем цикл while, согласно %%timeit.

%%timeit
n = 0
while n < 1e7:
    n += 1
# 1.96 s +- 211 ms per loop

%%timeit
for i in range(int(1e7)):
    pass
# 399 ms +- 28.1 ms per loop

Для тех, кто упоминает, что это «несправедливое» сравнение, цикл while не может зацикливаться без оператора n += 1, тогда как цикл for может. Так что эта строка необходима для цикла while и ненужна для цикла for, таким образом, это не «несправедливое» сравнение.

Моя конкретная проблема / код:

def euler_differentiate_mod(w, bounds = None, delta = 1e-3, itern = 1e3,
    force = False,
    tols = [10, 0.1], step_mults = [0.1, 10]):

    if bounds is None:
        bounds = [0]*len(w)

    if not force and itern >= 1e9:
        raise OverflowError("number of iterations is too big: {!s}" + "\n" + \
            "you can ignore this error by setting the `force` kwarg to `False`"
            .format(itern))

    itern = int(itern)

    var = bounds

    n = 1
    while n < itern: # used to be: for n in range(1, itern+1):

        pvar = copy.deepcopy(var)

        for i,_ in enumerate(var): # compute new variables
            var[i] += w[i](*[delta]+[pvar[j] for j in range(len(pvar))])

        fchanges = [abs(var[i]-pvar[i]) for i in range(1, len(var))]

        try:
            if len(check) > 2:
                n += 1
        except:
            check = []

        if max(fchanges) > tols[0]: # big change -> reduce delta
            try:
                check.append(n)
            except:
                check = []
            delta *= step_mults[0] if delta <= 1 else 1

        elif max(fchanges) < tols[1]: # small change -> increase delta
            try:
                check.append(n)
            except:
                check = []
            delta *= step_mults[1] if delta <= 1 else 1

        else:
            check = False
            n += 1

    return None

1 Ответ

6 голосов
/ 27 марта 2019

for петли не идут назад или вперед. Они просто берут объект итератора для данного итерируемого объекта , затем многократно вызывают __next__() объект , пока этот метод не вызовет StopIteration.

Для объектов последовательности итератор просто сохраняет внутренний индекс, который увеличивается каждый раз, когда вызывается __next__, чтобы получить следующее значение в последовательности. Этот внутренний индекс обычно недоступен.

Так что, если ваш конкретный вариант использования упрощается при «возврате», вам придется создать объект итератора, который предоставляет индекс или иным образом позволяет вам изменить то, какое значение будет создано для следующего __next__ по телефону:

class PositionableSequenceIterator:
    def __init__(self, sequence):
        self.seq = sequence
        self._nextpos = None

    @property
    def pos(self):
        pos = self._nextpos
        return 0 if pos is None else pos - 1

    @pos.setter
    def pos(self, newpos):
        if not 0 <= newpos < len(self.seq):
            raise IndexError(newpos)
        self._nextpos = newpos

    def __iter__(self):
        return self

    def __next__(self):
        try:
            return self.seq[self.nextpos or 0]
        except IndexError:
            raise StopIteration
        finally:
            self.nextpos += 1

так что теперь вы можете сделать

iterator = PositionableSequenceIterator(some_list)
for elem in iterator:
    if somecondition:
        iterator.pos -= 2
    # ...

чтобы пропустить два шага назад.

Однако я бы не ожидал, что это будет быстрее, чем цикл while. Циклы while не являются особенно быстрыми, тестирование условия while на каждой итерации не сильно отличается от вызова iterator.__next__(), на самом деле. В вашем временном тесте условие while медленнее, поскольку оно выполняет байт-код Python на каждой итерации (как для условия , так и для увеличения n в теле цикла), но итератор range() реализован полностью в C. Приведенный выше класс итератора снова реализует __next__ в коде Python, поэтому будет столь же медленным .

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

>>> import timeit
>>> count, total = timeit.Timer("n = 0\nwhile n < 10 ** 6:\n    n += 1").autorange()
>>> whileloop = total / count
>>> count, total = timeit.Timer("for i in range(10 ** 6):\n    pass").autorange()
>>> forloop = total / count
>>> count, total = timeit.Timer("n < 10 ** 6", "n = 10 ** 5 * 5").autorange()
>>> testbelow = total / count
>>> count, total = timeit.Timer("n += 1", "n = 0").autorange()
>>> increment = total / count
>>> count, total = timeit.Timer("nxt()", "nxt = iter(range(1 << 23)).__next__").autorange()  # enough room to find a good test range
>>> rangeitnext = total / count
>>> whileloop - forloop  # the for loop "speed advantage", per million iterations
0.03363728789991001
>>> (testbelow + increment) - rangeitnext  # relative difference per iteration
-9.191804809961469e-08
>>> ((testbelow + increment) - rangeitnext) * 10 ** 9  # microseconds
-91.9180480996147

Таким образом, в этих тестах я могу в лучшем случае доказать, что между каждым шагом итерации цикла есть только 92 микросекунд , с while быстрее , если это имеет смысл. Это потому, что если я повторю это достаточно часто, это около достигнет разницы (whileloop - forloop) / 10 ** 6, потому что эти цифры слишком малы, чтобы их действительно волновать.

Обратите внимание, что подобный выше итератор обычно является избыточным. Подавляющее большинство проблем, когда кто-то может захотеть «перемотать» итератор, на самом деле просто хотят отслеживать ранее просмотренные элементы. Это можно сделать тривиально, например, с помощью очереди:

from collections import deque

preceding = deque(maxlen=2)
for item in iterable:
    if condition:
        # process items in preceding

    preceding.append(item)

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

Или вы можете использовать zip() и независимые итераторы:

from itertools import islice

twoforward = islice(iterable, 2, None)
for twoback, current in zip(iterable, twoforward):
    # twoback and current are paired up at indices i - 2 and i.

Что касается вашей функции euler_differentiate_mod(), следующее выполняет ту же работу, без необходимости продвигать счетчик while. Ваша функция в основном вычисляет дельты до 3 раз за итерацию и переходит к следующей итерации, когда у вас есть максимальное изменение в пределах допуска или вы пробовали 3 раза:

def euler_differentiate_mod(
    w, bounds=None, delta=1e-3, itern=1000, tols=(10, 0.1), step_mults=(0.1, 10)
):
    if bounds is None:
        bounds = [0] * len(w)

    for _ in range(itern):
        for _ in range(3):
            deltas = [f(delta, *bounds) for f in w]
            maxchange = max(map(abs, deltas[1:]))  # ignore the first delta
            bounds[:] = [b + d for b, d in zip(bounds, deltas)]

            if delta > 1:
                delta *= step_mults[0] / maxchange

            if tols[1] <= maxchange <= tols[0]:
                break

            if delta > 1:
                if tols[0] < maxchange:
                    delta *= step_mults[0] / maxchange
                elif maxchange < tols[1]:
                    delta *= step_mults[1] / maxchange
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...