เป็นไปได้ไหมที่จะแก้ไขตัวแปรใน python ที่อยู่ในขอบเขตภายนอก แต่ไม่ใช่ global


110

ให้รหัสต่อไปนี้:

def A() :
    b = 1

    def B() :
        # I can access 'b' from here.
        print( b )
        # But can i modify 'b' here? 'global' and assignment will not work.

    B()
A()

สำหรับโค้ดในB()ตัวแปรฟังก์ชันbอยู่ในขอบเขตภายนอก แต่ไม่อยู่ในขอบเขตส่วนกลาง สามารถแก้ไขbตัวแปรจากภายในB()ฟังก์ชันได้หรือไม่? แน่นอนฉันสามารถอ่านได้จากที่นี่และprint()แต่จะแก้ไขได้อย่างไร?


ขออภัยแน่นอน 2.7 :) กฎการกำหนดขอบเขตของ python 3 มีการเปลี่ยนแปลง
grigoryvp

คุณสามารถตราบเท่าที่bไม่แน่นอน การมอบหมายให้bจะปิดบังขอบเขตภายนอก
JimB

4
เป็นความลำบากใจอย่างหนึ่งของ Python ที่nonlocalไม่ได้ย้อนกลับเป็น 2.x เป็นส่วนที่แท้จริงของการสนับสนุนการปิด
Glenn Maynard

คำตอบ:


99

งูหลาม 3.x มีคำหลักnonlocal ฉันคิดว่านี่เป็นสิ่งที่คุณต้องการ แต่ฉันไม่แน่ใจว่าคุณใช้ python 2 หรือ 3

คำสั่ง nonlocal ทำให้ตัวระบุที่แสดงรายการอ้างถึงตัวแปรที่ถูกผูกไว้ก่อนหน้านี้ในขอบเขตการปิดล้อมที่ใกล้ที่สุด สิ่งนี้สำคัญเนื่องจากลักษณะการทำงานเริ่มต้นสำหรับการเชื่อมโยงคือการค้นหาเนมสเปซในเครื่องก่อน คำสั่งอนุญาตให้โค้ดที่ห่อหุ้มสามารถ rebind ตัวแปรนอกขอบเขตโลคัลนอกเหนือจากขอบเขต global (โมดูล)

สำหรับ python 2 ฉันมักจะใช้วัตถุที่เปลี่ยนแปลงได้ (เช่น list หรือ dict) และเปลี่ยนค่าแทนการกำหนดใหม่

ตัวอย่าง:

def foo():
    a = []
    def bar():
        a.append(1)
    bar()
    bar()
    print a

foo()

ผลลัพธ์:

[1, 1]

16
วิธีที่ดีในการทำเช่นนี้อยู่class nonlocal: passในขอบเขตภายนอก จากนั้นnonlocal.xสามารถกำหนดให้อยู่ในขอบเขตด้านใน
kindall

1
จนถึงตอนนี้ฉันมีเคล็ดลับ Python สองข้อที่เรียบง่าย แต่มีประโยชน์มาก: ของคุณเป็นข้อที่สอง :) ขอบคุณ @kindall!
swdev

@kindall เป็นแฮ็คที่ยอดเยี่ยม - แตกต่างจากไวยากรณ์ Python 3 เล็กน้อยและอ่านได้ง่ายกว่าการส่งผ่านวัตถุที่ไม่แน่นอน
dimo414

2
@kindall เรียบร้อยมากขอบคุณกอง :) อาจต้องใช้ชื่ออื่นเพราะมันทำลายความเข้ากันได้ไปข้างหน้า ใน python 3 เป็นความขัดแย้งของคำหลักและจะทำให้เกิดไฟล์SyntaxError. บางทีNonLocal?
Adam Terrey

หรือเนื่องจากในทางเทคนิคแล้วเป็นชั้นเรียนNonlocal? :-)
ครับ

20

คุณสามารถใช้คลาสว่างเพื่อเก็บขอบเขตชั่วคราว มันเหมือนไม่แน่นอน แต่สวยกว่าเล็กน้อย

def outer_fn():
   class FnScope:
     b = 5
     c = 6
   def inner_fn():
      FnScope.b += 1
      FnScope.c += FnScope.b
   inner_fn()
   inner_fn()
   inner_fn()

สิ่งนี้ให้ผลลัพธ์แบบโต้ตอบต่อไปนี้:

>>> outer_fn()
8 27
>>> fs = FnScope()
NameError: name 'FnScope' is not defined

นี่เป็นเรื่องแปลกที่คลาสที่มีฟิลด์นั้น "มองเห็นได้" ในฟังก์ชันภายใน แต่ไม่ใช่ตัวแปรเว้นแต่คุณจะกำหนดตัวแปรภายนอกด้วยคีย์เวิร์ด "nonlocal"
Celdor

12

ฉันยังใหม่กับ Python แต่ฉันได้อ่านเกี่ยวกับเรื่องนี้มาเล็กน้อย ฉันเชื่อว่าสิ่งที่ดีที่สุดที่คุณจะได้รับนั้นคล้ายกับการแก้ปัญหาของ Java ซึ่งก็คือการรวมตัวแปรภายนอกของคุณไว้ในรายการ

def A():
   b = [1]
   def B():
      b[0] = 2
   B()
   print(b[0])

# The output is '2'

แก้ไข: ฉันเดาว่านี่อาจเป็นจริงก่อน Python 3 ดูเหมือนว่าnonlocalจะเป็นคำตอบของคุณ


4

ไม่คุณทำไม่ได้อย่างน้อยก็ด้วยวิธีนี้

เนื่องจาก "set operation" จะสร้างชื่อใหม่ในขอบเขตปัจจุบันซึ่งครอบคลุมชื่อภายนอก


"ที่ปิดฝาด้านนอก"คุณหมายถึงอะไร? การกำหนดวัตถุด้วยชื่อbในฟังก์ชันที่ซ้อนกันจะไม่มีผลต่อวัตถุที่มีชื่อเดียวกันในช่องว่างภายนอกของฟังก์ชันนี้
eyquem

1
@eyquem นั่นคือไม่ว่าคำสั่งมอบหมายจะอยู่ที่ใดก็ตามมันจะแนะนำชื่อในขอบเขตปัจจุบันทั้งหมด เช่นโค้ดตัวอย่างของคำถามถ้าเป็น: def C (): print (b) b = 2 "b = 2" จะแนะนำชื่อ b ในขอบเขต C func ทั้งหมดดังนั้นเมื่อพิมพ์ (b) ก็จะ พยายามรับ b ในขอบเขต C func ในเครื่อง แต่ไม่ใช่ขอบเขตภายนอก b ท้องถิ่นยังไม่ได้รับการเตรียมใช้งานดังนั้นจะมีข้อผิดพลาด
zchenah

1

สำหรับใครก็ตามที่กำลังมองหาสิ่งนี้ในภายหลังเกี่ยวกับวิธีแก้ปัญหาที่ปลอดภัยกว่า แต่หนักกว่าคือ โดยไม่จำเป็นต้องส่งตัวแปรเป็นพารามิเตอร์

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

1

คำตอบสั้น ๆ ที่จะทำงานโดยอัตโนมัติ

ฉันสร้างไลบรารี python เพื่อแก้ปัญหาเฉพาะนี้ มันถูกปล่อยออกมาภายใต้ความไร้เหตุผลดังนั้นจงใช้มันตามที่คุณต้องการ คุณสามารถติดตั้งpip install seapieหรือดูโฮมเพจได้ที่นี่https://github.com/hirsimaki-markus/SEAPIE

user@pc:home$ pip install seapie

from seapie import Seapie as seapie
def A():
    b = 1

    def B():
        seapie(1, "b=2")
        print(b)

    B()
A()

เอาต์พุต

2

อาร์กิวเมนต์มีความหมายดังต่อไปนี้:

  • อาร์กิวเมนต์แรกคือขอบเขตการดำเนินการ 0 หมายถึงท้องถิ่นB()1 หมายถึงผู้ปกครองA()และ 2 หมายถึงปู่ย่าตายาย<module>หรือที่เรียกว่า global
  • อาร์กิวเมนต์ที่สองคือสตริงหรือออบเจ็กต์รหัสที่คุณต้องการดำเนินการในขอบเขตที่กำหนด
  • คุณยังสามารถเรียกมันได้โดยไม่มีข้อโต้แย้งสำหรับเชลล์แบบโต้ตอบภายในโปรแกรมของคุณ

คำตอบยาว

ซับซ้อนกว่านี้ Seapie ทำงานโดยแก้ไขเฟรมใน call stack โดยใช้ CPython api CPython เป็นมาตรฐานโดยพฤตินัยดังนั้นคนส่วนใหญ่ไม่ต้องกังวลเกี่ยวกับเรื่องนี้

คำวิเศษที่คุณอาจจะถูกโต้ตอบมากที่สุดหากคุณกำลังอ่านข้อความต่อไปนี้:

frame = sys._getframe(1)          # 1 stands for previous frame
parent_locals = frame.f_locals    # true dictionary of parent locals
parent_globals = frame.f_globals  # true dictionary of parent globals

exec(codeblock, parent_globals, parent_locals)

ctypes.pythonapi.PyFrame_LocalsToFast(ctypes.py_object(frame),ctypes.c_int(1))
# the magic value 1 stands for ability to introduce new variables. 0 for update-only

ส่วนหลังจะบังคับให้การอัปเดตส่งผ่านไปยังขอบเขตท้องถิ่น อย่างไรก็ตามขอบเขตในพื้นที่ได้รับการปรับให้เหมาะสมแตกต่างจากขอบเขตส่วนกลางดังนั้นการหาวัตถุใหม่จึงมีปัญหาบางอย่างเมื่อคุณพยายามเรียกใช้โดยตรงหากไม่ได้เริ่มต้นด้วยวิธีใด ๆ ฉันจะคัดลอกสองสามวิธีในการหลีกเลี่ยงปัญหาเหล่านี้จากหน้า github

  • Assingn นำเข้าและกำหนดวัตถุของคุณล่วงหน้า
  • กำหนดตัวยึดตำแหน่งให้กับวัตถุของคุณล่วงหน้า
  • กำหนดออบเจ็กต์ให้ตัวเองในโปรแกรมหลักเพื่ออัปเดตตารางสัญลักษณ์: x = local () ["x"]
  • ใช้ exec () ในโปรแกรมหลักแทนการโทรโดยตรงเพื่อหลีกเลี่ยงการเพิ่มประสิทธิภาพ แทนที่จะเรียก x do: exec ("x")

หากคุณรู้สึกว่าการใช้งานexec()ไม่ใช่สิ่งที่คุณต้องการใช้คุณสามารถเลียนแบบพฤติกรรมได้โดยการอัปเดตพจนานุกรมท้องถิ่นที่แท้จริง (ไม่ใช่คำที่ชาวบ้านส่งคืน ()) ฉันจะคัดลอกตัวอย่างจากhttps://faster-cpython.readthedocs.io/mutable.html

import sys
import ctypes

def hack():
    # Get the frame object of the caller
    frame = sys._getframe(1)
    frame.f_locals['x'] = "hack!"
    # Force an update of locals array from locals dict
    ctypes.pythonapi.PyFrame_LocalsToFast(ctypes.py_object(frame),
                                          ctypes.c_int(0))

def func():
    x = 1
    hack()
    print(x)

func()

เอาท์พุต:

hack!

0

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

คุณสามารถทำให้ชัดเจนได้โดยทำให้ B เป็นวิธีสาธารณะและ C เป็นวิธีส่วนตัวในคลาส (วิธีที่ดีที่สุดอาจเป็นไปได้) หรือโดยใช้ประเภทที่เปลี่ยนแปลงได้เช่นรายการและส่งต่อไปยัง C:

def A():
    x = [0]
    def B(var): 
        var[0] = 1
    B(x)
    print x

A()

2
คุณจะเขียนฟังก์ชันโดยไม่รู้เกี่ยวกับฟังก์ชันที่ซ้อนอยู่ข้างในได้อย่างไร ฟังก์ชันที่ซ้อนกันและการปิดเป็นส่วนที่อยู่ภายในของฟังก์ชันที่อยู่ภายใน
Glenn Maynard

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

Python ไม่ได้บังคับให้คุณเป็นคนดีแน่นอนดังนั้นnonlocalคีย์เวิร์ด - แต่ขึ้นอยู่กับคุณที่จะใช้ด้วยความระมัดระวังเป็นอย่างยิ่ง
Sideshow Bob

5
@ บ็อบ: ฉันไม่เคยพบว่าการใช้การปิดแบบนี้จะเป็นอันตรายเลยนอกจากเกิดจากนิสัยใจคอทางภาษา คิดว่าชาวบ้านเป็นคลาสชั่วคราวและฟังก์ชันท้องถิ่นเป็นวิธีการในชั้นเรียนและไม่ซับซ้อนไปกว่านั้น YMMV ฉันเดา
Glenn Maynard

0

คุณสามารถทำได้ แต่คุณจะต้องใช้global statment (ไม่ใช่วิธีแก้ปัญหาที่ดีมากเช่นเคยเมื่อใช้ตัวแปรส่วนกลาง แต่ใช้ได้ผล):

def A():
    global b
    b = 1

    def B():
      global b
      print( b )
      b = 2

    B()
A()

ดูคำตอบของฉันที่อธิบายข้อเสียที่อาจเกิดขึ้นของโซลูชันนี้
eyquem

4
การใช้ตัวแปรส่วนกลางนั้นแตกต่างกันอย่างสิ้นเชิง
Glenn Maynard

0

ฉันไม่รู้ว่ามีแอตทริบิวต์ของฟังก์ชันที่ให้__dict__พื้นที่ภายนอกของฟังก์ชันหรือไม่เมื่อพื้นที่ภายนอกนี้ไม่ใช่พื้นที่ส่วนกลาง == โมดูลซึ่งเป็นกรณีที่ฟังก์ชันเป็นฟังก์ชันซ้อนกัน ใน Python 3

แต่ใน Python 2 เท่าที่ฉันรู้ไม่มีแอตทริบิวต์ดังกล่าว

ดังนั้นความเป็นไปได้เดียวที่จะทำในสิ่งที่คุณต้องการคือ:

1) ใช้วัตถุที่ไม่แน่นอนตามที่คนอื่นพูด

2)

def A() :
    b = 1
    print 'b before B() ==', b

    def B() :
        b = 10
        print 'b ==', b
        return b

    b = B()
    print 'b after B() ==', b

A()

ผลลัพธ์

b before B() == 1
b == 10
b after B() == 10

.

โนตา

วิธีแก้ปัญหาของCédric Julien มีข้อเสียเปรียบ:

def A() :
    global b # N1
    b = 1
    print '   b in function B before executing C() :', b

    def B() :
        global b # N2
        print '     b in function B before assigning b = 2 :', b
        b = 2
        print '     b in function B after  assigning b = 2 :', b

    B()
    print '   b in function A , after execution of B()', b

b = 450
print 'global b , before execution of A() :', b
A()
print 'global b , after execution of A() :', b

ผลลัพธ์

global b , before execution of A() : 450
   b in function B before executing B() : 1
     b in function B before assigning b = 2 : 1
     b in function B after  assigning b = 2 : 2
   b in function A , after execution of B() 2
global b , after execution of A() : 2

global bหลังจากการดำเนินการของA()ได้รับการแก้ไขแล้วและอาจไม่เป็นเช่นนั้น

ในกรณีนี้ก็ต่อเมื่อมีวัตถุที่มีตัวระบุbในเนมสเปซส่วนกลาง

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