Apakah mungkin untuk mengubah variabel di python yang ada di luar, tetapi tidak global, cakupan?

108

Diberikan kode berikut:

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

Untuk kode dalam B()variabel fungsi bberada di lingkup luar, tetapi tidak dalam lingkup global. Apakah mungkin untuk mengubah bvariabel dari dalam B()fungsi? Tentunya saya bisa membacanya dari sini dan print(), tapi bagaimana cara memodifikasinya?

grigoryvp.dll
sumber
Maaf, tentu saja 2.7 :). Untuk aturan pelingkupan python 3 telah berubah.
grigoryvp
Anda bisa selama bbisa berubah. Sebuah tugas untuk bmenutupi ruang lingkup luar.
JimB
4
Ini salah satu hal memalukan Python yang nonlocalbelum di-backport ke 2.x. Ini adalah bagian intrinsik dari dukungan penutupan.
Glenn Maynard

Jawaban:

98

Python 3.x memiliki nonlocalkata kunci . Saya pikir ini melakukan apa yang Anda inginkan, tetapi saya tidak yakin apakah Anda menjalankan python 2 atau 3.

Pernyataan nonlokal menyebabkan pengidentifikasi terdaftar merujuk ke variabel terikat sebelumnya dalam lingkup terlampir terdekat. Ini penting karena perilaku default untuk pengikatan adalah mencari ruang nama lokal terlebih dahulu. Pernyataan tersebut memungkinkan kode yang dienkapsulasi untuk me-rebind variabel di luar lingkup lokal selain lingkup global (modul).

Untuk python 2, saya biasanya hanya menggunakan objek yang bisa berubah (seperti daftar, atau dikt), dan mengubah nilainya alih-alih menetapkan ulang.

contoh:

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

foo()

Keluaran:

[1, 1]
Adam Wagner
sumber
16
Cara yang bagus untuk melakukan ini adalah class nonlocal: passdi lingkup luar. Kemudian nonlocal.xdapat ditugaskan ke dalam lingkup dalam.
kindall
1
Sampai saat ini, saya sudah memiliki dua tip python yang sederhana, tapi sangat membantu: tips Anda adalah yang kedua :) Terima kasih @kindall!
swdev
@kindall itu peretasan yang bagus - ini sedikit berbeda dari sintaks Python 3 dan jauh lebih mudah dibaca daripada meneruskan objek yang bisa berubah.
dimo414
2
@kindall sangat rapi terima kasih heaps :) mungkin perlu nama yang berbeda karena itu merusak kompatibilitas. Dalam python 3 ini adalah konflik kata kunci dan akan menyebabkan a SyntaxError. Mungkin NonLocal?
Adam Terrey
atau, karena secara teknis ini adalah kelas Nonlocal,? :-)
kindall
19

Anda dapat menggunakan kelas kosong untuk menampung ruang lingkup sementara. Ini seperti bisa berubah tapi sedikit lebih cantik.

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

Ini menghasilkan keluaran interaktif berikut:

>>> outer_fn()
8 27
>>> fs = FnScope()
NameError: name 'FnScope' is not defined
chrisk
sumber
Ini aneh bahwa kelas dengan bidangnya "terlihat" dalam fungsi dalam tetapi variabel tidak, kecuali Anda mendefinisikan variabel luar dengan kata kunci "nonlokal".
Celdor
12

Saya sedikit baru mengenal Python, tapi saya sudah membaca sedikit tentang ini. Saya percaya yang terbaik yang akan Anda dapatkan mirip dengan pekerjaan Java, yaitu membungkus variabel luar Anda dalam daftar.

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

# The output is '2'

Sunting: Saya kira ini mungkin benar sebelum Python 3. Sepertinya nonlocaladalah jawaban Anda.

Mike Edwards
sumber
4

Tidak, Anda tidak bisa, setidaknya dengan cara ini.

Karena "operasi set" akan membuat nama baru di lingkup saat ini, yang mencakup yang terluar.

zchenah
sumber
"yang menutupi bagian luar" Apa maksudmu? Mendefinisikan sebuah objek dengan nama b dalam fungsi bersarang tidak memiliki pengaruh pada sebuah objek dengan nama yang sama di luar angkasa fungsi ini
eyquem
1
@eyquem yaitu, di mana pun pernyataan penugasan berada, itu akan memperkenalkan nama di seluruh cakupan saat ini. Seperti contoh kode pertanyaan, jika itu adalah: def C (): print (b) b = 2 maka "b = 2" akan memperkenalkan nama b di seluruh lingkup fungsi C, jadi ketika print (b), itu akan mencoba untuk mendapatkan b dalam lingkup C func lokal tetapi tidak di luar, b lokal belum diinisialisasi, jadi akan ada kesalahan.
zchenah
1

Bagi siapa pun yang melihat hal ini nanti akan menemukan solusi yang lebih aman tetapi lebih berat. Tanpa perlu melewatkan variabel sebagai parameter.

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

Jawaban singkat yang hanya akan bekerja secara otomatis

Saya membuat perpustakaan python untuk memecahkan masalah khusus ini. Ini dirilis di bawah ketidakseimbangan jadi gunakan sesuka Anda. Anda dapat menginstalnya dengan pip install seapieatau melihat halaman beranda di sini 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()

keluaran

2

argumen memiliki arti sebagai berikut:

  • Argumen pertama adalah ruang lingkup eksekusi. 0 berarti lokal B(), 1 berarti orang tua A()dan 2 berarti kakek-nenek <module>alias global
  • Argumen kedua adalah string atau objek kode yang ingin Anda jalankan dalam cakupan tertentu
  • Anda juga dapat memanggilnya tanpa argumen untuk shell interaktif di dalam program Anda

Jawaban yang panjang

Ini lebih rumit. Seapie bekerja dengan mengedit bingkai dalam tumpukan panggilan menggunakan api CPython. CPython adalah standar de facto sehingga kebanyakan orang tidak perlu khawatir tentang itu.

Kata-kata ajaib yang kemungkinan besar akan Anda minati jika Anda membaca ini adalah sebagai berikut:

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

Yang terakhir akan memaksa pembaruan untuk masuk ke lingkup lokal. Namun cakupan lokal dioptimalkan secara berbeda dari cakupan global sehingga pengenalan objek baru memiliki beberapa masalah saat Anda mencoba memanggilnya secara langsung jika tidak diinisialisasi dengan cara apa pun. Saya akan menyalin beberapa cara untuk menghindari masalah ini dari halaman github

  • Assingn, impor dan tentukan objek Anda sebelumnya
  • Assingn placeholder ke objek Anda sebelumnya
  • Tetapkan ulang objek ke dirinya sendiri dalam program utama untuk memperbarui tabel simbol: x = lokal () ["x"]
  • Gunakan exec () dalam program utama daripada memanggil langsung untuk menghindari pengoptimalan. Daripada memanggil x do: exec ("x")

Jika Anda merasa bahwa menggunakan exec()bukanlah sesuatu yang Anda inginkan, Anda dapat meniru perilaku tersebut dengan memperbarui kamus lokal yang sebenarnya (bukan yang dikembalikan oleh penduduk setempat ()). Saya akan menyalin contoh dari 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()

Keluaran:

hack!
Markus Hirsimäki
sumber
0

Saya tidak berpikir Anda harus melakukan ini. Fungsi yang dapat mengubah hal-hal dalam konteks yang melingkupinya berbahaya, karena konteks tersebut dapat ditulis tanpa sepengetahuan fungsinya.

Anda bisa membuatnya eksplisit, baik dengan membuat B metode publik dan C metode privat di kelas (cara terbaik mungkin); atau dengan menggunakan tipe yang bisa berubah seperti daftar dan meneruskannya secara eksplisit ke C:

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

A()
Tontonan Bob
sumber
2
Bagaimana Anda bisa menulis fungsi tanpa mengetahui tentang fungsi bersarang di dalamnya? Fungsi dan closure bersarang adalah bagian intrinsik dari fungsi yang disertakan.
Glenn Maynard
Anda perlu tahu tentang antarmuka dari fungsi-fungsi yang ada di dalam Anda, tetapi Anda tidak perlu tahu tentang apa yang terjadi di dalamnya. Selain itu, Anda tidak dapat diharapkan untuk mengetahui apa yang terjadi dalam fungsi yang mereka panggil, dll! Jika suatu fungsi memodifikasi non-global atau non-anggota kelas biasanya harus membuatnya eksplisit melalui antarmukanya, yaitu menganggapnya sebagai parameter.
Sideshow Bob
Python tidak memaksa Anda untuk menjadi sebaik itu tentu saja, karena itu nonlocalkata kuncinya - tetapi terserah Anda untuk menggunakannya dengan sangat hati-hati.
Sideshow Bob
5
@ Bob: Saya tidak pernah menemukan penggunaan penutupan seperti ini berbahaya sama sekali, selain karena kebiasaan bahasa. Pikirkan penduduk setempat sebagai kelas sementara, dan fungsi lokal sebagai metode di kelas, dan itu tidak lebih rumit dari itu. YMMV, saya rasa.
Glenn Maynard
0

Anda bisa, tetapi Anda harus menggunakan pernyataan global (bukan solusi yang sangat baik seperti biasa saat menggunakan variabel global, tetapi berfungsi):

def A():
    global b
    b = 1

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

    B()
A()
Cédric Julien
sumber
Lihat jawaban saya menjelaskan potensi kelemahan dari solusi ini
eyquem
4
Menggunakan variabel global sangat berbeda.
Glenn Maynard
0

Saya tidak tahu apakah ada atribut dari suatu fungsi yang memberikan __dict__ruang luar dari fungsi ketika luar angkasa ini bukan ruang global == modul, yang merupakan kasus ketika fungsi tersebut adalah fungsi bersarang, dengan Python 3.

Tetapi di Python 2, sejauh yang saya tahu, tidak ada atribut seperti itu.

Jadi satu-satunya kemungkinan untuk melakukan apa yang Anda inginkan adalah:

1) menggunakan objek yang bisa berubah, seperti yang dikatakan oleh orang lain

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

hasil

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

.

Tidak a

Solusi Cédric Julien memiliki kelemahan:

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

hasil

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

B global setelah eksekusi A()telah dimodifikasi dan mungkin tidak diinginkan

Itulah kasusnya hanya jika ada objek dengan pengenal b di namespace global

eyquem
sumber