【问题标题】:Python: create a function to modify a list by reference not valuePython:创建一个函数来通过引用而不是值来修改列表
【发布时间】:2010-05-05 01:26:28
【问题描述】:

我正在做一些对性能至关重要的 Python 工作,并希望创建一个函数,如果它们满足某些条件,则从列表中删除一些元素。我宁愿不创建列表的任何副本,因为它充满了很多非常大的对象。

我要实现的功能:

def listCleanup(listOfElements):
    i = 0
    for element in listOfElements:
        if(element.meetsCriteria()):
            del(listOfElements[i])
        i += 1
    return listOfElements

myList = range(10000)
myList = listCleanup(listOfElements)

我不熟悉 Python 的低级工作。 myList 是按值传递还是按引用传递?

我怎样才能加快速度?

是否有可能以某种方式扩展列表类并在其中实现 listCleanup()?

myList = range(10000)
myList.listCleanup()

谢谢-

乔纳森

【问题讨论】:

  • 我想你会发现这种思路麻烦多多。只需复制列表,修改它,然后返回修改后的副本。在迭代时修改列表只是让人头疼。
  • del 是一个语句,而不是一个函数。不要将它的参数括在括号中。
  • “列表中”对象的大小无关紧要,因为 Python 没有将对象存储在列表中;它存储对对象的引用。因此,性能问题与列表的长度和用于对列表进行操作的算法有关,而不是与引用的对象的大小有关。

标签: python list


【解决方案1】:

Python 以相同的方式传递所有内容,但称其为“按值”或“按引用”并不能明确所有内容,因为 Python 的语义与这些术语通常适用的语言不同。如果我要描述它,我会说所有传递都是按值传递的,并且该值是对象引用。 (这就是我不想说的原因!)

如果你想从列表中过滤掉一些东西,你可以建立一个新列表

foo = range(100000)
new_foo = []
for item in foo:
    if item % 3 != 0: # Things divisble by 3 don't get through
        new_foo.append(item)

或者,使用列表理解语法

 new_foo = [item for item in foo if item % 3 != 0]

Python 不会复制列表中的对象,而是foonew_foo 都将引用相同的对象。 (Python 从不隐式复制任何对象。)


您曾建议您对此操作存在性能问题。使用旧列表中的重复 del 语句将导致代码不那么惯用且更难以处理,但它会引入二次性能,因为每次都必须重新洗牌整个列表。

解决性能问题:

  • 启动并运行它。除非你有代码工作,否则你无法弄清楚你的性能如何。这还将告诉您必须优化的是速度还是空间;您在代码中提到了对两者的担忧,但优化通常涉及以牺牲另一个为代价来获得一个。

  • 个人资料。您可以使用the stdlib tools 及时执行。有各种第三方内存分析器可能有些用处,但使用起来不太好。

  • 测量。Time 或在您进行更改时重新配置内存以查看更改是否会带来改进,如果是,那么改进是什么。

  • 为了使您的代码对内存更加敏感,您通常需要在存储数据的方式上进行范式转变,而不是像不构建第二个列表来进行过滤这样的微优化。 (时间也是如此,真的:改用更好的算法几乎总是能提供最好的加速。但是,很难概括速度优化)。

    在 Python 中优化内存消耗的一些常见范式转换包括

    1. 使用生成器。生成器是惰性的可迭代对象:它们不会一次将整个列表加载到内存中,它们会即时确定下一个项目是什么。要使用生成器,上面的 sn-ps 看起来像

      foo = xrange(100000) # Like generators, xrange is lazy
      def filter_divisible_by_three(iterable):
          for item in foo:
              if item % 3 != 0:
                  yield item
      
      new_foo = filter_divisible_by_three(foo)
      

      或者,使用生成器表达式语法,

      new_foo = (item for item in foo if item % 3 != 0)
      
    2. numpy 用于同质序列,尤其是数值数学的序列。这也可以加快执行大量向量操作的代码。

    3. 将数据存储到磁盘,例如数据库中。

【讨论】:

  • pb[r]v (pass-by-[reference]-value) 实际上可以应用于很多很多语言,包括(但不限于)Ruby、Java 和 C#。 (根据传递的“类型”,每个细节/机制都略有不同)。但是,我更喜欢说“一个对象就是它自己”和“一个对象在调用函数时不会被隐式复制/克隆/复制”(对于不可变/val 类型,即使这是一个谎言,语义也相似)在讨论 pb[r]v 语义时。它在大多数命令式语言中都非常一致,允许在处理高级语言时“查看过去”引用/指针。
  • 使用 python timeit 进行分析可能是一个好主意。
  • 这些术语当然可以适用于非常广泛的语言。根据我的经验,当有人听说 Python 是其中之一时,他们认为这包含了一些不真实的东西。将 Python 归类为“按值传递”通常会使人们认为他们可以依靠有关 Python 语义的额外信息来走捷径,但这是不正确的。
  • @kriss,timeit 并不真正适合 profiling,但它适用于对您发现的 profiling 进行微优化的时间。我在我的帖子中链接到 timeit 文档,虽然很难看到,因为链接在 SO 上并没有真正突出。
  • """整个列表每次都必须重新洗牌""" (1)“洗牌”是模棱两可的(它不像一副纸牌那样洗牌)(2)不管你怎么称呼它,之后del alist[i],操作是将 alist[i+1:] 指针向下移动一个槽;它不涉及整个列表。
【解决方案2】:

在 Python 中,列表总是通过引用传递。

列表中对象的大小不会影响列表的性能,因为列表只存储对对象的引用。但是,列表中的项目数确实会影响某些操作的性能——比如删除一个元素,它是 O(n)。

正如所写,listCleanup 是最坏情况 O(n**2),因为您在一个可能是 O(n) 本身的循环内有 O(n) del 操作。

如果元素的顺序无关紧要,您可以使用内置的set 类型而不是列表。 set 有 O(1) 次删除和插入。但是,您必须确保您的对象是不可变的和可散列的。

否则,您最好重新创建列表。那是 O(n),您的算法至少需要 O(n),因为您需要检查每个元素。您可以像这样在一行中过滤列表:

listOfElements[:] = [el for el in listOfElements if el.MeetsCriteria()]

【讨论】:

  • list 切换到set 可能不是尝试节省内存的好方法。 ;)
  • 据我所知,与 listOfElements = [el for el in listOfElements if el.MeetsCriteria()] 等更标准的 Python 技术相比,您的代码 sn-p 并没有真正做任何节省内存或其他方面的好处。
  • 我真的看不出我们知道这里需要切片分配的任何原因。有时必须改变原始列表,但这不是典型情况。
【解决方案3】:

看起来像过早的优化。在尝试优化之前,您应该尝试更好地了解 python 的工作原理。

在这种特殊情况下,您无需担心对象大小。复制列表是使用列表推导或切片将仅执行表面复制(复制对对象的引用,即使该术语并不真正适用于 python)。但是列表中的项目数可能很重要,因为 del 是 O(n)。可能还有其他解决方案,例如用 None 或传统的 Null 对象替换项目,或者使用其他数据结构(如集合或字典)​​删除项目的成本要低得多。

【讨论】:

    【解决方案4】:

    我认为没有人提到实际使用过滤器。由于很多答案都来自受人尊敬的人,我确信我是那个缺少某些东西的人。有人可以解释一下这会有什么问题:

    new_list = filter(lambda o: o.meetsCriteria(), myList)

    【讨论】:

      【解决方案5】:

      在迭代过程中修改数据结构就像是在踢自己的脚……迭代失败。你不妨听取别人的建议,然后重新列一个清单:

      myList = [element for element in listOfElements if not element.meetsCriteria()]
      

      旧列表——如果没有其他对它的引用——将被释放并回收内存。更好的是,甚至不要复制列表。将上面的内容更改为 生成器表达式 以获得更内存友好的版本:

      myList = (element for element in listOfElements if not element.meetsCriteria())
      

      所有 Python 对象访问都是通过引用进行的。创建对象,变量只是对这些对象的引用。但是,如果有人想问一个纯粹的问题,“Python 使用什么类型的调用语义,引用调用还是值调用?”答案必须是,“两者都不是……两者都不是。”原因是调用约定对 Python 来说不如对象类型重要。

      如果一个对象是可变的,无论你在什么范围内,它都可以被修改……只要你有一个有效的对象引用,这个对象就可以被改变。如果对象是不可变的,那么无论您身在何处或拥有什么引用,都无法更改该对象。

      【讨论】:

        【解决方案6】:

        原位删除列表元素是可能的,但不能通过向前遍历列表。您的简单代码不起作用——随着列表的缩小,您可能会错过检查元素。你需要倒退,这样缩小的部分就在你身后,代码相当可怕。在我向您展示之前,有一些初步的考虑:

        首先,这些垃圾是如何进入列表的?预防胜于治疗。

        其次,列表中有多少元素,有多少百分比可能需要删除?百分比越高,创建新列表的可能性就越大。

        好的,如果你还想在原地做,考虑一下:

        def list_cleanup_fail(alist, is_bad):
            i = 0
            for element in alist:
                print "i=%d alist=%r alist[i]=%d element=%d" % (i, alist, alist[i], element)
                if is_bad(element):
                    del alist[i]
                i += 1
        
        def list_cleanup_ok(alist, is_bad):
            for i in xrange(len(alist) - 1, -1, -1):
                print "i=%d alist=%r alist[i]=%d" % (i, alist, alist[i])
                if is_bad(alist[i]):
                    del alist[i]
        
        def is_not_mult_of_3(x):
            return x % 3 != 0
        
        for func in (list_cleanup_fail, list_cleanup_ok):
            print
            print func.__name__
            mylist = range(11)
            func(mylist, is_not_mult_of_3)
            print "result", mylist
        

        这是输出:

        list_cleanup_fail
        i=0 alist=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] alist[i]=0 element=0
        i=1 alist=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] alist[i]=1 element=1
        i=2 alist=[0, 2, 3, 4, 5, 6, 7, 8, 9, 10] alist[i]=3 element=3
        i=3 alist=[0, 2, 3, 4, 5, 6, 7, 8, 9, 10] alist[i]=4 element=4
        i=4 alist=[0, 2, 3, 5, 6, 7, 8, 9, 10] alist[i]=6 element=6
        i=5 alist=[0, 2, 3, 5, 6, 7, 8, 9, 10] alist[i]=7 element=7
        i=6 alist=[0, 2, 3, 5, 6, 8, 9, 10] alist[i]=9 element=9
        i=7 alist=[0, 2, 3, 5, 6, 8, 9, 10] alist[i]=10 element=10
        result [0, 2, 3, 5, 6, 8, 9]
        
        list_cleanup_ok
        i=10 alist=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] alist[i]=10
        i=9 alist=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] alist[i]=9
        i=8 alist=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] alist[i]=8
        i=7 alist=[0, 1, 2, 3, 4, 5, 6, 7, 9] alist[i]=7
        i=6 alist=[0, 1, 2, 3, 4, 5, 6, 9] alist[i]=6
        i=5 alist=[0, 1, 2, 3, 4, 5, 6, 9] alist[i]=5
        i=4 alist=[0, 1, 2, 3, 4, 6, 9] alist[i]=4
        i=3 alist=[0, 1, 2, 3, 6, 9] alist[i]=3
        i=2 alist=[0, 1, 2, 3, 6, 9] alist[i]=2
        i=1 alist=[0, 1, 3, 6, 9] alist[i]=1
        i=0 alist=[0, 3, 6, 9] alist[i]=0
        result [0, 3, 6, 9]
        

        【讨论】:

          【解决方案7】:

          要明确一点:

          def listCleanup(listOfElements):
              i = 0
              for element in listOfElements:
                  if(element.meetsCriteria()):
                      del(listOfElements[i])
                  i += 1
              return listOfElements
          
          myList = range(10000)
          myList = listCleanup(listOfElements)
          

          相同
          def listCleanup(listOfElements):
              i = 0
              for element in listOfElements:
                  if(element.meetsCriteria()):
                      del(listOfElements[i])
                  i += 1
          
          myList = range(10000)
          listCleanup(listOfElements)
          

          ?

          【讨论】:

          • @Daniel:是的,如果你纠正错字,最后一行应该是listCleanup(myList)
          • 请记住,Python 中的每个“名称”都只是一个参考。除非您明确复制可变对象,否则它们会被就地修改。
          猜你喜欢
          • 2016-06-09
          • 1970-01-01
          • 2023-02-09
          • 2014-11-02
          • 1970-01-01
          • 2017-12-16
          • 1970-01-01
          • 2016-03-26
          • 2020-01-15
          相关资源
          最近更新 更多