Чи можливо змінити змінну в python, що знаходиться у зовнішній, але не глобальній області застосування?


107

Дано наступний код:

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. Це невід'ємна частина підтримки закриття.
Гленн Мейнард

Відповіді:


97

Python 3.x має nonlocalключове слово . Я думаю, що це робить те, що ти хочеш, але я не впевнений, чи працюєш у python 2 або 3.

Нелокальний оператор змушує перелічені ідентифікатори посилатися на раніше пов'язані змінні в найближчій області, що обгороджує. Це важливо, оскільки поведінка за замовчуванням для прив'язки - це спочатку пошук локального простору імен. Заява дозволяє інкапсульованому коду відновлювати змінні за межами локальної області, крім глобальної (модульної) області.

Для python 2, як правило, я просто використовую змінений об'єкт (наприклад, список чи диктант) та замінюю значення замість переназначення.

приклад:

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

foo()

Виходи:

[1, 1]

16
Приємний спосіб зробити це class nonlocal: passу зовнішній рамці. Потім nonlocal.xможуть бути призначені у внутрішній області.
kindall

1
Поки що у мене вже є два поради пітона, які прості, але дуже корисні: ваш другий :) Дякую @kindall!
swdev

@kindall - це чудовий хак - він мінімально відрізняється від синтаксису Python 3 і набагато читає, ніж обходити об'єкт, що змінюється.
dimo414

2
@kindall дуже акуратний спасибі купи :) Мабуть, потрібна інша назва, хоча це порушує сумісність вперед. У python 3 це конфлікт ключових слів і призведе до SyntaxError. Можливо NonLocal?
Адам Террі

або, оскільки це технічно клас Nonlocal,? :-)
kindall

19

Ви можете використовувати порожній клас, щоб утримувати тимчасовий діапазон. Це як мінливий, але трохи гарніший.

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

Це не дивно, що клас із його полями "видимий" у внутрішній функції, але змінних немає, якщо ви не визначите зовнішню змінну за допомогою "нелокального" ключового слова.
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

Ні, ви не можете, принаймні таким чином.

Тому що "встановлена ​​операція" створить нове ім'я в поточному обсязі, яке охоплює зовнішнє.


"які охоплюють зовнішню" Що ви маєте на увазі? Визначення об'єкта з іменем b в вкладеній функції не впливає на об'єкт з тим самим іменем у космічному просторі цієї функції
eyquem

1
@eyquem, тобто де б не було твердження про призначення, воно введе ім'я у всій нинішній області. Наприклад, зразок коду питання, якщо це: def C (): print (b) b = 2, "b = 2" введе ім'я b у всій області функцій C, тому при print (b) воно буде спробуйте отримати b в локальній області C func, але не у зовнішній, локальний b ще не ініціалізований, тому буде помилка.
zchenah

1

Для тих, хто дивиться на це набагато пізніше на безпечніший, але важчий спосіб вирішення. Без необхідності передавати змінні як параметри.

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

1

Коротка відповідь, яка просто працюватиме автоматично

Я створив бібліотеку пітонів для вирішення цієї конкретної проблеми. Він випущений під нерозбірливістю, тому використовуйте його як завгодно. Ви можете встановити його 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>ака
  • Другий аргумент - це об'єкт рядка або коду, який потрібно виконати в заданій області
  • Ви також можете викликати його без аргументів для інтерактивної оболонки всередині вашої програми

Довга відповідь

Це складніше. Seapie працює, редагуючи кадри в стеку викликів за допомогою 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

  • Заздалегідь визначте, імпортуйте та визначте свої об'єкти
  • Заздалегідь визначте заповнювач заповнення об’єктів
  • Призначте об'єкт собі в основній програмі для оновлення таблиці символів: x = locals () ["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
Як можна написати функцію, не знаючи про вкладені функції всередині неї? Вкладені функції та закриття є невід'ємною частиною функції, до якої вони укладені.
Гленн Мейнард

Вам потрібно знати про інтерфейс функцій, укладених у вашій, але вам не слід знати про те, що відбувається всередині них. Крім того, від вас не можна буде знати, що відбувається у функціях, які вони викликають тощо! Якщо функція модифікує неглобальну чи некласмічну особу, вона зазвичай робить це явним через свій інтерфейс, тобто сприймає її як параметр.
Sideshow Bob

Звичайно, Python не змушує вас бути таким хорошим, звідси і nonlocalключове слово, але використовувати його потрібно з великою обережністю.
Sideshow Bob

5
@Bob: Я ніколи не вважав, щоб використання подібних закриттів було взагалі небезпечним, окрім випадків мовних вигадок. Розглядайте місцевих жителів як тимчасовий клас, а локальні функції як методи на уроці, і це не складніше за це. YMMV, я думаю.
Гленн Мейнард

0

Ви можете, але вам доведеться використовувати глобальний склад (не дуже хороше рішення, як завжди, коли ви використовуєте глобальні змінні, але це працює):

def A():
    global b
    b = 1

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

    B()
A()

Дивіться мою відповідь, що пояснює потенційний недолік цього рішення
eyquem

4
Використання глобальної змінної зовсім інше.
Гленн Мейнард

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

.

Нота

У рішення Седріка Жульєна є недолік:

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

Глобальний b після виконання A()був модифікований, і його можна не бажати

Це справа лише в тому випадку, якщо в глобальному просторі імен є об'єкт з ідентифікатором b

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.