Можно ли изменить переменную в Python, которая находится во внешней, но не глобальной области?

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. Это неотъемлемая часть поддержки закрытия.
Гленн Мейнард,

Ответы:

99

В Python 3.x есть nonlocalключевое слово . Я думаю, что это делает то, что вы хотите, но я не уверен, используете ли вы python 2 или 3.

Оператор nonlocal заставляет перечисленные идентификаторы ссылаться на ранее связанные переменные в ближайшей охватывающей области. Это важно, поскольку по умолчанию привязка выполняется сначала в локальном пространстве имен. Этот оператор позволяет инкапсулированному коду повторно связывать переменные за пределами локальной области, помимо глобальной (модульной) области.

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

пример:

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 01
2
@kindall очень аккуратно, куча благодарностей :), вероятно, нужно другое имя, потому что это нарушает совместимость вперед. В python 3 это конфликт ключевых слов, который вызовет ошибку SyntaxError. Возможно NonLocal?
Адам Терри
или, поскольку это технически класс Nonlocal,? :-)
kindall 08
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
крикнуть
источник
Странно, что класс с его полями «видим» во внутренней функции, а переменные - нет, если только вы не определите внешнюю переменную с помощью ключевого слова «нелокальный».
Celdor 02
12

Я немного новичок в Python, но я немного читал об этом. Я считаю, что лучшее, что вы получите, похоже на обходной путь Java, который заключается в том, чтобы заключить вашу внешнюю переменную в список.

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

# The output is '2'

Изменить: я думаю, это было правдой до Python 3. Похоже, nonlocalэто ваш ответ.

Майк Эдвардс
источник
4

Нет, не можете, по крайней мере, таким образом.

Потому что «операция установки» создаст новое имя в текущей области видимости, которая покрывает внешнюю.

zchenah
источник
"которые покрывают внешний" Что вы имеете в виду? Определение объекта с именем 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

Короткий ответ, который будет работать автоматически

Я создал библиотеку 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>или бабушку aka global
  • Второй аргумент - это строка или объект кода, который вы хотите выполнить в заданной области.
  • Вы также можете вызвать его без аргументов для интерактивной оболочки внутри вашей программы

Длинный ответ

Это более сложно. 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 выполните: exec ("x")

Если вы чувствуете, что использование exec()- это не то , с чем вы хотите идти, вы можете подражать поведению, обновив настоящий локальный словарь (не тот, который возвращается locals ()). Я скопирую пример с 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
Как можно написать функцию, не зная о вложенных в нее функциях? Вложенные функции и замыкания являются неотъемлемой частью функции, в которую они заключены.
Гленн Мейнард,
Вам нужно знать об интерфейсе функций, заключенных в вашем, но вам не нужно знать, что происходит внутри них. Кроме того, нельзя ожидать, что вы узнаете, что происходит в вызываемых ими функциях и т. Д.! Если функция изменяет неглобального или неклассового члена, она обычно должна сделать это явным образом через свой интерфейс, т.е. принять это как параметр.
Интермедия Боба
Python, конечно, не заставляет вас быть настолько хорошими, отсюда и nonlocalключевое слово, но вы должны использовать его с большой осторожностью.
Интермедия Боба
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

.

Nota

У решения Седрика Жюльена есть недостаток:

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.

Eyquem
источник