variable unboundlocalerror referenced non before assignment python performance

python - non - unboundlocalerror: local variable referenced before assignment



Python-rendimiento con variables globales vs local (4)

Todavía soy nuevo en Python, y he estado tratando de mejorar el rendimiento de mi script de Python, por lo que lo probé con y sin variables globales. Lo cronometré, y para mi sorpresa, se ejecutó más rápido con las variables globales declaradas en lugar de pasar las variables locales a las funciones. ¿Que esta pasando? ¿Pensé que la velocidad de ejecución era más rápida con las variables locales? (Sé que los globales no son seguros, sigo siendo curioso).


Los locales deberían ser más rápidos

Según esta página en locales y globales :

Cuando una línea de código solicita el valor de una variable x, Python buscará esa variable en todos los espacios de nombres disponibles, en orden:

  • espacio de nombres local - específico para la función actual o el método de clase. Si la función define una variable local x, o tiene un argumento x, Python usará esto y dejará de buscar.
  • espacio de nombres global - específico para el módulo actual. Si el módulo ha definido una variable, función o clase llamada x, Python lo usará y dejará de buscar.
  • espacio de nombres incorporado - global a todos los módulos. Como último recurso, Python asumirá que x es el nombre de la función o variable incorporada.

En base a eso, asumo que las variables locales son generalmente más rápidas. Mi conjetura es que lo que estás viendo es algo particular acerca de tu guión.

Los locales son más rápidos

Aquí hay un ejemplo trivial que usa una variable local, que toma alrededor de 0.5 segundos en mi máquina (0.3 en Python 3):

def func(): for i in range(10000000): x = 5 func()

Y la versión global, que toma alrededor de 0.7 (0.5 en Python 3):

def func(): global x for i in range(1000000): x = 5 func()

global hace algo extraño a las variables que ya son globales.

Curiosamente, esta versión se ejecuta en 0,8 segundos:

global x x = 5 for i in range(10000000): x = 5

Mientras esto se ejecuta en 0.9:

x = 5 for i in range(10000000): x = 5

Notará que en ambos casos, x es una variable global (ya que no hay funciones), y ambas son más lentas que el uso de locales. No tengo ni idea de por qué la declaración de global x ayudó en este caso.

Esta rareza no ocurre en Python 3 (ambas versiones tardan unos 0.6 segundos).

Mejores métodos de optimización.

Si desea optimizar su programa, lo mejor que puede hacer es perfilarlo . Esto le dirá qué es lo que lleva más tiempo, para que pueda concentrarse en eso. Su proceso debe ser algo como:

  1. Ejecute su programa con perfiles en.
  2. Mire el perfil en KCacheGrind o un programa similar para determinar qué funciones están tardando más tiempo.
  3. En esas funciones:
    • Busque lugares donde pueda almacenar en caché los resultados de las funciones (para que no tenga que hacer tanto trabajo).
    • Busque mejoras algorítmicas como reemplazar funciones recursivas con funciones de forma cerrada, o reemplazar búsquedas de lista con diccionarios.
    • Re-perfil para asegurarse de que la función sigue siendo un problema.
    • Considere el uso de multiprocessing .

Cuando Python compila una función, la función sabe antes de que se llame si las variables en ella son locales, cierres o globales.

Tenemos varias formas de referenciar variables en funciones:

  • globales
  • cierres
  • locales

Así que vamos a crear este tipo de variables en unas pocas funciones diferentes para que podamos ver por nosotros mismos:

global_foo = ''foo'' def globalfoo(): return global_foo def makeclosurefoo(): boundfoo = ''foo'' def innerfoo(): return boundfoo return innerfoo closurefoo = makeclosurefoo() def defaultfoo(foo=''foo''): return foo def localfoo(): foo = ''foo'' return foo

Desmontado

Podemos ver que cada función sabe dónde buscar la variable, no es necesario hacerlo en el tiempo de ejecución:

>>> import dis >>> dis.dis(globalfoo) 2 0 LOAD_GLOBAL 0 (global_foo) 2 RETURN_VALUE >>> dis.dis(closurefoo) 4 0 LOAD_DEREF 0 (boundfoo) 2 RETURN_VALUE >>> dis.dis(defaultfoo) 2 0 LOAD_FAST 0 (foo) 2 RETURN_VALUE >>> dis.dis(localfoo) 2 0 LOAD_CONST 1 (''foo'') 2 STORE_FAST 0 (foo) 3 4 LOAD_FAST 0 (foo) 6 RETURN_VALUE

Podemos ver que actualmente el byte-code para un global es LOAD_GLOBAL , una variable de cierre es LOAD_DEREF , y un local es LOAD_FAST . Estos son detalles de implementación de CPython, y pueden cambiar de una versión a otra, pero es útil poder ver que Python trata cada búsqueda de variable de manera diferente.

Pegue en un intérprete y vea por usted mismo:

import dis dis.dis(globalfoo) dis.dis(closurefoo) dis.dis(defaultfoo) dis.dis(localfoo)

Código de prueba

Código de prueba (no dude en probar en su sistema):

import sys sys.version import timeit min(timeit.repeat(globalfoo)) min(timeit.repeat(closurefoo)) min(timeit.repeat(defaultfoo)) min(timeit.repeat(localfoo))

Salida

En Windows, al menos en esta compilación, parece que los cierres reciben un poco de penalización, y usar un local que es el predeterminado es el más rápido, porque no tiene que asignar el local cada vez:

>>> import sys >>> sys.version ''3.6.1 |Anaconda 4.4.0 (64-bit)| (default, May 11 2017, 13:25:24) [MSC v.1900 64 bit (AMD64)]'' >>> import timeit >>> min(timeit.repeat(globalfoo)) 0.0728403456180331 >>> min(timeit.repeat(closurefoo)) 0.07465484920749077 >>> min(timeit.repeat(defaultfoo)) 0.06542038103088998 >>> min(timeit.repeat(localfoo)) 0.06801849537714588

En Linux:

>>> import sys >>> sys.version ''3.6.4 |Anaconda custom (64-bit)| (default, Mar 13 2018, 01:15:57) /n[GCC 7.2.0]'' >>> import timeit >>> min(timeit.repeat(globalfoo)) 0.08560040907468647 >>> min(timeit.repeat(closurefoo)) 0.08592104795388877 >>> min(timeit.repeat(defaultfoo)) 0.06587386003229767 >>> min(timeit.repeat(localfoo)) 0.06887826602905989

Agregaré otros sistemas ya que tengo la oportunidad de probarlos.


El tiempo que no está incluyendo es el tiempo del programador dedicado a rastrear los errores creados cuando el uso de un global tiene un efecto secundario en otra parte de su programa. Ese tiempo es muchas veces mayor que el tiempo dedicado a crear y liberar variables locales,


Respuesta simple:

Debido a la naturaleza dinámica de Python, cuando el intérprete se encuentra con una expresión como abc, busca un (probando primero el espacio de nombres local, luego el espacio de nombres global y finalmente el espacio de nombres incorporado), luego busca el espacio de nombres de ese objeto para resolver el nombre b, y finalmente busca en el espacio de nombres de ese objeto para resolver el nombre c. Estas búsquedas son razonablemente rápidas; Para las variables locales, las búsquedas son extremadamente rápidas, ya que el intérprete sabe qué variables son locales y puede asignarles una posición conocida en la memoria.

El intérprete sabe qué nombres dentro de sus funciones son locales y les asigna ubicaciones específicas (conocidas) dentro de la memoria de la llamada a la función. Esto hace que las referencias a los locales sean mucho más rápidas que a las globales y (más especialmente) a las integradas.

Código de ejemplo para explicar lo mismo:

>>> glen = len # provides a global reference to a built-in >>> >>> def flocal(): ... name = len ... for i in range(25): ... x = name ... >>> def fglobal(): ... for i in range(25): ... x = glen ... >>> def fbuiltin(): ... for i in range(25): ... x = len ... >>> timeit("flocal()", "from __main__ import flocal") 1.743438959121704 >>> timeit("fglobal()", "from __main__ import fglobal") 2.192162036895752 >>> timeit("fbuiltin()", "from __main__ import fbuiltin") 2.259413003921509 >>>