คีย์เวิร์ด nonlocal ใน Python 2.x


117

ฉันพยายามใช้การปิดใน Python 2.6 และฉันต้องการเข้าถึงตัวแปร nonlocal แต่ดูเหมือนว่าคีย์เวิร์ดนี้จะไม่มีใน python 2.x วิธีหนึ่งจะเข้าถึงตัวแปร nonlocal ในการปิดใน python เวอร์ชันเหล่านี้ได้อย่างไร

คำตอบ:


125

ฟังก์ชันภายในสามารถอ่านตัวแปร nonlocal ได้ใน 2.x เพียง แต่ไม่สามารถย้อนกลับได้ สิ่งนี้น่ารำคาญ แต่คุณสามารถแก้ไขได้ เพียงสร้างพจนานุกรมและจัดเก็บข้อมูลของคุณเป็นองค์ประกอบในนั้น ฟังก์ชันภายในไม่ได้รับอนุญาตจากการกลายพันธุ์ของวัตถุที่ตัวแปรที่ไม่ใช่โลคัลอ้างถึง

ในการใช้ตัวอย่างจาก 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
เหตุใดจึงสามารถแก้ไขค่าจากพจนานุกรมได้
coelhudo

9
@coelhudo เนื่องจากคุณสามารถแก้ไขตัวแปรที่ไม่ใช่โลคัล แต่คุณไม่สามารถกำหนดให้กับตัวแปรที่ไม่ใช่เฉพาะพื้นที่ได้ เช่นนี้จะเพิ่ม def inner(): print d; d = {'y': 1}UnboundLocalError: ที่นี่print dอ่านด้านนอกdจึงสร้างตัวแปร nonlocal dในขอบเขตภายใน
suzanshakya

21
ขอบคุณสำหรับคำตอบนี้ ฉันคิดว่าคุณสามารถปรับปรุงคำศัพท์ได้ แต่แทนที่จะเป็น "สามารถอ่านเปลี่ยนไม่ได้" อาจจะ "อ้างอิงได้ไม่สามารถกำหนดให้" คุณสามารถเปลี่ยนเนื้อหาของออบเจ็กต์ในขอบเขตที่ไม่ใช่โลคัล แต่คุณไม่สามารถเปลี่ยนอ็อบเจ็กต์ที่อ้างถึงได้
metamatt

3
หรือคุณสามารถใช้คลาสตามอำเภอใจและสร้างอินสแตนซ์อ็อบเจ็กต์แทนพจนานุกรมได้ ฉันพบว่ามันดูหรูหราและอ่านง่ายกว่ามากเนื่องจากโค้ดที่มีตัวอักษร (คีย์พจนานุกรม) ไม่ท่วมรวมทั้งคุณสามารถใช้วิธีต่างๆได้
Alois Mahdal

6
สำหรับ Python ฉันคิดว่าควรใช้คำกริยา "bind" หรือ "rebind" และใช้คำนาม "name" แทน "variable" ใน Python 2.x คุณสามารถปิดทับวัตถุได้ แต่คุณไม่สามารถผูกชื่อซ้ำในขอบเขตเดิมได้ ในภาษาเช่น C การประกาศตัวแปรจะสงวนพื้นที่จัดเก็บบางส่วน (ไม่ว่าจะเป็นหน่วยเก็บแบบคงที่หรือแบบชั่วคราวบนสแตก) ใน Python นิพจน์X = 1จะผูกชื่อXกับออบเจ็กต์เฉพาะ (an intwith the value 1) X = 1; Y = Xผูกชื่อสองชื่อเข้ากับวัตถุเดียวกัน อย่างไรก็ตามวัตถุบางอย่างไม่แน่นอนและคุณสามารถเปลี่ยนค่าได้
steveha

37

วิธีแก้ปัญหาต่อไปนี้ได้รับแรงบันดาลใจจากคำตอบของ Elias Zamariaแต่ตรงกันข้ามกับคำตอบนั้นจัดการการเรียกใช้ฟังก์ชันภายนอกได้หลายสายอย่างถูกต้อง ว่า "ตัวแปร" เป็นท้องถิ่นที่จะสายปัจจุบันของinner.y outerเพียง แต่ไม่ใช่ตัวแปรเนื่องจากเป็นสิ่งต้องห้าม แต่เป็นแอตทริบิวต์ของวัตถุ (วัตถุที่เป็นฟังก์ชันinnerนั้นเอง) สิ่งนี้น่าเกลียดมาก (โปรดทราบว่าแอตทริบิวต์สามารถสร้างได้หลังจากกำหนดinnerฟังก์ชันแล้วเท่านั้น) แต่ดูเหมือนจะได้ผล

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)

มันไม่ได้น่าเกลียด แทนที่จะใช้ inner.y ให้ใช้ outer.y คุณสามารถกำหนด outer.y = 0 ก่อน def inner
jgomo3

1
โอเคฉันเห็นว่าความคิดเห็นของฉันไม่ถูกต้อง Elias Zamaria ใช้โซลูชันด้านนอกด้วยเช่นกัน แต่ตามความเห็นของนาธาเนียล [1] คุณควรระวัง ฉันคิดว่าคำตอบนี้ควรได้รับการส่งเสริมให้เป็นวิธีแก้ปัญหา แต่โปรดสังเกตเกี่ยวกับการแก้ปัญหาด้านนอกและคำเตือน [1] stackoverflow.com/questions/3190706/…
jgomo3

สิ่งนี้จะน่าเกลียดถ้าคุณต้องการมากกว่าหนึ่งฟังก์ชันภายในที่แชร์สถานะที่ไม่สามารถเปลี่ยนแปลงได้ พูดinc()และdec()ส่งกลับจากภายนอกที่เพิ่มและลดตัวนับที่ใช้ร่วมกัน จากนั้นคุณต้องตัดสินใจว่าฟังก์ชันใดที่จะแนบค่าตัวนับปัจจุบันและอ้างอิงฟังก์ชันนั้นจากฟังก์ชันอื่น ซึ่งดูแปลกตาและสมส่วนอยู่บ้าง เช่นในdec()บรรทัดเช่นinc.value -= 1.
BlackJack

33

แทนที่จะเป็นพจนานุกรมมีความยุ่งเหยิงน้อยกว่าสำหรับคลาสที่ไม่ใช่คนในท้องถิ่น การปรับเปลี่ยนตัวอย่างของ @ ChrisB :

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

แล้วก็

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

การเรียกภายนอก () แต่ละครั้งจะสร้างคลาสใหม่ที่แตกต่างกันซึ่งเรียกว่าบริบท (ไม่ใช่แค่อินสแตนซ์ใหม่) ดังนั้นจึงหลีกเลี่ยง@ Nathaniel ระวังบริบทที่ใช้ร่วมกัน

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

assert f() == 5

ดี! นี่เป็นพจนานุกรมที่สวยงามและอ่านง่ายกว่า
Vincenzo

ฉันอยากจะแนะนำให้ใช้สล็อตโดยทั่วไปที่นี่ ช่วยปกป้องคุณจากการพิมพ์ผิด
DerWeh

@DerWeh น่าสนใจฉันไม่เคยได้ยินมาก่อน คุณสามารถพูดว่าชั้นเรียนเดียวกันได้หรือไม่? ฉันเกลียดการพิมพ์ผิด
Bob Stein

@BobStein ขออภัยฉันไม่เข้าใจจริงๆว่าคุณหมายถึงอะไร แต่ด้วยการเพิ่ม__slots__ = ()และสร้างวัตถุแทนการใช้คลาสเช่นcontext.z = 3จะเพิ่มAttributeErrorไฟล์. เป็นไปได้สำหรับทุกคลาสเว้นแต่จะสืบทอดมาจากคลาสที่ไม่ได้กำหนดสล็อต
DerWeh

@DerWeh ฉันไม่เคยได้ยินเรื่องการใช้ช่องเพื่อจับผิด คุณคิดถูกแล้วที่จะช่วยในอินสแตนซ์ของคลาสเท่านั้นไม่ใช่ตัวแปรคลาส บางทีคุณอาจต้องการสร้างตัวอย่างการทำงานบน pyfiddle จะต้องมีบรรทัดเพิ่มเติมอย่างน้อยสองบรรทัด นึกภาพไม่ออกว่าจะทำอย่างไรโดยไม่เกะกะ
Bob Stein

14

ฉันคิดว่ากุญแจสำคัญในที่นี้คือความหมายของ "การเข้าถึง" ไม่ควรมีปัญหาในการอ่านตัวแปรนอกขอบเขตการปิดเช่น

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

ควรทำงานตามที่คาดไว้ (การพิมพ์ 3) อย่างไรก็ตามการลบล้างค่าของ x ไม่ได้ผลเช่น

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

จะยังคงพิมพ์ 3 จากความเข้าใจของฉันเกี่ยวกับPEP-3104นี่คือสิ่งที่คำหลักที่ไม่ใช่คนท้องถิ่นหมายถึง ดังที่ได้กล่าวไว้ใน PEP คุณสามารถใช้คลาสเพื่อทำสิ่งเดียวกันให้สำเร็จ (แบบยุ่ง ๆ ):

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

แทนการสร้างชั้นเรียนและอินสแตนซ์มันหนึ่งก็สามารถสร้างฟังก์ชั่น: ตามมาด้วยdef ns(): pass ns.x = 3มันไม่สวย แต่ตาของฉันน่าเกลียดน้อยกว่าเล็กน้อย
davidchambers

2
เหตุผลใดเป็นพิเศษในการโหวตลด? ฉันยอมรับว่าของฉันไม่ใช่วิธีแก้ปัญหาที่หรูหราที่สุด แต่ใช้ได้ผล ...
ig0774

2
เกี่ยวกับอะไรclass Namespace: x = 3?
Feuermurmel

3
ฉันไม่คิดว่าวิธีนี้เป็นการจำลองแบบ nonlocal เนื่องจากเป็นการสร้างการอ้างอิงทั่วโลกแทนที่จะเป็นการอ้างอิงในการปิด
CarmeloS

1
ฉันเห็นด้วยกับ @CarmeloS โค้ดบล็อกสุดท้ายของคุณที่ไม่ทำตามที่ PEP-3104 แนะนำว่าเป็นวิธีที่จะบรรลุสิ่งที่คล้ายกันใน Python 2 nsเป็นวัตถุระดับโลกซึ่งเป็นเหตุผลที่คุณสามารถอ้างอิงns.xในระดับโมดูลในprintคำสั่งในตอนท้าย .
martineau

13

มีอีกวิธีหนึ่งในการใช้ตัวแปร nonlocal ใน Python 2 ในกรณีที่คำตอบใด ๆ ที่นี่ไม่เป็นที่ต้องการไม่ว่าด้วยเหตุผลใดก็ตาม:

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

การใช้ชื่อของฟังก์ชันในคำสั่งมอบหมายของตัวแปรนั้นซ้ำซ้อน แต่มันดูง่ายและสะอาดกว่าสำหรับฉันมากกว่าการใส่ตัวแปรในพจนานุกรม ค่าจะถูกจดจำจากสายหนึ่งไปยังอีกสายหนึ่งเช่นเดียวกับคำตอบของ Chris B.


19
โปรดระวัง: เมื่อนำวิธีนี้ถ้าคุณทำf = outer()และหลังจากนั้นก็ทำg = outer()แล้วf's เคาน์เตอร์จะถูกรีเซ็ต เนื่องจากทั้งสองแชร์ตัวแปรเดียว outer.yแทนที่จะมีตัวแปรอิสระของตัวเอง แม้ว่ารหัสนี้จะดูสวยงามกว่าคำตอบของ Chris B แต่วิธีของเขาดูเหมือนจะเป็นวิธีเดียวที่จะเลียนแบบการกำหนดขอบเขตคำศัพท์หากคุณต้องการโทรouterมากกว่าหนึ่งครั้ง
Nathaniel

@ นาธาเนียล: ให้ฉันดูว่าฉันเข้าใจถูกต้องหรือไม่ การกำหนดให้outer.yไม่เกี่ยวข้องกับการเรียกใช้ฟังก์ชันในระบบ (อินสแตนซ์) outer()แต่กำหนดให้กับแอตทริบิวต์ของอ็อบเจ็กต์ฟังก์ชันที่ผูกไว้กับชื่อouterในขอบเขตการปิดล้อม และดังนั้นจึงหนึ่งได้อย่างเท่าเทียมกันทั้งได้ใช้ในการเขียนouter.y, อื่น ๆชื่อแทนouterให้มันเป็นที่รู้จักกันที่จะผูกพันอยู่ในขอบเขตที่ ถูกต้องหรือไม่
Marc van Leeuwen

1
การแก้ไขฉันควรจะพูดหลังจาก "ถูกผูกไว้ในขอบเขตนั้น": กับวัตถุที่มีประเภทอนุญาตการตั้งค่าแอตทริบิวต์ (เช่นฟังก์ชันหรืออินสแตนซ์คลาสใด ๆ ) นอกจากนี้เนื่องจากขอบเขตนี้อยู่ไกลกว่าขอบเขตที่เราต้องการจริง ๆ สิ่งนี้จะไม่แนะนำวิธีแก้ปัญหาที่น่าเกลียดอย่างยิ่งต่อไปนี้: แทนที่จะouter.yใช้ชื่อinner.y(เนื่องจากinnerถูกผูกไว้ในการโทรouter()ซึ่งเป็นขอบเขตที่เราต้องการ) แต่การวาง การเริ่มต้นinner.y = 0 หลังจากนิยามภายใน (เนื่องจากวัตถุต้องมีอยู่เมื่อสร้างแอตทริบิวต์) แต่ก่อนหน้านี้แน่นอนreturn inner?
Marc van Leeuwen

@MarcvanLeeuwen ดูเหมือนว่าความคิดเห็นของคุณเป็นแรงบันดาลใจในการแก้ปัญหาด้วย inner.y โดย Elias คุณคิดดี
Ankur Agarwal

การเข้าถึงและอัปเดตตัวแปรส่วนกลางอาจไม่ใช่สิ่งที่คนส่วนใหญ่พยายามทำเมื่อทำการเปลี่ยนแปลงการจับการปิด
binki

12

นี่คือสิ่งที่ได้รับแรงบันดาลใจจากคำแนะนำ Alois Mahdal ที่แสดงความคิดเห็นเกี่ยวกับคำตอบอื่น:

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)

ปรับปรุง

หลังจากมองย้อนกลับไปเมื่อไม่นานมานี้ฉันรู้สึกทึ่งกับความเหมือนของมัณฑนากร - เมื่อฉันเริ่มคิดว่าการใช้มันเป็นสิ่งที่จะทำให้มันเป็นเรื่องธรรมดาและมีประโยชน์มากขึ้น (แม้ว่าการทำเช่นนั้นจะทำให้ความสามารถในการอ่านลดลงไปบ้างก็ตาม)

# 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)

โปรดทราบว่าทั้งสองเวอร์ชันทำงานได้ทั้งใน Python 2 และ 3


อันนี้ดูเหมือนจะดีที่สุดในแง่ของความสามารถในการอ่านและการรักษาขอบเขตคำศัพท์
Aaron S.Kurland

3

มีหูดอยู่ในกฎการกำหนดขอบเขตของไพ ธ อน - การกำหนดทำให้ตัวแปรในเครื่องไปยังขอบเขตฟังก์ชันที่ปิดทันที สำหรับตัวแปรส่วนกลางคุณจะแก้ปัญหานี้ด้วยglobalคำหลัก

วิธีแก้ปัญหาคือการแนะนำออบเจ็กต์ที่ใช้ร่วมกันระหว่างสองขอบเขตซึ่งมีตัวแปรที่ไม่แน่นอน แต่อ้างอิงตัวเองผ่านตัวแปรที่ไม่ได้กำหนด

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

อีกทางเลือกหนึ่งคือแฮ็กเกอร์ขอบเขตบางส่วน:

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

คุณอาจสามารถหากลอุบายบางอย่างเพื่อให้ได้ชื่อของพารามิเตอร์ไปouterแล้วส่งเป็น varname แต่ไม่ต้องอาศัยชื่อที่outerคุณต้องการต้องใช้ตัวผสม Y


ทั้งสองเวอร์ชันใช้งานได้ในกรณีนี้โดยเฉพาะ แต่ไม่สามารถทดแทนnonlocalได้ locals()สร้างพจนานุกรมouter()ของชาวบ้านในเวลาที่inner()ถูกกำหนดไว้ แต่เปลี่ยนพจนานุกรมที่ไม่ได้เปลี่ยนในv outer()สิ่งนี้จะไม่ทำงานอีกต่อไปเมื่อคุณมีฟังก์ชันภายในเพิ่มเติมที่ต้องการแชร์ตัวแปรปิด พูดว่าinc()และdec()เพิ่มและลดตัวนับที่ใช้ร่วมกัน
BlackJack

@BlackJack nonlocalเป็นฟีเจอร์python 3
Marcin

ใช่ฉันรู้. คำถามคือวิธีการเพื่อให้บรรลุผลของงูใหญ่ 3 ของnonlocalในหลาม 2 ในทั่วไป ความคิดของคุณจะไม่ครอบคลุมกรณีทั่วไป แต่เพียงอย่างใดอย่างหนึ่งกับหนึ่งฟังก์ชั่นภายใน ดูส่วนสำคัญนี้เป็นตัวอย่าง ฟังก์ชันภายในทั้งสองมีภาชนะของตัวเอง คุณต้องมีวัตถุที่เปลี่ยนแปลงได้ในขอบเขตของฟังก์ชันภายนอกตามที่คำตอบอื่น ๆ แนะนำไปแล้ว
BlackJack

@ BlackJack นั่นไม่ใช่คำถาม แต่ขอบคุณสำหรับข้อมูลของคุณ
Marcin

ใช่นั่นคือคำถาม ดูที่ด้านบนของหน้า adinsa มี Python 2.6 และต้องการเข้าถึงตัวแปร nonlocal ในการปิดโดยไม่มีnonlocalคีย์เวิร์ดที่แนะนำใน Python 3
BlackJack

3

อีกวิธีในการทำ (แม้ว่าจะดูละเอียดเกินไป):

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
จริงๆแล้วฉันชอบวิธีแก้ปัญหาโดยใช้พจนานุกรม แต่อันนี้เจ๋ง :)
Ezer Fernandes

0

การขยายโซลูชันที่หรูหราของ Martineau ด้านบนไปสู่กรณีการใช้งานที่ใช้งานได้จริงและค่อนข้างหรูหราฉันได้รับ:

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

ใช้ตัวแปรส่วนกลาง

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

โดยส่วนตัวแล้วฉันไม่ชอบตัวแปรระดับโลก แต่ข้อเสนอของฉันอ้างอิงจากhttps://stackoverflow.com/a/19877437/1083704คำตอบ

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

โดยที่ผู้ใช้ต้องประกาศตัวแปรส่วนกลางranksทุกครั้งที่คุณต้องเรียกใช้report. การปรับปรุงของฉันทำให้ไม่จำเป็นต้องเริ่มต้นตัวแปรฟังก์ชันจากผู้ใช้


การใช้พจนานุกรมจะดีกว่ามาก คุณสามารถอ้างอิงอินสแตนซ์ได้innerแต่ไม่สามารถกำหนดให้กับอินสแตนซ์ได้ แต่คุณสามารถแก้ไขคีย์และค่าได้ สิ่งนี้หลีกเลี่ยงการใช้ตัวแปรส่วนกลาง
johannestaas
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.