Правильный способ сброса csv.reader для нескольких итераций? - PullRequest
10 голосов
/ 20 июля 2011

Проблема с пользовательским итератором в том, что он будет перебирать файл только один раз. Я вызываю seek(0) в соответствующем файловом объекте между итерациями, но StopIteration выбрасывается при первом вызове next() при втором прогоне. Я чувствую, что упускаю из виду нечто очевидное, но буду признателен за свежим взглядом на это:

class MappedIterator(object):
    """
    Given an iterator of dicts or objects and a attribute mapping dict, 
    will make the objects accessible via the desired interface.

    Currently it will only produce dictionaries with string values. Can be 
    made to support actual objects later on. Somehow... :D
    """

    def __init__(self, obj=None, mapping={}, *args, **kwargs):

        self._obj = obj
        self._mapping = mapping
        self.cnt = 0

    def __iter__(self):

        return self

    def reset(self):

        self.cnt = 0

    def next(self):

        try:

            try:
                item = self._obj.next()
            except AttributeError:
                item = self._obj[self.cnt]

            # If no mapping is provided, an empty object will be returned.
            mapped_obj = {}

            for mapped_attr in self._mapping:

                attr = mapped_attr.attribute
                new_attr = mapped_attr.mapped_name

                val = item.get(attr, '')
                val = str(val).strip() # get rid of whitespace

                # TODO: apply transformers...

                # This allows multi attribute mapping or grouping of multiple
                # attributes in to one.
                try:
                    mapped_obj[new_attr] += val
                except KeyError:
                    mapped_obj[new_attr] = val

            self.cnt += 1

            return mapped_obj

        except (IndexError, StopIteration):

            self.reset()
            raise StopIteration


class CSVMapper(MappedIterator):

    def __init__(self, reader, mapping={}, *args, **kwargs):

        self._reader = reader
        self._mapping = mapping

        self._file = kwargs.pop('file')

        super(CSVMapper, self).__init__(self._reader, self._mapping, *args, **kwargs)

    @classmethod
    def from_csv(cls, file, mapping, *args, **kwargs):

        # TODO: Parse kwargs for various DictReader kwargs.
        return cls(reader=DictReader(file), mapping=mapping, file=file)

    def __len__(self):

      return int(self._reader.line_num)

    def reset(self):

      if self._file:

        self._file.seek(0)

      super(CSVMapper, self).reset()

Пример использования:

file = open('somefile.csv', 'rb') # say this file has 2 rows + a header row

mapping = MyMappingClass() # this isn't really relevant

reader = CSVMapper.from_csv(file, mapping)

# > 'John'
# > 'Bob'
for r in reader:

  print r['name']

# This won't print anything
for r in reader:

  print r['name']

Ответы [ 3 ]

9 голосов
/ 20 июля 2011

Я думаю, что вам лучше не пытаться выполнить .seek(0), а просто открывать файл из имени файла каждый раз.

И я не рекомендую просто возвращать self в __iter__() метод.Это означает, что у вас есть только один экземпляр вашего объекта.Я не знаю, насколько вероятно, что кто-то попытается использовать ваш объект из двух разных потоков, но если это произойдет, результаты будут удивительными.

Итак, сохраните имя файла, а затем в __iter__() метод создания свежего объекта со вновь инициализированным объектом считывателя и вновь открытым объектом дескриптора файла;вернуть этот новый объект из __iter__().Это будет работать каждый раз, независимо от того, что на самом деле является файловым объектом.Это может быть дескриптор сетевой функции, которая получает данные с сервера, или кто знает что, и он может не поддерживать метод .seek();но вы знаете, что если вы просто откроете его снова, вы получите новый объект дескриптора файла.И если кто-то использует модуль threading для параллельного запуска 10 экземпляров вашего класса, каждый из них всегда будет получать все строки из файла, а не каждый случайным образом получать примерно десятую часть строк.

Кроме того, я не рекомендую ваш обработчик исключений в методе .next() в MappedIterator.Метод .__iter__() должен возвращать объект, который может быть надежно повторен.Если глупый пользователь передает целочисленный объект (например, 3), это не будет повторяться.Внутри .__iter__() вы всегда можете явно вызвать iter() для аргумента, и если это уже итератор (например, объект дескриптора открытого файла), вы просто получите тот же объект обратно;но если это объект последовательности, вы получите итератор, который работает с последовательностью.Теперь, если пользователь передает 3, вызов iter() вызовет исключение, которое имеет смысл прямо на линии, где пользователь передал 3, а не исключение, исходящее от первого вызова .next().И в качестве бонуса вам больше не нужна переменная-член cnt, и ваш код будет немного быстрее.

Итак, если вы соберете все мои предложения, вы можете получить что-то вроде этого:

class CSVMapper(object):
    def __init__(self, reader, fname, mapping={}, **kwargs):
        self._reader = reader
        self._fname = fname
        self._mapping = mapping
        self._kwargs = kwargs
        self.line_num = 0

    def __iter__(self):
        cls = type(self)
        obj = cls(self._reader, self._fname, self._mapping, **self._kwargs)
        if "open_with" in self._kwargs:
            open_with = self._kwargs["open_with"]
            f = open_with(self._fname, **self._kwargs)
        else:
            f = open(self._fname, "rt")
        # "itr" is my standard abbreviation for an iterator instance
        obj.itr = obj._reader(f)
        return obj

    def next(self):
        item = self.itr.next()
        self.line_num += 1

        # If no mapping is provided, item is returned unchanged.
        if not self._mapping:
            return item  # csv.reader() returns a list of string values

        # we have a mapping so make a mapped object
        mapped_obj = {}

        key, value = item
        if key in self._mapping:
            return [self._mapping[key], value]
        else:
            return item

if __name__ == "__main__":
    lst_csv = [
        "foo, 0",
        "one, 1",
        "two, 2",
        "three, 3",
    ]

    import csv
    mapping = {"foo": "bar"}
    m = CSVMapper(csv.reader, lst_csv, mapping, open_with=iter)

    for item in m: # will print every item
        print item

    for item in m: # will print every item again
        print item

Теперь метод .__iter__() дает вам новый объект каждый раз, когда вы вызываете его.

Обратите внимание, как пример кода использует список строк вместо открытия файла.В этом примере вам нужно указать функцию open_with(), которая будет использоваться вместо значения по умолчанию open() для открытия файла.Поскольку наш список строк может быть повторен, чтобы возвращать одну строку за раз, мы можем просто использовать iter в качестве нашей open_with функции здесь.

Я не понял ваш код отображения.csv.reader возвращает список строковых значений, а не какой-то словарь, поэтому я написал некоторый тривиальный код отображения, который работает для файлов CSV с двумя столбцами, первый из которых является строкой.Ясно, что вы должны отрубить мой тривиальный код отображения и ввести желаемый код отображения.

Кроме того, я выбрал ваш .__len__() метод.Возвращает длину последовательности, когда вы делаете что-то вроде len(obj);он возвращает line_num, что означает, что значение len(obj) будет меняться каждый раз, когда вы вызываете метод .next().Если пользователи хотят узнать длину, они должны сохранить результаты в списке и взять длину списка или что-то в этом роде.

РЕДАКТИРОВАТЬ: я добавил **self._kwargs к вызову call_with() вметод .__iter__().Таким образом, если вашей функции call_with() нужны дополнительные аргументы, они будут переданы.До того, как я сделал это изменение, не было веской причины сохранять аргумент kwargs в объекте;было бы так же хорошо добавить аргумент call_with в метод класса .__init__() с аргументом по умолчанию None.Я думаю, что это хорошее изменение.

2 голосов
/ 19 сентября 2013

Для DictReader:

f = open(filename, "rb")
d = csv.DictReader(f, delimiter=",")

f.seek(0)
d.__init__(f, delimiter=",")

Для DictWriter:

f = open(filename, "rb+")
d = csv.DictWriter(f, fieldnames=fields, delimiter=",")

f.seek(0)
f.truncate(0)
d.__init__(f, fieldnames=fields, delimiter=",")
d.writeheader()
f.flush()
1 голос
/ 20 июля 2011

Объект DictReader, по-видимому, не следует команде seek() в открытом файле, поэтому вызовы next() постоянно выполняются с конца файла.

В вашем resetВы можете снова открыть файл (вам также нужно сохранить имя файла в self._filename):

def reset(self):
     if self._file:
         self._file.close()
         self._file = open(self._filename, 'rb')

Вы также можете посмотреть на подклассы вашего файлового объекта аналогично верхнему ответу на this вопрос.

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