杂乱地用Python思考

Python的方式解决数据转换问题,解释过多。

(要查看对所有这些进行什么样的测试,请查看我的后续文章“ 基于属性的混乱测试”

我是Puget Sound编程Python( PuPPy )用户组的成员,我们有一个活跃的Slack(加入我们!)。 我最喜欢的频道之一是#q-and-a ,任何人都可以问任何问题并获得帮助。 最近有人问到有关将一种数据结构转换为另一种数据结构的信息,尤其是关于如何用Python进行处理的问题,我能够提供帮助。

我正在使用该示例来写这篇文章,以解释我在1)处理数据2)尝试编写Pythonic代码时想到的一些事情。 我喜欢这里的示例,因为与某些情况不同,Pythonic解决方案的外观并不立即显而易见。

我知道有时候编写一些好的代码对于初级开发人员来说就像把魔术解决方案从帽子里掏出来一样 ,而且我想展示我的过程是多么渐进的 ,并且在事情不能按我预期的那样工作时会产生很多混乱 。 我试图重新创建解决问题的实际思维过程,并提供一些经验法则,以帮助面对类似情况的人们。

事不宜迟,这个问题。

给定一个像…的数据结构

  boop = { 
“ hello”:[1、2、3],
“再见”:[4、5、6]
}

我们如何将其转换为类似……的数据结构

 哔声= [ 
{“ hello”:1,“ bye”:4},
{“ hello”:1,“ bye”:5},
{“ hello”:1,“ bye”:6},
{“ hello”:2,“ bye”:4},
{“ hello”:2,“ bye”:5},
{“ hello”:2,“ bye”:6},
{“ hello”:3,“ bye”:4},
{“ hello”:3,“ bye”:5},
{“你好”:3,“再见”:6}
]

当我遇到这样的“格式转换”问题时,我通常从编辑单行开始,直到达到单行的极限 ,或者行之有效,然后找出正确的抽象。 这不是正确的方法,只是我有时使用的一种方法 。 这部分是因为我觉得单线很有趣,部分是因为我认为该过程提供了一种很好的“累积”方法,这可能最好通过示例来说明。

好的,所以首先, boop中的所有键和值也都出现在beep 。 因此,无论如何,我们都需要将那些东西赶走。 .items()是执行此操作的标准方法。 而且,我知道我们将要立即与他们做更多的事情,这对我来说意味着一种理解,这使我有了第一篇文章:

  (...对于boop.items()中的(k,vs) 

(我喜欢解释性的变量名 ,并且在以字典为中心的理解中,虽然我想弄点儿主意,但“ k代表“键”, vs代表“值”,它们会变好。)

好吧,所以我们正在努力解决问题,并对他们采取行动,但是我们在做什么呢? 在这里,我开始考虑另一端的需求,以及将一切正确结合在一起的“工作”。

事实证明,“工作”很容易,字典中每个“hello”“bye”中的值都有一个乘积(读作:每个组合); 毫不奇怪,我的解决方案将使用itertools.product()进行这种组合。 因此,如果我们正在使用itertools.product() ,结果将是什么? 这将有助于弄清结果。

我知道我需要制作字典,而我喜欢用Python用两种主要方法来做到这一点: dict函数,可以与多种形式的参数或字典理解一起使用。 我知道我正在使用的东西来自itertools.product() ,如果只有一个dict参数,则代码将变得最简单,因此我将写一个分行来看看它是什么样的:

  [dict(something)for itertools.product(.... somesomething in something using my如上所述)...]] 

我知道dict函数可以接受的一个方便的参数是键值2元组(成对,如(“hello”, 1) )的迭代,这似乎很适合—我正在获取键和值,重塑它们(以某种方式),在某个时候可能会有一个方便的地方将它们变成2元组,然后可以将其变成我需要使用该参数的字典。 dict的其他大多数选项都需要多个参数(不适合我当前正在使用的思维模型)或现有的dict(这意味着我已经完成了,所以不需要我想要的)。 同样,就像通常观察到的那样,我经常发现dict的元组版本很有用。

好的,那有帮助。 既然我知道我希望something可以是键值2元组的迭代,这意味着每个可迭代产品所需要的都是键值2元组,因为它要做的就是重新整理我提供的内容。 我目前有键和值列表……需要更多的理解! (我可能会有点理解。)

好的,所以我可以用kvs编写…… ((k, v) for v in vs) ,然后可以将其放到我写的第一部分中:

  ((在boop.items()中对于(k,vs)的v中的([[k,v]) 

请注意,开括号( 生成器理解 !)已经变得很荒谬。 不用担心,到了将要分解的东西的时候了。 但是在那之前……是时候让事情变得更荒谬了!

退一步,我们有一个迭代,其中每个值都是键值2元组的迭代。 这听起来很像我们需要传递给产品的东西! 让我看看,内部可迭代的示例值将是(“hello”, 2)(“bye”, 4) (如果不清楚,请告诉我,我将进行编辑以详细说明原因) ,因此这似乎已经是我们稍后所期望的事情。 现在我们如何让产品做所有事情?

如果我们仅将以上内容粘贴到产品中怎么办?

  >>> itertools.product((boop.items()中的(k,vs)的(v中的v的((k,v))) 
   

糟糕,忘记扩展要在REPL中查看的内容…

  >>>列表(itertools.product((在boop.items()中的(k,vs)中的(v,vs中的v的((k,v))) 
  [[(<发电机对象。在0x10302b8e0>,),(<<发电机对象。在0x10302b938>,]] 

好的,仍然看不到这些值,但请等待,列表中只有两个成员。 我们需要在输出中期望每个dict有一个值,而这个值要大于两个…

是时候看看itertools.product()文档,看看为什么它的表现不像所需要的了。 哦! 每个可迭代项都需要是一个单独的参数,但是我们提供的单个可迭代项中包含所有其他可迭代项。 幸运的是,Python有一种将可迭代对象投影到函数的参数中的方法:在它的前面加上* (在下面仔细看,这是很小的更改)。

所以…

  >>>列表(itertools.product(*(((在boop.items()中对于(k,vs)中的v,对于(k,v)中的v)) 
  [(('bye',1),('bye',4)),((('bye',1),('bye',5)),((''bye',1),('bye' ,6)),((('bye',2),('bye',4)),((('bye',2),('bye',5)),(('bye',2), ('bye',6)),((('bye',3),('bye',4)),((''bye',3),('bye',5)),((('bye' ,3),('bye',6))] 

哦,看起来很完美…等等。 什么是所有键“bye” ? 现在,这是我以前遇到过的事情,因此我知道它与Python的作用域规则有关。 现在,我们将通过将生成器理解之一转换为列表理解,使其具体而直接(而不是懒惰)来解决它。 查找使用[]而不是()

  >>>列表(itertools.product(*([在boop.items()中对于(k,vs)为[v]中的v的[[k,v)]] 
  [[(('hello',1),('bye',4)),((('hello',1),('bye',5)),((''hello',1),('bye' ,6)),((('hello',2),('bye',4)),((('hello',2),('bye',5)),((''hello',2), ('bye',6)),((('hello',3),('bye',4)),((''hello',3),('bye',5)),((('hello' ,3),('bye',6))] 

现在它正常工作了。 我非常确定这是可行的,因为发生的事情是, k仅显示最新值( “bye” ),并且将其传递给内部理解,因此我做到了,所以每次我们得到不同的k ,我们通过列表理解使其变得具体。 如果您在单线播放时看到这样的事情,而不是试图确切地确定作用域规则的工作原理,您可以将每种理解变成一种具体的 (列表,字典,集合)而不是生成器理解。 当您分手后,您总是可以退出,这通常通过引入新功能来单独解决范围问题。

好的,因此产生了一堆2元组的可迭代对象,例如(('hello', 1), ('bye', 4))(('hello', 2), ('bye', 4)) 。 这正是dict需要的,这意味着我们可以使用更早的公式!

  [在itertools.product中的某物的dict(某物)(*(在boop.items()中的(k,vs)中[v的v中为v的v []] 

而输出正是beep

  [{'hello':1,'bye':4},{'hello':1,1,'bye':5},{'hello':1,'bye':6},{'hello':2, 'bye':4},{'hello':2,'bye':5},{'hello':2,'bye':6},{'hello':3,'bye':4},{ 'hello':3,'bye':5},{'hello':3,'bye':6}] 

现在我们有一些工作了,我们如何使它变得不可读呢?

首先,是时候返回名字了。 我们将使它成为一个函数( 函数是非常Pythonic的 ),并且该函数需要一个名称。 像boop_to_beep诱人,抵抗。 在研究此过程时,我开始将“hello”“bye”视为我们需要找到其每种组合的类别。 因此,一个体面的功能签名(缺少其他信息)可能是:

  def组合(类别): 
返回...(见上文)...

接下来,是时候分手了。 我的一条规则是, 我不喜欢阅读嵌套的理解 ,所以我将从这里开始。 我确实喜欢如下形式的理解: [func(thing) for thing in things] ,因此,如果我可以提出一个易于理解的函数来进行内部理解,则可以使用该构造。 该功能有什么作用? 它将一个键和多个值变成一堆对。 对于函数名称,这不是最坏的主意:

  def对(键,值): 
return((键,值)表示值中的值)

还不错,但这并不是配对的完全通用功能。 它适用于非常特定的情况。 至少, 当我查看函数签名时,我不立即知道如何为单个键和一些值建立配对。 (在编写代码时,我经常问自己这样的问题。)相反,它使我们成为类别所需的配对。 那么,也许这种书写方式呢?

  def对(类别): 
键,值=类别
return((键,值)表示值中的值)

现在签名告诉我这是一个类别的对,并且我可以在下面看到一个类别是一个键和一些值,以及我们如何将它们变成对。 这对于我们在哪里调用它也很方便,将函数内部的拆包隔离开来:

  def组合(类别): 
返回[dict(something)表示itertools.product中的某物(*(pairs(category)表示category.items()中的类别))]

那条线太长了,读起来太复杂了,我们该怎么办? 我拥有的另一种规则是, 我喜欢使用 * 扩展作为变量而不是较大表达式的参数。 关于命名的一点思考导致:

  def组合(类别): 
category_pairs =(category.items()中类别的对(pairs(category))
返回[dict(something)for itertools.product(* category_pairs)中的某物]

嗯,我可以在这里停下来,但是对理解此函数的工作方式至关重要的主力函数 itertools.product() 有点隐蔽 ,而且最后一行对于可读性而言仍然有些复杂。 也许…

  def组合(类别): 
category_pairs =(category.items()中类别的对(pairs(category))
pair_combos = itertools.product(* category_pairs)
返回[pair(combos)中的组合的dict(combo)]

我们去了,这就是我发送的解决方案。 在全:

最初,当我写这篇文章时,我只是到此结束,但我想我可以尝试总结一下。 编写代码对每个人来说都是一团糟,即使我们中有些人有更多的经验来解决这些麻烦 。 我的长处之一是,我可以在脑海中摆弄许多思维模型-例如数据在不同点的样子。 您可以在上面有关如何组装所有这些部件的文章中看到这一点。 这也可能是您的长处,或者您可能拥有不同的长处,可以通过其他方式解决此问题。 但是,您解决了一个问题,逐步工作,一旦获得可运行的东西就尝试解决问题,并在图书馆工作不像您期望的那样(如上文所述)时查阅文档,无论您是否有十五年的工作经验,经验或五个月。