Ipython: IPythonShellEmbed falha ao reconhecer variáveis ​​locais

Criado em 19 jul. 2010  ·  18Comentários  ·  Fonte: ipython/ipython

Edição

Os shells IPython incorporados podem perder o controle das variáveis ​​locais.

Caso de teste

Caso de teste mínimo:

class Foo(object):
    """ Container-like object """
    def __setattr__(self, obj, val):
        self.__dict__[obj] = val

    def __getattr__(self, obj, val):
        return self.__dict__[obj]

f = Foo()
f.indices = set([1,2,3,4,5])
f.values = {}
for x in f.indices:
    f.values[x] = x

def bar(foo):
    import IPython
    IPython.Shell.IPShellEmbed()()
    return sum(foo.values[x] for x in foo.indices)

print bar(f)

Para ver o erro, primeiro execute o código em Python (ou IPython) e saia do shell gerado; a declaração de impressão final exibe corretamente '15'. Execute o código novamente, mas desta vez digite sum(foo.values[x] for x in foo.indices) no shell gerado e recebemos o erro

"NameError: o nome global 'foo' não está definido".

bug

Comentários muito úteis

Eu examinei esse problema um pouco e ele definitivamente pode ser corrigido (embora manter as correções para Python 2 e 3 possa ser complicado).

A solução ChainMap seria mais fácil de incluir no IPython adequado. No entanto, há um pequeno problema de que eval / exec exige que os globais sejam dict . Criar um class MyChainMap(ChainMap, dict): pass pode contornar isso.

Eu também escrevi uma correção para Python 3.5+ com base em uma estratégia diferente de simular células de fechamento e forçar o compilador Python a emitir o bytecode correto para trabalhar com elas. O arquivo relevante está aqui , parte da minha demonstração xdbg . Ele substitui get_ipython().run_ast_nodes .

Pelo que posso dizer, as duas abordagens diferem apenas no modo como lidam com os fechamentos. Quando xdbg está embutido em um escopo que fechou sobre algumas variáveis, ele pode acessar corretamente essas variáveis ​​por referência e alterá-las. Além disso, se quaisquer funções forem criadas no interpretador interativo, elas fecharão sobre quaisquer variáveis ​​locais de que precisam, enquanto permitem que o restante do escopo local seja coletado como lixo.

Todos 18 comentários

Hmm, onde você definiu 'foo'?

Se você apenas executar call foo fora da barra de funções, foo não deve existir (e obviamente não existe).

Ao executar "sum (f.values ​​[x] for x in f.indices)", você obtém 15 novamente ...

Muito correto. No entanto, estou me referindo ao uso de foo _inside_ o shell IPython gerado, que por sua vez é gerado dentro da definição de função, onde foo é uma variável local.

É provável que seja o mesmo problema do nº 62?

Não, @takluyver : este é um problema separado e, de fato, um bug real em nosso código de incorporação. Eu esperava que seu trabalho recente com namespaces o tivesse corrigido, mas não o fez. Para referência, aqui está o código de exemplo a ser executado com a API de incorporação atual:

class Foo(object):
    """ Container-like object """
    def __setattr__(self, obj, val):
        self.__dict__[obj] = val

    def __getattr__(self, obj, val):
        return self.__dict__[obj]

f = Foo()
f.indices = set([1,2,3,4,5])
f.values = {}
for x in f.indices:
    f.values[x] = x

def bar(foo):
    import IPython
    IPython.embed()
    return sum(foo.values[x] for x in foo.indices)

print bar(f)

Então, no IPython incorporado gerado, isso falha:

In [1]: sum(foo.values[x] for x in foo.indices)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
/home/fperez/tmp/junk/ipython/foo.py in <module>()
----> 1 sum(foo.values[x] for x in foo.indices)

/home/fperez/tmp/junk/ipython/foo.py in <genexpr>((x,))
----> 1 sum(foo.values[x] for x in foo.indices)

NameError: global name 'foo' is not defined

E não funciona mesmo se passarmos para a chamada de incorporação user_ns=locals() explicitamente, mas, nesse caso, obtemos, além disso, uma falha na saída:

Error in atexit._run_exitfuncs:
Traceback (most recent call last):
  File "/usr/lib/python2.7/atexit.py", line 24, in _run_exitfuncs
    func(*targs, **kargs)
  File "/home/fperez/usr/lib/python2.7/site-packages/IPython/core/interactiveshell.py", line 2702, in atexit_operations
    self.reset(new_session=False)
  File "/home/fperez/usr/lib/python2.7/site-packages/IPython/core/interactiveshell.py", line 1100, in reset
    self.displayhook.flush()
  File "/home/fperez/usr/lib/python2.7/site-packages/IPython/core/displayhook.py", line 319, in flush
    self.shell.user_ns['_oh'].clear()
KeyError: '_oh'

Parece que nossa máquina de embutir está em péssimo estado ...

Atribui isso a mim mesmo para olhar.

Excelente, obrigado.

Infelizmente, acho que essa é uma limitação do próprio Python. Parece que o código compilado dinamicamente não pode definir um encerramento, que é essencialmente o que estamos fazendo aqui com uma expressão geradora. Aqui está um caso de teste mínimo:

def f():
   x = 1
   exec "def g(): print x\ng()"

f()

Que dá:

Traceback (most recent call last):
  File "scopetest.py", line 5, in <module>
    f()
  File "scopetest.py", line 3, in f
    exec "def g(): print x\ng()"
  File "<string>", line 2, in <module>
  File "<string>", line 1, in g
NameError: global name 'x' is not defined

Observe que você ainda pode ver as variáveis ​​locais em IPython - no exemplo dado, print foo simples funciona. Mas você não pode fechar um novo escopo sobre eles.

Acho que pode ser possível fazer isso funcionar usando collections.ChainMap do Python 3.3 para que o IPython veja as variáveis ​​locais e globais onde está incorporado como globais. No entanto, pela falta de ruído sobre isso nos últimos dois anos, não acho que seja uma alta prioridade, então estou recapitulando adequadamente e espero chegar a isso algum tempo depois de 1.0.

É aceitável votar positivamente: +1:? Isso me afeta também. Posso adicionar meu caso de uso, se solicitado.

Eu estaria 100% bem se a correção para isso funcionasse apenas no Python 3.

Também estou tendo o mesmo problema no Python 3. Obrigado por reabrir.

O mesmo problema aqui: +1: ambos no Python 2 e 3.

Eu tenho esse problema no Python 2 e 3. Isso me afeta diariamente.

Eu examinei esse problema um pouco e ele definitivamente pode ser corrigido (embora manter as correções para Python 2 e 3 possa ser complicado).

A solução ChainMap seria mais fácil de incluir no IPython adequado. No entanto, há um pequeno problema de que eval / exec exige que os globais sejam dict . Criar um class MyChainMap(ChainMap, dict): pass pode contornar isso.

Eu também escrevi uma correção para Python 3.5+ com base em uma estratégia diferente de simular células de fechamento e forçar o compilador Python a emitir o bytecode correto para trabalhar com elas. O arquivo relevante está aqui , parte da minha demonstração xdbg . Ele substitui get_ipython().run_ast_nodes .

Pelo que posso dizer, as duas abordagens diferem apenas no modo como lidam com os fechamentos. Quando xdbg está embutido em um escopo que fechou sobre algumas variáveis, ele pode acessar corretamente essas variáveis ​​por referência e alterá-las. Além disso, se quaisquer funções forem criadas no interpretador interativo, elas fecharão sobre quaisquer variáveis ​​locais de que precisam, enquanto permitem que o restante do escopo local seja coletado como lixo.

IPython 6.0 e versões posteriores funcionam apenas em Python 3, portanto, se @nikitakit ou qualquer outra pessoa quiser abrir uma solicitação pulll com um caso de teste e uma correção para isso, isso seria bem-vindo.

Já se passou um ano desde meu último comentário aqui e, durante esse tempo, meu entendimento do problema mudou um pouco.

Inspecionar interativamente o escopo local continua sendo um recurso importante para mim, mas na verdade há uma série de questões inter-relacionadas com relação às variáveis ​​locais e ao mecanismo de incorporação. Por exemplo, modificar variáveis ​​locais dentro de um shell incorporado não funciona:

>>> import IPython
>>> def test():
...     x = 5
...     IPython.embed()
...     print('x is', x)
...
>>> test()
Python 3.5.1 |Continuum Analytics, Inc.| (default, Dec  7 2015, 11:24:55)
Type 'copyright', 'credits' or 'license' for more information
IPython 6.1.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: x
Out[1]: 5

In [2]: x = 6

In [3]:
Do you really want to exit ([y]/n)? y

x is 5

Uma abordagem baseada em ChainMap não ajudaria nesta situação.

Outra questão é o que acontece quando fechamentos definidos dentro de um shell embutido vazam para o escopo global. Considere executar o mesmo código acima, mas inserindo algo ao longo das linhas de IPython.get_my_x = lambda: x no shell IPython integrado. Uma solução baseada em ChainMap evitará causar NameError nesta situação, às custas de potencialmente introduzir duas cópias simultâneas de x que existem independentemente uma da outra (sendo uma a ChainMap , e o outro a variável local / célula de fechamento usada pelo interpretador python).

Dada a complexidade da situação, decidi concentrar meus esforços em uma abordagem mais abrangente para o problema (que também se alinha melhor com meu próprio uso de IPython). Isso levou ao desenvolvimento do xdbg , que é essencialmente um depurador que se integra ao IPython. A ideia principal é estender o shell IPython, oferecendo comandos de depuração por meio de magia (por exemplo, %break para definir pontos de interrupção). O fato de que os pontos de interrupção são definidos externamente, em vez de chamar a função embed no local, permitiu uma implementação que aborda muitos desses problemas com variáveis ​​locais.

Atualmente, não pretendo solicitar uma correção de bug estritamente direcionada para o núcleo do IPython. No entanto, estou muito interessado em saber o que os usuários e desenvolvedores de IPython pensam sobre o uso de uma interface inspirada em depurador para inspeção de código local. IPython (e agora também Jupyter) teve um grande impacto na capacidade de fazer codificação interativa em Python, mas ainda há muitas melhorias a serem feitas em como ele interage com encapsulamento pesado usando classes / funções / módulos.

Fui queimado várias vezes por esse bug. Por exemplo

[mcgibbon<strong i="6">@xps13</strong>:~]$ cat test.py 
x = 1
def func():    
    x = 2
    import IPython
    IPython.embed()

if __name__ == "__main__":
    func()
[mcgibbon<strong i="9">@xps13</strong>:~]$ python test.py 
Python 3.7.6 (default, Dec 18 2019, 19:23:55) 
Type 'copyright', 'credits' or 'license' for more information
IPython 7.12.0 -- An enhanced Interactive Python. Type '?' for help.
In [1]: [x for _ in range(2)]                                                                                                                                                                                                                 
Out[1]: [1, 1]  # lol whoops. looked up in wrong scope.

Não tenho certeza do que pode ser feito, apenas adicionando minha voz ao coro.

Baseado em https://bugs.python.org/issue13557 eo fato de que passar explicitamente habitantes () e globals () para um exec correções @takluyver de reprodução do bug , parece que este _ought_ ser possível correção no IPython passando os namespaces locais e globais corretos.

x = 1

def func():    
    x = 2
    exec("def g():\n print(x)\ng()")  # prints 1 :(
    exec("def g():\n print(x)\ng()", locals(), globals())  # prints 2 yay!


if __name__ == "__main__":
    func()
Esta página foi útil?
0 / 5 - 0 avaliações