Ленивая оценка карты

Недавно я прочитал, что одним из преимуществ map в Python 3 была его ленивость. значит лучше сделать

map(lambda x: x**2, range(10**100))

скорее, чем

[x**2 for x in range(10**100)]

Что мне любопытно, так это то, как я могу использовать эту лень. Если я сгенерирую объект карты, как я могу, например, получить доступ к определенному элементу в результирующей операции/списке. Почти в каждой документации по map, которую я видел, они будут делать что-то вроде print(map(...)) или for i in map(...), которые (насколько я понимаю) отказываются от ленивой концепции, поскольку неявно преобразуют карту в список.

Я полагаю, что я ищу возможность использовать объекты карты так же лениво, как range, где я могу делать x = range(10**100) и лениво генерировать x[10000] без огромных вычислительных нагрузок.

Если этой концепции не существует, то какая польза от map лени? Если вам всегда нужно преобразовать его в какой-нибудь неленивый объект, такой как список, какое значение имеет то, что map ленив?


person zephyr    schedule 24.05.2016    source источник
comment
Если вам нужен 10 000-й элемент, вам, вероятно, придется вычислить предыдущие 99 999 (если у вас нет простой функции, такой как x ** 2, в этом случае просто выполните 10000 ** 2), вы не можете индексировать его. Лень является преимуществом, потому что вам не нужно держать весь список в памяти сразу.   -  person jonrsharpe    schedule 24.05.2016
comment
Тогда моя точка зрения заключается в том, чем полезна (и не избыточна) ленивая карта? Просто используйте генератор, если вас беспокоит память.   -  person zephyr    schedule 24.05.2016
comment
Итерация по map в вашем примере ничуть не лучше, чем итерация по эквивалентному выражению генератора. Оба оцениваются лениво. Преимущество по сравнению со списком состоит в том, что вы не сохраняете весь список (что может быть ненужным или даже невозможным), что является преимуществом только в том случае, если он вам не нужен весь сразу. Непонятно, о чем вы пытаетесь спросить: map или список? map против выражения генератора? Если вам не нужно map, просто не беспокойтесь. Если вы предпочитаете синтаксис с выражением генератора, используйте его.   -  person jonrsharpe    schedule 24.05.2016
comment
Проблема в том, что у вас может быть что-то вроде x = 0; def f(y): global x; x+= 1; return x+y, а затем map(f, iterable). Теперь значение 10000-го элемента требует оценки всех предыдущих вызовов f (из-за изменения общего состояния между вызовами), поэтому, если вам действительно нужен ленивый map, где вы можете получить доступ к n-му элементу, не выполняя предыдущие вызовы, вы уже ограничиваете его, чтобы иметь только чистые функции в качестве аргументов.   -  person Bakuriu    schedule 24.05.2016
comment
@zephyr Ленивость полезна, потому что если бы у вас было for x in map(f, iterable): print(x), если map лениво, требуемая память - это только место для одного элемента в iterable, результат (x). Если бы map не был ленив, ему пришлось бы сначала построить полный список значений (который может быть огромным). На самом деле считают: from itertools import count; for x in map(lambda x:x+1, count()): print(x). С ленивой картой это печатает числа 1\n2\n3... до бесконечности, если map не ленив, этот код просто убьет ваш компьютер, поскольку вы не можете построить бесконечный список в памяти.   -  person Bakuriu    schedule 24.05.2016
comment
@Bakuriu Это хороший момент. Полагаю, я надеялся, что смогу лениво обращаться к элементам с карты, если у меня есть какая-то ленивая итерация, например range, но я думаю, что это было просто принятие желаемого за действительное.   -  person zephyr    schedule 24.05.2016


Ответы (3)


Вы тут сравниваете яблоки с апельсинами. range не просто ленивая итерация. Это конкретный объект, содержимое которого подчиняется определенным законам, что позволяет выполнять множество операций, фактически не создавая в памяти огромную последовательность. Это потому, что n-й элемент range в основном просто start + n*step (по модулю stop, знаки и т. д.)

Однако map предназначен для работы с любой функцией f. В частности, функции могут иметь совместно используемое/глобальное состояние, которое уже исключает любую возможность выполнения map(f, something)[100] без выполнения 100 вызовов функций. В противном случае нарушается правильность результата.

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

for x in map(f, iterable):
    # do something with x

где, если бы map стремился, он бы потреблял вдвое больше памяти, чем iterable, чтобы выполнить цикл, с ленивым map единственное требуемое пространство - это в основном x.

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

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

class PureMap:
    def __init__(self, function, sequence):
        self._f = function
        self._sequence = sequence

    def __iter__(self):
        return map(self._f, self._sequence)
    def __getitem__(self, i):
        return self._f(self._sequence[i])
    # etc.

Однако даже в этом случае у вас есть некоторые проблемы:

  1. Если sequence на самом деле является iterable, чтобы получить n-й элемент, вам нужно использовать первые n элементов. После этого вам нужно будет сохранить их как последовательность в вашем классе для будущего использования. Но это уже противоречит цели всего, так как выполнение PureMap(f, sequence)[1000] требует от вас хранить 1000 элементов в памяти в любом случае, хотя при этом избегаются 999 вызовы f.

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

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

  • Вызываемая функция чистая
  • Итерируемый аргумент — это что-то вроде range, который разрешает произвольный доступ без необходимости создавать другие элементы.
  • Функция, которую вы вызываете, работает быстро, так что вы можете пересчитать ее для различных элементов, не слишком беспокоясь о производительности.

Когда все эти предположения соблюдены, вы можете получить объект карты, который «работает как range».

person Bakuriu    schedule 24.05.2016

Во-первых, обратите внимание, что range (xrange в Python 2) — это особый случай. Это не простой генератор, и он не просто возвращает список. Он также поддерживает операции in, что не является стандартной функцией итерируемых объектов или итераторов.

Учтите, что map(func, iterable) можно вызывать для бесконечного итерируемого объекта или итерируемого объекта, где процесс выборки следующего значения занимает много времени.

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

Не каждый вариант использования требует произвольного доступа, а те, которые это делают, должны полностью создавать итерируемый объект или использовать другую функцию itertools, например islice.

person mobiusklein    schedule 24.05.2016

Есть много преимуществ; например, это упрощает написание кода с эффективным использованием памяти.

def take_up_a_lot_of_memory(*args):
    """
    A contrived example of a function that uses a lot of memory
    """
    return sum([i ** 2 for i in range(10 ** 6)])

megasum = sum(map(take_up_a_lot_of_memory, range(1000)))

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

person hilberts_drinking_problem    schedule 24.05.2016