【问题标题】:How can this be called Pass By Reference?这怎么能称为通过引用?
【发布时间】:2016-08-19 13:49:20
【问题描述】:

根据this 教程,Python 使用“通过引用传递”。

然后他们继续给出以下示例。这个“通过引用”在哪个星球上?对我来说,这似乎是一个明确的“按价值传递”案例。

想法?

def changeme( mylist ):
   mylist = [1,2,3,4];
   print "Values inside the function: ", mylist
   return

mylist = [10,20,30];
changeme( mylist );
print "Values outside the function: ", mylist

参数 mylist 是函数 changeme 的本地参数。在函数中更改 mylist 不会影响 mylist。该函数什么也没做,最后会产生以下结果:

# Values inside the function:  [1, 2, 3, 4]
# Values outside the function:  [10, 20, 30]

更新,结论:

  1. 根据下面viraptor 的回答,“根据文档,python 是按值传递的”。所以教程显然是错误的。

  2. Python 不是“引用调用”,而是“共享调用”。这意味着可以将引用传递给本地子例程,由子例程更新,并且更新保持在子例程的本地,不会自动影响引用的全局值。

【问题讨论】:

  • 如果您在函数内对mylist 进行就地操作,则更改会在函数外持续存在。赋值语句不是对现有对象的就地操作,而是创建一个新对象。
  • mylist = [1,2,3,4] 将本地 mylist 变量重新绑定到 [1,2,3,4] 对象。这将删除对其先前值的引用。
  • @tdelaney:OP 知道这一点,这就是提出问题的原因。在真正的传递引用中,原始变量绑定也会被重新绑定。
  • 谢谢,OP。我会将它添加到我永远不会推荐的 Python 教程列表中。

标签: python pass-by-reference terminology pass-by-value


【解决方案1】:

两者都不是。它是call by sharing。我也听说过使用“按参考值传递”这个术语。

也称为“对象调用”或“对象共享调用”,共享调用是一种评估策略,最早由 Barbara Liskov 等人提出。用于 1974 年的 CLU 语言。它被 Python、Iota、Java(用于对象引用)、Ruby、JavaScript、Scheme、OCaml、AppleScript 等语言使用。但是,“共享呼叫”一词并不常用。不同来源的术语不一致。例如,在 Java 社区中,他们说 Java 是按值调用的,而在 Ruby 社区中,他们说 Ruby 是按引用调用的,尽管这两种语言表现出相同的语义。共享调用意味着语言中的值基于对象而不是原始类型,即所有值都是“装箱”的。

共享调用的语义不同于引用调用,因为调用者看不到函数内函数参数的赋值(与引用语义不同),例如如果传递了一个变量,则无法在调用者的范围内模拟对该变量的赋值。但是,由于该函数可以访问与调用者相同的对象(不进行复制),因此如果对象是可变的,则对这些对象的突变对调用者是可见的,这可能看起来与按值调用不同语义。函数中可变对象的突变对调用者是可见的,因为该对象没有被复制或克隆——它是共享的。

【讨论】:

  • 其实不知道这叫什么,以为只是一种传递引用,为此大加1!
  • @Serdalis:几乎没有人知道这叫什么,这就是我们一开始就陷入困境的原因。 :P 大多数课程和教程仍然坚持二元区分,因此人们觉得有必要指定这两个标签之一,即使两者都不适合。
  • 这些术语太混乱了......即使在像C 这样非常成熟的语言中,你也会听到人们谈论通过引用传递和通过值传递,而事实上,C 总是按值传递。 “按引用传递”是通过按值传递指针(引用)来模拟的......
  • “两者都不是。”它是按值传递的。没有人质疑 Python 中传递和赋值的语义在 Java 中完全相同,在本网站和 Internet 上的其他地方总是将​​其描述为按值传递。
  • 确实应该叫传值。这是更标准的术语,请参阅下面的答案
【解决方案2】:

它是按值传递的,其中所有值都是指向对象的指针。您可能认为这意味着您可以使用传入的指针来更改调用者的变量,使其成为按引用传递,但您不能,所以不是。

理解 Python 值传递如何工作的关键是知道没有“未装箱”(非对象)值。整数是对象,“包含”整数的变量实际上是指向存储在变量以外的某个地方的整数对象的指针。浮点数,布尔值,同上。

变量在 Python 中并不像在 C 中那样真正“保存”值。因此赋值总是包括使变量名指向不同的对象。

如果传递给函数的对象是可变的,则函数可以更改它,但这必须完全在不更改其名称指向的对象的情况下完成。例如:

some_digits_of_pi = [3, 1, 4, 1, 5, 9, 2, 7]

def extend_pi(x):
    x[-1] = 6
    x += [5, 3, 5, 9]

这里我们在函数内部改变x。 (对于一个列表,+= 本质上是list.extend。)由于x 从未更改为指向不同的对象,因此对传入的列表进行更改。名称some_digits_of_pi 指的是相同的对象在函数中被修改,因此调用者将看到他们的名称列表已更改。

如果我们在函数末尾写x = [2, 7, 1, 8, 2, 8, 1, 8],那将创建一个新的列表对象并将本地名称x 指向它。它不会改变调用者的变量指向的内容,因此该语句不会改变该列表。

换句话说,您不能使 调用者的 变量(在这种情况下为some_digits_of_pi)指向不同的对象。如果您将x 更改为指向函数内的不同对象,则只有x 指向该对象。

数字、字符串、元组等的工作方式完全相同。传入对象的指针;如果你改变函数内部参数的值,它会指向不同的对象,这自然不会改变调用者的变量。它只是似乎不同,因为这些类型的对象不是可变的,并且没有任何方法可以就地更改它们。

另一个令人困惑的地方是它看起来ints 和lists 都有+= 运算符,但碰巧+=int 上做了一些事情与list 上的相同操作非常不同。在列表上,+= 返回相同的列表对象(修改后),而在整数上,它可能返回完全不同的对象(因为整数是不可变的)。

【讨论】:

  • 我喜欢这个解释,但是:“换句话说,你不能让调用者的变量指向不同的对象。” - 这就是传递引用的意思。我不认为“或者它是按引用传递,但您不能使用引用来更改调用者的变量。”说得通。我们已经给它起了一个名字——它是按值传递的,你的参数是一个指针/引用。
  • 改写了一些介绍。
  • 是的,这是正确的。为了更加清晰,我在答案中对其进行了扩展。我认为这应该是公认的答案!!!
【解决方案3】:

通话的命名因您与谁通话而异。绝对不是一件事,就是通过引用传递。

无论是技术上还是according to the docs,python 都是按值传递的。

[...] 否则,将参数的值放入槽中,填充它(即使表达式为 None,它也会填充槽)。处理完所有参数后,仍未填充的插槽将使用函数定义中的相应默认值填充。 [...]

现在,仅仅因为这些值实际上是引用,就会引起人们争论约定并定义新名称以从更实际的角度描述正在发生的事情。值引用、共享、句柄等调用都是人们可以使用的不同名称。

【讨论】:

  • 从技术上讲,如果你告诉我 Python 是按值传递的,我会期望一个参数被深度克隆,就像 C 中的结构一样。说“它是按值传递,但是value is a reference”比使用新名称时让学生更不清楚。这就像说“从技术上讲,大豆汉堡是肉,只是不是来自动物”。
  • 虽然结构是值类型。 Python 中不存在类似的东西,所以你不能在这里进行有效的比较。 (结构也只是复制,而不是深度克隆)这就是我所说的实用和技术描述。 Python 通过值传递,无论对学生来说是清晰还是混乱。如果其他名称更清楚,那就太好了。但仍然值得记住 pass-by-value 的实际含义。 (不,大豆汉堡在技术上不是肉,因为大豆中没有肉;技术上的意思是“基于精确的事实”,而不是“这样描述它很有趣”)
  • @Amadan:“我原以为参数会被深度克隆,就像 C 中的结构一样。”你在比较苹果和橘子。在 Python 中,所有值(包括变量和参数)都是指针;并且语义与所有变量都是 C 中的指针类型完全相同。仅仅因为类型没有在 Python 中写出并不会改变这一点。您说 C 中的结构类型,但在 Python 中不存在;仅仅因为 Python 缺少某种类型的类型并不会改变它的传递语义。
【解决方案4】:

我从来不知道它的名字,但根据 Amadan 的说法,它是“通过共享调用”。为了便于理解,我打个比方。

假设我有一只狗,我给它起名叫“布鲁图斯”。我带布鲁图斯去散步,遇到了我的邻居山姆。 Sam 不喜欢我选择的名字,并称我的狗为“Dodo”。如果他将一把刀插入 Dodo,它当然会影响 Brutus,因为它们是同一只狗。假设他没有,而另一个邻居比利正带着他的狗出去散步。山姆改变了主意,认定比利的狗是渡渡鸟。如果他将刀子放入 Dodo,它不会影响 Brutus,因为它们是不同的狗。

您的功能类似。 mylist 走到你的函数面前,你的函数决定叫他mylist,同名。然后,您的函数决定 [1,2,3,4]mylist。您现在已经丢弃了原始列表并定义了一个新列表。您的重新分配不会影响原始列表。

【讨论】:

  • 我喜欢这个。这很奇怪,但很有帮助。
【解决方案5】:

mylist = [1,2,3,4] 是在changeme() 内部创建的,mylist 是一个局部变量,它不是你传入的参数,即使它与参数同名。不引用参数mylist

【讨论】:

    【解决方案6】:

    根据@Amadan 的回答,Python 使用Call by Sharing

    在函数中,您已将引用重新分配给新列表,因此它不再指向您传入的列表。

    您可以通过以下代码看到这种情况:

    def changeme( mylist ):
       print locals()  # will show mylist value in function.
       mylist = [1,2,3,4];
       print locals()  # will show mylist value in function.
       print globals()['mylist'] # will show global mylist value
       print "Values inside the function: ", mylist
       return
    

    你会在locals列表中看到赋值后mylist的值发生变化,但是全局引用还是一样的。

    【讨论】:

      【解决方案7】:

      就我而言,Python 就像 Java 一样,事实上所有函数(方法)都是按值传递的。见this infamous answer。 “共享调用”的缺点是它让您认为不同类型正在发生不同的事情(即可变类型在函数调用中的行为与不可变类型不同)。事实并非如此,每次调用函数时都会发生同样的事情。另外,我读过的任何教科书都没有提到这个概念,我认为 pass-by-value 和 pass-by-reference 是更流行的术语。

      按值传递如何工作?

      它通过在函数中修改它之前复制传递给函数的“值”来工作。所以,举个例子:

      my_int = 4
      def pass_by_value(value):
          value += 1
          return value
      print pass_by_value(my_int)
      print my_int
      

      这个输出:

      5
      4
      

      观察my_int 的值没有改变。即使函数将传入的值递增为5,它仍保持在4。这是因为它被复制了。这里有几个微妙之处,我们将返回。忍受我。

      按值传递列表

      my_list = [1,2,3]
      def pass_by_value_list(li):
          li.append(4)
          return li
      print pass_by_value_list(my_list)
      print my_list
      

      这个输出:

      [1, 2, 3, 4]
      [1, 2, 3, 4]
      

      什么鬼。 my_list 的值已更改。您刚才说过,按值传递会复制值!因此,my_list 不应该改变!!!

      第一个例子太微妙了。我未能正确定义正在复制的“价值”。事实证明,被复制的值不是数据,而是数据的存储位置。通常 C/C++ 程序员将此称为 指针地址。让我们重温一下这些示例,但稍作修改以明确重点。

      按值传递如何工作? 2.0 版:

      my_int = 4
      def pass_by_value(value):
          print "Address of parameter before += is: ", id(value)
          value += 1
          print "Address of parameter after += is: ", id(value)
          return value
      print "Address of parameter outside of the function is: ", id(my_int)
      pass_by_value(my_int)
      

      当我运行这个时,我的输出是:

      Address of parameter outside of the function is:  40592528
      Address of parameter before += is:  40592528
      Address of parameter after += is:  40592504
      

      貌似+=运算符之前的参数地址是40592528。这也是函数外参数的“值”!但是在+= 运算符之后,“值”在函数内部 发生了变化!但是,地址的更改并未传播到外部函数,因为地址按值传递。函数内部的新地址是 40592504(与 40592528 不同,虽然很接近)。长话短说,+= 运算符创建了一个新的int,并且不对传入函数的地址数据进行操作。

      按值传递列表,版本 2.0:

      my_list = [1,2,3]
      def pass_by_value_list(li):
          print "Address of parameter before .append() is: ", id(li)
          li.append(4)
          print "Address of parameter after .append() is: ", id(li)
          return li
      print "Address of parameter outside of the function is: ", id(my_list)
      pass_by_value_list(my_list)
      

      这个输出:

      Address of parameter outside of the function is:  110841160
      Address of parameter before .append() is:  110841160
      Address of parameter after .append() is:  110841160
      

      嘿,这与整数大小写不同。看起来所有地址都是一样的!确实,这与整数情况不同。 append 函数对一个列表进行操作,并且不返回一个新列表。这就是为什么您会在函数之外看到对 my_list 的更改。 append 修改了传入函数的地址处的列表,因此我们可以看到整个程序中的数据都发生了变化。不过,没有改变的是地址。

      注意:

      my_list = [1,2,3]
      def dont_change_the_list(li):
          li.append(4)
          li = []
          return li
      print dont_change_the_list(my_list)
      print my_list
      

      输出

      []
      [1, 2, 3, 4]
      

      换句话说,对函数内部列表的更改并没有传播到函数外部,尽管这似乎是我们之前看到的行为。这是因为li = []语句改变了li的地址,但是我们在函数执行之前复制了地址。 参数的地址是被复制的“值”,不能在函数内部改变。所以,这并没有改变主程序中my_list中的数据。

      要点:Python 总是在其函数调用中使用按值传递的语义。该值恰好是传入的对象的地址,而不是传入的对象的数据。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2011-06-21
        • 1970-01-01
        • 2013-09-01
        • 1970-01-01
        • 1970-01-01
        • 2022-01-20
        • 2022-01-05
        • 2013-02-20
        相关资源
        最近更新 更多