Tại sao một hàm có thể sửa đổi một số đối số theo cảm nhận của người gọi, nhưng không phải là các đối số khác?


182

Tôi đang cố gắng hiểu cách tiếp cận của Python với phạm vi biến. Trong ví dụ này, tại sao f()có thể thay đổi giá trị của x, như được cảm nhận bên trong main(), nhưng không phải là giá trị của 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()

Đầu ra:

Before: 1 [0, 1, 2, 3]
In f(): 2 [0, 1, 2, 3, 4]
After:  1 [0, 1, 2, 3, 4]

7
giải thích rõ ở đây nedbatchelder.com/text/names.html
Roushan

Câu trả lời:


212

Một số câu trả lời có chứa từ "sao chép" trong ngữ cảnh của một lệnh gọi hàm. Tôi thấy khó hiểu.

Python không sao chép các đối tượng bạn vượt qua trong một cuộc gọi hàm bao giờ .

Các tham số chức năng là tên . Khi bạn gọi một hàm, Python liên kết các tham số này với bất kỳ đối tượng nào bạn vượt qua (thông qua tên trong phạm vi người gọi).

Các đối tượng có thể thay đổi (như danh sách) hoặc bất biến (như số nguyên, chuỗi trong Python). Đối tượng có thể thay đổi bạn có thể thay đổi. Bạn không thể thay đổi tên, bạn chỉ có thể liên kết nó với một đối tượng khác.

Ví dụ của bạn không phải là về phạm vi hoặc không gian tên , mà là về cách đặt tên và ràng buộc và khả năng biến đổi của một đối tượng trong 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

Dưới đây là những hình ảnh đẹp về sự khác biệt giữa các biến trong các ngôn ngữ và tên khác trong Python .


3
Bài viết này đã giúp tôi hiểu vấn đề tốt hơn và nó gợi ý một cách giải quyết và một số cách sử dụng nâng cao: Giá trị tham số mặc định trong Python
Gfy

@Gfy, tôi đã thấy các ví dụ tương tự trước đây nhưng với tôi nó không mô tả một tình huống trong thế giới thực. Nếu bạn đang sửa đổi một cái gì đó được thông qua thì sẽ không có ý nghĩa gì để mặc định nó.
Đánh dấu tiền chuộc

@MarkRansom, tôi nghĩ rằng nó có ý nghĩa nếu bạn muốn cung cấp đích đầu ra tùy chọn như trong : def foo(x, l=None): l=l or []; l.append(x**2); return l[-1].
Janusz Lenar

Đối với dòng mã cuối cùng của Sebastian, nó nói "# ở trên không có hiệu lực trong danh sách ban đầu". Nhưng theo tôi, nó chỉ không có tác dụng với "n", mà thay đổi "x" trong hàm main (). Tôi có đúng không?
dùng17670

1
@ user17670: x = []in f()không có hiệu lực trong danh sách xtrong chức năng chính. Tôi đã cập nhật nhận xét, để làm cho nó cụ thể hơn.
jfs

15

Bạn đã có một số câu trả lời rồi và tôi đồng ý rộng rãi với JF Sebastian, nhưng bạn có thể thấy điều này hữu ích như một lối tắt:

Bất cứ khi nào bạn thấy varname =, bạn đang tạo một ràng buộc tên mới trong phạm vi của hàm. Bất cứ giá trị nào varnamebị ràng buộc trước đó đều bị mất trong phạm vi này .

Bất cứ lúc nào bạn thấy varname.foo()bạn đang gọi một phương thức trên varname. Phương thức có thể thay đổi varname (ví dụ list.append). varname(hoặc, đúng hơn, đối tượng có varnametên) có thể tồn tại trong nhiều phạm vi và vì đó là cùng một đối tượng, nên mọi thay đổi sẽ được hiển thị trong tất cả các phạm vi.

[lưu ý rằng globaltừ khóa tạo ngoại lệ cho trường hợp đầu tiên]


13

fkhông thực sự thay đổi giá trị của x(luôn luôn cùng tham chiếu đến một thể hiện của danh sách). Thay vào đó, nó thay đổi nội dung của danh sách này.

Trong cả hai trường hợp, một bản sao của một tham chiếu được truyền cho hàm. Bên trong chức năng,

  • nđược gán một giá trị mới. Chỉ tham chiếu bên trong hàm được sửa đổi, không phải tham chiếu bên ngoài nó.
  • xkhông được gán một giá trị mới: cả tham chiếu bên trong lẫn bên ngoài hàm đều không được sửa đổi. Thay vào đó, giá trịx của được sửa đổi.

Vì cả xbên trong hàm và bên ngoài nó đều tham chiếu đến cùng một giá trị, cả hai đều thấy sự sửa đổi. Ngược lại, nbên trong hàm và bên ngoài nó tham chiếu đến các giá trị khác nhau sau khi nđược gán lại bên trong hàm.


8
"Bản sao" là sai lệch. Python không có các biến như C. Tất cả các tên trong Python đều là tham chiếu. Bạn không thể sửa đổi tên, bạn chỉ có thể liên kết nó với một đối tượng khác, đó là tất cả. Thật có ý nghĩa khi nói về đối tượng có thể thay đổi và bất biến trong Python không phải là tên.
jfs

1
@JF Sebastian: Tuyên bố của bạn là sai lệch nhất. Nó không hữu ích khi nghĩ về những con số như là tài liệu tham khảo.
Pitarou

9
@dysfunctor: số là tham chiếu đến các đối tượng bất biến. Nếu bạn nghĩ về họ theo một cách khác, bạn có một loạt các trường hợp đặc biệt để giải thích. Nếu bạn nghĩ về họ là bất biến, không có trường hợp đặc biệt.
S.Lott

@ S.Lott: Bất kể những gì đang diễn ra dưới mui xe, Guido van Rossum đã nỗ lực rất nhiều trong việc thiết kế Python để lập trình viên có thể coi các con số chỉ là ... số.
Pitarou

1
@JF, tài liệu tham khảo được sao chép.
thói quen

7

Tôi sẽ đổi tên các biến để giảm nhầm lẫn. n -> nf hoặc nmain . x -> xf hoặc 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()

Khi bạn gọi hàm f , bộ thực thi Python tạo một bản sao của xmain và gán nó cho xf , và tương tự gán một bản sao của nmain cho nf .

Trong trường hợp n , giá trị được sao chép là 1.

Trong trường hợp x giá trị được sao chép không phải là danh sách bằng chữ [0, 1, 2, 3] . Nó là một tài liệu tham khảo cho danh sách đó. xfxmain đang trỏ vào cùng một danh sách, vì vậy khi bạn sửa đổi xf, bạn cũng đang sửa đổi xmain .

Tuy nhiên, nếu bạn đã viết một cái gì đó như:

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

bạn sẽ thấy rằng xmain đã không thay đổi. Điều này là do, trong dòng xf = ["foo", "bar"] bạn đã thay đổi xf để trỏ đến một danh sách mới . Mọi thay đổi bạn thực hiện cho danh sách mới này sẽ không có hiệu ứng nào trong danh sách mà xmain vẫn chỉ đến.

Mong rằng sẽ giúp. :-)


2
"Trong trường hợp n, giá trị được sao chép ..." - Điều này là sai, không có sao chép nào được thực hiện ở đây (trừ khi bạn đếm các tài liệu tham khảo). Thay vào đó, python sử dụng 'tên' trỏ đến các đối tượng thực tế. nf và xf trỏ đến nmain và xmain, cho đến khi nf = 2, nơi tên nfđược thay đổi thành trỏ tới 2. Số là bất biến, danh sách là đột biến.
Casey Kuball

2

Nó phạm vì một danh sách là một đối tượng có thể thay đổi. Bạn không đặt x thành giá trị của [0,1,2,3], bạn đang xác định nhãn cho đối tượng [0,1,2,3].

Bạn nên khai báo hàm f () như thế này:

def f(n, x=None):
    if x is None:
        x = []
    ...

3
Nó không có gì để làm với khả năng biến đổi. Nếu bạn sẽ làm x = x + [4]thay vì x.append(4), bạn sẽ thấy không có thay đổi trong người gọi mặc dù một danh sách có thể thay đổi. Nó phải làm với nếu nó thực sự bị đột biến.
glglgl

1
OTOH, nếu bạn làm x += [4]sau đó xbị đột biến, giống như những gì xảy ra với x.append(4), vì vậy người gọi sẽ thấy sự thay đổi.
PM 2Ring

2

n là một int (không thay đổi) và một bản sao được truyền cho hàm, vì vậy trong hàm bạn đang thay đổi bản sao.

X là một danh sách (có thể thay đổi) và một bản sao của con trỏ được truyền o hàm để x.append (4) thay đổi nội dung của danh sách. Tuy nhiên, bạn đã nói x = [0,1,2,3,4] trong hàm của mình, bạn sẽ không thay đổi nội dung của x trong hàm main ().


3
Xem cụm từ "bản sao của con trỏ". Cả hai nơi đều có được tài liệu tham khảo cho các đối tượng. n là một tham chiếu đến một đối tượng bất biến; x là một tham chiếu đến một đối tượng có thể thay đổi.
S.Lott

2

Nếu các hàm được viết lại với các biến hoàn toàn khác nhau và chúng ta gọi id trên chúng, thì nó sẽ minh họa điểm tốt. Tôi đã không nhận được điều này lúc đầu và đọc bài đăng của jfs với lời giải thích tuyệt vời , vì vậy tôi đã cố gắng để hiểu / thuyết phục bản thân:

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 và x có cùng id. Chỉ cần các thẻ khác nhau cho cùng một cấu trúc cơ bản như bài viết nói.


0

Python là một ngôn ngữ truyền qua giá trị thuần túy nếu bạn nghĩ về nó đúng cách. Một biến python lưu trữ vị trí của một đối tượng trong bộ nhớ. Biến Python không lưu trữ chính đối tượng. Khi bạn truyền một biến cho một hàm, bạn đang truyền một bản sao địa chỉ của đối tượng được trỏ đến bởi biến đó.

Tương phản hai chức năng này

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

def goo(x):
    x = []

Bây giờ, khi bạn gõ vào vỏ

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

So sánh điều này với goo.

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

Trong trường hợp đầu tiên, chúng tôi chuyển một bản sao địa chỉ của con bò cho foo và foo đã sửa đổi trạng thái của đối tượng cư trú ở đó. Đối tượng được sửa đổi.

Trong trường hợp thứ hai, bạn chuyển một bản sao địa chỉ của con bò để goo. Sau đó goo tiến hành thay đổi bản sao đó. Tác dụng: không có.

Tôi gọi đây là nguyên tắc ngôi nhà màu hồng . Nếu bạn tạo một bản sao địa chỉ của bạn và bảo một họa sĩ vẽ ngôi nhà ở địa chỉ đó màu hồng, bạn sẽ kết thúc với một ngôi nhà màu hồng. Nếu bạn đưa cho họa sĩ một bản sao địa chỉ của bạn và bảo anh ta đổi nó thành một địa chỉ mới, địa chỉ nhà bạn sẽ không thay đổi.

Các giải thích loại bỏ rất nhiều nhầm lẫn. Python vượt qua các biến địa chỉ lưu trữ theo giá trị.


Một đường truyền thuần túy theo giá trị con trỏ không khác lắm so với đường chuyền bằng tham chiếu nếu bạn nghĩ về nó đúng cách ...
galinette

Nhìn kìa. Nếu bạn vượt qua hoàn toàn bằng cách tham chiếu, nó sẽ thay đổi đối số của nó. Không, Python không phải là ngôn ngữ chuyển qua tham chiếu thuần túy. Nó vượt qua các tham chiếu theo giá trị.
ncmathsadist

0

Python được sao chép theo giá trị tham chiếu. Một đối tượng chiếm một trường trong bộ nhớ và một tham chiếu được liên kết với đối tượng đó, nhưng chính nó chiếm một trường trong bộ nhớ. Và tên / giá trị được liên kết với một tài liệu tham khảo. Trong hàm python, nó luôn sao chép giá trị của tham chiếu, vì vậy trong mã của bạn, n được sao chép thành một tên mới, khi bạn gán nó, nó có một khoảng trắng mới trong ngăn xếp của người gọi. Nhưng đối với danh sách, tên cũng được sao chép, nhưng nó đề cập đến cùng một bộ nhớ (vì bạn không bao giờ gán cho danh sách một giá trị mới). Đó là một phép thuật trong trăn!


0

Hiểu biết chung của tôi là bất kỳ biến đối tượng nào (như danh sách hoặc lệnh, trong số các biến khác) có thể được sửa đổi thông qua các chức năng của nó. Những gì tôi tin rằng bạn không thể làm là gán lại tham số - tức là gán nó bằng tham chiếu trong một hàm có thể gọi được.

Điều đó phù hợp với nhiều ngôn ngữ khác.

Chạy đoạn script ngắn sau để xem nó hoạt động như thế nào:

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

y = 10
list1 = ["meaning"]
func1(y, list1)
print(y)
print(list1)

-3

Tôi đã sửa đổi câu trả lời của mình hàng tấn lần và nhận ra tôi không cần phải nói gì cả, trăn đã tự giải thích rồi.

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)

Ma quỷ này không phải là tham chiếu / giá trị / có thể thay đổi hoặc không / thể hiện, không gian tên hoặc biến / danh sách hoặc str, NÓ LÀ TỔNG HỢP, TÍN HIỆU THIẾT BỊ.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.