Pourquoi une fonction peut-elle modifier certains arguments perçus par l'appelant, mais pas d'autres?

182

J'essaie de comprendre l'approche de Python à la portée variable. Dans cet exemple, pourquoi est-il f()capable de modifier la valeur de x, telle qu'elle est perçue à l'intérieur main(), mais pas la valeur de n?

def f(n, x):
    n = 2
    x.append(4)
    print('In f():', n, x)

def main():
    n = 1
    x = [0,1,2,3]
    print('Before:', n, x)
    f(n, x)
    print('After: ', n, x)

main()

Production:

Before: 1 [0, 1, 2, 3]
In f(): 2 [0, 1, 2, 3, 4]
After:  1 [0, 1, 2, 3, 4]
FMc
la source
7
bien expliqué ici nedbatchelder.com/text/names.html
Roushan

Réponses:

212

Certaines réponses contiennent le mot «copie» dans le contexte d'un appel de fonction. Je trouve cela déroutant.

Python ne copie pas les objets que vous passez au cours d' un appel de fonction jamais .

Les paramètres de fonction sont des noms . Lorsque vous appelez une fonction, Python lie ces paramètres à tous les objets que vous passez (via des noms dans la portée d'un appelant).

Les objets peuvent être mutables (comme les listes) ou immuables (comme les entiers, les chaînes en Python). Objet mutable que vous pouvez modifier. Vous ne pouvez pas changer un nom, vous pouvez simplement le lier à un autre objet.

Votre exemple ne concerne pas les étendues ou les espaces de noms , il concerne la dénomination, la liaison et la mutabilité d'un objet en Python.

def f(n, x): # these `n`, `x` have nothing to do with `n` and `x` from main()
    n = 2    # put `n` label on `2` balloon
    x.append(4) # call `append` method of whatever object `x` is referring to.
    print('In f():', n, x)
    x = []   # put `x` label on `[]` ballon
    # x = [] has no effect on the original list that is passed into the function

Voici de belles images sur la différence entre les variables dans d'autres langages et les noms en Python .

jfs
la source
3
Cet article m'a aidé à mieux comprendre le problème et il suggère une solution de contournement et des utilisations avancées: Valeurs des paramètres par défaut en Python
Gfy
@Gfy, j'ai déjà vu des exemples similaires, mais pour moi, cela ne décrit pas une situation réelle. Si vous modifiez quelque chose qui est passé, il n'est pas logique de lui donner une valeur par défaut.
Mark Ransom
@MarkRansom, je pense qu'il ne sens faire si vous voulez fournir destination de sortie en option comme: def foo(x, l=None): l=l or []; l.append(x**2); return l[-1].
Janusz Lenar
Pour la dernière ligne du code de Sebastian, il est dit "# ce qui précède n'a aucun effet sur la liste d'origine". Mais à mon avis, cela n'a aucun effet sur "n", mais a changé le "x" dans la fonction main (). Ai-je raison?
user17670
1
@ user17670: x = []in f()n'a aucun effet sur la liste xdans la fonction principale. J'ai mis à jour le commentaire pour le rendre plus précis.
jfs
15

Vous avez déjà un certain nombre de réponses, et je suis globalement d'accord avec JF Sebastian, mais vous pourriez trouver cela utile comme raccourci:

Chaque fois que vous voyez varname =, vous créez une nouvelle liaison de nom dans la portée de la fonction. Quelle que soit la valeur à laquelle on varnameétait lié auparavant, elle est perdue dans cette portée .

Chaque fois que vous voyez varname.foo()que vous appelez une méthode varname. La méthode peut modifier varname (par exemple list.append). varname(ou, plutôt, l'objet qui varnamenomme) peut exister dans plusieurs portées, et comme il s'agit du même objet, toutes les modifications seront visibles dans toutes les portées.

[notez que le globalmot - clé crée une exception au premier cas]

John Fouhy
la source
13

fne modifie pas réellement la valeur de x(qui est toujours la même référence à une instance d'une liste). Au contraire, cela modifie le contenu de cette liste.

Dans les deux cas, une copie d'une référence est transmise à la fonction. À l'intérieur de la fonction,

  • nse voit attribuer une nouvelle valeur. Seule la référence à l'intérieur de la fonction est modifiée, pas celle à l'extérieur.
  • xne reçoit pas de nouvelle valeur: ni la référence à l'intérieur ni à l'extérieur de la fonction n'est modifiée. Au lieu de cela, xla valeur de est modifiée.

Étant donné que l' xintérieur et l'extérieur de la fonction font référence à la même valeur, les deux voient la modification. En revanche, l' nintérieur et l'extérieur de la fonction font référence à des valeurs différentes après avoir nété réaffecté à l'intérieur de la fonction.

Konrad Rudolph
la source
8
«copie» est trompeur. Python n'a pas de variables comme C. Tous les noms en Python sont des références. Vous ne pouvez pas modifier le nom, vous pouvez simplement le lier à un autre objet, c'est tout. Il est logique de parler d' objet mutable et immuable en Python, non ce sont des noms.
jfs
1
@JF Sebastian: Votre déclaration est au mieux trompeuse. Il n'est pas utile de considérer les nombres comme des références.
Pitarou
9
@dysfunctor: les nombres sont des références à des objets immuables. Si vous préférez penser à eux d'une autre manière, vous avez un tas de cas spéciaux étranges à expliquer. Si vous pensez qu'ils sont immuables, il n'y a pas de cas particuliers.
S.Lott
@ S.Lott: Indépendamment de ce qui se passe sous le capot, Guido van Rossum a déployé beaucoup d'efforts dans la conception de Python afin que le programmeur puisse considérer les nombres comme de simples ... nombres.
Pitarou le
1
@JF, la référence est copiée.
habnabit le
7

Je renommerai les variables pour réduire la confusion. n -> nf ou nmain . x -> xf ou xmain :

def f(nf, xf):
    nf = 2
    xf.append(4)
    print 'In f():', nf, xf

def main():
    nmain = 1
    xmain = [0,1,2,3]
    print 'Before:', nmain, xmain
    f(nmain, xmain)
    print 'After: ', nmain, xmain

main()

Lorsque vous appelez la fonction f , le runtime Python fait une copie de xmain et l'affecte à xf , et affecte de la même manière une copie de nmain à nf .

Dans le cas de n , la valeur copiée est 1.

Dans le cas de x, la valeur copiée n'est pas la liste littérale [0, 1, 2, 3] . C'est une référence à cette liste. xf et xmain pointent sur la même liste, donc lorsque vous modifiez xf, vous modifiez également xmain .

Si, cependant, vous deviez écrire quelque chose comme:

    xf = ["foo", "bar"]
    xf.append(4)

vous constaterez que xmain n'a pas changé. C'est parce que, dans la ligne xf = ["foo", "bar"], vous avez changé xf pour pointer vers une nouvelle liste. Toutes les modifications que vous apportez à cette nouvelle liste n'auront aucun effet sur la liste vers laquelle pointe toujours xmain .

J'espère que cela pourra aider. :-)

Pitarou
la source
2
"Dans le cas de n, la valeur qui est copiée ..." - C'est faux, il n'y a pas de copie ici (sauf si vous comptez les références). Au lieu de cela, python utilise des «noms» qui pointent vers les objets réels. nf et xf pointent vers nmain et xmain, jusqu'à nf = 2, où le nom nfest modifié pour pointer vers 2. Les nombres sont immuables, les listes sont modifiables.
Casey Kuball
2

C'est parce qu'une liste est un objet mutable. Vous ne définissez pas x sur la valeur de [0,1,2,3], vous définissez une étiquette pour l'objet [0,1,2,3].

Vous devez déclarer votre fonction f () comme ceci:

def f(n, x=None):
    if x is None:
        x = []
    ...
Luiz Damim
la source
3
Cela n'a rien à voir avec la mutabilité. Si vous le faites à la x = x + [4]place de x.append(4), vous ne verrez pas non plus de changement dans l'appelant, bien qu'une liste soit modifiable. Il s'agit de savoir s'il est effectivement muté.
glglgl
1
OTOH, si vous faites x += [4]alors xest muté, tout comme ce qui se passe avec x.append(4), donc l'appelant verra le changement.
PM 2 Ring
2

n est un int (immuable), et une copie est passée à la fonction, donc dans la fonction vous modifiez la copie.

X est une liste (mutable), et une copie du pointeur est passée à la fonction donc x.append (4) change le contenu de la liste. Cependant, vous avez dit x = [0,1,2,3,4] dans votre fonction, vous ne changeriez pas le contenu de x dans main ().

Jason Coon
la source
3
Regardez le phrasé "copie du pointeur". Les deux endroits obtiennent des références aux objets. n est une référence à un objet immuable; x est une référence à un objet mutable.
S.Lott
2

Si les fonctions sont réécrites avec des variables complètement différentes et que nous appelons id sur elles, cela illustre bien le point. Je ne l'ai pas compris au début et j'ai lu le message de jfs avec la bonne explication , alors j'ai essayé de comprendre / me convaincre:

def f(y, z):
    y = 2
    z.append(4)
    print ('In f():             ', id(y), id(z))

def main():
    n = 1
    x = [0,1,2,3]
    print ('Before in main:', n, x,id(n),id(x))
    f(n, x)
    print ('After in main:', n, x,id(n),id(x))

main()
Before in main: 1 [0, 1, 2, 3]   94635800628352 139808499830024
In f():                          94635800628384 139808499830024
After in main: 1 [0, 1, 2, 3, 4] 94635800628352 139808499830024

z et x ont le même identifiant. Juste des balises différentes pour la même structure sous-jacente que l'article dit.

jouell
la source
0

Python est un pur langage pass-par-valeur si vous y pensez correctement. Une variable python stocke l'emplacement d'un objet en mémoire. La variable Python ne stocke pas l'objet lui-même. Lorsque vous passez une variable à une fonction, vous transmettez une copie de l'adresse de l'objet pointé par la variable.

Contrastez ces deux fonctions

def foo(x):
    x[0] = 5

def goo(x):
    x = []

Maintenant, quand vous tapez dans le shell

>>> cow = [3,4,5]
>>> foo(cow)
>>> cow
[5,4,5]

Comparez cela à goo.

>>> cow = [3,4,5]
>>> goo(cow)
>>> goo
[3,4,5]

Dans le premier cas, on passe une copie de l'adresse de cow à foo et foo a modifié l'état de l'objet qui y réside. L'objet est modifié.

Dans le second cas, vous passez une copie de l'adresse de la vache à goo. Ensuite, goo procède à la modification de cette copie. Effet: aucun.

J'appelle cela le principe de la maison rose . Si vous faites une copie de votre adresse et dites à un peintre de peindre la maison à cette adresse en rose, vous vous retrouverez avec une maison rose. Si vous donnez au peintre une copie de votre adresse et lui dites de la changer pour une nouvelle adresse, l'adresse de votre maison ne change pas.

L'explication élimine beaucoup de confusion. Python transmet les variables d'adresses stockées par valeur.

ncmathsadist
la source
Une valeur de passage par pointeur pure n'est pas très différente d'un passage par référence si vous y réfléchissez bien ...
galinette
Regardez goo. Si vous étiez purement pass par référence, cela aurait changé son argument. Non, Python n'est pas un pur langage de passage par référence. Il transmet les références par valeur.
ncmathsadist
0

Python est une copie par valeur de référence. Un objet occupe un champ en mémoire, et une référence est associée à cet objet, mais elle-même occupe un champ en mémoire. Et le nom / valeur est associé à une référence. Dans la fonction python, il copie toujours la valeur de la référence, donc dans votre code, n est copié pour être un nouveau nom, lorsque vous l'affectez, il a un nouvel espace dans la pile des appelants. Mais pour la liste, le nom a également été copié, mais il fait référence à la même mémoire (puisque vous n'attribuez jamais de nouvelle valeur à la liste). C'est une magie en python!

sunxd
la source
0

Ma compréhension générale est que toute variable d'objet (telle qu'une liste ou un dict, entre autres) peut être modifiée via ses fonctions. Ce que je pense que vous ne pouvez pas faire est de réaffecter le paramètre - c'est-à-dire de l'affecter par référence dans une fonction appelable.

Cela est cohérent avec de nombreuses autres langues.

Exécutez le court script suivant pour voir comment cela fonctionne:

def func1(x, l1):
    x = 5
    l1.append("nonsense")

y = 10
list1 = ["meaning"]
func1(y, list1)
print(y)
print(list1)
Boris Epstein
la source
-3

J'avais modifié ma réponse des tonnes de fois et réalisé que je n'avais rien à dire, python s'était déjà expliqué.

a = 'string'
a.replace('t', '_')
print(a)
>>> 'string'

a = a.replace('t', '_')
print(a)
>>> 's_ring'

b = 100
b + 1
print(b)
>>> 100

b = b + 1
print(b)
>>> 101

def test_id(arg):
    c = id(arg)
    arg = 123
    d = id(arg)
    return

a = 'test ids'
b = id(a)
test_id(a)
e = id(a)

# b = c  = e != d
# this function do change original value
del change_like_mutable(arg):
    arg.append(1)
    arg.insert(0, 9)
    arg.remove(2)
    return

test_1 = [1, 2, 3]
change_like_mutable(test_1)



# this function doesn't 
def wont_change_like_str(arg):
     arg = [1, 2, 3]
     return


test_2 = [1, 1, 1]
wont_change_like_str(test_2)
print("Doesn't change like a imutable", test_2)

Ce diable n'est pas la référence / valeur / mutable ou non / instance, espace de nom ou variable / liste ou str, C'EST LA SYNTAXE, LE SIGNE ÉGAL.

Hotte
la source