Điều gì xảy ra khi bạn gán giá trị của một biến này cho một biến khác trong Python?


80

Đây là ngày thứ hai của tôi học python (tôi biết kiến ​​thức cơ bản về C ++ và một số OOP.) Và tôi có một số nhầm lẫn nhỏ về các biến trong python.

Đây là cách tôi hiểu chúng hiện tại:

Các biến trong Python là các tham chiếu (hoặc con trỏ?) Đến các đối tượng (có thể thay đổi hoặc bất biến). Khi chúng ta có một cái gì đó giống như num = 5, đối tượng không thay đổi 5được tạo ở đâu đó trong bộ nhớ và cặp tham chiếu tên-đối tượng numđược tạo trong một không gian tên nhất định. Khi chúng ta có a = num, không có gì đang được sao chép, nhưng bây giờ cả hai biến đều tham chiếu đến cùng một đối tượng và ađược thêm vào cùng một không gian tên.

Đây là nơi cuốn sách của tôi, Tự động hóa những thứ nhàm chán với Python , khiến tôi bối rối. Vì là sách dành cho người mới nên nó không đề cập đến các đối tượng, không gian tên, v.v. và cố gắng giải thích đoạn mã sau:

>>> spam = 42
>>> cheese = spam
>>> spam = 100
>>> spam
100
>>> cheese
42

Lời giải thích mà nó đưa ra hoàn toàn giống với lời giải thích của một cuốn sách C ++, điều mà tôi không hài lòng khi chúng ta đang xử lý các tham chiếu / con trỏ tới các đối tượng. Vì vậy, trong trường hợp này, tôi đoán rằng ở dòng thứ 3, vì các số nguyên là bất biến, spamđang được gán một con trỏ / tham chiếu hoàn toàn mới đến một vị trí khác trong bộ nhớ, tức là bộ nhớ mà ban đầu nó trỏ đến không được sửa đổi. Do đó, chúng tôi đã cheesetham chiếu đến đối tượng ban đầu được tham chiếu bởi spam. Đây có phải là lời giải thích chính xác?



5
Bạn đã lưu trữ 42trong spam, Bây giờ bạn được lưu trữ spamtrong pho mát có nghĩa là cheese = 42, Sau đó bạn thay thế spamthành 100, Bạn đã không chỉnh sửa cheeseđó là lý do tại sao cheesevẫn còn 42.

15
Như Jonas đã chỉ ra trong câu trả lời của mình, tính bất biến là một con cá trích đỏ ở đây: bạn sẽ nhận được kết quả chính xác nếu bạn sử dụng một đối tượng có thể thay đổi như một danh sách. Vấn đề là bạn gán lại biến cho một tham chiếu mới.
Daniel Roseman,

7
Có thể trùng lặp của nhiệm vụ biến tham chiếu Python
underscore_d

27
@mini: Tôi thích coi các biến Python là nhãn hơn là nhóm. Bạn dán nhãn spamvào số 42. Tiếp theo, bạn dán nhãn cheesevào thứ được dán nhãn là spam(không phải trên đầu nhãn, bạn nhớ nhé). Sau đó, bạn bóc vỏ các spamnhãn ra khỏi đó và đặt nó vào số 100.
Tim Pederick

Câu trả lời:


81

Là một nhà phát triển C ++, bạn có thể coi các biến Python là con trỏ.

Vì vậy, khi bạn viết spam = 100, điều này có nghĩa là bạn "gán con trỏ", mà trước đó đã trỏ đến đối tượng 42, trỏ tới đối tượng 100.

Trước đó, cheeseđã được chỉ định để chỉ vào cùng một đối tượng được spamchỉ đến, tình cờ 42ở thời điểm đó. Vì bạn đã không sửa đổi cheese, nó vẫn chỉ đến 42.

Tính bất biến không liên quan gì đến nó trong trường hợp này, vì việc gán con trỏ không thay đổi bất kỳ điều gì về đối tượng được trỏ tới.


2
giống như các đối tượng trong js
Dushyant Bangal

7
Tính bất biến rất quan trọng vì nó có nghĩa là bạn có thể coi tham chiếu một cách an toàn như thể nó là một giá trị. Xử lý các đối tượng có thể thay đổi như thể chúng là giá trị thì rủi ro hơn.
plugwash

21

Cách tôi nhìn nhận có những quan điểm khác nhau về một ngôn ngữ.

  • Quan điểm "luật sư ngôn ngữ".
  • Quan điểm "lập trình viên thực tế".
  • quan điểm của "người thực hiện".

Từ quan điểm luật sư ngôn ngữ, các biến python luôn "trỏ vào" một đối tượng. Tuy nhiên, không giống như Java và C ++, phạm vi của == <=> = vv phụ thuộc vào kiểu thời gian chạy của các đối tượng mà các biến trỏ tới. Hơn nữa trong quản lý bộ nhớ python được xử lý bởi ngôn ngữ.

Từ góc độ lập trình viên thực tế, chúng ta có thể coi thực tế là số nguyên, chuỗi, bộ giá trị, v.v. là các đối tượng bất biến * thay vì các giá trị thẳng như một chi tiết không thể thay đổi. Ngoại lệ là khi lưu trữ lượng lớn dữ liệu số, chúng ta có thể muốn sử dụng các kiểu có thể lưu trữ các giá trị trực tiếp (ví dụ: mảng số) thay vì các kiểu sẽ kết thúc bằng một mảng đầy tham chiếu đến các đối tượng nhỏ.

Từ quan điểm của người triển khai, hầu hết các ngôn ngữ đều có một số loại quy tắc như thể mà nếu các hành vi được chỉ định là đúng thì việc triển khai vẫn đúng bất kể mọi thứ thực sự được thực hiện như thế nào.

Vì vậy, có lời giải thích của bạn là đúng từ góc độ luật sư ngôn ngữ. Cuốn sách của bạn là chính xác từ góc độ lập trình viên thực tế. Những gì một triển khai thực sự làm phụ thuộc vào việc thực hiện. Trong cpython, số nguyên là các đối tượng thực mặc dù các số nguyên có giá trị nhỏ được lấy từ một nhóm bộ nhớ cache thay vì được tạo mới. Tôi không chắc các triển khai khác (ví dụ: pypy và jython) làm gì.

* Lưu ý sự phân biệt giữa các đối tượng có thể thay đổi và bất biến ở đây. Với một đối tượng có thể thay đổi, chúng ta phải cẩn thận về việc coi nó "giống như một giá trị" vì một số mã khác có thể thay đổi nó. Với một đối tượng bất biến, chúng tôi không có mối quan tâm như vậy.


20

Đúng là bạn có thể ít nhiều điều của các biến dưới dạng con trỏ. Tuy nhiên, mã ví dụ sẽ giúp ích rất nhiều trong việc giải thích cách thức hoạt động thực sự của nó.

Đầu tiên, chúng tôi sẽ sử dụng nhiều idchức năng:

Trả lại "danh tính" của một đối tượng. Đây là một số nguyên được đảm bảo là duy nhất và không đổi cho đối tượng này trong suốt thời gian tồn tại của nó. Hai đối tượng có vòng đời không trùng lặp có thể có cùng giá trị id ().

Có thể điều này sẽ trả về các giá trị tuyệt đối khác nhau trên máy của bạn.

Hãy xem xét ví dụ này:

>>> foo = 'a string'
>>> id(foo) 
4565302640
>>> bar = 'a different string'
>>> id(bar)
4565321816
>>> bar = foo
>>> id(bar) == id(foo)
True
>>> id(bar)
4565302640

Bạn có thể thấy rằng:

  • Foo / bar ban đầu có các id khác nhau, vì chúng trỏ đến các đối tượng khác nhau
  • Khi thanh được gán cho foo, id của chúng bây giờ giống nhau. Điều này tương tự như cả hai đều trỏ đến cùng một vị trí trong bộ nhớ mà bạn thấy khi tạo con trỏ C ++

khi chúng tôi thay đổi giá trị của foo, nó sẽ được gán cho một id khác:

>>> foo = 42
>>> id(foo)
4561661488
>>> foo = 'oh no'
>>> id(foo)
4565257832

Một quan sát thú vị nữa là các số nguyên ngầm có chức năng này lên đến 256:

>>> a = 100
>>> b = 100
>>> c = 100
>>> id(a) == id(b) == id(c)
True

Tuy nhiên ngoài 256 điều này không còn đúng nữa:

>>> a = 256
>>> b = 256
>>> id(a) == id(b)
True
>>> a = 257
>>> b = 257
>>> id(a) == id(b)
False

tuy nhiên việc gán acho bsẽ thực sự giữ id giống như được hiển thị trước đây:

>>> a = b
>>> id(a) == id(b)
True

18

Python không phải là giá trị truyền qua tham chiếu hay giá trị truyền qua. Các biến Python không phải là con trỏ, chúng không phải là tham chiếu, chúng không phải là giá trị. Các biến Python là tên .

Hãy coi nó là "pass-by-alias" nếu bạn cần cùng một loại cụm từ hoặc có thể là "pass-by-object", bởi vì bạn có thể thay đổi cùng một đối tượng từ bất kỳ biến nào chỉ ra nó, nếu nó có thể thay đổi, nhưng chỉ định lại một biến (bí danh) chỉ thay đổi một biến đó.

Nếu nó hữu ích: Các biến C là các hộp mà bạn ghi giá trị vào. Tên Python là các thẻ mà bạn đặt trên các giá trị.

Tên của một biến Python là một khóa trong không gian tên chung (hoặc cục bộ), nó thực sự là một từ điển. Giá trị cơ bản là một số đối tượng trong bộ nhớ. Phép gán đặt tên cho đối tượng đó. Việc gán một biến này cho một biến khác có nghĩa là cả hai biến đều là tên cho cùng một đối tượng. Việc gán lại một biến sẽ thay đổi đối tượng nào được đặt tên bởi biến đó mà không thay đổi biến khác. Bạn đã di chuyển thẻ nhưng không thay đổi đối tượng trước đó hoặc bất kỳ thẻ nào khác trên đó.

Trong mã C cơ bản của việc triển khai CPython, mọi đối tượng Python đều là a PyObject*, vì vậy bạn có thể coi nó hoạt động giống như C nếu bạn chỉ có con trỏ đến dữ liệu (không có con trỏ tới con trỏ, không có giá trị được truyền trực tiếp).

bạn có thể nói rằng Python là truyền qua giá trị, trong đó các giá trị là con trỏ… hoặc bạn có thể nói Python là tham chiếu truyền qua, trong đó các tham chiếu là bản sao.


1
Vấn đề khi gọi nó là "pass-by-name" là đã có một quy ước truyền tham số được gọi là "call by name", với một ý nghĩa hoàn toàn khác. Trong lệnh gọi theo tên, biểu thức tham số được đánh giá mỗi khi hàm sử dụng tham số và không bao giờ được đánh giá nếu hàm không sử dụng tham số.
user2357112 hỗ trợ Monica

11

Khi bạn chạy spam = 100python, hãy tạo thêm một đối tượng trong bộ nhớ nhưng không thay đổi hiện có. vì vậy bạn vẫn có con trỏ cheeseđến 42 và spamđến 100


8

Những gì đang xảy ra trong spam = 100dòng là thay thế giá trị trước đó (con trỏ đến đối tượng của kiểu intcó giá trị 42) bằng một con trỏ khác đến đối tượng khác (kiểu int, giá trị 100)


Số nguyên là đối tượng giá trị nằm trên ngăn xếp phải không?
Gert Kommer

Có, chúng giống như đối tượng bạn tạo bằng new Class()cú pháp trong C ++. Hơn nữa, trong Python, bất cứ thứ gì cũng là một thể hiện của objectlớp / lớp con.
bakatrouble vào

4
@GertKommer ít nhất trong CPython, tất cả các đối tượng đều nằm trên heap. Không có sự phân biệt của một "đối tượng giá trị". Chỉ có các đối tượng, và mọi thứ đều là một đối tượng . Đó là lý do tại sao kích thước của một int điển hình là khoảng 28 byte, tùy thuộc vào phiên bản Python, vì nó có toàn bộ chi phí Py_Object. Các int nhỏ được lưu vào bộ nhớ đệm như một tối ưu hóa CPython.
juanpa.arrivillaga

8

Như @DeepSpace đã đề cập trong các nhận xét, Ned Batchelder đã làm rất tốt công việc phân tích các biến (tên) và phép gán cho các giá trị trong blog, từ đó anh ấy đã có một bài nói chuyện tại PyCon 2015, Sự kiện và Huyền thoại về tên và giá trị Python . Nó có thể sâu sắc đối với Pythonistas ở bất kỳ cấp độ thành thạo nào.


1

Trong Python, một biến giữ tham chiếu đến đối tượng . Một đối tượng là một đoạn bộ nhớ phân bổ chứa một giá trị và một tiêu đề . Tiêu đề của đối tượng chứa kiểu của nó và một bộ đếm tham chiếu biểu thị số lần đối tượng này được tham chiếu trong mã nguồn để Bộ sưu tập rác có thể xác định liệu một đối tượng có thể được thu thập hay không.

Bây giờ khi bạn gán giá trị cho một biến, Python thực sự chỉ định các tham chiếucon trỏ đến các vị trí bộ nhớ được cấp phát cho các đối tượng:

# x holds a reference to the memory location allocated for  
# the object(type=string, value="Hello World", refCounter=1)

x = "Hello World" 

Bây giờ khi bạn gán các đối tượng khác kiểu cho cùng một biến, bạn thực sự thay đổi tham chiếu để nó trỏ đến một đối tượng khác (tức là vị trí bộ nhớ khác nhau). Vào thời điểm bạn gán một tham chiếu khác (và do đó là đối tượng) cho một biến, Garbage Collector sẽ ngay lập tức lấy lại không gian được cấp cho đối tượng trước đó, giả sử rằng nó không được tham chiếu bởi bất kỳ biến nào khác trong mã nguồn:

# x holds a reference to the memory location allocated for  
# the object(type=string, value="Hello World", refCounter=1)

x = "Hello World" 

# Now x holds the reference to a different object(type=int, value=10, refCounter=1)
# and object(type=string, value="Hello World", refCounter=0) -which is not refereced elsewhere
# will now be garbage-collected.
x = 10

Đến với ví dụ của bạn bây giờ,

spam giữ tham chiếu đến đối tượng (type = int, value = 42, refCounter = 1):

>>> spam = 42

Bây giờ cheesecũng sẽ giữ tham chiếu đến đối tượng (type = int, value = 42, refCounter = 2)

>>> cheese = spam

Bây giờ thư rác giữ một tham chiếu đến một đối tượng khác (type = int, value = 100, refCounter = 1)

>>> spam = 100
>>> spam
100

Nhưng pho mát sẽ tiếp tục trỏ đến đối tượng (type = int, value = 42, refCounter = 1)

>>> cheese
42

0

Khi bạn lưu trữ spam = 42, nó sẽ tạo ra một đối tượng trong bộ nhớ. Sau đó bạn gán cheese = spam, Nó chỉ định đối tượng được tham chiếu spamđến cheese. Và cuối cùng, khi bạn thay đổi spam = 100, nó chỉ thay đổi spamđối tượng. Vì vậy cheese = 42.


7
"Sau đó, bạn gán cheese = spam, nó tạo ra một đối tượng khác trong bộ nhớ" Không, nó không. Nó chỉ định đối tượng được tham chiếu spamđến cheese. Không có đối tượng mới nào được tạo.
juanpa.arrivillaga

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.