Pregunta

Me gustaría crear un decorador como el siguiente, pero parece que no puedo pensar en una implementación que funcione.Estoy empezando a pensar que no es posible, pero pensé en preguntarles a ustedes primero.

Me doy cuenta de que hay otras formas de crear variables estáticas en Python, pero esas formas las encuentro feas.Realmente me gustaría utilizar la siguiente sintaxis, si es posible.

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

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

No me importa si la implementación de static es largo o hackity, siempre que funcione como se indica arriba.

Creé esta versión, pero solo permite una <function>.<varname> sintaxis, que se vuelve engorrosa bastante rápidamente con funciones y nombres de variables más largos.

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

Varias cosas en las que pensé, pero no pude ponerme a trabajar fueron:

  1. Cambiar f (la función decorada) a una clase invocable y de alguna manera almacenar las variables estáticas en self transparentemente.
  2. Modificar los globales de f() dentro del decorador y de alguna manera insertar declaraciones 'globales x' en el código de f.
  3. Cambiando f a un generador donde vinculamos las variables manualmente y luego ejecutamos el código de f directamente.
¿Fue útil?

Solución

Aquí hay un decorador que parece funcionar. Tenga en cuenta que esto requiere retornar locales () al final de la función debido a que no puedo establecer locales desde el exterior (no tengo mucha experiencia en programación, así que si hay una manera, no lo sé).

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

El resultado sería:

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

EDITAR:

Encontré algo en http://code.activestate.com/recipes/410698/ y decidí intentar agregarlo a esto. Funciona sin el retorno ahora.

EDITAR de nuevo: se modificó para hacerlo unos segundos más rápido. Editar 3; cambiado a funcionar en lugar de clase

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

probado:

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

salida:

Timeit static function: 5.10412869535
Timeit global variable: 0.242917510783

El uso de settrace lo ralentiza drásticamente.

Otros consejos

Para cuando tu decorador obtenga el objeto de función f, ya ha sido compilado, específicamente, ha sido compilado con el conocimiento de que x es local (porque está asignado con el += asignación), la optimización normal (en 2.* Puedes derrotar la optimización, a un precio asombroso en rendimiento, iniciando f con exec '';en 2.*, no puedes derrotar la optimización).Esencialmente, para usar la sintaxis que deseas, debes recompilar f (recuperando sus fuentes, si sabe que estarán disponibles en tiempo de ejecución, o, mucho más difícil, mediante hacks de código de bytes) con fuentes modificadas de alguna manera; una vez que haya decidido seguir ese camino, el enfoque más simple probablemente sea cambiar x en f.x en todo el cuerpo de f.

Personalmente, si me encuentro luchando tan duramente contra el lenguaje (u otra tecnología) que estoy tratando de doblegar a mi voluntad para imponer mis deseos, reconozco que estoy usando el lenguaje (u otra tecnología) equivocado. , si esos deseos son absolutamente cruciales, entonces la solución debe ser cambiar la tecnología;o, si esos deseos no son tan cruciales, renunciar a ellos.

De cualquier manera, dejo de intentar distorsionar el lenguaje demasiado lejos de sus obvias intenciones de diseño:incluso si se me ocurriera algún truco frágil y complicado, sin duda sería imposible de mantener.En este caso, las intenciones de deseo de Python son muy claras:Los nombres básicos que se vuelven a vincular dentro de una función son locales de esa función a menos que se designen explícitamente como globales, punto.Entonces, su intento de hacer que los nombres básicos (que se vuelven a vincular dentro de una función) signifiquen algo completamente diferente a los "locales" es exactamente este tipo de pelea.

Editar:Si estás dispuesto a renunciar a la insistencia en usar nombres desnudos para tu "estática", entonces, de repente, ya no estás luchando contra Python, sino que "sigues la corriente" del lenguaje (a pesar del error de diseño de global [y nonlocal], pero esa es una perorata aparte ;-).Así por ejemplo:

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

Esto funciona tal como usted desea, imprimiendo 1 y luego 2.(Estoy usando __builtin__ para que sea más sencillo de usar with_static para decorar funciones habitables en cualquier módulo, por supuesto).Podría tener varias implementaciones diferentes, pero el punto clave de cualquier bien implementación es que las "variables estáticas" serán calificado nombres, no nombres básicos, haciendo explícito que no son variables locales, jugando con la esencia del lenguaje, etc.(Contenedores integrados similares y nombres calificados basados ​​en ellos, debería se han utilizado en el diseño de Python, en lugar del global y nonlocal fallas de diseño, para indicar otros tipos de variables que no son locales y por lo tanto no deberían usar nombres básicos...Ah, bueno, puedes implementar tú mismo un globvar contenedor especial en la misma línea del anterior static unos, sin siquiera necesitar decoración, aunque no estoy tan seguro de que sea del todo factible para el nonlocal caso [quizás con algo de decoración y una mínima cantidad de magia negra...;=)]).

Editar:un comentario señala que el código proporcionado no funciona cuando solo decoras una función que devuelve un cierre (en lugar de decorar el cierre en sí).Así es:Por supuesto, tienes que decorar la función específica que utiliza el static (y sólo puede haber uno, por definición de función-static variables!), no una función aleatoria que de hecho no utiliza el static sino que simplemente tiene alguna conexión léxica con el que hace.Por ejemplo:

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

x = f()
x()
x()

esto funciona, mientras mueve el decorador a f en lugar de g no lo hace (y no podría posiblemente).

Si lo que realmente se desea no son variables estáticas (visibles y utilizables sólo dentro de una única función), sino algo híbrido que se pueda utilizar en un determinado conjunto peculiar de funciones, eso debe especificarse con mucha precisión (y sin duda implementarse de manera muy diferente, dependiendo de cuáles son las especificaciones reales son) - e idealmente eso debe suceder en preguntas SO nuevas y separadas, porque esta (que trata específicamente sobre estático en su lugar), y esta respuesta a esta pregunta específica, ya son lo suficientemente grandes.

Aquí hay una solución realmente simple que funciona igual que las variables estáticas normales de Python.

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

Ejemplo de uso:

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

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

Esto coincide más estrechamente con la forma normal de hacer variables estáticas de Python

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

Podría hacer algo así (pero no lo he probado exhaustivamente; usé 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

Requiere declarar esas variables como globales y sombras variables globales de nivel superior, pero de lo contrario debería funcionar. Sin embargo, no estoy seguro sobre el rendimiento.

¿Qué tal esto, sin 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

Y aquí está en acción:

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

Aquí hay algo que podría ser mucho más claro. No involucra decoradores ni piratería.

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

f= F()

Ahora tiene su función f con una variable estática.

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

Cuando necesita guardar el estado entre invocaciones de una función, casi siempre es mejor usar un generador / corutina o un objeto. Como desea usar & Quot; bare & Quot; nombres de variables, entonces querrás la versión de rutina.

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

El resultado es una función que acumula los totales de los tres valores que se le pasan, exactamente como si tuviera variables estáticas, pero los acumuladores son viejas variables locales en lo que esencialmente es un generador infinito.

Un pequeño ajuste de otra respuesta :

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

Me pareció difícil anular un nombre incorporado con un nombre de argumento de función, así que cambié **dict por el más canónico **kwargs. También guardé algunas líneas e IMO hizo que el código fuera más limpio al construir un nuevo dict con dict(the_original, **the_updates). Por último, guardé algunas líneas accediendo al constructor de funciones a través de type(func) en lugar de una importación --- los objetos de tipo y clase son métodos de fábrica, ¡así que úselos!

También eliminé una declaración global. Esto funciona siempre que no vuelva a vincular la variable, es decir, eliminar let en efecto hace que el puntero (pero no el objeto) sea de solo lectura. Si lo usa de esta manera, tal vez static sea un nombre mejor que <=> para el enlace así introducido.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top