如何从一行文本创建字典?

10

我有一个生成的文件,里面有成千上万行像下面这样的内容:

CODE,XXX,DATE,20101201,TIME,070400,CONDITION_CODES,LTXT,PRICE,999.0000,QUANTITY,100,TSN,1510000001

有些行有更多的字段,有些则少一些,但都遵循键值对的相同模式,每行都有一个TSN字段。

当对该文件进行分析时,我编写了以下循环将文件读入字典:

#!/usr/bin/env python

from sys import argv

records = {}
for line in open(argv[1]):
    fields = line.strip().split(',')
    record = dict(zip(fields[::2], fields[1::2]))
    records[record['TSN']] = record

print 'Found %d records in the file.' % len(records)

...这很好,符合我想要的(print只是一个微不足道的例子)。

然而,对我来说,它并不特别“pythonic”,因为这一行:

dict(zip(fields[::2], fields[1::2]))

这种方法感觉很笨重(它会遍历字段多少次?)。

在Python 2.6中,有没有更好的方法只使用标准模块?


我认为这已经是最符合Python习惯的了。 - Kamil Szot
你只对TSN记录感兴趣吗?还是打算将其扩展到所有记录类型? - moinudin
对于任何查看此问题的人,我在另一个问题中发表了评论,指出现在可以使用海象运算符(3.8+)以简洁的方式完成此操作:for i, k in zip(_x := iter(mylist), _x): ... - Alex Just Alex
对于任何查看这个问题的人,我在另一个问题中已经评论过,海象运算符现在可以被使用(3.8+)以简洁的方式来完成这个操作:for i, k in zip(_x := iter(mylist), _x): ... - undefined
4个回答

27
在Python 2中,您可以使用itertools模块中的izip和生成器对象的神奇之处编写自己的函数来简化dict记录的值对创建。我从Python 2 itertools文档中一个同名(尽管功能不同)的recipe获取了pairwise()的想法。 在Python 3中使用这种方法,您只需使用普通的zip(),因为它会执行izip()在Python 2中的工作,导致后者从itertools中删除 - 下面的示例解决了这个问题,并且应该适用于两个版本。
try:
    from itertools import izip
except ImportError:  # Python 3
    izip = zip

def pairwise(iterable):
    "s -> (s0,s1), (s2,s3), (s4, s5), ..."
    a = iter(iterable)
    return izip(a, a)

可以在文件读取for循环中像这样使用:

from sys import argv

records = {}
for line in open(argv[1]):
    fields = (field.strip() for field in line.split(','))  # generator expr
    record = dict(pairwise(fields))
    records[record['TSN']] = record

print('Found %d records in the file.' % len(records))

但是,等等,这还不是全部!

你可以创建一个名为grouper()的通用版本,它对应于一个同名的itertools配方(该配方位于pairwise()的正下方):

def grouper(n, iterable):
    "s -> (s0,s1,...sn-1), (sn,sn+1,...s2n-1), (s2n,s2n+1,...s3n-1), ..."
    return izip(*[iter(iterable)]*n)

你的 for 循环可以这样使用:

    record = dict(grouper(2, fields))

当然,对于这种特定情况,可以很容易地使用 functools.partial() 并创建一个类似的 pairwise() 函数(它将在 Python 2 和 3 中都有效):

import functools
pairwise = functools.partial(grouper, 2)

后记

除非有非常巨大的字段数,否则你可以创建一个实际的序列来存储行项目对(而不是使用没有len()生成器表达式):

fields = tuple(field.strip() for field in line.split(','))
优点在于它允许使用简单的切片来进行分组:
try:
    xrange
except NameError:  # Python 3
    xrange = range

def grouper(n, sequence):
    for i in xrange(0, len(sequence), n):
        yield sequence[i:i+n]

pairwise = functools.partial(grouper, 2)

2
非常感谢。所有给出的答案都非常好,但您的代码在运行2.2 Gb文件时是最快的(甚至比itertools版本还要快),而且易于阅读和单元测试。我为没想到查看itertools而感到遗憾,那里有很多好东西。 - johnsyweb
2
@Johnsyweb:关于性能的好消息真是太棒了。我为此感到有点自豪,而且已经很高兴终于找到了一种相当优雅的方法来处理它,因为这是我在日常Python代码中经常需要的东西。 - martineau
在现代Python中内联pairwise()for i, k in zip(_x := iter(mylist), _x): ...(使用Python 3.8+的海象运算符)。 - Alex Just Alex

6

4
这里的诀窍是使用列表乘法和 *args“取消引用”来确保在传递参数给 zip 的同时,相同的对象被传递了两次,这样迭代器状态就可以共享,并且每次 zip 创建新的输出元组时都会前进两次。我们还可以用其他几种方式实现这个功能:x = iter(l); zip(x, x) 可能更容易阅读;(lambda x: zip(x, x))(iter(l)) 可能更熟悉于函数式编程人员,尽管这种方式几乎是为了假装我们在进行没有副作用的编程,而实际上我们完全依赖于一个副作用 ;) - Karl Knechtel
@Karl Knechtel:可以使用(lambda x=iter(x): zip(x, x))()代替(lambda x: zip(x, x))(iter(x)),这样更易读,尽管仍然依赖于[不同的]副作用。 - martineau

2
import itertools

def grouper(n, iterable, fillvalue=None):
    "grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx"
    args = [iter(iterable)] * n
    return itertools.izip_longest(fillvalue=fillvalue, *args)

record = dict(grouper(2, line.strip().split(","))

source


很不幸,我发现这只是itertools文档中一个配方的逐字复制,而我已经无法取消我的点赞了。由于没有提供任何参考或引用,我不得不称之为剽窃。 - martineau
3
他的确有一个小小的名为“源代码”的链接在下方。 - Ignacio Vazquez-Abrams
@Ignacio Vazquez-Abrams:哦...显然我错过了那个--抱歉@robert--但我仍然认为它不值得点赞。 - martineau

1

如果我们最终要将其抽象成一个函数,那么从头开始编写并不太难:

def pairs(iterable):
    iterator = iter(iterable)
    while True:
        try: yield (iterator.next(), iterator.next())
        except: return

罗伯特的食谱版本绝对在灵活性方面赢得了不少分数。


就此而言,这不是“罗伯特的配方”,请参见我的评论,位于他的答案下面。 - martineau

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接