[ATBS2nd]Chương 1 - Python cơ bản - Phần 9

Chào các bạn,
Chúng ta lại tiếp tục trong phần nói về hàm trong python.
Gọi các thành phần trong ngăn xếp (The call stack)
Hãy tưởng tượng rằng bạn có một cuộc trò chuyện quanh co với ai đó. Bạn nói về người bạn Alice của mình, sau đó nhắc bạn về một câu chuyện về đồng nghiệp Bob, nhưng trước tiên bạn phải giải thích điều gì đó về anh em họ Carol của bạn. Bạn kết thúc câu chuyện về Carol và quay lại nói về Bob, và khi bạn kết thúc câu chuyện về Bob, bạn quay lại nói về Alice. Nhưng sau đó, bạn được nhắc về anh trai David của mình, vì vậy bạn kể một câu chuyện về anh ta, và sau đó quay lại để hoàn thành câu chuyện ban đầu của bạn về Alice. Cuộc hội thoại của bạn tuân theo cấu trúc giống như ngăn xếp, như trong hình dưới đây.
Cuộc hội thoại giống như ngăn xếp vì chủ đề hiện tại luôn ở đầu ngăn xếp.


Tương tự như cuộc trò chuyện quanh co của chúng tôi, việc gọi một chức năng sẽ không gửi lệnh thực thi trong chuyến đi một chiều đến đỉnh của một chức năng. Python sẽ nhớ dòng mã nào được gọi là hàm để thực thi có thể quay lại đó khi nó gặp câu lệnh return. Nếu hàm ban đầu đó gọi các hàm khác, thì việc thực thi sẽ trở lại các lệnh gọi hàm đó trước, trước khi quay lại từ lệnh gọi hàm ban đầu.
Mở trình soạn thảo và copy dòng code bên dưới sau đấy lưu lại dưới tên abcdCallStack.py

def a():
    print('a() starts')
    b()
    d()
    print('a() returns')
def b():
    print('b() starts')
    c()
    print('b() returns')
def c():
    print('c() starts')
    print('c() returns')
def d():
    print('d() starts')
    print('d() returns')
a()

Khi chúng ta chạy chương trình này nó hiển thị ra như sau trên màn hình.

a() starts
b() starts
c() starts
c() returns
b() returns
d() starts
d() returns
a() returns

Khi a() được gọi, nó gọi b(), đến lượt nó gọi c(). Hàm c () không gọi bất cứ thứ gì; nó chỉ hiển thị c() bắt đầu và c() trả về trước khi quay lại dòng trong b() đã gọi nó. Khi thực thi trả về mã trong b() được gọi là c (), nó sẽ trả về dòng trong a() được gọi là b(). Việc thực thi tiếp tục đến dòng tiếp theo trong hàm b(), đó là một cuộc gọi đến d(). Giống như hàm c(), hàm d() cũng không gọi bất cứ thứ gì. Nó chỉ hiển thị d() starts và d() returns trước khi quay lại dòng trong b() đã gọi nó. Vì b() không chứa mã nào khác, nên việc thực thi trở về dòng trong a () được gọi là b(). Dòng cuối cùng trong a() hiển thị trả về () trước khi quay lại cuộc gọi a() ban đầu ở cuối chương trình.
Ngăn xếp cuộc gọi là cách Python ghi nhớ nơi trả về thực thi sau mỗi lần gọi hàm. Ngăn xếp cuộc gọi được lưu trữ trong một biến trong chương trình của bạn; đúng hơn, Python xử lý nó đằng sau hậu trường. Khi chương trình của bạn gọi một hàm, Python tạo một đối tượng khung trên đỉnh của ngăn xếp cuộc gọi. Các đối tượng khung lưu trữ số dòng của lệnh gọi hàm ban đầu để Python có thể nhớ nơi cần trả về. Nếu một cuộc gọi chức năng khác được thực hiện, Python sẽ đặt một đối tượng khung khác vào ngăn xếp cuộc gọi bên trên cuộc gọi khác.
Khi một lệnh gọi hàm trả về, Python sẽ xóa một đối tượng khung khỏi trên cùng của ngăn xếp và di chuyển thực thi đến số dòng được lưu trữ trong nó. Lưu ý rằng các đối tượng khung luôn được thêm và xóa khỏi đỉnh ngăn xếp chứ không phải từ bất kỳ nơi nào khác. Hình dưới đây minh họa trạng thái của ngăn xếp cuộc gọi trong abcdCallStack.py khi mỗi hàm được gọi và trả về.


Phần đầu của ngăn xếp cuộc gọi là chức năng hiện đang thực hiện trong. Khi ngăn xếp cuộc gọi trống, việc thực thi nằm trên một dòng bên ngoài tất cả các chức năng. Ngăn xếp cuộc gọi là một chi tiết kỹ thuật mà bạn không cần phải biết để viết chương trình. Nó đủ để hiểu rằng các chức năng gọi trở về số dòng họ được gọi từ.
Tuy nhiên, việc hiểu các ngăn xếp cuộc gọi giúp dễ hiểu phạm vi địa phương và toàn cầu hơn, được mô tả trong phần tiếp theo.
Phạm vi cục bộ và toàn cầu
Các tham số và biến được gán trong một hàm được gọi là tồn tại trong phạm vi cục bộ của hàm đó. Các biến được gán bên ngoài tất cả các hàm được cho là tồn tại trong phạm vi toàn cầu. Một biến tồn tại trong phạm vi cục bộ được gọi là biến cục bộ, trong khi biến tồn tại trong phạm vi toàn cầu được gọi là biến toàn cục. Một biến phải là một cục bộ hoặc là toàn cục; nó không thể là cả địa phương và toàn cầu.
Hãy nghĩ về một phạm vi như một thùng chứa cho các biến. Khi một phạm vi bị phá hủy, tất cả các giá trị được lưu trữ trong phạm vi biến, biến sẽ bị lãng quên. Chỉ có một phạm vi toàn cầu và nó được tạo khi chương trình của bạn bắt đầu. Khi chương trình của bạn kết thúc, phạm vi toàn cầu sẽ bị hủy và tất cả các biến của nó bị lãng quên. Mặt khác, lần sau khi bạn chạy chương trình, các biến sẽ ghi nhớ giá trị của chúng từ lần cuối bạn chạy chương trình.
Một phạm vi cục bộ được tạo ra bất cứ khi nào một chức năng được gọi. Bất kỳ biến nào được gán trong hàm đều tồn tại trong phạm vi hàm cục bộ. Khi hàm trả về, phạm vi cục bộ bị hủy và các biến này bị quên. Lần sau khi bạn gọi hàm, các biến cục bộ sẽ không nhớ các giá trị được lưu trong chúng từ lần cuối cùng hàm được gọi. Các biến cục bộ cũng được lưu trữ trong các đối tượng khung trên ngăn xếp cuộc gọi.
Vấn đề phạm vi của biến có vài lưu ý

  • Mã trong phạm vi toàn cầu, bên ngoài tất cả các chức năng, không thể sử dụng bất kỳ biến cục bộ nào.

  • Tuy nhiên, mã trong phạm vi cục bộ có thể truy cập các biến toàn cục.

  • Mã trong một hàm phạm vi cục bộ không thể sử dụng các biến trong bất kỳ phạm vi cục bộ nào khác.

  • Bạn có thể sử dụng cùng tên cho các biến khác nhau nếu chúng ở các phạm vi khác nhau. Đó là, có thể có một biến cục bộ có tên là spam và một biến toàn cục cũng có tên là spam
    Lý do Python có phạm vi khác nhau thay vì chỉ biến mọi thứ thành biến toàn cục là để khi các biến được sửa đổi bởi mã trong một lệnh gọi cụ thể, hàm chỉ tương tác với phần còn lại của chương trình thông qua các tham số và giá trị trả về. Điều này thu hẹp số lượng dòng mã có thể gây ra lỗi. Nếu chương trình của bạn không chứa gì ngoài các biến toàn cục và có lỗi do biến được đặt thành giá trị xấu, thì thật khó để theo dõi nơi đặt giá trị xấu này. Nó có thể đã được đặt từ bất cứ nơi nào trong chương trình và chương trình của bạn có thể dài hàng trăm hoặc hàng ngàn dòng! Nhưng nếu lỗi được gây ra bởi một biến cục bộ có giá trị xấu, bạn biết rằng chỉ có mã trong một hàm đó có thể đặt nó không chính xác.
    Mặc dù sử dụng các biến toàn cục trong các chương trình nhỏ là tốt, nhưng việc dựa vào các biến toàn cầu là một thói quen xấu khi các chương trình của bạn ngày càng lớn hơn…
    Những biến cục bộ không thể sử dụng trong phạm vi toàn cục
    Chúng ta xem xét chương trình sau và lí do xảy ra lỗi của chương trình

def spam():
    eggs = 31337
spam()
print(eggs)

Khi bạn chạy chương trình sẽ hiển thị lỗi như sau

Traceback (most recent call last):
    File "C:/test1.py", line 4, in <module>
        print(eggs)
NameError: name 'eggs' is not defined

Lỗi xảy ra do biến eggs chỉ tồn tại trong phạm vi cục bộ được tạo khi spam() được gọi là. Khi thực thi chương trình trả về từ spam, phạm vi cục bộ đó sẽ bị hủy và không còn biến số có tên là eggs. Vì vậy, khi chương trình của bạn cố chạy print(eggs), Python sẽ báo lỗi cho bạn rằng eggs không được xác định. Điều này có ý nghĩa nếu bạn nghĩ về nó; khi thực hiện chương trình trong phạm vi toàn cầu, không có phạm vi cục bộ nào tồn tại, do đó, có thể có bất kỳ biến cục bộ nào. Đây là lý do tại sao chỉ các biến toàn cục có thể được sử dụng trong phạm vi toàn cầu.
Phạm vi cục bộ không thể sử dụng những biến trong phạm vi cục bộ khác
Một phạm vi cục bộ mới được tạo bất cứ khi nào một chức năng được gọi, bao gồm cả khi một chức năng được gọi từ một chức năng khác. Hãy xem xét chương trình này:

def spam():
    eggs = 99
    bacon()
    print(eggs)
def bacon():
    ham = 101
    eggs = 0
spam()

Khi chương trình khởi động, hàm spam() được gọi và phạm vi cục bộ được tạo. eggs biến cục bộ được đặt thành 99. Sau đó, hàm bacon() được gọi và phạm vi cục bộ thứ hai được tạo. Nhiều phạm vi địa phương có thể tồn tại cùng một lúc. Trong phạm vi cục bộ mới này, ham biến cục bộ được đặt thành 101 và một biến eggs cục bộ trứng khác với phạm vi trong spam() phạm vi cục bộ của hàm cũng được tạo và đặt thành 0.
Khi bacon() trả về, phạm vi cục bộ của lệnh gọi đó sẽ bị hủy, bao gồm cả biến eggs của nó. Việc thực hiện chương trình tiếp tục trong hàm spam() để in giá trị của eggs. Do phạm vi cục bộ của lệnh gọi spam() vẫn tồn tại, nên biến số eggs duy nhất là hàm spam() biến eggs, được đặt thành 99. Đây là những gì chương trình in ra màn hình.
Kết quả cuối cùng là các biến cục bộ trong một hàm hoàn toàn tách biệt với các biến cục bộ trong một hàm khác.
Biến toàn cục có thể được đọc từ phạm vi cục bộ
Xem xét chương trình sau

def spam():
     print(eggs)
eggs = 42
spam()
print(eggs)

Vì không có tham số có tên là eggs hoặc bất kỳ mã nào gán cho eggs một giá trị trong hàm spam(), khi trứng được sử dụng trong spam(), Python coi đó là một tham chiếu đến eggs biến toàn cục. Đây là lý do tại sao 42 được in khi chương trình trước được chạy.
Biến cục bộ và toàn cục cùng tên
Về mặt kỹ thuật, hoàn toàn có thể chấp nhận sử dụng cùng một tên biến cho biến toàn cục và biến cục bộ trong các phạm vi khác nhau trong Python.Nhưng, để đơn giản hóa cuộc sống của bạn, tránh làm điều này. Để xem điều gì xảy ra, hãy nhập mã sau vào trình soạn thảo và lưu nó dưới dạng localGlobalSameName.py:

def spam():
    eggs = 'spam local'
    print(eggs) # prints 'spam local'
def bacon():
    eggs = 'bacon local'
    print(eggs) # prints 'bacon local'
spam()
print(eggs) # prints 'bacon local'
eggs = 'global'
bacon()
print(eggs) # prints 'global'

Khi bạn chạy chương trình, sẽ hiện thị ra màn hình như sau

bacon local
spam local
bacon local
global

Thực tế, có ba biến khác nhau trong chương trình này, nhưng điều khó hiểu là tất cả chúng đều được đặt tên là eggs. Các biến này được mô tả như sau:

  • Một biến có tên eggs tồn tại trong phạm vi cục bộ khi spam() được gọi.

  • Một biến có tên eggs tồn tại trong phạm vi cục bộ khi bacon() được gọi.

  • Một biến có tên eggs tồn tại trong phạm vi toàn cục.

Vì ba biến riêng biệt này đều có cùng tên, nên nó có thể là khó hiểu để theo dõi cái nào đang được sử dụng tại bất kỳ thời điểm nào. Đây là tại sao bạn nên tránh sử dụng cùng một tên biến trong các phạm vi khác nhau.
Lệnh global
Nếu bạn cần sửa đổi từ bên trong một hàm, hãy sử dụng câu lệnh gloabal. Nếu bạn có một dòng như global eggs ở đầu hàm, nó sẽ báo cho Python, trong hàm này, eggs đề cập đến biến toàn cục, vì vậy, đừng tạo một biến cục bộ có tên này. Ví dụ, nhập mã sau vào trình soạn thảo và lưu nó dưới dạng globalStatement.py:

def spam():
    global eggs
    eggs = 'spam'
eggs = 'global'
spam()
print(eggs)

Khi chạy chương trình hàm print() cuối sẽ in ra màn hình là:
`

spam

eggs được khai báo toàn cục ở đầu spam(), khi eggs được đặt thành ‘spam’, việc gán này được thực hiện cho eggs có phạm vi toàn cục. Không có biến eggs cục bộ nào được tạo.
Có bốn quy tắc để cho biết một biến nằm trong phạm vi cục bộ hay phạm vi toàn cầu:

  • Nếu một biến đang được sử dụng trong phạm vi toàn cục (nghĩa là bên ngoài tất cả các hàm), thì đó luôn là biến toàn cục.

  • Nếu có một câu lệnh global cho biến đó trong một hàm, thì đó là biến toàn cục.

  • Mặt khác, nếu biến được sử dụng trong câu lệnh gán trong hàm, thì đó là biến cục bộ.

  • Nhưng biến không được sử dụng trong câu lệnh gán, nó là biến toàn cục.
    Để hiểu rõ hơn về các quy tắc này, đây là một chương trình ví dụ. Nhập mã sau vào trình soạn thảo và lưu dưới dạng sameNameLocalGlobal.py:

def spam():
    global eggs
    eggs = 'spam' # this is the global
def bacon():
    eggs = 'bacon' # this is a local
def ham():
    print(eggs) # this is the global
eggs = 42 # this is the global
spam()
print(eggs)

Trong hàm spam(), eggs là biến số toàn cục vì có câu lệnh global về eggs ở đầu hàm. Trong bacon(), eggs là biến cục bộ vì có câu lệnh gán cho hàm đó trong hàm đó. Trong hàm ham(), eggs là biến toàn cục vì không có câu lệnh gán hoặc câu lệnh chung cho hàm đó. Nếu bạn chạy sameNameLocalGlobal.py, đầu ra sẽ như thế này:
`

spam

Trong một hàm, một biến sẽ luôn là toàn cục hoặc luôn luôn là cục bộ. Mã trong hàm không thể sử dụng một biến cục bộ có tên là eggs và sau đó sử dụng biến eggs toàn cục sau đó trong cùng hàm đó.
Chú ý: Nếu bạn muốn sửa đổi giá trị được lưu trữ trong một biến toàn cục từ trong một hàm, bạn phải sử dụng một câu lệnh global trên biến đó.
Nếu bạn cố gắng sử dụng biến cục bộ trong hàm trước khi gán giá trị với nó, như trong chương trình sau, Python sẽ báo lỗi cho bạn. Để thấy điều này, hãy nhập phần sau vào trình soạn thảo và lưu dưới dạng sameNameError.py:

def spam():
    print(eggs) # ERROR!
    eggs = 'spam local'
eggs = 'global'
spam()

Khi bạn chạy chương trình này sẽ có lỗi như sau:

Traceback (most recent call last):
    File "C:/sameNameError.py", line 6, in <module>
        spam()
    File "C:/sameNameError.py", line 2, in spam
        print(eggs) # ERROR!
UnboundLocalError: local variable 'eggs' referenced before assignment

Lỗi này xảy ra do Python thấy rằng có một câu lệnh gán cho eggs trong hàm spam() và do đó, coi eggs là cục bộ. Nhưng vì print(eggs) được thực thi trước khi eggs được gán bất cứ thứ gì, nên biến eggs cục bộ không ‘tồn tại’.Python sẽ không quay lại sử dụng biến số eggs toàn cục.
Hàm như hộp đen
Thông thường, tất cả những gì bạn cần biết về một hàm là đầu vào (tham số) và giá trị đầu ra của nó, bạn không phải luôn tự đặt gánh nặng cho cách mã của hàm thực sự hoạt động. Khi bạn nghĩ về các hàm theo cách cấp cao này, người ta thường nói rằng bạn đang đối xử với một chức năng như một hộp đen.
Ý tưởng này là nền tảng cho lập trình hiện đại. Các chương sau trong cuốn sách này sẽ cho bạn thấy một số mô-đun với các chức năng được viết bởi người khác. Khi bạn có thể xem qua mã nguồn nếu bạn tò mò, bạn không cần biết làm thế nào các hàm này hoạt động để sử dụng chúng. Và vì các hàm viết không có biến toàn cục được khuyến khích, bạn thường không phải lo lắng về mã của hàm tương tác với phần còn lại của chương trình.
Người dịch: Hungdh

2 Likes

example này bị lỗi ad ơi!

1 Like

Cám ơn bạn chắc mình viết nhầm.
Đoạn code đúng là.

def spam():

    eggs = 'spam local'

    print(eggs) # prints 'spam local'

def bacon():

    eggs = 'bacon local'

    print(eggs) # prints 'bacon local'

bacon()

spam()

eggs = 'global'

bacon()

print(eggs) # prints 'global'
1 Like