有一个自定义迭代器的问题,因为它只在文件上迭代一次。我在两次迭代之间对相关的文件对象调用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']
最佳答案
我认为您最好不要尝试执行.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()
参数添加到classkwargs
方法中,并使用默认参数call_with
也会很好。我认为这是一个很好的变化。