【问题标题】:How Does Calling Work In Python?调用在 Python 中是如何工作的?
【发布时间】:2012-05-28 12:08:29
【问题描述】:

对于我正在进行的一个项目,我正在实现一个链表数据结构,它基于对的概念,我将其定义为:

class Pair:
    def __init__(self, name, prefs, score):
        self.name = name
        self.score = score
        self.preferences = prefs
        self.next_pair = 0
        self.prev_pair = 0

其中self.next_pairself.prev_pair 分别是指向上一个和下一个链接的指针。

为了设置链表,我有一个如下所示的安装函数。

def install(i, pair):
    flag = 0
    try:
        old_pair = pair_array[i]
        while old_pair.next_pair != 0:
            if old_pair == pair:
                #if pair in remainders: remainders.remove(pair)
                return 0
            if old_pair.score < pair.score:
                flag = 1
                if old_pair.prev_pair == 0: # we are at the beginning
                    old_pair.prev_pair = pair
                    pair.next_pair = old_pair
                    pair_array[i] = pair
                    break
                else: # we are not at the beginning
                    pair.prev_pair = old_pair.prev_pair
                    pair.next_pair = old_pair
                    old_pair.prev_pair = pair
                    pair.prev_pair.next_pair = pair
                    break
            else:
                old_pair = old_pair.next_pair
        if flag==0:
            if old_pair == pair:
                #if pair in remainders: remainders.remove(pair)
                return 0
            if old_pair.score < pair.score:
                if old_pair.prev_pair==0:
                    old_pair.prev_pair = pair
                    pair.next_pair = old_pair
                    pair_array[i] = pair
                else:
                    pair.prev_pair = old_pair.prev_pair
                    pair.next_pair = old_pair
                    old_pair.prev_pair = pair
                    pair.prev_pair.next_pair = pair
            else:
                old_pair.next_pair = pair
                pair.prev_pair = old_pair
        except KeyError:
            pair_array[i] = pair
            pair.prev_pair = 0
            pair.next_pair = 0

在整个程序过程中,我正在为这些链接列表建立一个字典,并从一些链接中删除链接并将它们添加到其他链接中。在修剪和重新安装之间,链接存储在一个中间数组中。

在调试这个程序的过程中,我开始意识到我对 Python 将参数传递给函数的方式的理解是有缺陷的。考虑一下我写的这个测试用例:

def test_install():
    p = Pair(20000, [3, 1, 2, 50], 45)
    print p.next_pair
    print p.prev_pair
    parse_and_get(g)
    first_run()
    rat = len(juggler_array)/len(circuit_array)
    pref_size = get_pref_size()
    print pref_size
    print install(3, p)
    print p.next_pair.name
    print p.prev_pair             

当我运行这个测试时,我得到以下结果。

0
0
10
None
10108
0

我不明白为什么第二次调用 p.next_pair 会产生与第一次调用 (0) 不同的结果 (10108)。 install 不返回可以覆盖传入的 Pair 对象(它返回 None),也不是我在传递 install 指针。

我对按值调用的理解是解释器将传递的值复制到函数中,而调用者的变量保持不变。例如,如果我说

def foo(x):
     x = x+1
     return x

baz = 2
y = foo(baz)
print y
print baz

然后应该分别打印32。事实上,当我在 Python 解释器中测试它时,就会发生这种情况。

如果有人能在这里指出正确的方向,我将不胜感激。

【问题讨论】:

  • Python 没有按值调用。它也没有引用调用。
  • 仅供参考,您可以使用编辑框顶部的 {} 按钮将选定的代码块缩进 4 个空格 - 这是保持正确缩进的简单方法。
  • Pair 类看起来应该是 dict
  • 我确定这是一个班级作业,而链表的实现才是重点;如果这是针对除类分配之外的其他实用的东西,那么列表和其他集合的所有方式都有许多更实用的实现。

标签: python programming-languages


【解决方案1】:

在 Python 中,一切都是对象。简单赋值存储一个引用到被赋值对象中的被赋值对象。因此,将 Python 变量视为分配给对象的名称更直接,而不是存储在命名位置的对象。

例如:

baz = 2

... 在baz 中存储指向存储在别处的整数对象2 的指针或引用。 (由于 int 类型是不可变的,Python 实际上有一个小整数池,并且在任何地方都重用了相同的 2 对象,但这是一个实现细节,我们不需要过多关注。)

当你调用foo(baz)时,foo()的局部变量x一开始也指向整数对象2。也就是说,foo()-本地名称x 和全局名称baz 是同一个对象的名称2。然后执行x = x + 1。这会将x 更改为指向不同的对象:3

理解这一点很重要:x 不是一个包含2 的框,2 然后递增为3。不,x 最初指向 2,然后该指针更改为指向 3。当然,由于我们没有改变baz指向的对象,它仍然指向2

另一种解释方式是,在 Python 中,所有参数传递都是按值传递的,但所有值都是对对象的引用。

一个反直觉的结果是,如果一个对象是可变的,它可以通过任何引用进行修改,并且所有引用都会“看到”更改。例如,考虑一下:

baz = [1, 2, 3]

def foo(x):
   x[0] = x[0] + 1

foo(baz)
print baz
>>> [2, 2, 3]

似乎与我们的第一个示例非常不同。但实际上,参数以相同的方式传递。 foo() 在名称 x 下接收指向 baz 的指针,然后对其执行更改它的操作(在这种情况下,列表的第一个元素指向不同的 int 对象)。不同之处在于名称x 永远不会指向新对象; x[0] 被修改为指向不同的对象。 x 本身仍然指向与 baz 相同的对象。 (事实上​​,对x[0] 的赋值实际上变成了一个方法调用:x.__setitem__()。)因此baz“看到”了对列表的修改。怎么可能没有?

对于整数和字符串,您不会看到这种行为,因为您无法更改整数或字符串;它们是不可变的类型,当你修改它们时(例如x = x + 1),你实际上并没有修改它们,而是将你的变量名绑定到一个完全不同的对象。如果您将 baz 更改为元组,例如baz = (1, 2, 3),你会发现foo() 给你一个错误,因为你不能分配给一个元组的元素;元组是另一种不可变类型。 “更改”一个元组需要创建一个新元组,然后赋值将变量指向新对象。

您定义的类的对象是可变的,因此您的Pair 实例可以通过它传入的任何函数进行修改——也就是说,可以添加、删除或重新分配属性给其他对象。这些东西都不会重新绑定指向您的对象的任何名称,因此当前指向它的所有名称都将“看到”更改。

【讨论】:

  • .. 完整的答案是 superb。谢谢!
  • 优秀的答案。这是我见过的最好的解释。
【解决方案2】:

将变量传递给函数时,Python 不会复制任何内容。它既不是按值调用也不是按引用调用,但在这两者中,它更类似于按引用调用。您可以将其视为“按值调用,但值是引用”。

如果您将 可变 对象传递给函数,则在函数内部修改该对象将影响该对象出现的任何位置。 (如果您将 不可变 对象传递给函数,例如字符串或整数,那么根据定义,您根本无法修改该对象。)

这在技术上不是通过引用传递的原因是您可以重新绑定一个名称,以便该名称完全引用其他东西。 (对于不可变对象的名称,这是您唯一可以对它们执行的操作。)重新绑定仅存在于函数内部的名称不会影响可能存在于函数外部的任何名称。

在您使用 Pair 对象的第一个示例中,您正在修改一个对象,因此您会看到函数之外的效果。

在您的第二个示例中,您没有修改任何对象,您只是将名称重新绑定到其他对象(在这种情况下为其他整数)。 baz 是一个名称,指向一个值为 2 的整数对象(在 Python 中,一切都是对象,甚至是整数)。当您将 baz 传递给 foo(x) 时,名称 x 是在本地内部创建的堆栈上的foo 函数,并且x 设置为传递给函数的指针——与baz 相同的指针。但是xbaz 不是一回事,它们只包含指向同一个对象的指针。在x = x+1 行中,x 被重新绑定以指向一个值为 3 的整数对象,该指针是从函数返回并用于将整数对象绑定到 y 的指针。

如果您重写第一个示例以根据传递给它的 Pair 对象的信息在函数内显式创建一个新的 Pair 对象(无论这是您随后修改的副本,还是创建修改数据的构造函数在构造上),那么您的函数将不会产生修改传入对象的副作用。

编辑:顺便说一句,在 Python 中,您不应该使用 0 作为占位符来表示“我没有价值”——使用 None。同样,您不应该使用0 来表示False,就像您在flag 中所做的那样。但是所有的0NoneFalse 在布尔表达式中都计算为False,所以无论你使用哪一个,你都可以说if not flag而不是if flag == 0

【讨论】:

  • 感谢您的深思熟虑的回复,我想我对 Python 评估程序时发生的事情有了更好的了解。关于您的最终编辑,为什么使用 None 被认为更 Pythonic?仅仅是因为它更具可读性,还是使用 0 可能会导致错误?
  • 只是因为它更具可读性。使用 0 表示 None 没有明显的问题,除非它可能与有效值混淆(例如,如果您曾经在该位置存储一个整数或尝试用它做数学运算)。
【解决方案3】:

我建议您忘记实现链表,而只需使用 Python list 的实例。如果您需要默认 Python list 以外的其他内容,也许您可​​以使用 Python 模块中的内容,例如 collections

跟随链表中的链接的 Python 循环将以 Python 解释器的速度运行,也就是说,速度很慢。如果你简单地使用内置的list 类,你的列表操作将发生在 Python 的 C 代码中,你将获得速度。

如果您需要类似列表但具有快速插入和快速删除功能的东西,您可以使用dict 吗?如果有某种 ID 值(字符串或整数或其他)可用于对您的值进行排序,您可以将其用作键值并获得闪电般的快速插入和删除值。然后如果需要按顺序提取值,可以使用dict.keys()方法函数获取键值列表并使用。

但如果你真的需要链表,我建议你找别人编写和调试的代码,并根据你的需要进行调整。谷歌搜索“python 链表配方”或“python 链表模块”。

【讨论】:

    【解决方案4】:

    我要加入一个稍微复杂的因素:

    >>> def foo(x):
    ...   x *= 2
    ...   return x
    ... 
    

    使用我知道的数字、列表和字符串支持的方法定义一个稍微不同的函数。

    首先,用字符串调用它:

    >>> baz = "hello"
    >>> y = foo(baz)
    >>> y
    'hellohello'
    >>> baz
    'hello'
    

    接下来,用列表调用它:

    >>> baz=[1,2,2]
    >>> y = foo(baz)
    >>> y
    [1, 2, 2, 1, 2, 2]
    >>> baz
    [1, 2, 2, 1, 2, 2]
    >>> 
    

    对于字符串,参数不会被修改。对于列表,参数会被修改。

    如果是我,我会避免在方法中修改参数。

    【讨论】:

    • 好吧,避免在可能是可变类型的参数上使用就地运算符,除非您打算改变它们。
    • @Kindall:这更加优雅和精确。不错。
    猜你喜欢
    • 2011-11-08
    • 1970-01-01
    • 2012-04-01
    • 2020-12-27
    • 1970-01-01
    • 1970-01-01
    • 2023-03-04
    • 2014-03-15
    相关资源
    最近更新 更多