La Programación Orientada a Objetos

La programación orientada a objetos (POO, u OOPsegún sus siglas en inglés) es un paradigma de programación que viene a innovar la forma de obtener resultados. Los objetos manipulan los datos de entrada para la obtención de datos de salida específicos, donde cada objeto ofrece una funcionalidad especial. [WikipediA]

En programación, como en el mundo real, los objetos son entidades que tienen un determinado estado, comportamiento e identidad. Un objeto se puede identificar (aunque sea idéntico a otro objeto), tiene una serie de características o atributos (estado) y una funcionalidad interna e externa perfectamente definida (comportamiento).

En programación es sumamente fácil crear y duplicar objetos, basta con disponer de una plantilla a la que generalmente llamaremos clase y crear un ejemplo o instancia de esa clase. La Programación Orientada a Objetos pretende que se reutilice cualquier clase, tanto para crear diferentes objetos de la misma clase como para crear nuevas clases derivadas, con los mismos atributos y funcionalidad que la clase original y con la posibilidad de añadir nuevos atributos o funciones o incluso modificar el comportamiento original de alguna función.

Python es un lenguaje orientado a objetos. Durante el curso utilizaremos diferentes objetos en el desarrollo de aplicaciones. La mayoría de objetos que usaremos ya tienen una clase definida, tendremos que aprender a buscar dónde se ha declarado esa clase, a definir objetos de esa clase, a inicializar el estado del objeto y a usar las funciones que define la clase tanto para modificar su estado como para comunicarse con otros objetos de nuestras aplicaciones.

Módulos

Una de las principales características de la POO es la reutilización del código que ya sabemos que funciona correctamente. Para conseguirlo se ha de guardar ese código en archivos, de otro modo se perdería al salir de la consola interactiva de Python o al apagar el ordenador. Al desarrollar programas de cierta longitud es necesario guardarlos en archivos, y al hacerlo estamos creando un módulo.

En Python se agrupan funciones, clases y métodos mediante módulos, que se añaden a nuestra aplicación mediante el comando import.

Supongamos que hemos desarrollado una aplicación para averiguar la letra del NIF correspondiente a un DNI. Para reutilizar la aplicación la hemos guardado en el archivo 1-2-1-letra-NIF.py.

import sys

letras = 'TRWAGMYFPDXBNJZSQVHLCKE'

dni = input('Escribe el número del DNI: ')

try:
    i_dni = int(dni)
except:
    print('No has escrito un número entero')
    sys.exit(1)

print('NIF:', dni + letras[i_dni % 23])

Para ejecutar la aplicación escribimos en la consola (situada en el mismo directorio en que está el archivo):

$ python 1-2-1-letra-NIF.py

Para usar el código en otra aplicación deberíamos convertir la aplicación en una función que reciba el DNI y devuelva la letra del NIF correspondiente. Podríamos hacerlo en el archivo 1-2-2-funcion-letra-NIF.py:

import sys

def letraNIF(dni):
    letras = 'TRWAGMYFPDXBNJZSQVHLCKE'
    try:
        i_dni = int(dni)
    except:
        print('No has escrito un número entero')
        sys.exit(1)

    return letras[i_dni % 23]


dni = input('Escribe el número del DNI: ')
print('NIF:', dni + letraNIF(dni))

Y ejecutarlo escribiendo en la consola:

$ python 1-2-2-funcion-letra-NIF.py

Sin embargo, si importamos este archivo en una nueva aplicación, se ejecutarán las dos últimas líneas, sobre las que no tenemos ningún control desde la nueva aplicación.

Aquí entra en juego una de las características de Python. Cada archivo tiene asociada la variable interna __name__, que guarda el nombre del archivo sin su extensión. Sin embargo, el archivo principal que se ejecute, sea cual sea su nombre de archivo, guarda en dicha variable el valor __main__.

Si queremos importar la función NIF hemos de hacer varios cambios en el archivo 1-2-2-funcion-letra-NIF.py:

  • El nombre de archivo se utilizará como identificador en el archivo que lo importe, por lo que ha de cumplir las normas que hay para identificadores en Python: no puede comenzar por un dígito y sólo puede contener caracteres alfanuméricos anglosajones, dígitos y guiones bajos. Lo guardaremos en el archivo moduloLetraNIF.py.
  • No debe contener líneas que se ejecuten sin algún control. Todo el contenido de la función NIF se ejecutará sólo si se llama a la función, pero las líneas finales del archivo 1-2-2-funcion-letra-NIF.py no tienen ningún control. Como no queremos borrarlas para poder hacer pruebas sobre el fichero que contiene la función, limitaremos su ejecución en función del nombre almacenado en su variable interna __name__.

Crearemos, pues, el archivo moduloLetraNIF.py:

import sys

def letraNIF(dni):
    letras = 'TRWAGMYFPDXBNJZSQVHLCKE'
    try:
        i_dni = int(dni)
    except:
        print('No has escrito un número entero')
        sys.exit(1)

    return letras[i_dni % 23]


if __name__ == '__main__':
    dni = input('Escribe el número del DNI: ')
    print('NIF:', dni + letraNIF(dni))

Ahora podemos probar directamente el módulo, como si se tratara simplemente de una aplicación:

python moduloLetraNIF.py

Y también podemos usar la función NIF en otra aplicación, por ejemplo creando el archivo 1-2-3-funcion-letra-NIF.py:

from moduloLetraNIF import NIF

dni = input('Escribe el número del DNI: ')
print('NIF:', dni + NIF(dni))

y ejecutando

python 1-2-3-funcion-letra-NIF.py

Ejercicios

  1. Crea el módulo fibo.py y utilízalo según se explica en http://docs.python.org.ar/tutorial/3/modules.html

  2. Añade al módulo fibo.py la posibilidad de ser ejecutado por sí mismo con python fibo.py, de modo que le pregunte al usuario el parámetro de las funciones.

  3. Cambia el módulo anterior para que el módulo reciba el argumento sin interactuar con el usuario, directamente desde la línea de comandos con python fibo.py 1000.

Documentación interna de Python

La guía de estilo de Python sugiere que se documente todo el código convenientemente. Cuando instalamos Python se instalan una serie de funciones internas, entre las que destacamos dir() y help() para obtener ayuda sobre cualquier módulo, clase, atributo o método.

dir()

La función interna de Python dir(módulo) devuelve una lista ordenada de los nombres definidos en el módulo.

Ejercicio

Abre la consola interactiva de Python y comprueba qué nombres están definidos en moduloLetraNIF.

Sin parámetro, dir() devuelve los nombres que se han definido en la sesión actual (módulos importados, variables y funciones definidas...).

Esta función nos puede ayudar en el desarrollo de nuestras aplicaciones. Para usarla podemos abrir una consola interactiva de Python, importar un módulo y pedir la información que contiene. Por ejemplo, el módulo builtins proporciona acceso directo a las funciones (dir, help, abs...) y constantes (False, True, None...) definidas de forma interna en el propio lenguaje de programación.

import builtins
dir(builtins)

Lo que genera la salida:

['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning', 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError', 'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False', 'FileExistsError', 'FileNotFoundError', 'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError', 'InterruptedError', 'IsADirectoryError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'ModuleNotFoundError', 'NameError', 'None', 'NotADirectoryError', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError', 'PendingDeprecationWarning', 'PermissionError', 'ProcessLookupError', 'RecursionError', 'ReferenceError', 'ResourceWarning', 'RuntimeError', 'RuntimeWarning', 'StopAsyncIteration', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'TimeoutError', 'True', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning', 'ZeroDivisionError', '__build_class__', '__debug__', '__doc__', '__import__', '__loader__', '__name__', '__package__', '__spec__', 'abs', 'all', 'any', 'ascii', 'bin', 'bool', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod', 'compile', 'complex', 'copyright', 'credits', 'delattr', 'dict', 'dir', 'divmod', 'enumerate', 'eval', 'exec', 'exit', 'filter', 'float', 'format', 'frozenset', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property', 'quit', 'range', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip']

Si queremos ver la salida como una lista basta con usar un bucle:

for i in dir(builtins): print(i)

obteniendo:

ArithmeticError
AssertionError
...
vars
zip

Y puede ser útil enumerar sus ítems:

for n, i in enumerate(dir(builtins)): print(n+1, i)

obteniendo:

1 ArithmeticError
2 AssertionError
...
151 vars
152 zip

help()

La función interna de Python help(objeto) devuelve la documentación oficial del objeto. En esta documentación se manifiesta uno de los aspectos importantes de la programación orientada a objetos. En primer lugar se indica que si un método está definido en esta clase, como puede ser un método propio, heredado de otro objeto o incluso heredado pero sobreescrito, cuando se llama al un método desde el objeto se ejecutará el que primero se encuentre en la lista de precedencias. Por ejemplo, con

 >>> import tkinter; help(tkinter.Menu)

obtenemos:

class Menu(Widget)
 |  Menu widget which allows displaying menu bars, pull-down menus and pop-up menus.
 |  
 |  Method resolution order:
 |      Menu
 |      Widget
 |      BaseWidget
 |      Misc
 |      Pack
 |      Place
 |      Grid
 |      builtins.object

Al presentar los métodos del objeto, comienza con los métodos propios del objeto:

 |  Methods defined here:
 |  
 |  __init__(self, master=None, cnf={}, **kw)
 |      Construct menu widget with the parent MASTER.
 |      
 |      Valid resource names: activebackground, activeborderwidth,
 |      activeforeground, background, bd, bg, borderwidth, cursor,
 |      disabledforeground, fg, font, foreground, postcommand, relief,
 |      selectcolor, takefocus, tearoff, tearoffcommand, title, type.
 |  
 |  activate(self, index)
 |      Activate entry at INDEX.
 |  
 |  add(self, itemType, cnf={}, **kw)
 |      Internal function.
 |  
...

y continúa con los métodos heredados de otras clases:

 |  ----------------------------------------------------------------------
 |  Methods inherited from BaseWidget:
 |  
 |  destroy(self)
 |      Destroy this and all descendants widgets.
 |  
 |  ----------------------------------------------------------------------
 |  Methods inherited from Misc:
 |  
 |  __getitem__ = cget(self, key)
 |      Return the resource value for a KEY given as string.
 |  
...
 |  ----------------------------------------------------------------------
 |  Methods inherited from Pack:
 |  
 |  forget = pack_forget(self)
 |      Unmap this widget and do not use it for the packing order.
 |  
... |  ----------------------------------------------------------------------
 |  Methods inherited from Place:
 |  
 |  place = place_configure(self, cnf={}, **kw)
...
 |  ----------------------------------------------------------------------
 |  Methods inherited from Grid:
 |  
 |  grid = grid_configure(self, cnf={}, **kw)
...

Lo que nos da también información sobre la estructura del objeto que vamos a utilizar.

Cuando trabajemos con otros objetos tendremos que aprender cómo usar sus métodos propios, y podremos aprovechar que ya sabemos cómo utilizar métodos heredados de las clases que ya conocemos a través de esta información, como BaseWidget.destroy(self), método común a todos los widgets excepto a tkinter.Tk, donde el método se ha sobreescrito para finalizar la aplicaición después de destruir todos los widgets que descienden de la ventana principal.

El método destroy() es lo que en POO se conoce como destructor de un objeto. No todos los objetos lo tienen definido, en muchos casos no es necesario hacer nada especial cuando se destruye un objeto.

__init__

Cuando se construye un objeto en Python se llama a su método __init__ para que realice las tareas necearias para poder gestionar el objeto. Este método es lo que en POO se conoce como constructor de un objeto.

master

Las aplicaciones que tienen muchos objetos han de gestionar bien el uso de memoria que hacen. El desarrollador construye objetos en su aplcación, con lo que está reservando memoria del sistema para poder gestionarla, pero ¿qué ocurre cuando ya no es necesario un objeto? Si el desarrollador no quiere preocuparse también de la destrucción del objeto y la liberación de memoria que ello supone debe delegar en alguien esta tarea.

En las aplicaciones visuales es habitual crear objetos que contienen otros objetos, por ejemplo la ventana principal puede contener un Frame que a su vez contiene un Label y un Button. En Python, la ventana principal posee un método, destroy(), que se encarga de liberar la memoria utilizada por ella y por los objetos que contiene, además de finalizar la aplicación. Para indicar qué objetos contiene, cuando el desarrollador crea un objeto para colocarlo en la ventana puede indicarlo con el parámetro master, que por defecto guarda el valor None. En el ejemplo descrito, al crear el Frame se indicará que su master es la ventana principal de la aplicación, y al crear el Label y el Button se indicará que su masteres el Frame. De este modo, cuando se destruya la ventana principal, ésta liberará la memoria utilizada por el Frame, para lo que tiene antes que liberar la memoria utilizada por los otros dos widgets.

Referencias

results matching ""

    No results matching ""