palavra-chave não local em Python 2.x

117

Estou tentando implementar um encerramento em Python 2.6 e preciso acessar uma variável não local, mas parece que essa palavra-chave não está disponível em python 2.x. Como acessar variáveis ​​não locais em fechamentos nessas versões de python?

adinsa
fonte

Respostas:

125

As funções internas podem ler variáveis ​​não locais em 2.x, mas não religá- las. Isso é irritante, mas você pode contornar isso. Basta criar um dicionário e armazenar seus dados como elementos nele. As funções internas não estão proibidas de alterar os objetos aos quais as variáveis ​​não locais se referem.

Para usar o exemplo da 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.
fonte
4
por que é possível modificar o valor do dicionário?
coelhudo
9
@coelhudo Porque você pode modificar variáveis ​​não locais. Mas você não pode fazer atribuições a variáveis ​​não locais. Por exemplo, isso irá aumentar UnboundLocalError: def inner(): print d; d = {'y': 1}. Aqui, print dlê externo d, criando assim uma variável não local dno escopo interno.
suzanshakya
21
Obrigado por esta resposta. Acho que você poderia melhorar a terminologia: em vez de "pode ​​ler, não pode mudar", talvez "pode ​​fazer referência, não pode atribuir a". Você pode alterar o conteúdo de um objeto em um escopo não local, mas não pode alterar a qual objeto se refere.
metamatt
3
Como alternativa, você pode usar classe arbitrária e instanciar objeto em vez do dicionário. Acho muito mais elegante e legível, pois não inunda o código com literais (chaves de dicionário), além disso, você pode fazer uso de métodos.
Alois Mahdal de
6
Para Python, acho que é melhor usar o verbo "vincular" ou "religar" e usar o substantivo "nome" em vez de "variável". No Python 2.x, você pode fechar sobre um objeto, mas não pode religar o nome no escopo original. Em uma linguagem como C, declarar uma variável reserva algum armazenamento (armazenamento estático ou temporário na pilha). Em Python, a expressão X = 1simplesmente vincula o nome Xa um objeto específico ( inte ao valor 1). X = 1; Y = Xvincula dois nomes ao mesmo objeto exato. De qualquer forma, alguns objetos são mutáveis e você pode alterar seus valores.
steveha
37

A solução a seguir é inspirada na resposta de Elias Zamaria , mas, ao contrário dessa resposta, trata corretamente várias chamadas da função externa. A "variável" inner.yé local para a chamada atual de outer. Só que não é uma variável, já que isso é proibido, mas um atributo do objeto (sendo o objeto a innerprópria função ). Isso é muito feio (observe que o atributo só pode ser criado depois que a innerfunção for definida), mas parece eficaz.

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
fonte
Não precisa ser feio. Em vez de usar inner.y, use outer.y. Você pode definir outer.y = 0 antes de def inner.
jgomo3
1
Ok, vejo que meu comentário está errado. Elias Zamaria implementou a solução outer.y também. Mas, como comentado por Nathaniel [1], você deve ficar atento. Acho que esta resposta deve ser promovida como a solução, mas observe sobre a solução outer.y e cavetes. [1] stackoverflow.com/questions/3190706/…
jgomo3
Isso fica feio se você quiser mais de uma função interna compartilhando um estado mutável não local. Digamos que um inc()e um dec()retornou de exterior que aumentar e diminuir um contador compartilhada. Em seguida, você precisa decidir a qual função anexar o valor do contador atual e fazer referência a essa função a partir das outras. O que parece um tanto estranho e assimétrico. Por exemplo, em dec()uma linha semelhante inc.value -= 1.
BlackJack
33

Em vez de um dicionário, há menos confusão em uma classe não local . Modificando o exemplo de @ChrisB :

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

Então

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

Cada chamada outer () cria uma classe nova e distinta chamada contexto (não apenas uma nova instância). Portanto, evita o cuidado de @Nathaniel sobre o contexto compartilhado.

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

assert f() == 5
Bob Stein
fonte
Agradável! Isso é realmente mais elegante e legível do que o dicionário.
Vincenzo
Eu recomendaria usar slots em geral aqui. Ele protege você contra erros de digitação.
DerWeh 01 de
@DerWeh interessante, nunca ouvi falar disso. Você poderia dizer o mesmo de alguma aula? Eu odeio ficar confuso com erros de digitação.
Bob Stein
@BobStein Desculpe, eu realmente não entendo o que você quer dizer. Mas ao adicionar __slots__ = ()e criar um objeto em vez de usar a classe, por exemplo context.z = 3, levantaria um AttributeError. É possível para todas as classes, a menos que herdem de uma classe que não define slots.
DerWeh
@DerWeh Nunca ouvi falar de slots para detectar erros de digitação. Você está certo, só ajudaria em instâncias de classe, não variáveis ​​de classe. Talvez você queira criar um exemplo funcional no pyfiddle. Isso exigiria pelo menos duas linhas adicionais. Não consigo imaginar como você faria isso sem mais desordem.
Bob Stein,
14

Acho que a chave aqui é o que você entende por "acesso". Não deve haver problema com a leitura de uma variável fora do escopo de fechamento, por exemplo,

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

deve funcionar como esperado (impressão 3). No entanto, substituir o valor de x não funciona, por exemplo,

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

continuará a imprimir 3. Pelo que entendi do PEP-3104, é isso que a palavra-chave não local deve abranger. Conforme mencionado no PEP, você pode usar uma classe para realizar a mesma coisa (meio confuso):

class Namespace(object): pass
ns = Namespace()
ns.x = 3
def outer():
    def inner():
        ns.x = 5
    inner()
outer()
print ns.x
ig0774
fonte
Em vez de criar uma classe e instanciá-la, pode-se simplesmente criar uma função: def ns(): passseguida por ns.x = 3. Não é bonito, mas é um pouco menos feio para os meus olhos.
davidchambers
2
Algum motivo particular para o downvote? Admito que a minha solução não é a mais elegante, mas funciona ...
ig0774
2
Sobre o quê class Namespace: x = 3?
Feuermurmel
3
Não acho que dessa forma simule o não local, pois cria uma referência global em vez de uma referência em um encerramento.
CarmeloS
1
Eu concordo com @CarmeloS, seu último bloco de código em não fazer o que PEP-3104 sugere como forma de realizar algo semelhante no Python 2. nsé um objeto global que é o motivo pelo qual você pode fazer referência ns.xno nível do módulo na printinstrução no final .
martineau de
13

Há outra maneira de implementar variáveis ​​não locais no Python 2, caso alguma das respostas aqui seja indesejável por qualquer motivo:

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

É redundante usar o nome da função na instrução de atribuição da variável, mas parece mais simples e limpo para mim do que colocar a variável em um dicionário. O valor é lembrado de uma chamada para outra, assim como na resposta de Chris B..

Elias Zamaria
fonte
19
Atenção: quando implementado desta forma, se o fizer f = outer()e depois o fizer g = outer(), fo contador de será reiniciado. Isso ocorre porque ambos compartilham uma única outer.y variável, em vez de cada um ter sua própria variável independente. Embora este código pareça mais esteticamente agradável do que a resposta de Chris B, sua maneira parece ser a única maneira de emular o escopo léxico se você quiser chamar outermais de uma vez.
Nathaniel
@Nathaniel: Deixe-me ver se entendi bem. A atribuição a outer.ynão envolve nada local para a chamada de função (instância) outer(), mas atribui a um atributo do objeto de função que está vinculado ao nome outerem seu escopo delimitador . E, portanto, alguém poderia igualmente ter usado, por escrito outer.y, qualquer outro nome em vez de outer, desde que seja conhecido por estar vinculado a esse escopo. Isso está correto?
Marc van Leeuwen
1
Correção, eu deveria ter dito, depois de "vinculado a esse escopo": a um objeto cujo tipo permite definir atributos (como uma função, ou qualquer instância de classe). Além disso, uma vez que este escopo está realmente mais além do que queremos, isso não sugere a seguinte solução extremamente feia: em vez de outer.yusar o nome inner.y(uma vez que innerestá vinculado dentro da chamada outer(), que é exatamente o escopo que queremos), mas colocando a inicialização inner.y = 0 após a definição de interno (já que o objeto deve existir quando seu atributo é criado), mas é claro antes return inner?
Marc van Leeuwen
@MarcvanLeeuwen Parece que seu comentário inspirou a solução com inner.y de Elias. Bom pensamento por você.
Ankur Agarwal de
Acessar e atualizar variáveis ​​globais provavelmente não é o que a maioria das pessoas está tentando fazer ao alterar as capturas de um encerramento.
binki
12

Aqui está algo inspirado por uma sugestão de Alois Mahdal feita em um comentário sobre outra resposta :

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)

Atualizar

Depois de olhar para trás recentemente, fiquei impressionado com o quão parecido com o decorador ele era - quando me dei conta de que implementá-lo como um iria torná-lo mais genérico e útil (embora isso possa degradar sua legibilidade em algum grau).

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

Observe que ambas as versões funcionam em Python 2 e 3.

martineau
fonte
Este parece ser o melhor em termos de legibilidade e preservação do escopo lexical.
Aaron S. Kurland,
3

Há uma verruga nas regras de escopo do python - a atribuição torna uma variável local para seu escopo de função imediatamente envolvente. Para uma variável global, você resolveria isso com a globalpalavra - chave.

A solução é introduzir um objeto que é compartilhado entre os dois escopos, que contém variáveis ​​mutáveis, mas é referenciado por meio de uma variável que não é atribuída.

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

Uma alternativa é o hackeamento de escopos:

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

Você pode ser capaz de descobrir alguns truques para obter o nome do parâmetro e outer, em seguida, passá-lo como varname, mas sem depender do nome, outervocê gostaria de usar um combinador Y.

Marcin
fonte
Ambas as versões funcionam neste caso particular, mas não são uma substituição para nonlocal. locals()cria um dicionário de outer()locais no momento em que inner()é definido, mas alterar esse dicionário não altera o vin outer(). Isso não funcionará mais quando você tiver mais funções internas que desejam compartilhar uma variável fechada. Digamos que um inc()e dec()que aumentar e diminuir um contador compartilhada.
BlackJack
@BlackJack nonlocalé um recurso do Python 3.
Marcin
Sim eu conheço. A questão era como obter o efeito do Python 3 nonlocalno Python 2 em geral . Suas ideias não cobrem o caso geral, mas apenas aquele com uma função interna. Dê uma olhada nesta essência para um exemplo. Ambas as funções internas têm seu próprio contêiner. Você precisa de um objeto mutável no escopo da função externa, como outras respostas já sugeridas.
BlackJack
@BlackJack Essa não é a questão, mas obrigado por sua contribuição.
Marcin
Sim, essa é a questão. Dê uma olhada no topo da página. adinsa tem Python 2.6 e precisa de acesso a variáveis ​​não locais em um encerramento sem a nonlocalpalavra - chave introduzida no Python 3.
BlackJack
3

Outra maneira de fazer isso (embora seja muito prolixo):

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
fonte
1
na verdade eu prefiro a solução usando um dicionário, mas esse é legal :)
Ezer Fernandes
0

Estendendo a elegante solução Martineau acima para um caso de uso prático e um pouco menos elegante, recebo:

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
fonte
-3

Use uma variável 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

Pessoalmente, não gosto das variáveis ​​globais. Mas, minha proposta é baseada em https://stackoverflow.com/a/19877437/1083704 resposta

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

onde o usuário precisa declarar uma variável global ranks, toda vez que você precisa chamar o report. Meu aprimoramento elimina a necessidade de inicializar as variáveis ​​de função do usuário.

Val
fonte
É muito melhor usar um dicionário. Você pode fazer referência à instância em inner, mas não pode atribuir a ela, mas pode modificar suas chaves e valores. Isso evita o uso de variáveis ​​globais.
johannestaas