2015-09-22 17 views
5

Dường như việc truy cập vào phạm vi từ vựng có thể được thực hiện tại thời gian biên dịch (hoặc bởi một máy phân tích tĩnh, vì ví dụ của tôi là bằng Python) chỉ đơn giản là dựa vào vị trí trong mã nguồn.Phạm vi từ vựng có khía cạnh động không?

Dưới đây là một ví dụ rất đơn giản trong đó một hàm có hai bao đóng với các giá trị khác nhau cho a.

def elvis(a): 
    def f(s): 
    return a + ' for the ' + s 
    return f 

f1 = elvis('one') 
f2 = elvis('two') 
print f1('money'), f2('show') 

Tôi không có vấn đề với ý kiến ​​cho rằng khi chúng ta đang đọc mã cho chức năng f, khi chúng ta thấy a, nó không được định nghĩa trong f, vì vậy chúng tôi bật lên để chức năng kèm theo và tìm ai ở đó và đó là những gì a trong số f đề cập đến. Vị trí trong mã nguồn là đủ để cho tôi biết rằng f nhận giá trị cho a từ phạm vi bao quanh.

Nhưng như được mô tả here, khi một hàm được gọi, khung cục bộ của nó mở rộng môi trường mẹ của nó. Vì vậy, làm tra cứu môi trường trong thời gian chạy là không có vấn đề. Nhưng những gì tôi không chắc chắn là một máy phân tích tĩnh luôn luôn có thể làm việc ra mà đóng cửa được đề cập đến tại thời gian biên dịch, trước khi mã được chạy. Trong ví dụ trên, rõ ràng là elvis có hai bao đóng và rất dễ theo dõi chúng, nhưng các trường hợp khác sẽ không đơn giản như vậy. Bằng trực giác, tôi lo lắng rằng một nỗ lực phân tích tĩnh có thể xảy ra một vấn đề tạm dừng nói chung.

Vì vậy, phạm vi từ vựng thực sự có khía cạnh động đối với nó, vị trí trong mã nguồn cho chúng ta biết phạm vi bao hàm có liên quan nhưng không nhất thiết phải đóng cửa? Hay đây là một vấn đề được giải quyết trong các trình biên dịch, và tất cả các tham chiếu trong các hàm đóng của chúng thực sự có thể được làm việc chi tiết một cách tĩnh không?

Hoặc câu trả lời có phụ thuộc vào ngôn ngữ lập trình - trong trường hợp đó phạm vi từ vựng không phải là một khái niệm mạnh như tôi nghĩ?

[EDIT @comments:

Về ví dụ của tôi, tôi có thể trình bày lại câu hỏi của tôi: Tôi đọc tuyên bố như "độ phân giải từ vựng có thể được xác định tại thời gian biên dịch", nhưng tự hỏi như thế nào tham chiếu đến giá trị của a trong f1f2 có thể được tính toán tĩnh/tại thời gian biên dịch (nói chung).

Giải pháp là phạm vi từ vựng không đòi hỏi quá nhiều. L.S. có thể cho chúng tôi biết, tại thời điểm biên dịch, được gọi là sẽ được xác định bất cứ khi nào tôi đang ở f (và điều này rõ ràng có thể được tính toán tĩnh), nhưng xác định giá trị giá trị (hoặc, đóng cửa đang hoạt động) là 1) vượt quá LS khái niệm, 2) thực hiện tại thời gian chạy (không tĩnh) vì vậy là năng động trong một ý nghĩa, nhưng tất nhiên 3) sử dụng một quy tắc khác nhau từ phạm vi năng động.

Thông báo takeaway, để báo @PatrickMaupin, là "Một số công việc năng động vẫn phải được thực hiện". ]

+0

Điều gì khác có thể có nghĩa là gì? Ngôn ngữ lập trình phải được xác định, nếu không chúng tôi sẽ không sử dụng chúng. Mọi thứ đều khó khăn như vậy. – wallyk

+0

Tôi không chắc mình hiểu câu hỏi. Bạn có hỏi liệu các giá trị của 'f1' và' f2' có thể được xác định bằng cách sử dụng phân tích tĩnh không? – Barmar

+0

Theo định nghĩa, phạm vi từ vựng hoàn toàn là từ vựng. Bạn chỉ cần nhìn vào hàm lexical kèm theo, và hàm kèm theo của nó, v.v. – Barmar

Trả lời

5

Đóng cửa có thể được thực hiện theo nhiều cách. Một trong số đó là để thực sự nắm bắt được môi trường ... nói cách khác xem xét ví dụ

def foo(x): 
    y = 1 
    z = 2 
    def bar(a): 
     return (x, y, a) 
    return bar 

Giải pháp env-chụp đi như thế:

  1. foo được nhập và một khung địa phương được xây dựng có chứa x , y, z, bar tên. Tên x được ràng buộc với tham số, tên yz đến 1 và 2, tên bar để đóng cửa
  2. việc đóng cửa giao cho bar thực sự nắm bắt được khung cả cha mẹ nên khi nó được gọi là nó có thể tra cứu tên a trong khung cục bộ của riêng nó và có thể tra cứu xy thay vì trong khung cha mẹ đã chụp.

Với phương pháp này (có nghĩa là không phương pháp được sử dụng bởi Python) biến z sẽ vẫn còn sống càng lâu càng đóng cửa vẫn còn sống, ngay cả khi nó không được tham chiếu bởi việc đóng cửa.

Một tùy chọn khác, hơi phức tạp để thực hiện công trình thay vì như:

  1. tại thời gian biên dịch mã được phân tích và đóng cửa giao cho bar được phát hiện bắt tên xy từ phạm vi hiện tại.
  2. do đó hai biến này được phân loại là "ô" và chúng được phân bổ riêng biệt với khung địa phương
  3. đóng cửa lưu trữ địa chỉ của các biến này và mỗi truy cập vào chúng yêu cầu một đôi (một ô là một con trỏ đến nơi giá trị thực sự được lưu trữ)

Điều này đòi hỏi phải trả thêm một ít thời gian khi đóng cửa được tạo ra bởi vì mỗi tế bào bắt đơn đòi hỏi phải được sao chép bên trong đối tượng đóng cửa (thay vì chỉ cần sao chép một con trỏ vào khung mẹ) nhưng có lợi thế là không chụp toàn bộ khung hình, ví dụ: z sẽ không còn hoạt động sau khi trả về foo, chỉ xy sẽ.

Đây là những gì Python làm ... về cơ bản tại thời gian biên dịch khi đóng (hoặc một hàm được đặt tên hoặc lambda) được tìm thấy một trình biên dịch phụ được thực hiện. Trong quá trình biên dịch khi có một tra cứu giải quyết một hàm cha, biến được đánh dấu là một ô.

Một chút phiền toái là khi tham số được chụp (như trong ví dụ foo) cũng cần phải thực hiện thao tác sao chép bổ sung trong phần mở đầu để chuyển đổi giá trị đã truyền trong ô. Điều này trong Python không hiển thị trong bytecode nhưng được thực hiện trực tiếp bởi máy móc cuộc gọi.

Một sự khó chịu khác là mỗi truy cập vào một biến bị bắt buộc đòi hỏi phải có một cặp đôi ngay cả trong bối cảnh gốc.

Lợi thế là việc đóng chỉ nắm bắt các biến tham chiếu thực sự và khi chúng không nắm bắt bất kỳ mã được tạo nào hiệu quả như một hàm bình thường.

Để xem cách làm việc này bằng Python bạn có thể sử dụng các mô-đun dis kiểm tra bytecode tạo:

>>> dis.dis(foo) 
    2   0 LOAD_CONST    1 (1) 
       3 STORE_DEREF    1 (y) 

    3   6 LOAD_CONST    2 (2) 
       9 STORE_FAST    1 (z) 

    4   12 LOAD_CLOSURE    0 (x) 
      15 LOAD_CLOSURE    1 (y) 
      18 BUILD_TUPLE    2 
      21 LOAD_CONST    3 (<code object bar at 0x7f6ff6582270, file "<stdin>", line 4>) 
      24 LOAD_CONST    4 ('foo.<locals>.bar') 
      27 MAKE_CLOSURE    0 
      30 STORE_FAST    2 (bar) 

    6   33 LOAD_FAST    2 (bar) 
      36 RETURN_VALUE 
>>> 

như bạn có thể thấy các cửa hàng mã được tạo 1 vào y với một STORE_DEREF (các hoạt động mà ghi vào một tế bào, do đó sử dụng một đôi hướng) và thay vào đó lưu trữ 2 vào z bằng cách sử dụng STORE_FAST (z không được chụp và chỉ là một địa phương trong khung hiện tại). Khi mã số foo bắt đầu thực hiện x đã được gói trong một ô bằng máy móc cuộc gọi.

bar chỉ là một biến địa phương, vì vậy STORE_FAST được sử dụng để viết thư cho nó, nhưng để xây dựng đóng xy cần phải được sao chép cá nhân (họ đang đưa vào một tuple trước khi gọi MAKE_CLOSURE opcode).

Mã cho việc đóng cửa chính nó là có thể nhìn thấy với:

>>> dis.dis(foo(12)) 
    5   0 LOAD_DEREF    0 (x) 
       3 LOAD_DEREF    1 (y) 
       6 LOAD_FAST    0 (a) 
       9 BUILD_TUPLE    3 
      12 RETURN_VALUE 

và bạn có thể thấy rằng bên trong đóng cửa lại xy được truy cập với một LOAD_DEREF. Không có vấn đề bao nhiêu cấp độ "lên" trong hệ thống phân cấp chức năng lồng nhau một biến được định nghĩa, nó thực sự chỉ là một đôi-indirection đi bởi vì giá được trả tiền trong khi xây dựng đóng cửa. Các biến đóng kín chỉ chậm hơn một chút để truy cập (bởi một yếu tố không đổi) đối với người dân địa phương ... không có "chuỗi phạm vi" cần phải được duyệt qua trong thời gian chạy.

Trình biên dịch thậm chí còn phức tạp hơn như SBCL (trình biên dịch tối ưu hóa cho mã nguồn gốc Common Lisp) cũng thực hiện "phân tích thoát" để phát hiện xem đóng có thể thực sự tồn tại chức năng kèm theo hay không. Khi điều này không xảy ra (ví dụ: nếu bar chỉ được sử dụng bên trong foo và không được lưu trữ hoặc trả lại) các ô có thể được cấp phát trong ngăn xếp thay vì trên heap, giảm số lượng thời gian chạy "consing" (phân bổ các đối tượng trên heap) yêu cầu thu gom rác để thu hồi).

Sự khác biệt này là trong văn học được gọi là "xu hướng xuống/xuống"; tức là nếu các biến bị bắt chỉ hiển thị ở các cấp thấp hơn (tức là đóng cửa hoặc đóng sâu hơn được tạo bên trong phần đóng) hoặc ở cấp cao hơn (tức là nếu người gọi gọi của chúng tôi sẽ có thể truy cập vào những người dân địa phương đã bắt của tôi).

Để giải quyết vấn đề funarg hướng lên, người thu gom rác là cần thiết và đó là lý do tại sao đóng cửa C++ không cung cấp khả năng này.

+0

Giải thích tuyệt vời! –

+0

Điều này trả lời một câu hỏi lớn hơn nhiều, tôi là một phần nhỏ của nó! Cung cấp các mảnh của câu đố tôi đã mất tích (đề cập đến funarg downward-up là đặc biệt hữu ích). – gnarledRoot

+0

Sử dụng 'dis' là một tiền thưởng sáng - một lý do nữa tôi vui vì tôi đã viết mã mẫu của mình bằng Python. – gnarledRoot

1

Đây là một vấn đề được giải quyết ... dù bằng cách nào. Python sử dụng phạm vi từ vựng thuần túy, và việc đóng cửa được xác định tĩnh. Các ngôn ngữ khác cho phép phạm vi động - và đóng cửa được xác định trong thời gian chạy, tìm kiếm theo cách của nó lên ngăn xếp cuộc gọi thời gian chạy thay vì ngăn xếp phân tích cú pháp.

Đây có phải là giải thích đầy đủ không?

+1

_not_ được xác định trong thời gian chạy? – Barmar

+0

OOPS. Tôi đã thay đổi từ ngữ và không hoàn tất chỉnh sửa. Đã sửa lỗi. – Prune

1

Trong Python, một biến được xác định là địa phương nếu nó được gán cho (xuất hiện trên LHS của một bài tập) và không được khai báo rõ ràng toàn cục hoặc không trung tâm.

Vì vậy, có thể thiết lập chuỗi phạm vi từ lexical để xác định tĩnh số nhận dạng nào sẽ được tìm thấy trong hàm nào. Tuy nhiên, một số công việc động vẫn phải được thực hiện bởi vì bạn có thể tùy ý lồng các hàm, do đó, nếu hàm A bao gồm hàm B bao gồm hàm C, thì đối với hàm C để truy cập biến từ hàm A, bạn phải tìm đúng khung A. (Điều tương tự cho việc đóng cửa.)

Các vấn đề liên quan