từ khóa nonlocal trong Python 2.x


117

Tôi đang cố gắng triển khai một bao đóng trong Python 2.6 và tôi cần truy cập một biến phi địa phương nhưng có vẻ như từ khóa này không khả dụng trong python 2.x. Làm cách nào để truy cập các biến phi địa phương trong các bao đóng trong các phiên bản python này?

Câu trả lời:


125

Các hàm bên trong có thể đọc các biến phi địa phương trong 2.x, chỉ cần không lặp lại chúng. Điều này thật khó chịu, nhưng bạn có thể giải quyết nó. Chỉ cần tạo một từ điển và lưu trữ dữ liệu của bạn dưới dạng các phần tử trong đó. Các hàm bên trong không bị cấm thay đổi các đối tượng mà các biến phi địa phương tham chiếu đến.

Để sử dụng ví dụ từ Wikipedia:

def outer():
    d = {'y' : 0}
    def inner():
        d['y'] += 1
        return d['y']
    return inner

f = outer()
print(f(), f(), f()) #prints 1 2 3

4
tại sao có thể sửa đổi giá trị từ từ điển?
coelhudo

9
@coelhudo Vì bạn có thể sửa đổi các biến phi địa phương. Nhưng bạn không thể gán cho các biến phi địa phương. Ví dụ, điều này sẽ làm tăng UnboundLocalError: def inner(): print d; d = {'y': 1}. Ở đây, print dđọc bên ngoài ddo đó tạo biến phi địa phương dtrong phạm vi bên trong.
suzanshakya

21
Cảm ơn vì câu trả lời này. Tôi nghĩ bạn có thể cải thiện thuật ngữ: thay vì "có thể đọc, không thể thay đổi", có thể "có thể tham chiếu, không thể gán cho". Bạn có thể thay đổi nội dung của một đối tượng trong một phạm vi phi địa phương, nhưng bạn không thể thay đổi đối tượng nào được tham chiếu.
metamatt

3
Ngoài ra, bạn có thể sử dụng lớp tùy ý và đối tượng khởi tạo thay vì từ điển. Tôi thấy nó thanh lịch và dễ đọc hơn nhiều vì nó không làm ngập mã bằng các từ (khóa từ điển), ngoài ra bạn có thể sử dụng các phương thức.
Alois Mahdal

6
Đối với Python, tôi nghĩ tốt nhất là sử dụng động từ "bind" hoặc "rebind" và sử dụng danh từ "name" thay vì "biến". Trong Python 2.x, bạn có thể đóng trên một đối tượng nhưng bạn không thể gắn lại tên trong phạm vi ban đầu. Trong một ngôn ngữ như C, việc khai báo một biến dự trữ một số dung lượng lưu trữ (hoặc lưu trữ tĩnh hoặc tạm thời trên ngăn xếp). Trong Python, biểu thức X = 1chỉ đơn giản là liên kết tên Xvới một đối tượng cụ thể (an intvới giá trị 1). X = 1; Y = Xliên kết hai tên với cùng một đối tượng chính xác. Dù sao, một số đối tượng có thể thay đổi và bạn có thể thay đổi giá trị của chúng.
steveha

37

Giải pháp sau đây được lấy cảm hứng từ câu trả lời của Elias Zamaria , nhưng trái ngược với câu trả lời đó xử lý nhiều lệnh gọi của hàm ngoài một cách chính xác. "Biến" inner.ylà cục bộ cho cuộc gọi hiện tại của outer. Chỉ nó không phải là một biến, vì điều đó bị cấm, mà là một thuộc tính đối tượng (đối tượng là innerchính hàm ). Điều này rất xấu (lưu ý rằng thuộc tính chỉ có thể được tạo sau khi innerhàm được xác định) nhưng có vẻ hiệu quả.

def outer():
    def inner():
        inner.y += 1
        return inner.y
    inner.y = 0
    return inner

f = outer()
g = outer()
print(f(), f(), g(), f(), g()) #prints (1, 2, 1, 3, 2)

Nó không phải là uggly. Thay vì sử dụng inner.y, hãy sử dụng ngoài.y. Bạn có thể định nghĩa bên ngoài.y = 0 trước phần tử bên trong.
jgomo3

1
Ok, tôi thấy nhận xét của tôi là sai. Elias Zamaria cũng đã triển khai giải pháp ngoài. Nhưng như nhận xét của Nathaniel [1], bạn nên cẩn thận. Tôi nghĩ câu trả lời này nên được quảng bá là giải pháp, nhưng lưu ý về giải pháp bên ngoài. [1] stackoverflow.com/questions/3190706/…
jgomo3

Điều này trở nên tồi tệ nếu bạn muốn nhiều hơn một hàm bên trong chia sẻ trạng thái có thể thay đổi phi địa phương. Nói a inc()và a dec()trả về từ bên ngoài mà tăng và giảm một bộ đếm dùng chung. Sau đó, bạn phải quyết định chức năng nào để gắn giá trị bộ đếm hiện tại và tham chiếu chức năng đó từ (các) chức năng khác. Trông hơi lạ và không đối xứng. Ví dụ: trong dec()một dòng như thế inc.value -= 1.
BlackJack

33

Thay vì một từ điển, có ít sự lộn xộn hơn đối với một lớp phi địa phương . Sửa đổi ví dụ của @ ChrisB :

def outer():
    class context:
        y = 0
    def inner():
        context.y += 1
        return context.y
    return inner

Sau đó

f = outer()
assert f() == 1
assert f() == 2
assert f() == 3
assert f() == 4

Mỗi lệnh gọi bên ngoài () tạo ra một lớp mới và riêng biệt được gọi là ngữ cảnh (không chỉ đơn thuần là một thể hiện mới). Vì vậy, nó tránh được @ Nathaniel cẩn thận về ngữ cảnh được chia sẻ.

g = outer()
assert g() == 1
assert g() == 2

assert f() == 5

Đẹp! Điều này thực sự là thanh lịch và dễ đọc hơn từ điển.
Vincenzo

Tôi khuyên bạn nên sử dụng các khe nói chung ở đây. Nó bảo vệ bạn khỏi lỗi chính tả.
DerWeh

@DerWeh thật thú vị, tôi chưa bao giờ nghe nói về điều đó. Bạn có thể nói giống nhau của bất kỳ lớp nào? Tôi ghét bị bối rối bởi lỗi chính tả.
Bob Stein

@BobStein Xin lỗi, tôi không thực sự hiểu ý bạn. Nhưng bằng cách thêm __slots__ = ()và tạo một đối tượng thay vì sử dụng lớp, ví dụ: context.z = 3sẽ nâng cao một AttributeError. Điều này có thể xảy ra cho tất cả các lớp, trừ khi chúng kế thừa từ một lớp không xác định vị trí.
DerWeh

@DerWeh Tôi chưa bao giờ nghe nói về việc sử dụng khe để bắt lỗi chính tả. Bạn nói đúng, nó chỉ hữu ích trong các trường hợp lớp, không phải các biến lớp. Có thể bạn muốn tạo một ví dụ làm việc trên pyfiddle. Nó sẽ yêu cầu ít nhất hai dòng bổ sung. Không thể hình dung bạn sẽ làm như thế nào nếu không có nhiều lộn xộn.
Bob Stein

14

Tôi nghĩ rằng chìa khóa ở đây là những gì bạn có nghĩa là "truy cập". Sẽ không có vấn đề gì khi đọc một biến bên ngoài phạm vi đóng, ví dụ:

x = 3
def outer():
    def inner():
        print x
    inner()
outer()

sẽ hoạt động như mong đợi (in 3). Tuy nhiên, ghi đè giá trị của x không hoạt động, ví dụ:

x = 3
def outer():
    def inner():
        x = 5
    inner()
outer()
print x

sẽ vẫn in 3. Theo hiểu biết của tôi về PEP-3104, đây là những gì từ khóa phi địa phương có nghĩa là bao gồm. Như đã đề cập trong PEP, bạn có thể sử dụng một lớp để thực hiện điều tương tự (hơi lộn xộn):

class Namespace(object): pass
ns = Namespace()
ns.x = 3
def outer():
    def inner():
        ns.x = 5
    inner()
outer()
print ns.x

Thay vì tạo một lớp và khởi tạo nó, người ta có thể chỉ cần tạo một hàm: def ns(): passtheo sau là ns.x = 3. Nó không đẹp, nhưng nó hơi xấu đối với mắt tôi.
davidchambers

2
Bất kỳ lý do cụ thể nào cho việc giảm giá? Tôi thừa nhận tôi không phải là giải pháp thanh lịch nhất, nhưng nó hoạt động ...
ig0774

2
Về class Namespace: x = 3thì sao?
Feuermurmel

3
Tôi không nghĩ cách này mô phỏng phi địa phương, vì nó tạo ra một tham chiếu toàn cục thay vì một tham chiếu trong một bao đóng.
CarmeloS

1
Tôi đồng ý với @CarmeloS, khối mã cuối cùng của bạn không thực hiện những gì PEP-3104 đề xuất như một cách để thực hiện điều gì đó tương tự trong Python 2. nslà một đối tượng toàn cục, đó là lý do tại sao bạn có thể tham chiếu ns.xở cấp mô-đun trong printcâu lệnh ở cuối .
martineau

13

Có một cách khác để triển khai các biến phi địa phương trong Python 2, trong trường hợp bất kỳ câu trả lời nào ở đây là không mong muốn vì bất kỳ lý do gì:

def outer():
    outer.y = 0
    def inner():
        outer.y += 1
        return outer.y
    return inner

f = outer()
print(f(), f(), f()) #prints 1 2 3

Việc sử dụng tên của hàm trong câu lệnh gán của biến là hơi thừa, nhưng đối với tôi thì nó trông đơn giản và gọn gàng hơn là đặt biến vào từ điển. Giá trị được ghi nhớ từ lệnh gọi này đến lệnh gọi khác, giống như trong câu trả lời của Chris B.


19
Hãy lưu ý: khi thực hiện theo cách này, nếu bạn làm f = outer()và sau đó làm g = outer(), thì fbộ đếm của sẽ được đặt lại. Điều này là do cả hai đều chia sẻ một biến duy nhất outer.y , thay vì mỗi biến có một biến độc lập của riêng mình. Mặc dù mã này trông có vẻ thẩm mỹ hơn câu trả lời của Chris B, nhưng cách của anh ấy dường như là cách duy nhất để mô phỏng phạm vi từ vựng nếu bạn muốn gọi outernhiều lần.
Nathaniel

@Nathaniel: Để tôi xem tôi có hiểu đúng về điều này không. Việc gán cho outer.ykhông liên quan đến bất kỳ thứ gì cục bộ đối với lời gọi hàm (phiên bản) outer(), nhưng gán cho một thuộc tính của đối tượng hàm được liên kết với tên outertrong phạm vi bao quanh của nó . Và do đó, người ta có thể sử dụng, bằng văn bản outer.y, bất kỳ tên nào khác thay thế outer, miễn là nó được biết là bị ràng buộc trong phạm vi đó. Điều này có chính xác?
Marc van Leeuwen

1
Tôi nên sửa lại, sau khi "ràng buộc trong phạm vi đó": với một đối tượng có kiểu cho phép thiết lập các thuộc tính (như một hàm hoặc bất kỳ cá thể lớp nào). Ngoài ra, vì phạm vi này thực sự xa hơn phạm vi chúng tôi muốn, điều này sẽ không đề xuất giải pháp cực kỳ xấu xí sau: thay vì outer.ysử dụng tên inner.y(vì innerđược ràng buộc bên trong cuộc gọi outer(), đó chính xác là phạm vi chúng tôi muốn), nhưng đặt khởi tạo inner.y = 0 sau khi định nghĩa bên trong (vì đối tượng phải tồn tại khi thuộc tính của nó được tạo), nhưng tất nhiên là trước return inner?
Marc van Leeuwen

@MarcvanLeeuwen Có vẻ như nhận xét của bạn đã truyền cảm hứng cho giải pháp với inner.y của Elias. Suy nghĩ tốt của bạn.
Ankur Agarwal

Truy cập và cập nhật các biến toàn cục có lẽ không phải là điều mà hầu hết mọi người đang cố gắng làm khi thay đổi các bản ghi của một bao đóng.
binki

12

Đây là điều gì đó được truyền cảm hứng từ một gợi ý mà Alois Mahdal đưa ra trong một bình luận về một câu trả lời khác :

class Nonlocal(object):
    """ Helper to implement nonlocal names in Python 2.x """
    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)


def outer():
    nl = Nonlocal(y=0)
    def inner():
        nl.y += 1
        return nl.y
    return inner

f = outer()
print(f(), f(), f()) # -> (1 2 3)

Cập nhật

Sau khi nhìn lại điều này gần đây, tôi đã bị ấn tượng bởi nó giống như một nhà trang trí - khi nó cho tôi thấy rằng việc triển khai nó như một người sẽ làm cho nó chung chung và hữu ích hơn (mặc dù làm như vậy có thể làm giảm khả năng đọc của nó ở một mức độ nào đó).

# Implemented as a decorator.

class Nonlocal(object):
    """ Decorator class to help implement nonlocal names in Python 2.x """
    def __init__(self, **kwargs):
        self._vars = kwargs

    def __call__(self, func):
        for k, v in self._vars.items():
            setattr(func, k, v)
        return func


@Nonlocal(y=0)
def outer():
    def inner():
        outer.y += 1
        return outer.y
    return inner


f = outer()
print(f(), f(), f()) # -> (1 2 3)

Lưu ý rằng cả hai phiên bản đều hoạt động trên cả Python 2 và 3.


Cái này có vẻ là tốt nhất về khả năng đọc và bảo quản phạm vi từ vựng.
Aaron S. Kurland

3

Có một vấn đề trong quy tắc xác định phạm vi của python - phép gán làm cho một biến cục bộ trở thành phạm vi hàm bao quanh ngay lập tức của nó. Đối với một biến toàn cục, bạn sẽ giải quyết điều này bằng globaltừ khóa.

Giải pháp là giới thiệu một đối tượng được chia sẻ giữa hai phạm vi, có chứa các biến có thể thay đổi, nhưng chính nó được tham chiếu thông qua một biến không được gán.

def outer(v):
    def inner(container = [v]):
        container[0] += 1
        return container[0]
    return inner

Một giải pháp thay thế là một số hackery phạm vi:

def outer(v):
    def inner(varname = 'v', scope = locals()):
        scope[varname] += 1
        return scope[varname]
    return inner

Bạn có thể tìm ra một số thủ thuật để lấy tên của tham số outer, sau đó chuyển nó dưới dạng varname, nhưng không dựa vào tên outerbạn muốn, bạn cần sử dụng bộ tổ hợp Y.


Cả hai phiên bản đều hoạt động trong trường hợp cụ thể này nhưng không thay thế cho nonlocal. locals()tạo một từ điển của outer()các địa phương tại thời điểm inner()được xác định nhưng việc thay đổi từ điển đó không thay đổi vtrong outer(). Điều này sẽ không hoạt động nữa khi bạn có nhiều hàm bên trong muốn chia sẻ một biến đóng trên. Nói a inc()dec()tăng và giảm một bộ đếm dùng chung.
BlackJack

@BlackJack nonlocallà một tính năng của python 3.
Marcin

Vâng tôi biết. Câu hỏi đặt ra là làm thế nào để đạt được hiệu quả của Python 3 nonlocaltrong Python 2 nói chung . Ý tưởng của bạn không bao gồm trường hợp chung mà chỉ là trường hợp có một chức năng bên trong. Hãy xem ý chính này để làm ví dụ. Cả hai chức năng bên trong đều có hộp đựng riêng. Bạn cần một đối tượng có thể thay đổi trong phạm vi của hàm bên ngoài, như các câu trả lời khác đã đề xuất.
BlackJack

@BlackJack Đó không phải là câu hỏi, nhưng cảm ơn bạn đã đóng góp ý kiến.
Marcin

Vâng, đó câu hỏi. Hãy nhìn vào đầu trang. adinsa có Python 2.6 và cần truy cập vào các biến không cục bộ trong một kết thúc mà không có nonlocaltừ khóa được giới thiệu trong Python 3.
BlackJack

3

Một cách khác để làm điều đó (mặc dù nó quá dài dòng):

import ctypes

def outer():
    y = 0
    def inner():
        ctypes.pythonapi.PyCell_Set(id(inner.func_closure[0]), id(y + 1))
        return y
    return inner

x = outer()
x()
>> 1
x()
>> 2
y = outer()
y()
>> 1
x()
>> 3

1
thực sự tôi thích giải pháp sử dụng một cuốn từ điển, nhưng điều này là mát :)
Ezer Fernandes

0

Mở rộng giải pháp thanh lịch Martineau ở trên sang một trường hợp sử dụng thực tế và hơi kém thanh lịch mà tôi nhận được:

class nonlocals(object):
""" Helper to implement nonlocal names in Python 2.x.
Usage example:
def outer():
     nl = nonlocals( n=0, m=1 )
     def inner():
         nl.n += 1
     inner() # will increment nl.n

or...
    sums = nonlocals( { k:v for k,v in locals().iteritems() if k.startswith('tot_') } )
"""
def __init__(self, **kwargs):
    self.__dict__.update(kwargs)

def __init__(self, a_dict):
    self.__dict__.update(a_dict)

-3

Sử dụng một biến toàn cục

def outer():
    global y # import1
    y = 0
    def inner():
        global y # import2 - requires import1
        y += 1
        return y
    return inner

f = outer()
print(f(), f(), f()) #prints 1 2 3

Cá nhân tôi không thích các biến toàn cục. Tuy nhiên, đề xuất của tôi dựa trên câu trả lời https://stackoverflow.com/a/19877437/1083704

def report():
        class Rank: 
            def __init__(self):
                report.ranks += 1
        rank = Rank()
report.ranks = 0
report()

nơi người dùng cần khai báo một biến toàn cục ranks, mỗi khi bạn cần gọi report. Cải tiến của tôi loại bỏ nhu cầu khởi tạo các biến hàm từ người dùng.


Tốt hơn là sử dụng từ điển. Bạn có thể tham chiếu phiên bản trong inner, nhưng không thể gán cho nó, nhưng bạn có thể sửa đổi các khóa và giá trị của nó. Điều này tránh sử dụng các biến toàn cục.
johannestaas
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.