kata kunci nonlokal dengan Python 2.x

117

Saya mencoba menerapkan closure dengan Python 2.6 dan saya perlu mengakses variabel nonlokal tetapi sepertinya kata kunci ini tidak tersedia di python 2.x. Bagaimana cara mengakses variabel nonlokal dalam closure di versi python ini?

adinsa
sumber

Jawaban:

125

Fungsi dalam dapat membaca variabel nonlokal di 2.x, hanya saja tidak mengikatnya kembali. Ini menjengkelkan, tetapi Anda bisa mengatasinya. Buat saja kamus, dan simpan data Anda sebagai elemen di dalamnya. Fungsi dalam tidak dilarang untuk mengubah objek yang dirujuk oleh variabel nonlokal.

Untuk menggunakan contoh dari 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
Chris B.
sumber
4
mengapa mungkin untuk mengubah nilai dari kamus?
coelhudo
9
@coelhudo Karena Anda dapat memodifikasi variabel nonlokal. Tetapi Anda tidak dapat melakukan tugas ke variabel nonlokal. Misalnya, ini akan meningkatkan UnboundLocalError: def inner(): print d; d = {'y': 1}. Di sini, print dmembaca di luar dsehingga menciptakan variabel nonlokal ddalam lingkup dalam.
suzanshakya
21
Terima kasih atas jawaban ini. Saya pikir Anda dapat meningkatkan terminologi meskipun: alih-alih "dapat membaca, tidak dapat mengubah", mungkin "dapat merujuk, tidak dapat menetapkan ke". Anda dapat mengubah konten suatu objek dalam lingkup nonlokal, tetapi Anda tidak dapat mengubah objek mana yang dirujuk.
metamatt
3
Alternatifnya, Anda bisa menggunakan kelas arbitrer dan membuat instance objek alih-alih kamus. Saya merasa jauh lebih elegan dan mudah dibaca karena tidak membanjiri kode dengan literal (kunci kamus), ditambah lagi Anda dapat menggunakan metode.
Alois Mahdal
6
Untuk Python menurut saya yang terbaik adalah menggunakan kata kerja "bind" atau "rebind" dan menggunakan kata benda "name" daripada "variable". Dalam Python 2.x, Anda dapat menutup sebuah objek tetapi Anda tidak dapat mengubah namanya dalam cakupan aslinya. Dalam bahasa seperti C, mendeklarasikan variabel mencadangkan beberapa penyimpanan (baik penyimpanan statis atau sementara di stack). Dalam Python, ekspresi tersebut X = 1hanya mengikat nama Xdengan objek tertentu ( intdengan nilai 1). X = 1; Y = Xmengikat dua nama ke objek yang sama persis. Bagaimanapun, beberapa objek dapat berubah dan Anda dapat mengubah nilainya.
steveha
37

Solusi berikut ini terinspirasi oleh jawaban Elias Zamaria , tetapi berlawanan dengan jawaban tersebut, jawaban tersebut menangani banyak panggilan dari fungsi luar dengan benar. "Variabel" inner.ybersifat lokal untuk panggilan saat ini outer. Hanya itu bukan variabel, karena itu dilarang, tetapi atribut objek (objek adalah fungsinya innersendiri). Ini sangat jelek (perhatikan bahwa atribut hanya dapat dibuat setelah innerfungsi ditentukan) tetapi tampaknya efektif.

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)
Marc van Leeuwen
sumber
Itu tidak harus uggly. Daripada menggunakan inner.y, gunakan outer.y. Anda dapat menentukan outer.y = 0 sebelum def inner.
jgomo3
1
Oke, saya melihat komentar saya salah. Elias Zamaria juga menerapkan solusi outer.y. Namun seperti yang dikomentari oleh Nathaniel [1], Anda harus berhati-hati. Saya pikir jawaban ini harus dipromosikan sebagai solusinya, tetapi perhatikan tentang solusi outer.y dan cavets. [1] stackoverflow.com/questions/3190706/…
jgomo3
Ini menjadi jelek jika Anda ingin lebih dari satu fungsi bagian dalam berbagi keadaan nonlokal yang bisa berubah. Ucapkan a inc()dan dec()kembali dari luar kenaikan dan penurunan itu sebagai penghitung bersama. Kemudian Anda harus memutuskan fungsi mana untuk melampirkan nilai counter saat ini dan mereferensikan fungsi itu dari yang lain. Yang terlihat agak aneh dan asimetris. Misal di dec()baris seperti inc.value -= 1.
BlackJack
33

Daripada kamus, ada lebih sedikit kekacauan di kelas nonlokal . Mengubah contoh @ ChrisB :

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

Kemudian

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

Setiap panggilan outer () membuat kelas baru dan berbeda yang disebut konteks (bukan hanya instance baru). Jadi ini menghindari kewaspadaan @ Nathaniel tentang konteks bersama.

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

assert f() == 5
Bob Stein
sumber
Bagus! Ini memang lebih elegan dan mudah dibaca daripada kamus.
Vincenzo
Saya akan merekomendasikan menggunakan slot secara umum di sini. Ini melindungi Anda dari kesalahan ketik.
DerWeh
@DerWeh menarik, saya belum pernah mendengarnya . Bisakah Anda mengatakan hal yang sama untuk kelas mana pun? Saya benci dibuat bingung oleh kesalahan ketik.
Bob Stein
@BobStein Maaf, saya tidak begitu mengerti apa yang Anda maksud. Tetapi dengan menambahkan __slots__ = ()dan membuat sebuah objek daripada menggunakan kelas, misalnya context.z = 3akan memunculkan sebuah AttributeError. Hal ini dimungkinkan untuk semua kelas, kecuali mereka mewarisi dari kelas yang tidak menentukan slot.
DerWeh
@DerWeh Saya tidak pernah mendengar tentang penggunaan slot untuk menangkap kesalahan ketik. Anda benar, itu hanya akan membantu dalam instance kelas, bukan variabel kelas. Mungkin Anda ingin membuat contoh yang berfungsi di pyfiddle. Ini akan membutuhkan setidaknya dua baris tambahan. Tidak dapat membayangkan bagaimana Anda akan melakukannya tanpa lebih banyak kekacauan.
Bob Stein
14

Saya pikir kuncinya di sini adalah apa yang Anda maksud dengan "akses". Seharusnya tidak ada masalah dengan membaca variabel di luar lingkup closure, misalnya,

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

harus bekerja seperti yang diharapkan (pencetakan 3). Namun, menimpa nilai x tidak akan berhasil, misalnya,

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

akan tetap mencetak 3. Dari pemahaman saya tentang PEP-3104, inilah yang dimaksud dengan kata kunci nonlokal. Seperti yang disebutkan di PEP, Anda dapat menggunakan kelas untuk mencapai hal yang sama (agak berantakan):

class Namespace(object): pass
ns = Namespace()
ns.x = 3
def outer():
    def inner():
        ns.x = 5
    inner()
outer()
print ns.x
ig0774
sumber
Alih-alih membuat kelas dan membuat instance-nya, seseorang cukup membuat sebuah fungsi: def ns(): passdiikuti oleh ns.x = 3. Itu tidak cantik, tapi sedikit kurang jelek di mataku.
davidchambers
2
Adakah alasan khusus untuk downvote tersebut? Saya akui solusi saya bukanlah solusi yang paling elegan, tetapi berhasil ...
ig0774
2
Tentang apa class Namespace: x = 3?
Feuermurmel
3
Saya tidak berpikir cara ini mensimulasikan nonlokal, karena ini menciptakan referensi global, bukan referensi dalam closure.
CarmeloS
1
Saya setuju dengan @CarmeloS, blok kode terakhir Anda karena tidak melakukan apa yang disarankan PEP-3104 sebagai cara untuk mencapai sesuatu yang serupa dengan Python 2. nsadalah objek global yang mengapa Anda dapat merujuk ns.xpada level modul dalam printpernyataan di bagian paling akhir .
martineau
13

Ada cara lain untuk mengimplementasikan variabel nonlokal dengan Python 2, jika ada jawaban di sini yang tidak diinginkan karena alasan apa pun:

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

Tidak ada gunanya menggunakan nama fungsi dalam pernyataan penugasan variabel, tetapi terlihat lebih sederhana dan lebih bersih daripada meletakkan variabel dalam kamus. Nilainya diingat dari satu panggilan ke panggilan lainnya, seperti dalam jawaban Chris B.

Elias Zamaria
sumber
19
Harap berhati-hati: jika diterapkan dengan cara ini, jika Anda melakukannya f = outer()dan kemudian melakukannya g = outer(), maka fpenghitung akan disetel ulang. Ini karena keduanya berbagi variabel tunggal outer.y , bukan masing-masing memiliki variabel independen sendiri. Meskipun kode ini terlihat lebih estetis daripada jawaban Chris B, caranya tampaknya menjadi satu-satunya cara untuk meniru pelingkupan leksikal jika Anda ingin menelepon outerlebih dari sekali.
Nathaniel
@ Nathaniel: Biarkan saya melihat apakah saya memahami ini dengan benar. Penetapan ke outer.ytidak melibatkan apa pun yang bersifat lokal ke pemanggilan fungsi (instance) outer(), tetapi menetapkan ke atribut objek fungsi yang terikat ke nama outerdalam cakupan yang melingkupinya . Dan oleh karena itu seseorang dapat juga menggunakan, secara tertulis outer.y, nama lain selain outer, asalkan diketahui terikat dalam ruang lingkup itu. Apakah ini benar?
Marc van Leeuwen
1
Koreksi, saya seharusnya mengatakan, setelah "terikat dalam ruang lingkup itu": ke objek yang tipenya memungkinkan pengaturan atribut (seperti fungsi, atau instance kelas apa pun). Juga, karena cakupan ini sebenarnya lebih jauh dari yang kita inginkan, bukankah ini akan menyarankan solusi yang sangat jelek berikut ini: daripada outer.ymenggunakan nama inner.y(karena innerterikat di dalam panggilan outer(), yang merupakan cakupan yang kita inginkan), tetapi meletakkan inisialisasi inner.y = 0 setelah definisi inner (sebagai objek harus ada saat atributnya dibuat), tapi tentu saja sebelumnya return inner?
Marc van Leeuwen
@MarcvanLeeuwen Sepertinya komentar Anda menginspirasi solusi dengan inner.y oleh Elias. Pemikiran bagus untukmu.
Ankur Agarwal
Mengakses dan memperbarui variabel global mungkin bukanlah apa yang kebanyakan orang coba lakukan saat memutasi tangkapan closure.
binki
12

Berikut adalah sesuatu yang terinspirasi dari saran Alois Mahdal dalam komentarnya tentang jawaban lain :

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)

Memperbarui

Setelah melihat kembali hal ini baru-baru ini, saya terkejut dengan bagaimana dekorator-seperti itu — ketika saya sadar bahwa menerapkannya sebagai seseorang akan membuatnya lebih umum & berguna (meskipun melakukannya bisa dibilang menurunkan keterbacaannya sampai taraf tertentu).

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

Perhatikan bahwa kedua versi bekerja dengan Python 2 dan 3.

martineau.dll
sumber
Yang satu ini tampaknya menjadi yang terbaik dalam hal keterbacaan dan menjaga pelingkupan leksikal.
Aaron S. Kurland
3

Ada kutil dalam aturan pelingkupan python - tugas membuat variabel lokal ke lingkup fungsi yang langsung melingkupinya. Untuk variabel global, Anda akan menyelesaikannya dengan globalkata kunci.

Solusinya adalah dengan memperkenalkan objek yang dibagi di antara dua cakupan, yang berisi variabel yang bisa berubah, tetapi itu sendiri direferensikan melalui variabel yang tidak ditetapkan.

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

Alternatifnya adalah beberapa peretasan cakupan:

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

Anda mungkin bisa menemukan beberapa trik untuk mendapatkan nama parameter outer, dan kemudian meneruskannya sebagai varname, tetapi tanpa bergantung pada nama outerAnda ingin menggunakan kombinator Y.

Marcin
sumber
Kedua versi berfungsi dalam kasus khusus ini tetapi bukan pengganti nonlocal. locals()membuat kamus dari orang outer()lokal pada saat inner()itu ditentukan tetapi mengubah kamus itu tidak mengubah vdalam outer(). Ini tidak akan berfungsi lagi ketika Anda memiliki lebih banyak fungsi dalam yang ingin berbagi variabel tertutup. Ucapkan a inc()dan dec()itu kenaikan dan penurunan penghitung bersama.
BlackJack
@BlackJack nonlocaladalah fitur python 3.
Marcin
Ya saya tahu. Pertanyaannya adalah bagaimana cara mencapai efek Python 3 nonlocaldi Python 2 secara umum . Ide Anda tidak mencakup kasus umum tetapi hanya satu dengan satu fungsi batin. Lihat inti ini sebagai contoh. Kedua fungsi dalam tersebut memiliki wadahnya masing-masing. Anda memerlukan objek yang bisa berubah dalam lingkup fungsi luar, seperti jawaban lain yang sudah disarankan.
BlackJack
@BlackJack Bukan itu pertanyaannya, tapi terima kasih atas masukan Anda.
Marcin
Ya itu adalah pertanyaan. Lihat di bagian atas halaman. adinsa memiliki Python 2.6 dan membutuhkan akses ke variabel nonlokal dalam penutupan tanpa nonlocalkata kunci yang diperkenalkan di Python 3.
BlackJack
3

Cara lain untuk melakukannya (meskipun terlalu bertele-tele):

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
Ezer Fernandes
sumber
1
sebenarnya saya lebih suka solusi menggunakan kamus, tapi yang ini keren :)
Ezer Fernandes
0

Memperluas solusi elegan Martineau di atas menjadi kasus penggunaan yang praktis dan agak kurang elegan, saya dapatkan:

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)
Amnon Harel
sumber
-3

Gunakan variabel global

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

Secara pribadi, saya tidak suka variabel global. Tapi, proposal saya didasarkan pada https://stackoverflow.com/a/19877437/1083704 jawaban

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

di mana pengguna perlu mendeklarasikan variabel global ranks, setiap kali Anda perlu memanggil report. Peningkatan saya menghilangkan kebutuhan untuk menginisialisasi variabel fungsi dari pengguna.

Val
sumber
Jauh lebih baik menggunakan kamus. Anda dapat mereferensikan instance dalam inner, tetapi tidak dapat menetapkannya, tetapi Anda dapat mengubah kunci dan nilainya. Ini menghindari penggunaan variabel global.
johannestaas