Métodos y variables públicas, privadas y protegidas:
Cuando definimos un objeto en Python por defecto todos sus atributos y métodos son públicos. Para refrescar un poco las lecciones anteriores, os mostraré un ejemplo de definición de clase y el uso de un par de instancias de ésta:
class footballer():
# Attributes
name = None
average = 50
# Constructor
def __init__(self, name, average):
this.name = name
this.average = average
v_footballer_1 = footballer("Cristiano Ronaldo", 89)
v_footballer_2 = footballer("Messi", 90)
v_footballer_2 .name = "Paco Alcacer"
En el ejemplo anterior hemos definido una clase footballer y hemos creado dos instancias: Cristiano Ronaldo y Messi. Acto seguido, aprovechando que name es un atributo público le hemos cambiado el nombre a Messi por "Paco Alcacer".
No obstante, en Python podemos definir un método o atributo como protegido (sólo accesible por ella misma y sus clases hijas) si le ponemos delante el símbolo _. Además, si hacemos el mismo símbolo dos veces (__), estaremos definiendo el atributo o método como privado (sólo accesible por ella misma). Es decir, un atributo privado sólo puede ser modificado por la propia instancia de una clase y un método privado sólo puede ser ejecutado desde la la propia instancia.
class footballer():
# Attributes
_name = None
__average = 50
# Constructor
def __init__(self, name, average):
this._name = name
this.__average = average
En éste ejemplo el atributo name es protegido, pudiendo ser editable por clases que hereden de footballer, mientras que el atributo average sólo puede ser accesible o modificable dentro de la clase footballer.
class footballer():
# Attributes
__name = None
__average = 50
# Constructor
def __init__(self, name, average): this.__name = name
this.__average = average
def _is_footballer(self):
return "yes"
def _is_coach(self):
return "no"
def __age():
return 18
def change_name(self, name):
this.__name = name
En cambio, en este segundo ejemplo name y average pasan a ser privados, pudiendo ser accesibles sólo para un objeto de footballer. Por otro lado, si definidos un objeto que herede de una clase de footballer, podemos hacer uso de los métodos _is_footballer o _is_coach. Por último, hemos definido un método público que nos permite cambiar el valor del atributo privado name.
Threads
Al igual que otros lenguajes de programación, Python permite el uso de hilos de ejecución para poder ejecutarlos de forma concurrente. El uso de hilos de forma concurrente es preferible al de manejar varios procesos a la vez porque agiliza los tiempos de cambio de contexto en el procesador.
La mejor forma de explicar el buen uso de los hilos es llevarlo al campo de los videojuegos: Podemos tener un hilo de música de fondo, un hilo de efectos sonoros, un hilo que lee de teclado (o gamepad), un hilo que dibuja los gráficos en pantalla y un hilo de ejecución que analiza la consistencia del juego o la inteligencia artificial de la máquina. Todas esas acciones trabajan aparentemente de forma simultánea en tiempo real y las acciones que se transmiten en una puede afectar al resto: Matamos un enemigo, se reproduce un grito de dolor; Nos movemos, se actualizan los gráficos; Llegamos a una sección crítica, la música cambia a un ritmo rápido, etcétera...
Volviendo al principio, podemos definir hilos en Python. Para ello debemos de importar el paquete threading y definir una clase que herede de threading.Thread. En el constructor de la clase llamaremos a su vez al constructor padre con threading.Thread.__init__(self) y además habrá que definir un método público "run" donde pondremos el código a ejecutar. Por último, muy importante, la instancia del hilo debe invocarse ejecutando el método público start. Es decir, cuando invocamos el método start, realmente ejecutamos el método run que hemos definido.
Ejemplo, hilo que cuenta del 0 al 10 en un hilo:
import threading
class thread_test(threading.Thread):
__stop = False
__counter = 0
def __init__(self):
threading.Thread.__init__(self)
self.__stop = False
self.__counter = 0
def run(self):
while self.__stop == False:
self.__counter = self.__counter + 1
print "%d" % self.__counter
if self.__counter == 10:
self.__stop = True
thread_test().start()
La ejecución resultante es la siguiente:
python thread_test.py
1
2
3
4
5
6
7
8
9
10
Ahora bien, si entendemos de concurrencia, podemos ejecutar varios hilos en paralelo para realizar varias cuentas al mismo tiempo:
import threading
class thread_test(threading.Thread):
__stop = False
__counter = 0
def __init__(self):
threading.Thread.__init__(self)
self.__stop = False
self.__counter = 0
def run(self):
while self.__stop == False:
self.__counter = self.__counter + 1
print "%d" % self.__counter
if self.__counter == 10:
self.__stop = True
thread_test().start()
thread_test().start()
thread_test().start()
thread_test().start()
Y aquí el resultado:
sebas@macbookUbuntu:~/Descargas/tmp/python$ python thread_test.py
1
2
3
4
5
6
7
8
9
10
1
2
131
2
3
4
4
5
62
3
7
4
85
6
9
7
10
8
9
10
5
6
7
8
9
10
Como veis el resultado es más caótico, puesto que estamos realizando 4 cuentas de forma concurrente donde se entremezcla una única salida estándar, produciendo que un hilo escriba por pantalla mientras otro hilo lo está haciendo, mostrando por ejemplo, varios números sin retorno de carro en la misma línea o varias líneas sin números (varios retornos de carro seguidos).
Este efecto se conoce como condición de carrera y puede afectar de forma drástica si el cambio de contexto entre hilos se produce modificando una variable o recurso global. Para evitar esto hay que definir mútex (o semáforos) en las zonas que definamos como críticas. No obstante, mi consejo es que uséis Python 3.X, puesto que dicha versión ya viene con sincronización de hilos de serie, sin necesidad de adaptar nada:
python3 thread_test.py
1
2
3
4
5
6
7
1
1
8
2
1
9
3
10
4
2
5
3
6
4
7
5
2
8
6
3
9
7
10
8
4
9
5
10
6
7
8
9
10
(Nota, para que este ejemplo funcione en Python 3, hay que cambiar la línea "print "%d" % self.__counter" por "print(self.__counter)").