Comportamiento de la función exec en Python 2 y Python 3


El siguiente código da una salida diferente en Python2 y en Python3:

from sys import version

print(version)

def execute(a, st):
    b = 42
    exec("b = {}\nprint('b:', b)".format(st))
    print(b)
a = 1.
execute(a, "1.E6*a")

Python2 impresiones:

2.7.2 (default, Jun 12 2011, 15:08:59) [MSC v.1500 32 bit (Intel)]
('b:', 1000000.0)
1000000.0

Python3 impresiones:

3.2.3 (default, Apr 11 2012, 07:15:24) [MSC v.1500 32 bit (Intel)]
b: 1000000.0
42

¿Por qué Python2 enlaza la variable b dentro de la función execute a los valores en la cadena de la función exec, mientras que Python3 no hace esto? ¿Cómo puedo lograr el comportamiento de Python2 en Python3? Ya intenté pasar diccionarios para globales y locales a la función exec en Python3, pero nada funcionó hasta ahora.

--- EDITAR ---

Después de leer la respuesta de Martijns, analicé esto con Python3. En el siguiente ejemplo doy la dicción locals() como d a exec, pero d['b'] imprime algo más que solo imprimir b.

from sys import version

print(version)

def execute(a, st):
    b = 42
    d = locals()
    exec("b = {}\nprint('b:', b)".format(st), globals(), d)
    print(b)                     # This prints 42
    print(d['b'])                # This prints 1000000.0
    print(id(d) == id(locals())) # This prints True
a = 1.
execute(a, "1.E6*a")

3.2.3 (default, Apr 11 2012, 07:15:24) [MSC v.1500 32 bit (Intel)]
b: 1000000.0
42
1000000.0
True

La comparación de los id de d y locals() muestra que son el mismo objeto. Pero bajo estas condiciones b debe ser lo mismo que d['b']. ¿Qué está mal en mi ejemplo?

Author: Eric Leschinski, 2013-02-26

4 answers

Hay una gran diferencia entre exec en Python 2 y exec() en Python 3. Estás tratando exec como una función, pero realmente es una instrucción en Python 2.

Debido a esta diferencia, no puede cambiar las variables locales en el ámbito de la función en Python 3 usando exec, a pesar de que era posible en Python 2. Ni siquiera las variables declaradas previamente.

locals() solo refleja las variables locales en una dirección. Lo siguiente nunca funcionó en 2 o 3:

def foo():
    a = 'spam'
    locals()['a'] = 'ham'
    print(a)              # prints 'spam'

En Python 2, usar la instrucción exec significaba que el compilador sabía desactivar las optimizaciones de alcance local (cambiar de LOAD_FAST a LOAD_NAME, por ejemplo, para buscar variables en los ámbitos local y global). Con exec() siendo una función, esa opción ya no está disponible y los ámbitos de función ahora están siempre optimizados.

Además, en Python 2, la instrucción exec copia explícitamente todas las variables encontradas en locals() a la función locals usando PyFrame_LocalsToFast, pero solo si no se suministraron parámetros globales y locales.

La solución adecuada es usar un nuevo espacio de nombres (un diccionario) para su llamada exec():

def execute(a, st):
    namespace = {}
    exec("b = {}\nprint('b:', b)".format(st), namespace)
    print(namespace['b'])

El exec() la documentación es muy explícita sobre esta limitación:

Nota: El default locals actúa como se describe para function locals() a continuación: no se deben intentar las modificaciones al default locals diccionario. Pase un diccionario explicit locals si necesita ver los efectos del código en locals después de que function exec() regrese.

 37
Author: Martijn Pieters,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-12-12 13:45:12

Yo diría que es un error de python3.

def u():
    exec("a=2")
    print(locals()['a'])
u()

Imprime "2".

def u():
    exec("a=2")
    a=2
    print(a)
u()

Imprime "2".

Pero

def u():
    exec("a=2")
    print(locals()['a'])
    a=2
u()

Falla con

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in u
KeyError: 'a'

- - - EDITAR --- Otro comportamiento interesante:

def u():
    a=1
    l=locals()
    exec("a=2")
    print(l)
u()
def u():
    a=1
    l=locals()
    exec("a=2")
    locals()
    print(l)
u()

Salidas

{'l': {...}, 'a': 2}
{'l': {...}, 'a': 1}

Y también

def u():
    l=locals()
    exec("a=2")
    print(l)
    print(locals())
u()
def u():
    l=locals()
    exec("a=2")
    print(l)
    print(locals())
    a=1
u()

Salidas

{'l': {...}, 'a': 2}
{'l': {...}, 'a': 2}
{'l': {...}, 'a': 2}
{'l': {...}}

Aparentemente, la acción de exec sobre los locales es la siguiente:

  • Si se establece una variable dentro de exec y esta variable era una variable local, entonces exec modifica diccionario interno (el que devuelve locals()) y no lo devuelve a su estado original. Una llamada a locals() actualiza el diccionario (como se documenta en la sección 2 de la documentación de python), y el valor establecido dentro de exec se olvida. La necesidad de llamar a locals() para actualizar el diccionario no es un error de python3, porque está documentado, pero no es intuitivo. Además, el hecho de que las modificaciones de los locales dentro de exec no cambian los locales de la función es una diferencia documentada con python2 (la documentación dice "Pasa un diccionario local explícito si necesitas ver los efectos del código en los locales después de que la función exec () regrese"), y yo prefiero el comportamiento de python2.
  • Si una variable se establece dentro de exec y esta variable no existía antes, entonces exec modifica el diccionario interno a menos que la variable se establezca después. Parece que hay un error en la forma en que locals() actualiza el diccionario; este error da acceso al valor establecido dentro de exec llamando locals() después de exec.
 5
Author: LRGH,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-05-20 10:14:15

Me temo que no puedo explicarlo exactamente, pero básicamente proviene del hecho de que b dentro de la función es local, y exec() parece asignar a la b global. Tendrás que declarar b como global dentro de la función, y dentro de la sentencia exec.

Prueba esto:

from sys import version

print(version)

def execute1(a, st):
    b = 42
    exec("b = {}\nprint('b:', b)".format(st))
    print(b)

def execute2(a, st):
    global b
    b = 42
    exec("global b; b = {}\nprint('b:', b)".format(st))
    print(b)

a = 1.
execute1(a, "1.E6*a")
print()
execute2(a, "1.E6*a")
print()
b = 42
exec("b = {}\nprint('b:', b)".format('1.E6*a'))
print(b)

Que me da

3.3.0 (default, Oct  5 2012, 11:34:49) 
[GCC 4.4.5]
b: 1000000.0
42

b: 1000000.0
1000000.0

b: 1000000.0
1000000.0

Se puede ver que fuera de la función, el b global se recoge automáticamente. Dentro de la función, estás imprimiendo el local b.

Tenga en cuenta que me gustaría he pensado que exec() siempre usa primero la b global, de modo que en execute2(), no es necesario declararlo dentro de la función exec(). Pero encuentro que no funciona(que es la parte que no puedo explicar exactamente).

 1
Author: ,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-02-26 10:56:33

Para resumirlo: {[47]]}

  • No hay ningún error en Python 2 ni en Python 3
  • El comportamiento diferente de exec proviene de exec siendo una instrucción en Python 2, mientras que se convirtió en una función en Python 3.

Tenga en cuenta:

No digo nada nuevo aquí. Esto es solo una asamblea de la verdad por ahí se encuentra en todas las otras respuestas y comentarios. Todo lo que intento aquí es traer luz a algunos de los más oscuros detalles.

La única diferencia entre Python 2 y Python 3 es que, de hecho, exec es capaz de cambiar el ámbito local de la función que encierra en Python 2 (porque es una instrucción y puede acceder al ámbito local actual) y no puede hacer esto más en Python 3 (porque ahora es una función, por lo que se ejecuta en su propio ámbito local).

La irritación, sin embargo, no tiene nada que ver con la declaración exec, solo se deriva de un detalle de comportamiento especial:

locals() devuelve algo, que quiero llamar "un singleton mutable de ámbito que, después de la llamada a locals(), siempre solo hace referencia a todas las variables en el ámbito local".

Tenga en cuenta que el comportamiento de locals() no cambió entre Python 2 y 3. Por lo tanto, este comportamiento junto con el cambio de cómo exec funciona parece ser errático, pero no lo es, ya que solo expone algunos detalles, que siempre estuvieron allí.

¿Qué hace "un singleton mutable de alcance que hace referencia a variables en local alcance " ¿significa?

  • Es un scope-wise singleton, ya que independientemente de la frecuencia con la que llame a locals() en el mismo ámbito, el objeto devuelto es siempre el mismo.
    • De ahí la observación, que id(d) == id(locals()), porque d y locals() se refieren al mismo objeto, el mismo singleton, ya que solo puede haber uno (en un ámbito diferente obtienes un objeto diferente, pero en el mismo ámbito solo ves este único).
  • es mutable, ya que es un objeto normal, por lo que puede alterar se.
    • locals() obliga a todas las entradas en el objeto a hacer referencia a las variables en el ámbito local de nuevo.
    • Si cambia algo en el objeto (a través de d), esto altera el objeto, ya que es un objeto mutable normal.
  • Estos cambios del singleton no se propagan de nuevo al ámbito local, porque todas las entradas en el objeto son references to the variables in the local scope. Así que si altera las entradas, esto cambia el objeto singleton, y no el contenido de donde " las referencias apuntado antes de cambiar la referencia" (por lo tanto, no altera la variable local).

    • En Python, las cadenas y los Números no son mutables. Esto significa que, si asigna algo a una entrada, no cambia el objeto al que apunta la entrada, introduce un nuevo objeto y asigna una referencia a ese a la entrada. Ejemplo:

      a = 1
      d = locals()
      d['a'] = 300
      # d['a']==300
      locals()
      # d['a']==1
      

    Además de la optimización esto hace:

    • Crear nuevo número de objeto (1) - que es algún otro singleton, BTW.
    • almacene el puntero a este número (1) en LOCALS['a']
      (donde LOCALS será el ámbito interno local)
    • Si aún no existe, crea SINGLETON objeto
    • actualizar SINGLETON, por lo que hace referencia a todas las entradas en LOCALS
    • almacenar puntero de la SINGLETON en LOCALS['d']
    • Crear Número(300), que es no un singleton, POR cierto.
    • almacenar puntero a estos Números (300) en d['a']
    • por lo tanto, el SINGLETON también se actualiza.
    • pero LOCALS está no actualizado, así que la variable local a o LOCALS['a'] sigue siendo el Número (1)
    • Ahora, locals() se llama de nuevo, el SINGLETON se actualiza.
    • Como d se refiere a SINGLETON, no LOCALS, d los cambios, también!

Para más información sobre este sorprendente detalle, por qué 1 es un singleton mientras que 300 no lo es, ver https://stackoverflow.com/a/306353

Pero por favor no se olvide: Los números son inmutables, por lo que si intenta cambiar un número a otro valor, se crea efectivamente otro objeto.

Conclusión:

No puede devolver el comportamiento exec de Python 2 a Python 3 (excepto cambiando su código), ya que ya no hay forma de alterar las variables locales fuera del flujo del programa.

Sin embargo, puede llevar el comportamiento de Python 3 a Python 2, de modo que, hoy en día, puede escribir programas que ejecuten lo mismo, independientemente de si se ejecutan con Python 3 o Python 2. Esto es porque en (más reciente) Python 2 puede usar exec con argumentos de función como también (de hecho, es una tupla 2 o 3), con permite usar la misma sintaxis con la misma semántica conocida de Python 3:

exec "code"

(que solo funciona en Python 2) se convierte en (que funciona para Python 2 y 3):

exec("code", globals(), locals())

Pero cuidado, que "code" ya no puede alterar el ámbito local de esta manera. Véase también https://docs.python.org/2/reference/simple_stmts.html#exec

Algunos muy últimos palabras:

El cambio de exec en Python 3 es bueno. Debido a la optimización.

En Python 2 no se pudo optimizar a través de exec, porque el estado de todas las variables locales que contenían contenidos inmutables podía cambiar de manera impredecible. Esto ya no puede suceder. Ahora las reglas usuales de invocaciones de funciones se aplican a exec() como a todas las demás funciones, también.

 1
Author: Tino,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-11-23 18:12:58