4

Tengo una función que es llamada por varias funciones distintas. Es llamada en forma directa o, dependiendo de la rutina es llamada por un proceso Process. Esta función también utiliza una variable global definida al inicio del script. El problema es que cuando la función es llamada mediante un Process la variable global ya no está definida. Expongo un ejemplo [Muy simplificado] :

 #!/usr/bin/python3.5
 from multiprocessing import Process

 def funcion1():

    print(mi_variable)


 def funcionProceso():

    try:

        evaluacion = Process(target=funcion1)
        evaluacion.start()
        evaluacion.join()     

    except Exception as e:
        e = str(e)      
        print(e)     


if __name__ == '__main__':        

    mi_variable = 'Hola'

    ##Al llamar la función obviamente se imprime mi_variable en forma correcta 
    funcion1()

    ##Al llamar la función a través  de la función funcionProceso
    # ocurre el error: NameError: name 'mi_variable' is not defined
    funcionProceso()

En funcion1, podría verificar si la variable está definida o no y definirla en caso de que no pero de todas formas me queda la pregunta ya que no tengo idea por qué pasa lo que expongo anteriormente:

  def funcion1():

     global mi_variable   

      try:
        mi_variable
     except NameError:
        mi_variable = 'Hola'
        print(mi_variable)

     else:

        print(mi_variable)
FJSevilla
  • 55,603
  • 7
  • 35
  • 58
Eduardo Munizaga
  • 2,694
  • 1
  • 13
  • 34
  • 1
    He verificado tu codigo y a mi me muestra dos 'Hola' https://repl.it/repls/BlushingVastDrawings – DevMind Apr 23 '19 at 13:53
  • Así veo entonces es algo que tendrá que ver con otra cosa: Te resumo el error que me da por Debug; Traceback (most recent call last): File process.py", line 258, in _bootstrap self.run(), File process.py", line 93, in run self._target(*self._args, **self._kwargs) , File test.py , line 6, in funcion1 print(mi_variable) NameError: name 'mi_variable' is not defined – Eduardo Munizaga Apr 23 '19 at 14:04

2 Answers2

6

Completando un poco la respuesta de Trauma.

Existen distintas formas de implementar el nuevo proceso y depende del sistema operativo de turno básicamente, las dos formas principales y más conocidas (a muy a grandes rasgos) son:

  • fork: en este caso se crea una copia del proceso padre, el proceso secundario ya está en posesión de exactamente los mismos recursos que el principal, cada estructura de datos, archivo abierto, conexión, etc que existía en el proceso principal todavía está ahí y se puede usar en el secundario inmediatamente. A partir de aquí se produce una bifurcación (de ahí lo de "fork") y cada proceso continua su trabajo por su lado. La creación de procesos es mucho más rápida y más liviana en recursos.

  • spawn: en este caso se crea un nuevo proceso desde cero, con su propio intérprete Python y carga todos los módulos nuevamente.

El método fork es propio de sistemas POSIX y no va a estar disponible en algunos casos por tanto, como ocurre en Windows, que carece de la posibilidad de llamar a fork().

En el caso de usar spawn, se lanza un nuevo intérprete y ejecuta el módulo de nuevo (importa todo de nuevo, instancia objetos globales desde cero, etc), es casi como si ejecutaramos el módulo en una nueva terminal, digo "casi" porque lógicamente hay algunas instrucciones especiales. El proceso hijo no ejecuta la sección if __name__ == '__main__', que es dónde defines tu variable, sencillamente porque el intérprete del proceso hijo no ejecuta el módulo como "modulo principal". Al no ejecutar el condicional, no va a existir la variable cuando la función es llamada.

De hecho, el condicional if __name__ == '__main__' se usa para proteger el punto de entrada y evitar que se ejecuten cosas que no deberían en el proceso hijo, lo más obvio que no se vuelva a llamar a Process desde el proceso hijo. El uso de este condicional, aunque siempre es buena idea, es especialmente relevante en Windows o cuando forzamos el uso de spawn.

De esto se deduce que si declaras la variable en el espacio global, pero fuera del condicional, no tendrás el problema comentado:

#!/usr/bin/python3.5
import multiprocessing as mp


mi_variable = "hola"


def funcion1():
    print(mi_variable)


def funcionProceso():

    try:

        evaluacion = mp.Process(target=funcion1)
        evaluacion.start()
        evaluacion.join()     

    except Exception as e:
        e = str(e)      
        print(e)     


if __name__ == '__main__':        
    funcion1()
    funcionProceso()

En los sistemas POSIX la creación de procesos pueden utilizar la bifurcación como hemos dicho y todo el estado global del padre se mantiene intacto de inicio. En este caso la variable si que existe porque fue creada por el proceso padre, que si ejecutaste como módulo principal, y luego "copiada" por el hijo para preparar la bifurcación, por lo que no muestra el error que describes.

Las consecuencias y bondades de un sistema u otro a la hora de levantar un nuevo proceso van más lejos y es una larga discusión.

Si alguien quiere reproducir el error en POSIX basta con cambiar el modo de creación del proceso, que es fork por defecto, para que use spawn:

import multiprocessing as mp



def funcion1():
    print(mi_variable)

def funcionProceso():  
    try:    
        evaluacion = mp.Process(target=funcion1)
        evaluacion.start()
        evaluacion.join()     
    except Exception as e:
        e = str(e)      
        print(e)     


if __name__ == '__main__':        
    mp.set_start_method('spawn')

    mi_variable = 'Hola'
    funcion1()
    funcionProceso()

De cualquier forma, cuidado con las variables globales, si además las unes a multiproceso/multihilo... Ten en cuenta que la variable en todo caso se "copia", no se comparte jamás entre procesos, si el proceso hijo la modifica el padre no se va a enterar y viceversa sin importar que se use fork o spawn. Como comenta Trauma en su respuesta en ningún caso puedes presuponer que la variable tenga el mismo valor en ambos procesos ni siquiera de inicio, por ejemplo suponiendo que se usa spawn y tu variable se define mediante mi_variable = random.randint(1, 100) lo más seguro es que en cada proceso tenga un valor distinto. Es generalmente más apropiado pasar las variables como argumento, siempre que el objeto sea "picklable", pero serán variables distintas de todas formas:

#!/usr/bin/python3.5
import multiprocessing as mp



def funcion(lista_compartida):
    lista_compartida.append(3)
    print("Desde proceso hijo:", lista_compartida)



if __name__ == '__main__':        
    lista_compartida = [1, 2]
    evaluacion = mp.Process(target=funcion, args=(lista_compartida, ))
    evaluacion.start()
    evaluacion.join()
    print("Desde proceso padre:", lista_compartida)

Desde proceso hijo: [1, 2, 3]
Desde proceso padre: [1, 2]

Si necesitas compartir realmente la variable debes usar métodos adecuados y seguros para ello como son las colas, memoria compartida (multiprocesing.Value, multiprocesing.Array), Manager, etc

#!/usr/bin/python3.5
import multiprocessing as mp


def funcion(lista_compartida):
    lista_compartida.append(3)
    print("Desde proceso hijo:", lista_compartida)


if __name__ == '__main__':        
    manager = mp.Manager()
    lista_compartida = manager.list([1, 2])

    evaluacion = mp.Process(target=funcion, args=(lista_compartida, ))
    evaluacion.start()
    evaluacion.join()
    print("Desde proceso padre:", lista_compartida)

Desde proceso hijo: [1, 2, 3]
Desde proceso padre: [1, 2, 3]

FJSevilla
  • 55,603
  • 7
  • 35
  • 58
  • 1
    +1 `Completando un poco` dice :-O Me entran ganas de borrar la mia; entendería que el AP te la marcara como *aceptada* :-) – Trauma Apr 23 '19 at 15:29
  • 2
    Jajaja, pues si en principio esa era la idea, solo completar tu respuesta con el tema del `if __name__ == "__main__"` que junto a lo que comentas explica porqué pasa esto en unos sistemas y no en otros. Al final me ha pasado como siempre... la tentación de editar y escribir "tochos" me puede... Y si eliminas la tuya me veré obligado a incluir en la mía lo que tu explicas y las referencias a la documentación que das y más tocho yo tendré... Así que no me **trauma**tices más :). Un saludo. – FJSevilla Apr 23 '19 at 16:33
4

De la documentación de python:

The spawn and forkserver start methods

There are a few extra restriction which don’t apply to the fork start method.
...

Global variables

Bear in mind that if code run in a child process tries to access a global variable, then the value it sees (if any) may not be the same as the value in the parent process at the time that Process.start was called.

However, global variables which are just module level constants cause no problems.

A grandes rasgos, el modo en el que python implementa la duplicación de procesos varía de una plataforma a otra; incluso en una misma plataforma, puede existir mas de una forma de duplicar el programa.

Algunos de estos métodos son mas restrictivos que otros: en concreto, los métodos spawn y forkserver incluyen la siguiente advertencia (traducción mia):

Ten en cuenta que si el código que se ejecuta en un proceso hijo trata de acceder a una variable global, el valor de esta (si lo tiene) puede o no ser igual que el valor de la misma variable en el proceso padre.

El efecto que estás observando se debe a eso: en tu plataforma, el método empleado para clonar tu proceso provoca ese efecto.

De aquí también deducimos una cosa: el efecto no tiene porqué producirse en plataformas distintas. En la tuya vemos que si, en otra distinta no tiene porqué.

Trauma
  • 25,297
  • 4
  • 37
  • 60
  • Mira que buena respuesta, resuelve mi duda y el misterio planteado por @Mario Guiber. Gracias – Eduardo Munizaga Apr 23 '19 at 14:06
  • @EduardoMunizaga Segun la doc, hay formas de establecer el *modo* en el que python se *clona* ... no tengo experiencia con ello, no puedo serte de mucha ayuda ahí, pero, si quieres experimentar ... – Trauma Apr 23 '19 at 14:08
  • La función es esta: https://docs.python.org/3/library/multiprocessing.html#multiprocessing.set_start_method – Trauma Apr 23 '19 at 14:09
  • 1
    de momento lo estoy solucionando como indico en la segunda parte de mi pregunta pero creo que vale la pena investigar lo que expones. La verdad muy útil. Saludos – Eduardo Munizaga Apr 23 '19 at 14:09