jjzjj

突变和重新分配列表之间的 Python 区别(列表 = 和列表 [:] = )

coder 2023-08-24 原文

所以我经常按照这样的模式编写代码:

_list = list(range(10)) # Or whatever
_list = [some_function(x) for x in _list]
_list = [some_other_function(x) for x in _list]

等等

我现在在一个不同的问题上看到一条评论,解释了这种方法如何每次创建一个新列表,最好改变现有列表,如下所示:
_list[:] = [some_function(x) for x in _list]

这是我第一次看到这个明确的建议,我想知道它的含义是什么:

1)突变是否节省内存?据推测,在重新分配后对“旧”列表的引用将降至零并且“旧”列表被忽略,但是在此之前是否有延迟,我可能会使用比我需要的更多的内存重新分配而不是改变列表?

2)使用变异是否有计算成本?我怀疑就地更改某些内容比创建新列表并删除旧列表更昂贵?

在安全方面,我写了一个脚本来测试:
def some_function(number: int):
    return number*10

def main():
    _list1 = list(range(10))
    _list2 = list(range(10))

    a = _list1
    b = _list2 

    _list1 = [some_function(x) for x in _list1]
    _list2[:] = [some_function(x) for x in _list2]

    print(f"list a: {a}")
    print(f"list b: {b}")


if __name__=="__main__":
    main()

哪些输出:
list a: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
list b: [0, 10, 20, 30, 40, 50, 60, 70, 80, 90]

因此,突变似乎确实具有更可能引起副作用的缺点。尽管这些可能是可取的。是否有任何 PEP 讨论此安全方面或其他最佳实践指南?

谢谢你。

编辑:相互矛盾的答案:对内存进行更多测试
所以到目前为止我收到了两个相互矛盾的答案。在评论中,jasonharper 写道,方程的右侧不知道左侧,因此内存使用不可能受左侧出现的影响。然而,在答案中,Masoud 写道“当使用 [reassignment] 时,会创建两个具有两个不同身份和值的新旧 _list。之后,旧 _list 被垃圾收集。但是当一个容器发生变异时,每个单独的值被检索,在 CPU 中更改并一一更新。因此列表不会重复。”这似乎表明进行重新分配有很大的内存成本。

我决定尝试使用 memory-profiler深入挖掘。这是测试脚本:
from memory_profiler import profile


def normalise_number(number: int):
    return number%1000


def change_to_string(number: int):
    return "Number as a string: " + str(number) + "something" * number


def average_word_length(string: str):
    return len(string)/len(string.split())


@profile(precision=8)
def mutate_list(_list):
    _list[:] = [normalise_number(x) for x in _list]
    _list[:] = [change_to_string(x) for x in _list]
    _list[:] = [average_word_length(x) for x in _list]


@profile(precision=8)
def replace_list(_list):
    _list = [normalise_number(x) for x in _list]
    _list = [change_to_string(x) for x in _list]
    _list = [average_word_length(x) for x in _list]
    return _list


def main():
    _list1 = list(range(1000))
    mutate_list(_list1)

    _list2 = list(range(1000))
    _list2 = replace_list(_list2)

if __name__ == "__main__":
    main()

请注意,我知道,例如,这个查找平均字长函数写得不是特别好。只是为了测试。

结果如下:
Line #    Mem usage    Increment   Line Contents
================================================
    16  32.17968750 MiB  32.17968750 MiB   @profile(precision=8)
    17                             def mutate_list(_list):
    18  32.17968750 MiB   0.00000000 MiB       _list[:] = [normalise_number(x) for x in _list]
    19  39.01953125 MiB   0.25781250 MiB       _list[:] = [change_to_string(x) for x in _list]
    20  39.01953125 MiB   0.00000000 MiB       _list[:] = [average_word_length(x) for x in _list]


Filename: temp2.py

Line #    Mem usage    Increment   Line Contents
================================================
    23  32.42187500 MiB  32.42187500 MiB   @profile(precision=8)
    24                             def replace_list(_list):
    25  32.42187500 MiB   0.00000000 MiB       _list = [normalise_number(x) for x in _list]
    26  39.11328125 MiB   0.25781250 MiB       _list = [change_to_string(x) for x in _list]
    27  39.11328125 MiB   0.00000000 MiB       _list = [average_word_length(x) for x in _list]
    28  32.46484375 MiB   0.00000000 MiB       return _list

我发现,即使我将列表大小增加到 100000,重新分配始终会使用更多内存,但是,例如,可能仅多 1%。这让我认为额外的内存成本可能只是某个地方的额外指针,而不是整个列表的成本。

为了进一步检验这个假设,我以 0.00001 秒的间隔执行了基于时间的分析并绘制了结果。我想看看是否有内存使用量的瞬时峰值由于垃圾收集(引用计数)而立即消失。但是,唉,我还没有发现这样的尖峰。

谁能解释这些结果?这里到底发生了什么导致内存使用量出现这种轻微但持续增加的情况?

最佳答案

很难规范地回答这个问题,因为实际细节是依赖于实现的,甚至是依赖于类型的。

例如在 CPython 当一个对象达到引用计数为零时,它就会被处理掉并立即释放内存。然而,有些类型有一个额外的“池”,它在你不知道的情况下引用实例。例如,CPython 有一个未使用的“池”list实例。当最后引用一个 list在 Python 代码中删除它 5 月 被添加到这个“空闲列表”而不是释放内存(需要调用一些东西 PyList_ClearFreeList 来回收该内存)。

但是列表不仅仅是列表所需的内存,一个列表包含 对象。即使当列表的内存被回收时,列表中的对象仍然可以保留,例如,在其他地方仍然存在对该对象的引用,或者该类型本身也有一个“空闲列表”。

如果您查看其他实现,例如 PyPy 那么即使没有“池”,当没有人再引用它时,对象也不会立即处理,它只会“最终”处理。

那么这与您可能想知道的示例有何关系。

让我们来看看你的例子:

_list = [some_function(x) for x in _list]

在这一行运行之前,有一个分配给变量 _list 的列表实例。 .然后你创建一个 新名单使用列表理解并将其分配给名称 _list .在此分配之前不久,内存中有两个列表。旧列表和由理解创建的列表。在分配之后,有一个名称引用的列表 _list (新列表)和一个引用计数减 1 的列表。如果旧列表没有在其他任何地方被引用并因此达到 0 的引用计数,它可能会返回到池中,它可能是处置或可能最终处置。旧列表的内容相同。

另一个例子呢:
_list[:] = [some_function(x) for x in _list]

在此行运行之前,再次为名称 _list 分配了一个列表。 .当该行执行时,它还会通过列表理解创建一个新列表。但不是将新列表分配给名称 _list它将用新列表的内容替换旧列表的内容。然而,当它清除旧列表时,它会有 两个保存在内存中的列表。在此分配之后,旧列表仍可通过名称 _list 获得。但是列表理解创建的列表不再被引用,它的引用计数为 0 并且会发生什么取决于它。它可以放入空闲列表的“池”中,可以立即处理,也可以在将来某个未知时间点处理。清除旧列表的原始内容也是如此。

那么区别在哪里:

其实并没有太大的区别。在这两种情况下,Python 都必须将两个列表完全保留在内存中。然而,第一种方法释放对旧列表的引用比第二种方法释放对内存中的中间列表的引用更快,仅仅是因为在复制内容时它必须保持事件状态。

然而,更快地释放引用并不能保证它实际上会导致“更少的内存”,因为它可能会返回到池中,或者实现只会在将来的某个(未知)点释放内存。

内存成本较低的替代方案

您可以链接迭代器/生成器并在需要迭代它们时使用它们(或者您需要实际列表),而不是创建和丢弃列表。

所以,而不是做:
_list = list(range(10)) # Or whatever
_list = [some_function(x) for x in _list]
_list = [some_other_function(x) for x in _list]

你可以这样做:
def generate_values(it):
    for x in it:
        x = some_function(x)
        x = some_other_function(x)
        yield x

然后简单地消耗它:
for item in generate_values(range(10)):
    print(item)

或者用一个列表来消费它:
list(generate_values(range(10)))

这些不会(除非您将它传递给 list )创建任何列表。生成器是一种状态机,可在请求时一次处理一个元素。

关于突变和重新分配列表之间的 Python 区别(列表 = 和列表 [:] = ),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56308475/

有关突变和重新分配列表之间的 Python 区别(列表 = 和列表 [:] = )的更多相关文章

  1. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  2. ruby-on-rails - Rails 应用程序之间的通信 - 2

    我构建了两个需要相互通信和发送文件的Rails应用程序。例如,一个Rails应用程序会发送请求以查看其他应用程序数据库中的表。然后另一个应用程序将呈现该表的json并将其发回。我还希望一个应用程序将存储在其公共(public)目录中的文本文件发送到另一个应用程序的公共(public)目录。我从来没有做过这样的事情,所以我什至不知道从哪里开始。任何帮助,将不胜感激。谢谢! 最佳答案 无论Rails是什么,几乎所有Web应用程序都有您的要求,大多数现代Web应用程序都需要相互通信。但是有一个小小的理解需要你坚持下去,网站不应直接访问彼此

  3. ruby - 如何在续集中重新加载表模式? - 2

    鉴于我有以下迁移:Sequel.migrationdoupdoalter_table:usersdoadd_column:is_admin,:default=>falseend#SequelrunsaDESCRIBEtablestatement,whenthemodelisloaded.#Atthispoint,itdoesnotknowthatusershaveais_adminflag.#Soitfails.@user=User.find(:email=>"admin@fancy-startup.example")@user.is_admin=true@user.save!ende

  4. Ruby Koans about_array_assignment - 非平行与平行分配歧视 - 2

    通过ruby​​koans.com,我在about_array_assignment.rb中遇到了这两段代码你怎么知道第一个是非并行赋值,第二个是一个变量的并行赋值?在我看来,除了命名差异之外,代码几乎完全相同。4deftest_non_parallel_assignment5names=["John","Smith"]6assert_equal["John","Smith"],names7end45deftest_parallel_assignment_with_one_variable46first_name,=["John","Smith"]47assert_equal'John

  5. ruby - RVM 使用列表[0] - 2

    是否有类似“RVMuse1”或“RVMuselist[0]”之类的内容而不是键入整个版本号。在任何时候,我们都会看到一个可能包含5个或更多ruby的列表,我们可以轻松地键入一个数字而不是X.X.X。这也有助于rvmgemset。 最佳答案 这在RVM2.0中是可能的=>https://docs.google.com/document/d/1xW9GeEpLOWPcddDg_hOPvK4oeLxJmU3Q5FiCNT7nTAc/edit?usp=sharing-知道链接的任何人都可以发表评论

  6. ruby-on-rails - active_admin 目录中的常量警告重新声明 - 2

    我正在使用active_admin,我在Rails3应用程序的应用程序中有一个目录管理,其中包含模型和页面的声明。时不时地我也有一个类,当那个类有一个常量时,就像这样:classFooBAR="bar"end然后,我在每个必须在我的Rails应用程序中重新加载一些代码的请求中收到此警告:/Users/pupeno/helloworld/app/admin/billing.rb:12:warning:alreadyinitializedconstantBAR知道发生了什么以及如何避免这些警告吗? 最佳答案 在纯Ruby中:classA

  7. ruby - 触发器 ruby​​ 中 3 点范围运算符和 2 点范围运算符的区别 - 2

    请帮助我理解范围运算符...和..之间的区别,作为Ruby中使用的“触发器”。这是PragmaticProgrammersguidetoRuby中的一个示例:a=(11..20).collect{|i|(i%4==0)..(i%3==0)?i:nil}返回:[nil,12,nil,nil,nil,16,17,18,nil,20]还有:a=(11..20).collect{|i|(i%4==0)...(i%3==0)?i:nil}返回:[nil,12,13,14,15,16,17,18,nil,20] 最佳答案 触发器(又名f/f)是

  8. ruby - #之间? Cooper 的 *Beginning Ruby* 中的错误或异常 - 2

    在Cooper的书BeginningRuby中,第166页有一个我无法重现的示例。classSongincludeComparableattr_accessor:lengthdef(other)@lengthother.lengthenddefinitialize(song_name,length)@song_name=song_name@length=lengthendenda=Song.new('Rockaroundtheclock',143)b=Song.new('BohemianRhapsody',544)c=Song.new('MinuteWaltz',60)a.betwee

  9. ruby-on-rails - `a ||= b` 和 `a = b if a.nil 之间的区别? - 2

    我正在检查一个Rails项目。在ERubyHTML模板页面上,我看到了这样几行:我不明白为什么不这样写:在这种情况下,||=和ifnil?有什么区别? 最佳答案 在这种特殊情况下没有区别,但可能是出于习惯。每当我看到nil?被使用时,它几乎总是使用不当。在Ruby中,很少有东西在逻辑上是假的,只有文字false和nil是。这意味着像if(!x.nil?)这样的代码几乎总是更好地表示为if(x)除非期望x可能是文字false。我会将其切换为||=false,因为它具有相同的结果,但这在很大程度上取决于偏好。唯一的缺点是赋值会在每次运行

  10. ruby - 这两个 Ruby 类初始化定义有什么区别? - 2

    我正在阅读一本关于Ruby的书,作者在编写类初始化定义时使用的形式与他在本书前几节中使用的形式略有不同。它看起来像这样:classTicketattr_accessor:venue,:datedefinitialize(venue,date)self.venue=venueself.date=dateendend在本书的前几节中,它的定义如下:classTicketattr_accessor:venue,:datedefinitialize(venue,date)@venue=venue@date=dateendend在第一个示例中使用setter方法与在第二个示例中使用实例变量之间是

随机推荐