【问题标题】:Does Slicing `a` (e.g. `a[1:] == a[:-1]`) create copies of the `a`?切片 `a`(例如 `a[1:] == a[:-1]`)会创建 `a` 的副本吗?
【发布时间】:2013-12-25 09:13:50
【问题描述】:

我的一个朋友向我展示了以下 Python 代码:

a[1:] == a[:-1]

如果 a 中的所有项目都相同,则返回 True。

我认为代码乍一看很难理解,而且 - 它在内存使用方面效率低下,因为将创建两个 a 副本用于比较。

我使用 Python 的 dis 来查看 a[1:]==a[:-1] 的幕后发生了什么:

>>> def stanga_compare(a):
...     return a[1:]==a[:-1]
...
>>> a=range(10)
>>> stanga_compare(a)
False
>>> a=[0 for i in range(10)]
>>> stanga_compare(a)
True

>>> dis.dis(stanga_compare)
  2           0 LOAD_FAST                0 (a)
              3 LOAD_CONST               1 (1)
              6 SLICE+1
              7 LOAD_FAST                0 (a)
             10 LOAD_CONST               2 (-1)
             13 SLICE+2
             14 COMPARE_OP               2 (==)
             17 RETURN_VALUE

归结为两个切片命令 - SLICE+1SLICE+2The documentation is unclear 这些操作码是否真的创建了 a 的新副本,或者只是对它的引用。

  • SLICE 命令是否复制a
  • 答案是否因 Python 实现(Cython、Jython)而异?

更新

这个 sn-p 显然是不可读和令人困惑的,我不会在实际中使用它 代码。我的兴趣纯粹是技术性的 - 切片是否会复制列表,以及答案是否会在不同情况下有所不同。

【问题讨论】:

  • IMO 的“变化”评论没有多大意义。即使x == y 可以为用户定义的类做任何事情(包括打开弹出窗口或连接到数据库服务器),所以呢?
  • 它是python的一个特性,而不仅仅是Cpython。我已经链接了python教程中的部分。

标签: python cpython memory-efficient python-internals


【解决方案1】:

文档不清楚,因为切片不同的对象会做不同的事情。 In the case of a list, slicing does make a (shallow) copy1。请注意this is a feature of python lists independent of python implementation。对于其他对象(如 numpy 数组),它可能不会创建副本。

如果您想要一种更好的方法来检查列表中的所有元素是否相同,我可能会推荐:

 all(lst[0] == item for item in lst)

从性能的角度来看,您朋友的代码实际上可能会在小型列表中表现得更好,因为列表切片经过了如此优化。但是,恕我直言,这更容易判断发生了什么,并且一旦发现不匹配,就有机会“短路”。

1要查看的实际函数是list_subscript,但在大多数情况下,它只是调用list_slice

【讨论】:

  • 在下面查看我的评论 - 似乎列表只复制了一次,分两片。
  • @AdamMatan -- 不。我已经在下面回答了你的评论。
  • 或任意迭代:i = iter(lst); all(next(i, object()) == item for item in i) 或其他东西
  • @JonClements -- 我认为这也不太有效。您需要将next 排除在循环之外。 (我已经考虑过了)。你会很不方便地为[1,1,2,2,3,3] 工作。但这个想法是对的。
  • @mgilson 是的...看起来我从来没有时间真正更新该评论:)
【解决方案2】:

如果a 是一个列表、元组或字符串,len(a)n,并且n > 0,那么每个切片(在 C 级别)创建一个长度为 n-1 的新数组。在 C 级别,CPython 中的所有对象都实现为指针,因此这些新数组包含从 a 复制的 n-1 指针(好吧,实际上不是字符串 - 字符串表示更节俭)。

但是,正如@mgilson 所说,切片返回的内容取决于a 的类型。某些类型可能返回一个紧凑的描述符而不是复制任何东西。并且一个类型甚至可能以这样一种方式实现切片,以致显示的代码无法按预期工作。

但是你真的有一个清单;-)

【讨论】:

  • Numpy 是一个完美的例子,上面的代码无法按预期工作。你会得到一个没有真值的布尔 array :-)。
  • 为什么我们使用两次切片时只有一个副本?在我的示例中,前两项打印相同的地址
  • @mgilson: numpy 也重新定义了any/all。见stackoverflow.com/q/16334860/320726
  • n > 0 不是必要条件:试试a=[]; b=[:]; b.append(1); print(a)
  • @larsmans, n > 0 在我写的内容中是必要的,因为声称“每个切片(在 C 级别)创建一个长度为 n-1 的新数组”。很难创建一个长度为 -1 的新数组 ;-)
【解决方案3】:

是的,对于list 对象,Python 在切片时会创建浅拷贝,但是循环是用 C 语言(用于 cpython)制作的,因此它比用 Python 编写的任何东西都要快得多。在 C 中循环两次以获取浅拷贝并再次循环以进行比较将比仅在 Python 中循环一次更快。

请记住,cpython 通常足够快,但 Python 代码仍然比 C 代码慢 100 倍左右。因此,如果您想要一点速度,让 cpython 为您执行循环通常会更好。请注意,即使像 Python 中的 c = a + b 这样的东西也意味着执行大量逻辑(包括分支和内存分配)。

另一方面,如果对于您的代码,这种微优化是基本,那么 Python 可能不是解决您正在解决的问题的正确工具,您应该考虑其他选择(例如编写与 sip 接口的小型 C++ 扩展,使用 Cython、PyPy ...)。

为了确保代码对于未经训练的人来说是不可读的,并且如果列表很长并且通常不是恒定,那么all(y == x[0] for y in x)会因为短路而更快(即使循环是在 Python 中,并且对每个元素都进行了额外的下标操作)。

可读性很重要。很多。

编辑

另一个让 C 代码循环遍历元素的有趣选项是

x and x.count(x[0]) == len(x)

这不提供短路,但在我的电脑上,对于 1000 个元素的列表,它比基于 all 的解决方案快大约 75 倍,并且比 x[1:] == x[:-1] 快大约 6 倍。

我发现它也比 x[1:] == x[:-1] 更具可读性,但这可能是一个品味问题。

【讨论】:

    【解决方案4】:

    对于普通列表,切片确实会创建一个副本。您可以改为使用迭代来防止复制:

    import itertools
    a1 = iter(a)
    a2 = iter(a)
    a2.next() # start a2 iterator one removed
    all_are_identical = all((i1 == i2 for i1, i2 in itertools.izip(a1, a2)))
    

    (i1 == i2 for i1, i2 in itertools.izip(a1, a2)) 行创建了一个生成器,它将返回 a 中的每个元素是否等于下一个元素,一次一个,返回到 all。结果是逐个评估的,而不是先放入列表中,因此您可以以牺牲一些性能为代价来节省内存。

    【讨论】:

    • @mgilson 你能举个例子,上面的内容不等于a[1:] == a[:-1],(代码要复制的问题中的公式)?
    • @mgilson all_are_identical = False for a = [1, 2, 3, 4, 5],正如预期的那样。
    • 哦,我的错。我错过了a2.next() 电话。不过,我可能会将其写为next(a2) 以供将来证明。
    猜你喜欢
    • 2017-01-30
    • 2012-10-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-09-07
    • 2013-04-24
    • 2023-04-04
    • 1970-01-01
    相关资源
    最近更新 更多