итеративная вложенная глубокая копия (или улучшенный itertools.tee для итерируемых итерируемых) - PullRequest
0 голосов
/ 14 декабря 2018

Предисловие

У меня есть тест, в котором я работаю с вложенными итерациями (под вложенными итерациями Я имею в виду итерации с только итерациями в качестве элементов).

В качестве тестового каскада рассмотрим

from itertools import tee
from typing import (Any,
                    Iterable)


def foo(nested_iterable: Iterable[Iterable[Any]]) -> Any:
    ...


def test_foo(nested_iterable: Iterable[Iterable[Any]]) -> None:
    original, target = tee(nested_iterable)  # this doesn't copy iterators elements

    result = foo(target)

    assert is_contract_satisfied(result, original)


def is_contract_satisfied(result: Any,
                          original: Iterable[Iterable[Any]]) -> bool:
    ...

Например, foo может быть простой функцией идентификации

def foo(nested_iterable: Iterable[Iterable[Any]]) -> Iterable[Iterable[Any]]:
    return nested_iterable

, а контракт просто проверяет, что сплющенные итерируемые элементы имеют одинаковые элементы

from itertools import (chain,
                       starmap,
                       zip_longest)
from operator import eq
...
flatten = chain.from_iterable


def is_contract_satisfied(result: Iterable[Iterable[Any]],
                          original: Iterable[Iterable[Any]]) -> bool:
    return all(starmap(eq,
                       zip_longest(flatten(result), flatten(original),
                                   # we're assuming that ``object()``
                                   # will create some unique object
                                   # not presented in any of arguments
                                   fillvalue=object())))

Но если некоторые из элементов nested_iterable являются итераторами, он может быть исчерпан, поскольку tee делает неглубокие копии, а не глубокие, т. Е. Для заданного foo и is_contract_satisfied следующего оператора

>>> test_foo([iter(range(10))])

приводит к предсказуемости

Traceback (most recent call last):
  ...
    test_foo([iter(range(10))])
  File "...", line 19, in test_foo
    assert is_contract_satisfied(result, original)
AssertionError

Задача

Как выполнить глубокое копирование произвольной вложенной итерации?

Примечание

IМне известна функция copy.deepcopy , но она не будет работать для файловых объектов.

Ответы [ 2 ]

0 голосов
/ 15 декабря 2018

Наивное решение

Простой алгоритм будет

  1. Выполнять поэлементное копирование исходных вложенных итераций.
  2. Делать n копий поэлементной копии.
  3. Получите координаты, относящиеся к каждой независимой копии.

, которая может быть реализована как

from itertools import tee
from operator import itemgetter
from typing import (Any,
                    Iterable,
                    Tuple,
                    TypeVar)

Domain = TypeVar('Domain')


def copy_nested_iterable(nested_iterable: Iterable[Iterable[Domain]],
                         *,
                         count: int = 2
                         ) -> Tuple[Iterable[Iterable[Domain]], ...]:
    def shallow_copy(iterable: Iterable[Domain]) -> Tuple[Iterable[Domain], ...]:
        return tee(iterable, count)

    copies = shallow_copy(map(shallow_copy, nested_iterable))
    return tuple(map(itemgetter(index), iterables)
                 for index, iterables in enumerate(copies))

Плюсы:

  • довольно легко читается иобъяснить.

Минусы:

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

Мы можем сделать лучше.

Улучшенное решение

Если мы посмотрим на itertools.tee документацию по функциям , она содержит Pythonрецепт, который с помощью functools.singledispatch оформитель может быть переписан как

from collections import (abc,
                         deque)
from functools import singledispatch
from itertools import repeat
from typing import (Iterable,
                    Tuple,
                    TypeVar)

Domain = TypeVar('Domain')


@functools.singledispatch
def copy(object_: Domain,
         *,
         count: int) -> Iterable[Domain]:
    raise TypeError('Unsupported object type: {type}.'
                    .format(type=type(object_)))

# handle general case
@copy.register(object)
# immutable strings represent a special kind of iterables
# that can be copied by simply repeating
@copy.register(bytes)
@copy.register(str)
# mappings cannot be copied as other iterables
# since they are iterable only by key
@copy.register(abc.Mapping)
def copy_object(object_: Domain,
                *,
                count: int) -> Iterable[Domain]:
    return itertools.repeat(object_, count)


@copy.register(abc.Iterable)
def copy_iterable(object_: Iterable[Domain],
                  *,
                  count: int = 2) -> Tuple[Iterable[Domain], ...]:
    iterator = iter(object_)
    # we are using `itertools.repeat` instead of `range` here
    # due to efficiency of the former
    # more info at
    # /6616473/kakova-tsel-v-python-itertools-repeat#6616580
    queues = [deque() for _ in repeat(None, count)]

    def replica(queue: deque) -> Iterable[Domain]:
        while True:
            if not queue:
                try:
                    element = next(iterator)
                except StopIteration:
                    return
                element_copies = copy(element,
                                           count=count)
                for sub_queue, element_copy in zip(queues, element_copies):
                    sub_queue.append(element_copy)
            yield queue.popleft()

    return tuple(replica(queue) for queue in queues)

Плюсы:

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

Минусы:

  • менее читаемые (но, как мы знаем * ) практичность превосходитpurity "),
  • предоставляет некоторые накладные расходы, связанные с диспетчеризацией (но это нормально, поскольку он основан на поиске в словаре, который имеет O(1) сложность).

Тест

Подготовка

Давайте определим нашу вложенную итерацию следующим образом

nested_iterable = [range(10 ** index) for index in range(1, 7)]

Поскольку создание итераторов ничего не говорит о производительности базовых копий, давайте определим функцию для исчерпания итераторов (описано * 1069)* здесь )

exhaust_iterable = deque(maxlen=0).extend

Время

Использование timeit пакета

import timeit

def naive(): exhaust_iterable(copy_nested_iterable(nested_iterable))

def improved(): exhaust_iterable(copy_iterable(nested_iterable))

print('naive approach:', min(timeit.repeat(naive)))
print('improved approach:', min(timeit.repeat(improved)))

У меня на ноутбуке с Windows 10 x64 в Python 3.5.4

naive approach: 5.1863865
improved approach: 3.5602296000000013

Память

Использование memory_profiler пакета

Line #    Mem usage    Increment   Line Contents
================================================
    78     17.2 MiB     17.2 MiB   @profile
    79                             def profile_memory(nested_iterable: Iterable[Iterable[Any]]) -> None:
    80     68.6 MiB     51.4 MiB       result = list(flatten(flatten(copy_nested_iterable(nested_iterable))))

для "наивного" подхода и

Line #    Mem usage    Increment   Line Contents
================================================
    78     17.2 MiB     17.2 MiB   @profile
    79                             def profile_memory(nested_iterable: Iterable[Iterable[Any]]) -> None:
    80     68.7 MiB     51.4 MiB       result = list(flatten(flatten(copy_iterable(nested_iterable))))

для «улучшенного».

Примечание : я сделал разные запуски скрипта, потому что делал их сразуне является репрезентативным, поскольку второе выражение будет повторно использовать ранее созданные скрытые объекты int.


Заключение

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

Реклама

Я добавил «улучшенное» решение для lz пакета из 0.4.0 версии, котораяможет использоваться как

>>> from lz.replication import replicate
>>> iterable = iter(range(5))
>>> list(map(list, replicate(iterable,
                             count=3)))
[[0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4]]

Это тестирование на основе свойств с использованием hypothesis framework , поэтому мы можем быть уверены, что оно работает как положено.

0 голосов
/ 14 декабря 2018

Решение вашего вопроса: Как выполнить глубокое копирование вложенной итерации?

Вы можете использовать deepcopy из стандартной библиотеки:

>>> from copy import deepcopy
>>> 
>>> ni = [1, [2,3,4]]
>>> ci = deepcopy(ni)
>>> ci[1][0] = "Modified"
>>> ci
[1, ['Modified', 3, 4]]
>>> ni
[1, [2,3,4]]

Обновление

@ Азат Ибраков сказал: вы работаете с последовательностями, попробуйте, например, выполнить глубокое копирование файлового объекта (подсказка: это не удастся)

Нет, глубокое копирование файлового объекта,не удастся, вы можете глубоко копировать файл объекта, демонстрация:

import copy

with open('example.txt', 'w') as f:
     f.writelines(["{}\n".format(i) for i in range(100)])

with open('example.txt', 'r') as f:
    l = [1, [f]]
    c = copy.deepcopy(l)
    print(isinstance(c[1][0], file))  # Prints  True.
    print("\n".join(dir(c[1][0])))

Отпечатки:

True
__class__
__delattr__
__doc__
__enter__
__exit__
__format__
__getattribute__
...
write
writelines
xreadlines

Проблема в концепции.

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

У вас не будет всех элементов объекта, реализующего протокол итератора(как файловые объекты), пока вы не пройдете весь итератор (выполняйте next() до тех пор, пока не будет сгенерировано исключение StopIteration).

Это потому, что нет способа точно определить результат выполнения next (__next__ для Python 2.x) метод итератора

См. Следующий пример:

import random

class RandomNumberIterator:

    def __init__(self):
        self.count = 0
        self.internal_it = range(10)  # For later demostration on deepcopy

    def __iter__(self):
        return self

    def next(self):
        self.count += 1
        if self.count == 10:
            raise StopIteration
        return random.randint(0, 1000)

ri = RandomNumberIterator()

for i in ri:
    print(i)  # This will print randor numbers each time.
              # Can you come out with some sort of mechanism to be able
              # to copy **THE CONTENT** of the `ri` iterator? 

Опять вы можете:

from copy import deepcopy

cri = deepcopy(ri)

for i in cri.internal_it:
    print(i)   # Will print numbers 0..9
               # Deepcopy on ri successful!

Файловый объект - это особый случай, здесь есть обработчики файлов, прежде чем,вы видите, что вы можете копировать глубже файловый объект, но он будет иметь состояние closed.

Alternative.

Вы можете вызвать list для ваших итераций, что автоматически оценит итерациии тогда вы сможете снова протестировать СОДЕРЖАНИЕ ITERABLE .

Возвращение к файлам:

with open('example.txt', 'w') as f:
         f.writelines(["{}\n".format(i) for i in range(5)])

with open('example.txt', 'r') as f:
    print(list(f))  # Prints ['0\n', '1\n', '2\n', '3\n', '4\n']

Итак, возобновление

Вы можете глубоко копировать вложенные итерации, но вы не можете оценивать итерации, пока они копируются, простонет смысла (помните RandomNumberIterator).

Если вам нужно проверить итерируемые элементы CONTENT , вам нужно оценить их.

...