Pergunta

Gostaria de criar um decorador como abaixo, mas não consigo pensar em uma implementação que funcione.Estou começando a achar que não é possível, mas pensei em perguntar a vocês primeiro.

Sei que existem várias outras maneiras de criar variáveis ​​estáticas em Python, mas acho essas formas feias.Eu realmente gostaria de usar a sintaxe abaixo, se possível.

@static(x=0)
def f():
    x += 1
    print x

f() #prints 1
f() #prints 2

Eu não me importo se a implementação de static é longo ou hacky, desde que funcione como acima.

Eu criei esta versão, mas ela só permite um <function>.<varname> sintaxe, que se torna complicada rapidamente com funções e nomes de variáveis ​​mais longos.

def static(**assignments):
    def decorate(func):
        for var, val in assignments.items():
            setattr(func, var, val)
        return func
    return decorate

Várias coisas que pensei, mas não consegui colocar em prática foram:

  1. Mudar f (a função decorada) para uma classe que pode ser chamada e, de alguma forma, armazenar os vars estáticos em self transparentemente.
  2. Modificando os globais de f() dentro do decorador e de alguma forma inserindo instruções 'global x' no código de f.
  3. Transformando f em um gerador onde vinculamos as variáveis ​​manualmente e depois executamos o código de f diretamente.
Foi útil?

Solução

Aqui está um decorador que parece funcionar.Observe que isso requer return locals() no final da função devido à impossibilidade de definir locais de fora (não tenho muita experiência em programação, então se houver uma maneira, não sei).

class Static(object):
    def __init__(self, **kwargs):
        self.kwargs = kwargs

    def __call__(self, f):
        def wrapped_f():
            try:
                new_kwargs = {}
                for key in self.kwargs:
                    i = getattr(f, key)
                    new_kwargs[key] = i
                self.kwargs = new_kwargs
            except:
                pass
            for key, value in f(**self.kwargs).items():
                setattr(f, key, value)
        return wrapped_f

@Static(x=0, y=5, z='...')
def f(x, y, z):
    x += 1
    y += 5
    print x, y, z
    return locals()

A saída seria:

>>> f()
1 10 ...
>>> f()
2 15 ...
>>> f()
3 20 ...

EDITAR:

encontrei algo em http://code.activestate.com/recipes/410698/ e decidi tentar adicioná-lo a isso.Funciona sem o retorno agora.

EDITAR novamente:Alterado para para torná-lo alguns segundos mais rápido.Editar 3;alterado para função em vez de classe

def static(**kwargs):
    def wrap_f(function):
        def probeFunc(frame, event, arg):
            if event == 'call':
                frame.f_locals.update(kwargs)
                frame.f_globals.update(kwargs)
            elif event == 'return':
                for key in kwargs:
                    kwargs[key] = frame.f_locals[key]
                sys.settrace(None)
            return probeFunc
        def traced():
            sys.settrace(probeFunc)
            function()
        return traced
    return wrap_f

testado:

@static(x=1)
def f():
    x += 1

global_x = 1
def test_non_static():
    global global_x
    global_x += 1


print 'Timeit static function: %s' % timeit.timeit(f)
print 'Timeit global variable: %s' % timeit.timeit(test_non_static)

saída:

Timeit static function: 5.10412869535
Timeit global variable: 0.242917510783

Usar settrace diminui drasticamente a velocidade.

Outras dicas

No momento em que seu decorador obtém o objeto de função f, ele já foi compilado - especificamente, foi compilado com o conhecimento de que x é local (porque é atribuído com o += atribuição), a otimização normal (em 2.* você pode derrotar a otimização, com um preço impressionante em desempenho, começando f com exec '';em 2.*, você não pode anular a otimização).Essencialmente, para usar a sintaxe desejada, você precisa recompilar f (recuperando suas fontes, se você sabe que elas estarão disponíveis em tempo de execução, ou, muito mais difícil, por hacks de bytecode) com fontes modificadas de alguma forma - uma vez que você decidiu seguir esse caminho, a abordagem mais simples é provavelmente mudar x em f.x por todo o corpo de f.

Pessoalmente, se e quando me vejo lutando tanto contra a linguagem (ou outra tecnologia) que estou tentando submeter-me à minha vontade para impor meus desejos, reconheço que estou usando a linguagem errada (ou outra tecnologia) , se esses desejos são absolutamente cruciais, então a solução deve ser mudar a tecnologia;ou, se esses desejos não forem tão cruciais, desista deles.

De qualquer forma, desisto de tentar distorcer a linguagem muito longe de suas intenções óbvias de design:mesmo que eu inventasse algum truque hackeado e frágil, sem dúvida seria insustentável.Neste caso, as intenções desejadas do Python são muito claras:barenames que são vinculados novamente a uma função são locais dessa função, a menos que sejam explicitamente designados como globais - ponto final.Portanto, sua tentativa de fazer com que barenames (que são religados a uma função) signifiquem algo completamente diferente de "locais" é exatamente esse tipo de luta.

Editar:Se você estiver disposto a desistir da insistência em usar nomes simples para sua "estática", então, de repente, você não estará mais lutando contra o Python, mas sim "seguindo a tendência" da linguagem (apesar da falha de design do global [e nonlocal], mas isso é um discurso à parte ;-).Então, por exemplo:

class _StaticStuff(object):
  _static_stack = []
  def push(self, d):
    self._static_stack.append(d)
  def pop(self):
    self._static_stack.pop()
  def __getattr__(self, n):
    return self._static_stack[-1][n]
  def __setattr__(self, n, v):
    self._static_stack[-1][n] = v
import __builtin__
__builtin__.static = _StaticStuff()

def with_static(**variables):
  def dowrap(f):
    def wrapper(*a, **k):
      static.push(variables)
      try: return f(*a, **k)
      finally: static.pop()
    return wrapper
  return dowrap

@with_static(x=0)
def f():
    static.x += 1
    print static.x

f()
f()

Isso funciona exatamente como você deseja, imprimindo 1 e depois 2.(Estou a usar __builtin__ para torná-lo mais simples de usar with_static para decorar funções que vivem em qualquer módulo, é claro).Você poderia ter diversas implementações diferentes, mas o ponto chave de qualquer bom implementação é que "variáveis ​​estáticas" serão qualificado nomes, não barenames - deixando explícito que não são variáveis ​​locais, brincando com a granularidade da linguagem e assim por diante.(Contêineres integrados semelhantes e nomes qualificados baseados neles, deve foram usados ​​​​no design do Python, em vez do global e nonlocal falhas de design, para indicar outros tipos de variáveis ​​que não são locais e, portanto, não deveriam usar barenames...ah, bem, você pode implementar um globvar recipiente especial nas mesmas linhas acima static uns, sem sequer precisar de decoração, embora não tenha tanta certeza de que isso seja totalmente viável para o nonlocal caso [talvez com alguma decoração e um pouquinho de magia negra...;=)]).

Editar:um comentário aponta que o código fornecido não funciona quando você apenas decora uma função que retorna um fechamento (em vez de decorar o próprio fechamento).Isso mesmo:claro, você tem que decorar a função específica que usa o static (e só pode haver um, por definição de função-static variáveis!), não uma função aleatória que de fato não usa o static mas acontece que está em alguma conexão lexical com aquela que faz.Por exemplo:

def f():
  @with_static(x=0)
  def g():
    static.x += 1
    print static.x
  return g

x = f()
x()
x()

isso funciona, enquanto move o decorador para f em vez de g não (e não poderia).

Se os desejos reais não são sobre variáveis ​​estáticas (visíveis e utilizáveis ​​apenas dentro de uma única função), mas alguma coisa híbrida que pode ser usada em um certo conjunto peculiar de funções, isso precisa ser especificado com muita precisão (e sem dúvida implementado de maneira muito diferente, dependendo de quais são as especificações reais são) - e idealmente isso precisa acontecer em perguntas SO novas e separadas, porque esta (que é especificamente sobre estático em vez disso), e esta resposta a esta pergunta específica, já são grandes o suficiente.

Aqui está uma solução realmente simples que funciona como variáveis ​​estáticas normais do python.

def static(**kwargs):
  def wrap(f):
    for key, value in kwargs.items():
      setattr(f, key, value)
    return f
  return wrap

Exemplo de uso:

@static(a=0)
def foo(x):
  foo.a += 1
  return x+foo.a

foo(1)  # => 2
foo(2)  # => 4
foo(14) # => 17

Isso corresponde mais à maneira normal de executar variáveis ​​estáticas do python

def foo(x):
  foo.a += 1
  return x+foo.a
foo.a = 10

Você poderia fazer algo assim (mas não testei isso extensivamente;usou CPython 2.6):

import types

def static(**dict):
    def doWrap(func):
        scope = func.func_globals
        scope.update(dict)
        return types.FunctionType(func.func_code, scope)
    return doWrap

# if foo() prints 43, then it's wrong
x = 42

@static(x = 0)
def foo():
   global x
   x += 1
   print(x)

foo() # => 1
foo() # => 2

Requer declarar essas variáveis ​​como globais e sombrear variáveis ​​globais de nível superior, mas caso contrário, deve funcionar.Não tenho certeza sobre o desempenho, no entanto.

Que tal isso, sem decoradores?

class State(dict):
    """Object interface to dict."""
    def __getattr__(self, name):
        try:
            return self[name]
        except KeyError:
            raise AttributeError, name

def f(d=State(x=0)):
    d.x += 1
    return d.x

E aqui está em ação:

>>> f()
1
>>> f()
2
>>> f()
3

Aqui está algo que pode ser muito mais claro.Não envolve decoradores ou hackers.

class F( object ):
    def __init__( self ):
        self.x= 0
    def __call__( self ):
        self.x += 1
        print self.x

f= F()

Agora você tem sua função f com uma variável estática.

f() #prints 1
f() #prints 2

Quando você precisa salvar o estado entre as invocações de uma função, quase sempre é melhor usar um gerador/co-rotina ou um objeto.Como você deseja usar nomes de variáveis ​​"básicos", você desejará a versão da corrotina.

# the coroutine holds the state and yields rather than returns values
def totalgen(x=0, y=0, z=0):
    while True:
       a, b, c = (yield x, y, z)
       x += a
       y += b
       z += c

# the function provides the expected interface to callers
def runningtotal(a, b, c, totalgen=totalgen()):
    try:
        return totalgen.send((a, b, c))    # conveniently gives TypeError 1st time
    except TypeError:
        totalgen.next()                    # initialize, discard results
        return totalgen.send((a, b, c))

O resultado é uma função que acumula os totais dos três valores passados ​​para ela, exatamente como se tivesse variáveis ​​estáticas, mas os acumuladores são simples e antigas variáveis ​​locais no que é essencialmente um gerador infinito.

Um leve ajuste de outra resposta:

def static(**kwargs):
    def decorator(func):
        return type(func)(func.func_code, dict(func.func_globals, **kwargs))
    return decorator

message = "goodbye, world!"
@static(message="hello, world!")
def hello(): print message

hello()

Achei nojento substituir um nome interno por um nome de argumento de função, então mudei **dict para o mais canônico **kwargs.Também salvei algumas linhas e a IMO tornou o código mais limpo construindo um novo dict com dict(the_original, **the_updates).Por último salvei algumas linhas acessando o construtor da função via type(func) em vez de uma importação --- objetos de tipo e classe são métodos de fábrica, então use-os!

Eu também removi um global declaração.Isso funciona desde que você não revincule a variável, ou seja,removendo global na verdade, torna o ponteiro (mas não o objeto) somente leitura.Se você usar dessa forma, talvez let é um nome melhor do que static para a vinculação assim introduzida.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top