25.3. Cómo separar los Métodos de Retrollamada de los Manejadores de Señal

25.3.1. Introducción

No es necesario almacenar todas las retrollamadas en un único archivo de programa. Se pueden separar en clases independientes, en archivos separados. De esta forma la aplicación principal puede derivar sus métodos de esas clases mediante herencia. Al final se acaba con toda la funcionalidad inicial con el beneficio de un mantenimiento más sencillo, reusabilidad del código y menores tamaños de archivo, lo que significa menor carga para los editores de texto.

25.3.2. Herencia

La herencia es una forma de reutilizar el código. Una clase puede heredar su funcionalidad de otras clases, y un aspecto estupendo de la herencia es que podemos usarla para dividir un programa enorme en grupos lógicos de partes menores, más fáciles de mantener.

Ahora, dediquemos un momento a la terminología. Una clase derivada, también denominada subclase o clase hija, es una clase que deriva parte de su funcionalidad de otras clases. Una clase base, también llamada superclase o clase madre, es de dónde hereda una clase derivada.

Abajo se muestra un pequeño ejemplo para familiarizarse con la herencia. Es una buena idea probar esto en el intérprete de Python para ganar experiencia de primera mano.

Creamos dos clases base:

class base1:
   base1_attribute = 1
   def base1_method(self):
     return "hola desde la clase base 1"

class base2:
   base2_attribute = 2
   def base2_method(self):
     return "hola desde la clase base 2"
    

Luego creamos una clase derivada que hereda de esas dos clases base:

class derived(base1, base2):  # clase derivada de dos clases base
   var3 = 3
    

Ahora la clase derivada tiene toda la funcionalidad de las clases base.

x = derived()        # crea una instancia de la clase derivada
x.base1_attribute    # 1
x.base2_attribute    # 2
x.var3               # 3
x.base1_method()     # hola desde la clase base 1
x.base2_method()     # hola desde la clase base 2
    

El objeto llamado x tiene la habilidad de acceder a las variables y métodos de las clases base porque ha heredado su funcionalidad. Ahora apliquemos este concepto a una aplicación de PyGTK.

25.3.3. Herencia aplicada a PyGTK

Crea un archivo llamado gui.py, y luego copia este código en él:

#Un archivo llamado: gui.py

import pygtk
import gtk

# Crea una definición de clase llamada gui
class gui:
  #
  #          MÉTODOS DE RETROLLAMADA
  #------------------------------------
  def open(self, widget):
    print "abre cosas"
  def save(self, widget):
    print "guarda cosas"
  def undo(self, widget):
    print "deshace cosas"
  def destroy(self, widget):
    gtk.main_quit()


  def __init__(self):
    #
    #        CÓDIGO DE CONSTRUCCIÓN DE GUI
    #-----------------------------------------------
    self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
    self.window.show()
    self.vbox1 = gtk.VBox(False, 25)
    self.window.add(self.vbox1)
    open_button = gtk.Button(label="Abre cosas")
    open_button.set_use_stock(True)
    self.vbox1.pack_start(open_button, False, False, 0)
    open_button.show()
    save_button = gtk.Button(label="Guarda Cosas")
    self.vbox1.pack_start(save_button, False, False, 0)
    save_button.show()
    undo_button = gtk.Button(label="Deshacer")
    self.vbox1.pack_start(undo_button, False, False, 0)
    undo_button.show()
    self.vbox1.show()
    #
    #        MANEJADORES DE SEÑAL
    #------------------------------------------------
    open_button.connect("clicked", self.open)
    save_button.connect("clicked", self.save)
    undo_button.connect("clicked", self.undo)
    self.window.connect("destroy", self.destroy)

  def main(self):
   gtk.main()

if __name__ == "__main__":
 gui_instance = gui()       # crea un objeto gui
 gui_instance.main()        # llama al método principal
    

Si se ejecuta el programa se comprueba que se trata de una única ventana con algunos botones. Tal como está organizado ahora el programa, todo el código está en un único archivo. Pero en un momento se verá cómo dividir el programa en múltiples archivos. La idea es retirar esos cuatro métodos de retrollamada de la clase gui y ponerlos en sus propias clases, en archivos independientes. En el caso de que se tuviesen cientos de métodos de retrollamada se procuraría agruparlos de alguna forma lógica como, por ejemplo, situando todos los métodos que se dedican a entrada/salida en la misma clase, y de esa manera se harían otras clases para grupos de métodos.

Lo primero que tenemos que hacer es construir algunas clases para los métodos del archivo gui.py. Creamos tres nuevos archivos de texto, llamados io.py, undo.py y destroy.py, y sitúa esos archivos en el mismo subdirectorio que el archivo gui.py. Copia el código siguiente en el archivo io.py:

class io:
  def open(self, widget):
    print "abre cosas"

  def save(self, widget):
    print "guarda cosas"
    

Estos son los dos métodos de retrollamada, open y save, del programa gui.py. Copia el siguiente bloque de código en el archivo undo.py:

class undo:
  def undo(self, widget):
    print "deshace cosas"
    

Este es el método undo de gui.py. Y, finalmente, copia el siguiente código en destroy.py:

import gtk

class destroy:
  def destroy(self, widget):
    gtk.main_quit()
    

Ahora todos los métodos están separados en clases independientes.

Importante

En tus futuras aplicaciones querrás importar módulos como gtk, pango, os ect... en tu clase derivada (la que contiene todo el código de inicialización de GUI), pero recuerda que necesitarás importar también algunos de esos módulos en las clases base. Podrías tener que crear una instancia de un control de gtk en el método de una clase base, y en ese caso necesitarías importar gtk.

Este es solamente un ejemplo de una clase base en la que sería necesario importar gtk.

  import gtk

  class Font_io
    def Font_Chooser(self,widget):
      self.fontchooser = gtk.FontSelectionDialog("Elige Fuente")
      self.fontchooser.show()
      

Hay que observar que define un control gtk: un diálogo de selección de fuente. Normalmente se importaría gtk en la clase principal (la clase derivada) y todo funcionaría correctamente. Pero desde el momento que se extrae este Fon_Chooser de la clase principal y se pone en su propia clase y luego se intenta heredar de ella, nos encontraríamos con un error. En este aso, ni siquiera se detectaría el error hasta el momento de ejecutar la aplicación. Pero cuando se intente usar el Font_Chooser se encontraría que gtk no está definido, aunque se haya importado en la clase derivada. Así que se ha de recordar que, cuando se crean clases base, es necesario añadir sus propios import.

Con esas tres clases en sus tres archivos py, es necesario cambiar el código en gui.py de tres modos:

  1. Importar las clases que se han creado.

  2. Modificar la definición de la clase.

  3. Eliminar los métodos de retrollamada.

El siguiente código actualizado muestra cómo hacerlo:

#Un archivo llamado:  gui.py
#(versión actualizada)
#(con herencia múltiple)

import pygtk
import gtk

from io import file_io                       #
from undo import undo                        # 1. Importa tus clases
from destroy import destroy                  #

# Crea una definición de clase llamada gui
class gui(io, undo, destroy):                # 2. Definición de clase modificada
                                             # 3. Retrollamadas eliminadas
  def __init__(self):
    #
    #        CÓDIGO DE CONSTRUCCIÓN DE GUI
    #-----------------------------------------------
    self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
    self.window.show()
    self.vbox1 = gtk.VBox(False, 25)
    self.window.add(self.vbox1)
    open_button = gtk.Button(label="Open Stuff")
    open_button.set_use_stock(True)
    self.vbox1.pack_start(open_button, False, False, 0)
    open_button.show()
    save_button = gtk.Button(label="Save Stuff")
    self.vbox1.pack_start(save_button, False, False, 0)
    save_button.show()
    undo_button = gtk.Button(label="Undo")
    self.vbox1.pack_start(undo_button, False, False, 0)
    undo_button.show()
    self.vbox1.show()
    #
    #        MANEJADORES DE SEÑAL
    #------------------------------------------------
    open_button.connect("clicked", self.open_method)
    save_button.connect("clicked", self.save_method)
    undo_button.connect("clicked", self.undo_method)
    self.window.connect("destroy", self.destroy)

  def main(self):
   gtk.main()

if __name__ == "__main__":
 gui_instance = gui()       # crea un objeto gui
 gui_instance.main()        # llamada al método principal
    

Estas tres líneas son nuevas:

       from io import io
       from undo import undo
       from destroy import destroy
    

Las instrucciones import tienen la forma:

       from [nombre de archivo de la clase] import [nombre de la clase]
    

Este es el cambio de la definición de la clase:

       class gui(io, undo, destroy):
    

Los nombres de las clases base van entre paréntesis en la definición de la clase. Ahora la clase gui es una clase derivada y es capaz de usar todos los atributos y métodos definidos en sus clases base. También, la case gui hereda de múltiples clases (dos o más); es lo que se conoce como heréncia múltiple.

Ahora cambia el archivo gui.py a la versión actualizada y ejecuta la aplicación de nuevo. Verás que funciona exactamente del mismo modo, salvo que ahora todos los métodos de retrollamada están en clases independients, y son heredadas por la clase gui.

Hay otro tema de interés que comentar. Mientras que tu aplicación gui.py y tus archivos de clases base estén en el mismo directorio todo funcionará correctamente. Pero si quieres crear otro directorio en el que situar los archivos con las clases base, entonces es necesario añadir dos líneas más de código junto al resto de intrucciones import en gui.py. Así:

        import sys
        sys.path.append("classes")
    

en donde "classes" es el nombre del directiro en el que se guardan las clases base. Esto permite que Python sepa dónde localizarlas. Pruébalo. Simplemente crea un directorio llamado classes en el directorio en el que está el programa gui.py. Luego añade los archivos de las tres clases base al directorio classes. Añade las dos líneas de código anteriores a la parte superior del archivo gui.py. Y, ¡listo!

Una nota final para quienes usan py2exe para compilar aplicaciones Python. Si se ponen las cases base en el directorio de Python, entonces py2exe los incluirá en la versión compilada de la aplicación como con cualquier otro módulo Python.