É possível modificar a variável em python que está no escopo externo, mas não global?

107

Dado o seguinte código:

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

Pois o código na B()variável de função bestá no escopo externo, mas não no escopo global. É possível modificar a bvariável de dentro da B()função? Certamente posso ler daqui e print(), mas como modificá-lo?

Grigoryvp
fonte
Desculpe, claro 2.7 :). Para python 3, as regras de escopo foram alteradas.
grigoryvp
Você pode, desde que bseja mutável. Uma atribuição para bmascarará o escopo externo.
JimB
4
É um dos constrangimentos do Python que nonlocalnão foi retransmitido para 2.x. É uma parte intrínseca do suporte de fechamento.
Glenn Maynard

Respostas:

96

Python 3.x contém a nonlocalpalavra - chave . Acho que isso faz o que você deseja, mas não tenho certeza se você está executando o python 2 ou 3.

A instrução não local faz com que os identificadores listados se refiram a variáveis ​​previamente associadas no escopo delimitador mais próximo. Isso é importante porque o comportamento padrão para associação é pesquisar primeiro o namespace local. A instrução permite que o código encapsulado religue variáveis ​​fora do escopo local, além do escopo global (módulo).

Para Python 2, geralmente uso apenas um objeto mutável (como uma lista ou dict) e altero o valor em vez de reatribuir.

exemplo:

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

foo()

Saídas:

[1, 1]
Adam Wagner
fonte
16
Uma boa maneira de fazer isso é class nonlocal: passno escopo externo. Em seguida, nonlocal.xpode ser atribuído no escopo interno.
kindall
1
Até agora, já tenho duas dicas de python que são simples, mas muito úteis: a sua é a segunda :) Obrigado @kindall!
swdev
@kindall é um ótimo hack - é minimamente diferente da sintaxe do Python 3 e muito mais legível do que passar um objeto mutável.
dimo414 01 de
2
@kindall muito legal, muito obrigado :) provavelmente precisa de um nome diferente, porque quebra a compatibilidade. Em python 3, é um conflito de palavras-chave e causará a SyntaxError. Talvez NonLocal?
Adam Terrey
ou, já que é tecnicamente uma classe , Nonlocal? :-)
kindall
19

Você pode usar uma classe vazia para manter um escopo temporário. É como o mutável, mas um pouco mais bonito.

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

Isso produz a seguinte saída interativa:

>>> outer_fn()
8 27
>>> fs = FnScope()
NameError: name 'FnScope' is not defined
chrisk
fonte
É estranho que a classe com seus campos seja "visível" em uma função interna, mas as variáveis ​​não, a menos que você defina a variável externa com a palavra-chave "não local".
Celdor
12

Sou um pouco novo em Python, mas li um pouco sobre isso. Acredito que o melhor que você conseguirá será semelhante à solução alternativa do Java, que consiste em agrupar sua variável externa em uma lista.

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

# The output is '2'

Edit: Eu acho que isso provavelmente era verdade antes do Python 3. Parece que nonlocalé sua resposta.

Mike Edwards
fonte
4

Não, você não pode, pelo menos desta forma.

Porque a "operação de definição" criará um novo nome no escopo atual, que abrange o externo.

zchenah
fonte
"que cobre o externo" O que você quer dizer? Definir um objeto com o nome b em uma função aninhada não tem influência sobre um objeto com o mesmo nome no espaço exterior desta função
eyquem 09/12/2011
1
@eyquem ou seja, onde quer que esteja a instrução de atribuição, ela apresentará o nome em todo o escopo atual. Tal como o código de amostra da pergunta, se for: def C (): print (b) b = 2 o "b = 2" irá introduzir o nome b em todo o escopo da função C, então, quando imprimir (b), ele irá tente obter b no escopo da função C local, mas não no externo, o b local ainda não foi inicializado, então haverá um erro.
zchenah
1

Para qualquer um que olhe para isso muito mais tarde, uma solução alternativa mais segura, porém mais pesada, é. Sem necessidade de passar variáveis ​​como parâmetros.

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

A resposta curta que funcionará de forma automática e mágica

Criei uma biblioteca python para resolver esse problema específico. Ele é liberado sob a ausência de luz, então use-o como quiser. Você pode instalá-lo pip install seapieou verificar a página inicial aqui 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()

saídas

2

os argumentos têm o seguinte significado:

  • O primeiro argumento é o escopo de execução. 0 significaria local B(), 1 significaria pai A()e 2 significaria avô <module>também conhecido como global
  • O segundo argumento é uma string ou objeto de código que você deseja executar no escopo dado
  • Você também pode chamá-lo sem argumentos para o shell interativo dentro do seu programa

A longa resposta

Isso é mais complicado. Seapie trabalha editando os quadros na pilha de chamadas usando CPython api. CPython é o padrão de fato, então a maioria das pessoas não precisa se preocupar com isso.

As palavras mágicas nas quais você provavelmente está interessado se estiver lendo isto são as seguintes:

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

O último forçará as atualizações a passarem para o escopo local. escopos locais são, entretanto, otimizados de maneira diferente do escopo global, portanto, introduzir novos objetos tem alguns problemas quando você tenta chamá-los diretamente, se eles não forem inicializados de alguma forma. Vou copiar algumas maneiras de contornar esses problemas da página do github

  • Assingn, importar e definir seus objetos de antemão
  • Assingn placeholder para seus objetos de antemão
  • Reatribuir o objeto a ele mesmo no programa principal para atualizar a tabela de símbolos: x = locals () ["x"]
  • Use exec () no programa principal em vez de chamar diretamente para evitar a otimização. Em vez de chamar x, faça: exec ("x")

Se você acha que usar exec()não é algo que deseja acompanhar, pode emular o comportamento atualizando o dicionário local verdadeiro (não aquele retornado por locals ()). Vou copiar um exemplo de 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()

Resultado:

hack!
Markus Hirsimäki
fonte
0

Eu não acho que você deveria querer fazer isso. Funções que podem alterar coisas em seu contexto envolvente são perigosas, pois esse contexto pode ser escrito sem o conhecimento da função.

Você poderia torná-lo explícito, tornando B um método público e C um método privado em uma classe (provavelmente a melhor maneira); ou usando um tipo mutável, como uma lista, e passando-o explicitamente para C:

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

A()
Sideshow Bob
fonte
2
Como você pode escrever uma função sem saber sobre as funções aninhadas dentro dela? Funções aninhadas e encerramentos são uma parte intrínseca da função em que estão incluídos.
Glenn Maynard
Você precisa saber sobre a interface das funções incluídas na sua, mas não deveria saber o que se passa dentro delas. Além disso, não se pode esperar que você saiba o que acontece nas funções que eles chamam, etc! Se uma função modifica um não global ou não membro da classe, ela normalmente deve tornar isso explícito por meio de sua interface, ou seja, tomá-lo como um parâmetro.
Sideshow Bob
Python não o força a ser tão bom, é claro, daí a nonlocalpalavra-chave - mas depende de você usá-lo com muito cuidado.
Sideshow Bob
5
@Bob: Nunca achei o uso de encerramentos como esse perigoso, a não ser devido a peculiaridades da linguagem. Pense nos locais como uma classe temporária e nas funções locais como métodos da classe, e não é mais complicado do que isso. YMMV, eu acho.
Glenn Maynard
0

Você pode, mas terá que usar a instrução global (não é uma solução muito boa como sempre ao usar variáveis ​​globais, mas funciona):

def A():
    global b
    b = 1

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

    B()
A()
Cédric Julien
fonte
Veja minha resposta explicando a desvantagem potencial dessa solução
eyquem 09/12/2011
4
Usar uma variável global é completamente diferente.
Glenn Maynard
0

Não sei se existe um atributo de uma função que dá o __dict__do espaço exterior da função quando este espaço exterior não é o espaço global == o módulo, que é o caso quando a função é uma função aninhada, em Python 3.

Mas em Python 2, até onde eu sei, não existe tal atributo.

Portanto, a única possibilidade de fazer o que você deseja é:

1) usando um objeto mutável, como dito por outros

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

resultado

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

.

Nota

A solução de Cédric Julien tem uma desvantagem:

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

resultado

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

O b global após a execução de A()foi modificado e pode não ser desejado

Esse é o caso apenas se houver um objeto com o identificador b no namespace global

eyquem
fonte