0% encontró este documento útil (0 votos)
253 vistas

PCAP - Programming Essentials in Python 2 - Modulo 06

Este módulo cubre los fundamentos de la programación orientada a objetos en Python, incluyendo clases, métodos, objetos, herencia y manejo de excepciones. También cubre temas como jerarquías de clases, qué es un objeto, y los atributos que contiene un objeto como nombre, propiedades y métodos.

Cargado por

Jesús Moronta
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
0% encontró este documento útil (0 votos)
253 vistas

PCAP - Programming Essentials in Python 2 - Modulo 06

Este módulo cubre los fundamentos de la programación orientada a objetos en Python, incluyendo clases, métodos, objetos, herencia y manejo de excepciones. También cubre temas como jerarquías de clases, qué es un objeto, y los atributos que contiene un objeto como nombre, propiedades y métodos.

Cargado por

Jesús Moronta
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
Está en la página 1/ 96

Fundamentos de Programación en Python: Módulo 6

En este módulo, aprenderás sobre:

 Los fundamentos y enfoque de programación orientada a objetos.


 Clases, métodos y objetos.
 Manejo de excepciones.
 Manejo de archivos.

Los conceptos básicos del enfoque orientado a objetos


Demos un paso fuera de la programación y las computadoras, y analicemos temas de
programación orientada a objetos.

Casi todos los programas y técnicas que has utilizado hasta ahora pertenecen al estilo de
programación procedimental. Es cierto que has utilizado algunos objetos incorporados, pero
cuando nos referimos a ellos, se mencionan lo mínimo posible.

La programación procedimental fue el enfoque dominante para el desarrollo de software


durante décadas de TI, y todavía se usa en la actualidad. Además, no va a desaparecer en el
futuro, ya que funciona muy bien para proyectos específicos (en general, no muy complejos y
no grandes, pero existen muchas excepciones a esa regla).
El enfoque orientado a objetos es bastante joven (mucho más joven que el enfoque
procedimental) y es particularmente útil cuando se aplica a proyectos grandes y complejos
llevados a cabo por grandes equipos formados por muchos desarrolladores.

Este tipo de programación en un proyecto facilita muchas tareas importantes, por ejemplo,
dividir el proyecto en partes pequeñas e independientes y el desarrollo independiente de
diferentes elementos del proyecto.

Python es una herramienta universal para la programación procedimental y


orientada a objetos. Se puede utilizar con éxito en ambas.

Además, puedes crear muchas aplicaciones útiles, incluso si no se sabe nada sobre clases y
objetos, pero debes tener en cuenta que algunos de los problemas (por ejemplo, el manejo
de la interfaz gráfica de usuario) puede requerir un enfoque estricto de objetos.

Afortunadamente, la programación orientada a objetos es relativamente simple.


Enfoque procedimental versus el enfoque orientado a objetos
En el enfoque procedimental, es posible distinguir dos mundos diferentes y
completamente separados: el mundo de los datos y el mundo del código. El mundo de
los datos está poblado con variables de diferentes tipos, mientras que el mundo del código
está habitado por códigos agrupados en módulos y funciones.

Las funciones pueden usar datos, pero no al revés. Además, las funciones pueden abusar de
los datos, es decir, usar el valor de manera no autorizada (por ejemplo, cuando la función
seno recibe el saldo de una cuenta bancaria como parámetro).

Los datos no pueden usar funciones. ¿Pero es esto completamente cierto? ¿Hay algunos tipos
especiales de datos que pueden usar funciones?

Sí, los hay, los llamados métodos. Estas son funciones que se invocan desde dentro de los
datos, no junto con ellos. Si puedes ver esta distinción, has dado el primer paso en la
programación de objetos.

El enfoque orientado a objetos sugiere una forma de pensar completamente diferente.


Los datos y el código están encapsulados juntos en el mismo mundo, divididos en clases.
Cada clase es como una receta que se puede usar cuando quieres crear un objeto
útil. Puedes producir tantos objetos como necesites para resolver tu problema.

Cada objeto tiene un conjunto de rasgos (se denominan propiedades o atributos; usaremos
ambas palabras como sinónimos) y es capaz de realizar un conjunto de actividades (que se
denominan métodos).

Las recetas pueden modificarse si son inadecuadas para fines específicos y, en efecto,
pueden crearse nuevas clases. Estas nuevas clases heredan propiedades y métodos de los
originales, y generalmente agregan algunos nuevos, creando nuevas herramientas más
específicas.

Los objetos son encarnaciones de las ideas expresadas en clases, como un pastel de
queso en tu plato, es una encarnación de la idea expresada en una receta impresa en un
viejo libro de cocina.

Los objetos interactúan entre sí, intercambian datos o activan sus métodos. Una clase
construida adecuadamente (y, por lo tanto, sus objetos) puede proteger los datos sensibles y
ocultarlos de modificaciones no autorizadas.

No existe un límite claro entre los datos y el código: viven como uno solo dentro de los
objetos.

Todos estos conceptos no son tan abstractos como pudieras pensar al principio. Por el
contrario, todos están tomados de experiencias de la vida real y, por lo tanto, son
extremadamente útiles en la programación de computadoras: no crean vida
artificial reflejan hechos reales, relaciones y circunstancias.
Jerarquías de clase
La palabra clases tiene muchos significados, pero no todos son compatibles con las ideas que
queremos discutir aquí. La clase que nos concierne es como una categoría, como resultado
de similitudes definidas con precisión.

Intentaremos señalar algunas clases que son buenos ejemplos de este concepto.
Veamos por un momento los vehículos. Todos los vehículos existentes (y los que aún no
existen) estan relacionados por una sola característica importante: la capacidad de
moverse. Puedes argumentar que un perro también se mueve; ¿Es un perro un vehículo? No
lo es. Tenemos que mejorar la definición, es decir, enriquecerla con otros criterios, distinguir
los vehículos de otros seres y crear una conexión más fuerte. Consideremos las siguientes
circunstancias: los vehículos son entidades creadas artificialmente que se utilizan para el
transporte, movidos por fuerzas de la naturaleza y dirigidos (conducidos) por humanos.

Según esta definición, un perro no es un vehículo.

La clase vehículos es muy amplia. Tenemos que definir clases especializadas. Las clases
especializadas son las subclases. La clase vehículos será una superclase para todas ellas.

Nota: la jerarquía crece de arriba hacia abajo, como raíces de árboles, no ramas. La
clase más general y más amplia siempre está en la parte superior (la superclase) mientras
que sus descendientes se encuentran abajo (las subclases).

A estas alturas, probablemente puedas señalar algunas subclases potenciales para la


superclase Vehículos. Hay muchas clasificaciones posibles. Elegimos subclases basadas en el
medio ambiente y decimos que hay (al menos) cuatro subclases:

 Vehículos Terrestres.
 Vehículos Acuáticos.
 Vehículos Aéreos.
 Vehículos Espaciales.

En este ejemplo, discutiremos solo la primera subclase: vehículos terrestres. Si lo deseas,


puedes continuar con las clases restantes.

Los vehículos terrestres pueden dividirse aún más, según el método con el que impactan el
suelo. Entonces, podemos enumerar:

 Vehículos de ruedas.
 Vehículos oruga.
 Aerodeslizadores.

La figura ilustra la jerarquía que hemos creado.


Ten en cuenta la dirección de las flechas: siempre apuntan a la superclase. La clase de nivel
superior es una excepción: no tiene su propia superclase.

Jerarquías de clase: continuación


Otro ejemplo es la jerarquía del reino taxonómico de los animales.

Podemos decir que todos los animales (nuestra clase de nivel superior) se puede dividir en
cinco subclases:

 Mamíferos.
 Reptiles.
 Pájaros.
 Peces.
 Anfibios.

Tomaremos el primero para un análisis más detallado.

Hemos identificado las siguientes subclases:

 Mamíferos salvajes.
 Mamíferos domesticados.

Intenta extender la jerarquía de la forma que quieras y encuentra el lugar adecuado para los
humanos.

¿Qué es un objeto?
Una clase (entre otras definiciones) es un conjunto de objetos. Un objeto es un ser
perteneciente a una clase.

Un objeto es una encarnación de los requisitos, rasgos y cualidades asignados a una


clase específica. Esto puede sonar simple, pero ten en cuenta las siguientes circunstancias
importantes. Las clases forman una jerarquía. Esto puede significar que un objeto que
pertenece a una clase específica pertenece a todas las superclases al mismo tiempo.
También puede significar que cualquier objeto perteneciente a una superclase puede no
pertenecer a ninguna de sus subclases.

Por ejemplo: cualquier automóvil personal es un objeto que pertenece a la clase vehículos
terrestres. También significa que el mismo automóvil pertenece a todas las superclases de su
clase local; por lo tanto, también es miembro de la clase vehículos. Tu perro (o tu gato) es un
objeto incluido en la clase Mamíferos domesticados, lo que significa explícitamente que
también está incluido en la clase animales.

Cada subclase es más especializada (o más específica) que su superclase. Por el


contrario, cada superclase es más general (más abstracta) que cualquiera de sus
subclases. Ten en cuenta que hemos supuesto que una clase solo puede tener una
superclase; esto no siempre es cierto, pero discutiremos este tema más adelante.

Herencia
Definamos uno de los conceptos fundamentales de la programación de objetos,
llamado herencia. Cualquier objeto vinculado a un nivel específico de una jerarquía de
clases hereda todos los rasgos (así como los requisitos y cualidades) definidos
dentro de cualquiera de las superclases.

La clase de inicio del objeto puede definir nuevos rasgos (así como requisitos y cualidades)
que serán heredados por cualquiera de sus superclases.

No deberías tener ningún problema para hacer coincidir esta regla con ejemplos específicos,
ya sea que se aplique a animales o vehículos.

¿Qué contiene un objeto?


La programación orientada a objetos supone que cada objeto existente puede estar
equipado con tres grupos de atributos:
 Un objeto tiene un nombre que lo identifica de forma exclusiva dentro de su
namespace (aunque también puede haber algunos objetos anónimos).
 Un objeto tiene un conjunto de propiedades individuales que lo hacen original,
único o sobresaliente (aunque es posible que algunos objetos no tengan
propiedades).
 Un objeto tiene un conjunto de habilidades para realizar actividades
específicas, capaz de cambiar el objeto en sí, o algunos de los otros objetos.

Hay una pista (aunque esto no siempre funciona) que te puede ayudar a identificar
cualquiera de las tres esferas anteriores. Cada vez que se describe un objeto y se usa:

 Un sustantivo: probablemente se este definiendo el nombre del objeto.


 Un adjetivo: probablemente se este definiendo una propiedad del objeto.
 Un verbo: probablemente se este definiendo una actividad del objeto.

Dos ejemplos deberían servir como un buen ejemplo:

 Max es un gato grande que duerme todo el día.


Nombre del objeto = Max
Clase de inicio = Gato
Propiedad = Tamaño (grande)
Actividad = Dormir (todo el día)

 Un Cadillac rosa pasó rápidamente.


Nombre del objeto = Cadillac
Clase de inicio = Vehículo terrestre
Propiedad = Color (rosa)
Actividad = Pasar (rápidamente)

Tu primera clase
La programación orientada a objetos es el arte de definir y expandir clases. Una clase es
un modelo de una parte muy específica de la realidad, que refleja las propiedades y
actividades que se encuentran en el mundo real.

Las clases definidas al principio son demasiado generales e imprecisas para cubrir el mayor
número posible de casos reales.

No hay obstáculo para definir nuevas subclases más precisas. Heredarán todo de su
superclase, por lo que el trabajo que se utilizó para su creación no se desperdicia.

La nueva clase puede agregar nuevas propiedades y nuevas actividades y, por lo tanto,
puede ser más útil en aplicaciones específicas. Obviamente, se puede usar como una
superclase para cualquier número de subclases recién creadas.

El proceso no necesita tener un final. Puedes crear tantas clases como necesites.

La clase que se define no tiene nada que ver con el objeto: la existencia de una clase no
significa que ninguno de los objetos compatibles se creará automáticamente. La
clase en sí misma no puede crear un objeto: debes crearlo tu mismo y Python te permite
hacerlo.

Es hora de definir la clase más simple y crear un objeto. Echa un vistazo al siguiente ejemplo:

class ClaseSimple:

pass

Hemos definido una clase. La clase es bastante pobre: no contiene propiedades ni


actividades. Esta vacía, pero eso no importa por ahora. Cuanto más simple sea la clase,
mejor para nuestros propósitos.

La definición comienza con la palabra clave reservada class . La palabra clave


reservada es seguida por un identificador que nombrará la clase (nota: no lo confundas
con el nombre del objeto: estas son dos cosas diferentes).

A continuación, se agregan dos puntos:), como clases, como funciones, forman su propio
bloque anidado. El contenido dentro del bloque define todas las propiedades y actividades de
la clase.

La palabra clave reservada pass llena la clase con nada. No contiene ningún método ni
propiedades.

Tu primer objeto
La clase recién definida se convierte en una herramienta que puede crear nuevos objetos. La
herramienta debe usarse explícitamente, bajo demanda.

Imagina que deseas crear un objeto (exactamente uno) de la clase ClaseSimple .

Para hacer esto, debes asignar una variable para almacenar el objeto recién creado de esa
clase y crear un objeto al mismo tiempo.

Se hace de la siguiente manera:


miPrimerObjeto = ClaseSimple()

Nota:

 El nombre de la clase intenta fingir que es una función, ¿puedes ver esto? Lo
discutiremos pronto.
 El objeto recién creado está equipado con todo lo que trae la clase; Como esta clase
está completamente vacía, el objeto también está vacío.

El acto de crear un objeto de la clase seleccionada también se


llama instanciación (ya que el objeto se convierte en una instancia de la clase).

Dejemos las clases en paz por un breve momento, ya que ahora diremos algunas
palabras sobre pilas. Sabemos que el concepto de clases y objetos puede no estar
completamente claro todavía. No te preocupes, te explicaremos todo muy pronto.

¿Qué es una pila?


Una pila es una estructura desarrollada para almacenar datos de una manera muy
específica.. Imagina una pila de monedas. No puedes poner una moneda en ningún otro
lugar sino en la parte superior de la pila. Del mismo modo, no puedes sacar una moneda de
la pila desde ningún lugar que no sea la parte superior de la pila. Si deseas obtener la
moneda que se encuentra en la parte inferior, debes eliminar todas las monedas de los
niveles superiores.

El nombre alternativo para una pila (pero solo en la terminología de TI) es UEPS (LIFO son
sus siglas en íngles). Es una abreviatura para una descripción muy clara del
comportamiento de la pila: Último en Entrar - Primero en Salir (Last In - First Out). La
moneda que quedó en último lugar en la pila saldrá primero.

Una pila es un objeto con dos operaciones elementales, denominadas


convencionalmente push (cuando un nuevo elemento se coloca en la parte superior)
y pop (cuando un elemento existente se retira de la parte superior).

Las pilas se usan muy a menudo en muchos algoritmos clásicos, y es difícil imaginar la
implementación de muchas herramientas ampliamente utilizadas sin el uso de pilas.
Implementemos una pila en Python. Esta será una pila muy simple, y te mostraremos cómo
hacerlo en dos enfoques independientes: de manera procedimental y orientado a objetos.

Comencemos con el primero.

La pila: el enfoque procedimental


Primero, debes decidir cómo almacenar los valores que llegarán a la pila. Sugerimos utilizar el método
más simple, y emplear una lista para esta tarea. Supongamos que el tamaño de la pila no está limitado
de ninguna manera. Supongamos también que el último elemento de la lista almacena el elemento
superior.

La pila en sí ya está creada:

pila = []

Estamos listos para definir una función que pone un valor en la pila. Aquí están las presuposiciones
para ello:

 El nombre para la función es push .


 La función obtiene un parámetro (este es el valor que se debe colocar en la pila).
 La función no devuelve nada.
 La función agrega el valor del parámetro al final de la pila.

Así es como lo hemos hecho, echa un vistazo:

def push(val):
pila.append(val)

Ahora es tiempo de que una función quite un valor de la pila. Así es como puedes hacerlo:

 El nombre de la función es pop.


 La función no obtiene ningún parámetro.
 La función devuelve el valor tomado de la pila.
 La función lee el valor de la parte superior de la pila y lo elimina.

La función esta aqui:

def pop():
val = pila[-1]
del pila[-1]
return val

Nota: la función no verifica si hay algún elemento en la pila.

Armemos todas las piezas juntas para poner la pila en movimiento. El programa completo empuja (push)
tres números a la pila, los saca e imprime sus valores en pantalla. Puedes verlo en la ventana del editor.

El programa muestra el siguiente texto en pantalla:

1
2
3

Pruébalo.

La pila: el enfoque procedimental frente al enfoque orientado a


objetos
La pila procedimental está lista. Por supuesto, hay algunas debilidades, y la implementación
podría mejorarse de muchas maneras (aprovechar las excepciones es una buena idea), pero
en general la pila está completamente implementada, y puedes usarla si lo necesitas.

Pero cuanto más la uses, más desventajas encontrarás. Éstas son algunas de ellas:

 La variable esencial (la lista de la pila) es altamente vulnerable; cualquiera puede


modificarla de forma incontrolable, destruyendo la pila; esto no significa que se haya
hecho de manera maliciosa; por el contrario, puede ocurrir como resultado de un
descuido, por ejemplo, cuando alguien confunde nombres de variables; imagina que
accidentalmente has escrito algo como esto:

pila[0] = 0
El funcionamiento de la pila estará completamente desorganizado.

 También puede suceder que un día necesites más de una pila; tendrás que crear otra
lista para el almacenamiento de la pila, y probablemente otras
funciones push y pop .
 También puede suceder que no solo necesites funciones push y pop , pero también
algunas otras funciones; ciertamente podrías implementarlas, pero intenta imaginar
qué sucedería si tuvieras docenas de pilas implementadas por separado.

El enfoque orientado a objetos ofrece soluciones para cada uno de los problemas anteriores.
Vamos a nombrarlos primero:

 La capacidad de ocultar (proteger) los valores seleccionados contra el acceso no


autorizado se llama encapsulamiento; no se puede acceder a los valores
encapsulados ni modificarlos si deseas utilizarlos exclusivamente.

 Cuando tienes una clase que implementa todos los comportamientos de pila
necesarios, puedes producir tantas pilas como desees; no necesitas copiar ni replicar
ninguna parte del código.

 La capacidad de enriquecer la pila con nuevas funciones proviene de la herencia;


puedes crear una nueva clase (una subclase) que herede todos los rasgos existentes
de la superclase y agregue algunos nuevos.

Ahora escribamos una nueva implementación de pila desde cero. Esta vez, utilizaremos el
enfoque orientado a objetos, que te guiará paso a paso en el mundo de la programación de
objetos.

La pila - el enfoque orientado a objetos


Por supuesto, la idea principal sigue siendo la misma. Usaremos una lista como almacenamiento de la
pila. Solo tenemos que saber cómo poner la lista en la clase.

Comencemos desde el principio: así es como comienza la pila de orientada a objetos:

class Pila:

Ahora, esperamos dos cosas de la clase:


 Queremos que la clase tenga una propiedad como el almacenamiento de la pila - tenemos
que "instalar" una lista dentro de cada objeto de la clase (nota: cada objeto debe tener su
propia lista; la lista no debe compartirse entre diferentes pilas).
 Despues, queremos que la lista esté oculta de la vista de los usuarios de la clase.

¿Cómo se hace esto?

A diferencia de otros lenguajes de programación, Python no tiene medios para permitirte declarar una
propiedad como esa.

En su lugar, debes agregar una instrucción específica. Las propiedades deben agregarse a la clase
manualmente.

¿Cómo garantizar que dicha actividad tiene lugar cada vez que se crea una nueva pila?

Hay una manera simple de hacerlo - tienes que equipar a la clase con una función específica:

 Tiene que ser nombrada de forma estricta.


 Se invoca implícitamente cuando se crea el nuevo objeto.

Tal función es llamada el constructor, ya que su propósito general es construir un nuevo objeto. El
constructor debe saber todo acerca de la estructura del objeto y debe realizar todas las inicializaciones
necesarias.

Agreguemos un constructor muy simple a la nueva clase. Echa un vistazo al código:

class Pila:
def __init__(self):
print("¡Hola!")

objetoPila = Pila()

Expliquemos más a detalle:

 El nombre del constructor es siempre __init__ .


 Tiene que tener al menos un parámetro (discutiremos esto más tarde); el parámetro se usa
para representar el objeto recién creado: puedes usar el parámetro para manipular el objeto y
enriquecerlo con las propiedades necesarias; harás uso de esto pronto.
 Nota: el parámetro obligatorio generalmente se denomina self - es solo una sugerencía, pero
deberías seguirla - simplifica el proceso de lectura y comprensión de tu código.

El código está en el editor. Ejecútalo ahora.

Aquí está su salida:

¡Hola!

Nota: no hay rastro de la invocación del constructor dentro del código. Ha sido invocado implícita y
automáticamente. Hagamos uso de eso ahora.

La pila - el enfoque orientado a objetos: continuación


Cualquier cambio que realices dentro del constructor que modifique el estado del parámetro self se
verá reflejado en el objeto recien creado.
Esto significa que puedes agregar cualquier propiedad al objeto y la propiedad permanecerá allí hasta que
el objeto termine su vida o la propiedad se elimine explícitamente.

Ahora agreguemos solo una propiedad al nuevo objeto - una lista para la pila. La
nombraremos listaPila .

Justo como aqui:

class Pila:
def __init__(self):
self.listaPila = []

objetoPila = Pila()
print(len(objetoPila.listaPila))

Nota:

 Hemos usado la notación punteada, al igual que cuando se invocan métodos. Esta es la
manera general para acceder a las propiedades de un objeto: debes nombrar el objeto, poner un
punto ( . ) después de el, y especificar el nombre de la propiedad deseada, ¡no uses paréntesis!
No deseas invocar un método, deseas acceder a una propiedad.
 Si estableces el valor de una propiedad por primera vez (como en el constructor), lo estás
creando; a partir de ese momento, el objeto tiene la propiedad y está listo para usar su valor.
 Hemos hecho algo más en el código: hemos intentado acceder a la
propiedad listaPila desde fuera de la clase inmediatamente después de que se haya
creado el objeto; queremos verificar la longitud actual de la pila, ¿lo hemos logrado?

Sí, por supuesto: el código produce el siguiente resultado:

Esto no es lo que queremos de la pila. Nosotros queremos que listaPila este escondida del mundo
exterior. ¿Es eso posible?

Sí, y es simple, pero no muy intuitivo.

La pila - el enfoque orientado a objetos: continuación


Echa un vistazo: hemos agregado dos guiones bajos antes del nombre listaPila - nada mas:

class Pila:

def __init__(self):

self.__listaPila = []

objetoPila = Pila()

print(len(objetoPila.__listaPila))

El cambio invalida el programa..

¿Por qué?
Cuando cualquier componente de la clase tiene un nombre que comienza con dos guiones bajos ( __ ),
se vuelve privado - esto significa que solo se puede acceder desde la clase.

No puedes verlo desde el mundo exterior. Así es como Python implementa el concepto
de encapsulación.

Ejecuta el programa para probar nuestras suposiciones: una excepción AttributeError debe ser
lanzada.

El enfoque orientado a objetos: una pila desde cero


Ahora es el momento de que las dos funciones (métodos) implementen las operaciones push y pop.
Python supone que una función de este tipo debería estar inmersa dentro del cuerpo de la clase - como
el constructor.

Queremos invocar estas funciones para agregar (push) y quitar (pop) valores de la pila. Esto
significa que ambos deben ser accesibles para el usuario de la clase (en contraste con la lista
previamente construida, que está oculta para los usuarios de la clase ordinaria).

Tal componente es llamado publico, por ello no puede comenzar su nombre con dos (o más) guiones
bajos. Hay un requisito más - el nombre no debe tener más de un guión bajo.

Las funciones en sí son simples. Echa un vistazo:

class Pila:
def __init__(self):
self.__listaPila = []

def push(self, val):


self.__listaPila.append(val)

def pop(self):
val = self.__listaPila[-1]
del self.__listaPila[-1]
return val

objetoPila = Pila()

objetoPila.push(3)
objetoPila.push(2)
objetoPila.push(1)

print(objetoPila.pop())
print(objetoPila.pop())
print(objetoPila.pop())

Sin embargo, hay algo realmente extraño en el código. Las funciones parecen familiares, pero tienen más
parámetros que sus contrapartes procedimentales.

Aquí, ambas funciones tienen un parámetro llamado self en la primera posición de la lista de
parámetros.

¿Es necesario? Si, lo es.

Todos los métodos deben tener este parámetro. Desempeña el mismo papel que el primer parámetro
constructor.
Permite que el método acceda a entidades (propiedades y actividades / métodos) del objeto. No
puedes omitirlo. Cada vez que Python invoca un método, envía implícitamente el objeto actual como el
primer argumento.

Esto significa que el método está obligado a tener al menos un parámetro, que Python mismo
utiliza - no tienes ninguna influencia sobre el.

Si tu método no necesita ningún parámetro, este debe especificarse de todos modos. Si está diseñado
para procesar solo un parámetro, debes especificar dos, ya que la función del primero sigue siendo la
misma.

Hay una cosa más que requiere explicación: la forma en que se invocan los métodos desde la
variable __listaPila .

Afortunadamente, es mucho más simple de lo que parece:

 La primera etapa entrega el objeto como un todo → self .


 A continuación, debes llegar a la lista __listaPila → self.__listaPila .
 Con __listaPila lista para ser usada, puedes realizar el tercer y último paso
→ self.__listaPila.append(val) .

La declaración de la clase está completa y se han enumerado todos sus componentes. La clase está lista
para usarse.

El enfoque orientado a objetos: una pila desde cero


Tener tal clase abre nuevas posibilidades. Por ejemplo, ahora puedes hacer que más de una pila se
comporte de la misma manera. Cada pila tendrá su propia copia de datos privados, pero utilizará el mismo
conjunto de métodos.

Esto es exactamente lo que queremos para este ejemplo.

Analiza el código:

class Pila:

def __init__(self):

self.__listaPila = []

def push(self, val):

self.__listaPila.append(val)

def pop(self):

val = self.__listaPila[-1]

del self.__listaPila[-1]

return val

objetoPila1 = Pila()
objetoPila2 = Pila()

objetoPila1.push(3)

objetoPila2.push(objetoPila1.pop())

print(objetoPila2.pop())

Existen dos pilas creadas a partir de la misma clase base. Trabajan independientemente. Puedes
crear más si quieres.

Ejecuta el código en el editor y ve qué sucede. Realiza tus propios experimentos.

El enfoque orientado a objetos: una pila desde cero


(continuación)
Analiza el fragmento a continuación: hemos creado tres objetos de la clase Pila . Después, hemos
hecho malabarismos. Intenta predecir el valor que se muestra en la pantalla.

class Pila:

def __init__(self):

self.__listaPila = []

def push(self, val):

self.__listaPila.append(val)

def pop(self):

val = self.__listaPila[-1]

del self.__listaPila[-1]

return val

pequeñaPila = Pila()

otraPila = Pila()

graciosaPila = Pila()

pequeñaPila.push(1)

otraPila.push(pequeñaPila.pop() + 1)

graciosaPila.push(otraPila.pop() - 2)
print(graciosaPila.pop())

Entonces, ¿cuál es el resultado? Ejecuta el programa y comprueba si tenías razón.

El enfoque orientado a objetos: una pila desde cero


(continuación)
Ahora vamos un poco más lejos. Vamos a agregar una nueva clase para manejar pilas.

La nueva clase debería poder evaluar la suma de todos los elementos almacenados actualmente en
la pila.

No queremos modificar la pila previamente definida. Ya es lo suficientemente buena en sus aplicaciones,


y no queremos que cambie de ninguna manera. Queremos una nueva pila con nuevas capacidades. En
otras palabras, queremos construir una subclase de la ya existente clase Pila .

El primer paso es fácil: solo define una nueva subclase que apunte a la clase que se usará como
superclase.

Así es como se ve:

class SumarPila(Pila):
pass

La clase aún no define ningún componente nuevo, pero eso no significa que esté vacía. Obtiene (hereda)
todos los componentes definidos por su superclase - el nombre de la superclase se escribe después
de los dos puntos, después del nombre de la nueva clase.

Esto es lo que queremos de la nueva pila:

 Queremos que el método push no solo inserte el valor en la pila, sino que también sume el
valor a la variable sum .
 Queremos que la función pop no solo extraiga el valor de la pila, sino que también reste el valor
de la variable sum .

En primer lugar, agreguemos una nueva variable a la clase. Sera una variable privada, al igual que la
lista de pila. No queremos que nadie manipule el valor de la variable sum .

Como ya sabes, el constructor agrega una nueva propiedad a la clase. Ya sabes cómo hacerlo, pero hay
algo realmente intrigante dentro del constructor. Echa un vistazo:

class SumarPila(Pila):
def __init__(self):
Pila.__init__(self)
self.__sum = 0

La segunda línea del cuerpo del constructor crea una propiedad llamada __sum - almacenará el total de
todos los valores de la pila.

Pero la línea anterior se ve diferente. ¿Qué hace? ¿Es realmente necesaria? Sí lo es.
Al contrario de muchos otros lenguajes, Python te obliga a invocar explícitamente el constructor de
una superclase. Omitir este punto tendrá efectos nocivos: el objeto se verá privado de la
lista __listaPila . Tal pila no funcionará correctamente.

Esta es la única vez que puedes invocar a cualquiera de los constructores disponibles explícitamente; se
puede hacer dentro del constructor de la superclase.

Ten en cuenta la sintaxis:

 Se especifica el nombre de la superclase (esta es la clase cuyo constructor se desea ejecutar).


 Se pone un punto ( . ) después del nombre.
 Se especifica el nombre del constructor.
 Se debe señalar al objeto (la instancia de la clase) que debe ser inicializado por el constructor;
es por eso que se debe especificar el argumento y utilizar la variable self aquí;
recuerda: invocar cualquier método (incluidos los constructores) desde fuera de la clase
nunca requiere colocar el argumento self en la lista de argumentos - invocar un método
desde dentro de la clase exige el uso explícito del argumento self , y tiene que ser el primero
en la lista.

Nota: generalmente es una práctica recomendada invocar al constructor de la superclase antes de


cualquier otra inicialización que desees realizar dentro de la subclase. Esta es la regla que hemos seguido
en el código.

El enfoque orientado a objetos: una pila desde cero


(continuación)
En segundo lugar, agreguemos dos métodos. Pero, ¿realmente estamos agregándolos? Ya tenemos
estos métodos en la superclase. ¿Podemos hacer algo así?

Si podemos. Significa que vamos a cambiar la funcionalidad de los métodos, no sus nombres.
Podemos decir con mayor precisión que la interfaz (la forma en que se manejan los objetos) de la clase
permanece igual al cambiar la implementación al mismo tiempo.

Comencemos con la implementación de la función push . Esto es lo que esperamos de la función:

 Agregar el valor a la variable __sum .


 Agregar el valor a la pila.

Nota: la segunda actividad ya se implementó dentro de la superclase, por lo que podemos usarla.
Además, tenemos que usarla, ya que no hay otra forma de acceder a la variable __listaPila .

Así es como se mira el método push dentro de la subclase:

def push(self, val):


self.__sum += val
Pila.push(self, val)

Toma en cuenta la forma en que hemos invocado la implementación anterior del método push (el
disponible en la superclase):

 Tenemos que especificar el nombre de la superclase; esto es necesario para indicar claramente
la clase que contiene el método, para evitar confundirlo con cualquier otra función del mismo
nombre.
 Tenemos que especificar el objeto de destino y pasarlo como primer argumento (no se agrega
implícitamente a la invocación en este contexto).
Se dice que el método push ha sido anulado - el mismo nombre que en la superclase ahora representa
una funcionalidad diferente.

El enfoque orientado a objetos: una pila desde cero


(continuación)
Esta es la nueva función pop :

def pop(self):

val = Pila.pop(self)

self.__sum -= val

return val

Hasta ahora, hemos definido la variable __sum , pero no hemos proporcionado un método para obtener
su valor. Parece estar escondido. ¿Cómo podemos mostrarlo y que al mismo tiempo se proteja de
modificaciones?

Tenemos que definir un nuevo método. Lo nombraremos getSuma . Su única tarea será devolver el
valor de __sum .

Aquí está:

def getSuma(self):

return self.__sum

Entonces, veamos el programa en el editor. El código completo de la clase está ahí. Podemos ahora
verificar su funcionamiento, y lo hacemos con la ayuda de unas pocas líneas de código adicionales.

Como puedes ver, agregamos cinco valores subsiguientes en la pila, imprimimos su suma y los sacamos
todos de la pila.

Bien, esta ha sido una breve introducción a la programación de orientada a objetos de Python. Pronto te
contaremos todo con más detalle.

Variables de instancia
En general, una clase puede equiparse con dos tipos diferentes de datos para formar las
propiedades de una clase. Ya viste uno de ellos cuando estábamos estudiando pilas.

Este tipo de propiedad existe solo cuando se crea explícitamente y se agrega a un objeto.
Como ya sabes, esto se puede hacer durante la inicialización del objeto, realizada por el
constructor.

Además, se puede hacer en cualquier momento de la vida del objeto. Es importante


mencionar también que cualquier propiedad existente se puede eliminar en cualquier
momento.

Tal enfoque tiene algunas consecuencias importantes:

 Diferentes objetos de la misma clase pueden poseer diferentes conjuntos de


propiedades.
 Debe haber una manera de verificar con seguridad si un objeto específico
posee la propiedad que deseas utilizar (a menos que quieras provocar una
excepción, siempre vale la pena considerarlo).
 Cada objeto lleva su propio conjunto de propiedades - no interfieren entre sí de
ninguna manera.

Tales variables (propiedades) se llaman variables de instancia.

La palabra instancia sugiere que están estrechamente conectadas a los objetos (que son
instancias de clase), no a las clases mismas. Echemos un vistazo más de cerca a ellas.

Aquí hay un ejemplo:

class ClaseEjemplo:
def __init__(self, val = 1):
self.primera = val

def setSegunda(self, val):


self.segunda = val

objetoEjemplo1 = ClaseEjemplo()
objetoEjemplo2 = ClaseEjemplo(2)

objetoEjemplo2.setSegunda(3)

objetoEjemplo3 = ClaseEjemplo(4)
objetoEjemplo3.tercera = 5

print(objetoEjemplo1.__dict__)
print(objetoEjemplo2.__dict__)
print(objetoEjemplo3.__dict__)

Se necesita una explicación adicional antes de entrar en más detalles. Echa un vistazo a las
últimas tres líneas del código.

Los objetos de Python, cuando se crean, están dotados de un pequeño conjunto de


propiedades y métodos predefinidos. Cada objeto los tiene, los quieras o no. Uno de
ellos es una variable llamada __dict__ (es un diccionario).

La variable contiene los nombres y valores de todas las propiedades (variables) que el objeto
contiene actualmente. Vamos a usarla para presentar de forma segura el contenido de un
objeto.

Vamos a sumergirnos en el código ahora:

 La clase llamada ClaseEjemplo tiene un constructor, el cual crea


incondicionalmente una variable de instancia llamada primera , y le asigna el
valor pasado a través del primer argumento (desde la perspectiva del usuario de la
clase) o el segundo argumento (desde la perspectiva del constructor); ten en cuenta
el valor predeterminado del parámetro: cualquier cosa que puedas hacer con un
parámetro de función regular también se puede aplicar a los métodos.

 La clase también tiene un método que crea otra variable de instancia,


llamada segunda .
 Hemos creado tres objetos de la clase ClaseEjemplo , pero todas estas instancias
difieren:

o objetoEjemplo1 solo tiene una propiedad llamada primera .

o objetoEjemplo2 tiene dos propiedades: primera y segunda .

o objetoEjemplo3 ha sido enriquecido sobre la marcha con una propiedad


llamada tercera , fuera del código de la clase: esto es posible y totalmente
permisible.

La salida del programa muestra claramente que nuestras suposiciones son correctas: aquí
están:

{'primera': 1}
{'primera': 2, 'segunda': 3}
{'primera': 4, 'tercera': 5}

Hay una conclusión adicional que debería mencionarse aquí: el modificar una variable de
instancia de cualquier objeto no tiene impacto en todos los objetos restantes. Las
variables de instancia están perfectamente aisladas unas de otras.

Variables de instancia: continuación


Observa el ejemplo modificado en el editor.

Es casi lo mismo que el anterior. La única diferencia está en los nombres de las propiedades.
Hemos agregado dos guiones bajos ( __ ) en frente de ellos.

Como sabes, tal adición hace que la variable de instancia sea privada - se vuelve inaccesible desde el
mundo exterior.

El comportamiento real de estos nombres es un poco más complicado, así que ejecutemos el programa.
Esta es la salida:

{'_ClaseEjemplo__primera': 1}
{'_ClaseEjemplo__primera': 2, '_ClaseEjemplo__segunda': 3}
{'_ClaseEjemplo__primera': 4, '__tercera': 5}

¿Puedes ver estos nombres extraños llenos de guiones bajos? ¿De dónde provienen?

Cuando Python ve que deseas agregar una variable de instancia a un objeto y lo vas a hacer dentro de
cualquiera de los métodos del objeto, maneja la operación de la siguiente manera:

 Coloca un nombre de clase antes de tu nombre.


 Coloca un guión bajo adicional al principio.

Es por ello que __primera se convierte en _ClaseEjemplo__primera .


El nombre ahora es completamente accesible desde fuera de la clase. Puedes ejecutar un código
como este:

print(objetoEjemplo1._ClaseEjemplo__primera)

y obtendrás un resultado válido sin errores ni excepciones.

Como puedes ver, hacer que una propiedad sea privada es limitado.

No funcionará si agregas una variable de instancia fuera del código de clase. En este caso, se
comportará como cualquier otra propiedad ordinaria.

Variables de Clase
Una variable de clase es una propiedad que existe en una sola copia y se almacena
fuera de cualquier objeto.

Nota: no existe una variable de instancia si no hay ningún objeto en la clase; existe una
variable de clase en una copia, incluso si no hay objetos en la clase.

Las variables de clase se crean de manera diferente. El ejemplo te dirá más:

class ClaseEjemplo:
contador = 0
def __init__(self, val = 1):
self.__primera = val
ClaseEjemplo.contador += 1

objetoEjemplo1 = ClaseEjemplo()
objetoEjemplo2 = ClaseEjemplo(2)
objetoEjemplo3 = ClaseEjemplo(4)

print(objetoEjemplo1.__dict__, objetoEjemplo1.contador)
print(objetoEjemplo2.__dict__, objetoEjemplo2.contador)
print(objetoEjemplo3.__dict__, objetoEjemplo3.contador)

Observa:

 Hay una asignación en la primera linea de la definición de clase: establece la variable


denominada contador a 0; inicializando la variable dentro de la clase pero fuera de
cualquiera de sus métodos hace que la variable sea una variable de clase.
 El acceder a dicha variable tiene el mismo aspecto que acceder a cualquier atributo
de instancia; está en el cuerpo del constructor; como puedes ver, el constructor
incrementa la variable en uno; en efecto, la variable cuenta todos los objetos
creados.

Ejecutar el código causará el siguiente resultado:

{'_ClaseEjemplo__primera': 1} 3
{'_ClaseEjemplo__primera': 2} 3
{'_ClaseEjemplo__primera': 4} 3

Dos conclusiones importantes provienen del ejemplo:


 Las variables de clase no se muestran en el diccionario de un
objeto __dict__ (esto es natural ya que las variables de clase no son partes de un
objeto), pero siempre puedes intentar buscar en la variable del mismo nombre, pero
a nivel de clase, te mostraremos esto muy pronto.
 Una variable de clase siempre presenta el mismo valor en todas las instancias de
clase (objetos).

Variables de Clase: continuación


Mira el ejemplo en el editor. ¿Puedes adivinar su salida?

Ejecuta el programa y verifica si tus predicciones fueron correctas. Todo funciona como se esperaba,
¿no?

Variables de Clase: continuación


Hemos dicho antes que las variables de clase existen incluso cuando no se creó ninguna instancia de
clase (objeto).

Ahora aprovecharemos la oportunidad para mostrarte la diferencia entre estas dos


variables __dict__ , la de la clase y la del objeto.

Observa el código en el editor. La prueba está ahí.

Echemos un vistazo más de cerca:

 Definimos una clase llamada ClaseEjemplo .


 La clase define una variable de clase llamada varia .
 El constructor de la clase establece la variable con el valor del parámetro.
 Nombrar la variable es el aspecto más importante del ejemplo porque:
o El cambiar la asignación a self.varia = val crearía una variable de
instancia con el mismo nombre que la clase.
o El cambiar la asignación a varia = val operaría en la variable local de un
método; (te recomendamos probar los dos casos anteriores; esto te facilitará
recordar la diferencia).
 La primera línea del código fuera de la clase imprime el valor del
atributo ClaseEjemplo.varia . Nota: utilizamos el valor antes de instanciar el primer objeto
de la clase.

Ejecuta el código en el editor y verifica su salida.

Como puedes ver __dict__ contiene muchos más datos que la contraparte de su objeto. La mayoría
de ellos son inútiles ahora - el que queremos que verifiques cuidadosamente muestra el valor actual
de varia .

Nota que el __dict__ del objeto está vacío - el objeto no tiene variables de instancia.

Comprobando la existencia de un atributo


La actitud de Python hacia la instanciación de objetos plantea una cuestión importante: en contraste con
otros lenguajes de programación, es posible que no esperes que todos los objetos de la misma clase
tengan los mismos conjuntos de propiedades.

Justo como en el ejemplo en el editor. Míralo cuidadosamente.

El objeto creado por el constructor solo puede tener uno de los dos atributos posibles: a o b .
La ejecución del código producirá el siguiente resultado:

Traceback (most recent call last):

File ".main.py", line 11, in

print(objetoEjemplo.b)

AttributeError: 'ClaseEjemplo' object has no attribute 'b'

Como puedes ver, acceder a un atributo de objeto (clase) no existente provoca una
excepción AttributeError.

Comprobando la existencia de un atributo: continuación


La instrucción try-except te brinda la oportunidad de evitar problemas con propiedades inexistentes.

Es fácil: mira el código en el editor.

Como puedes ver, esta acción no es muy sofisticada. Esencialmente, acabamos de barrer el tema debajo
de la alfombra.

Afortunadamente, hay una forma más de hacer frente al problema.

Python proporciona una función que puede verificar con seguridad si algún objeto / clase contiene
una propiedad específica. La función se llama hasattr , y espera que le pasen dos argumentos:

 La clase o el objeto que se verifica.


 El nombre de la propiedad cuya existencia se debe informar (Nota: debe ser una cadena que
contenga el nombre del atributo).

La función retorna True o False.

Así es como puedes utilizarla:

class ClaseEjemplo:
def __init__(self, val):
if val % 2 != 0:
self.a = 1
else:
self.b = 1

objetoEjemplo = ClaseEjemplo(1)
print(objetoEjemplo.a)

if hasattr(objetoEjemplo, 'b'):
print(objetoEjemplo.b)

Comprobando la existencia de un atributo: continuación


No olvides que la función hasattr() también puede operar en clases. Puedes usarlo para averiguar
si una variable de clase está disponible, como en el ejemplo en el editor.
La función devuelve True si la clase especificada contiene un atributo dado, y False de lo contrario.

¿Puedes adivinar la salida del código? Ejecútalo para verificar tus conjeturas.

Un ejemplo más: analiza el código a continuación e intenta predecir su salida:

class ClaseEjemplo:

a = 1

def __init__(self):

self.b = 2

objetoEjemplo = ClaseEjemplo()

print(hasattr(objetoEjemplo, 'b'))

print(hasattr(objetoEjemplo, 'a'))

print(hasattr(ClaseEjemplo, 'b'))

print(hasattr(ClaseEjemplo, 'a'))

¿Tuviste éxito? Ejecuta el código para verificar tus predicciones.

Bien, hemos llegado al final de esta sección. En la siguiente sección vamos a hablar sobre los métodos,
ya que los métodos dirigen los objetos y los activan.

Métodos a detalle
Resumamos todos los hechos relacionados con el uso de métodos en las clases de Python.

Como ya sabes, un método es una función que está dentro de una clase.

Hay un requisito fundamental: un método está obligado a tener al menos un parámetro (no existen
métodos sin parámetros; un método puede invocarse sin un argumento, pero no puede declararse sin
parámetros).

El primer (o único) parámetro generalmente se denomina self . Te sugerimos que lo sigas nombrando
de esta manera, darle otros nombres puede causar sorpresas inesperadas.

El nombre self sugiere el propósito del parámetro - identifica el objeto para el cual se invoca el
método.

Si vas a invocar un método, no debes pasar el argumento para el parámetro self - Python lo
configurará por ti.

El ejemplo en el editor muestra la diferencia.

El código da como salida:


método

Toma en cuenta la forma en que hemos creado el objeto - hemos tratado el nombre de la clase como
una función, y devuelve un objeto recién instanciado de la clase.

Si deseas que el método acepte parámetros distintos a self , debes:

 Colocarlos después de self en la definición del método.


 Pasarlos como argumentos durante la invocación sin especificar self .

Justo como aqui:

class conClase:
def metodo(self, par):
print("método:", par)

obj = conClase()
obj.metodo(1)
obj.metodo(2)
obj.metodo(3)

El código da como salida:

método: 1
método: 2
método: 3

Métodos a detalle: continuación


El parámetro self es usado para obtener acceso a la instancia del objeto y las variables de clase.

El ejemplo muestra ambas formas de utilizar el parámetro self :

class conClase:

varia = 2

def metodo(self):

print(self.varia, self.var)

obj = conClase()

obj.var = 3

obj.metodo()

El código da como salida:

2 3
El parámetro self también se usa para invocar otros métodos desde dentro de la clase.

Justo como aquí:

class conClase():

def otro(self):

print("otro")

def metodo(self):

print("método")

self.otro()

obj = conClase()

obj.metodo()

El código da como salida:

método

otro

Métodos a detalle: continuación


Si se nombra un método de esta manera: __init__ , no será un método regular, será un constructor.

Si una clase tiene un constructor, este se invoca automática e implícitamente cuando se instancia el
objeto de la clase.

El constructor:

 Esta obligado a tener el parámetro self (se configura automáticamente).


 Pudiera (pero no necesariamente) tener mas parámetros que solo self ; si esto sucede, la
forma en que se usa el nombre de la clase para crear el objeto debe tener la
definición __init__ .
 Se puede utilizar para configurar el objeto, es decir, inicializa adecuadamente su estado
interno, crea variables de instancia, crea instancias de cualquier otro objeto si es necesario, etc.

Observa el código en el editor. El ejemplo muestra un constructor muy simple pero funcional.

Ejecutalo. El código da como salida:

objeto
Ten en cuenta que el constructor:

 No puede retornar un valor, ya que está diseñado para devolver un objeto recién creado y
nada más.
 No se puede invocar directamente desde el objeto o desde dentro de la clase (puedes
invocar un constructor desde cualquiera de las superclases del objeto, pero discutiremos esto
más adelante).

Métodos a detalle: continuación


Como __init__ es un método, y un método es una función, puedes hacer los mismos trucos con
constructores y métodos que con las funciones ordinarias.

El ejemplo en el editor muestra cómo definir un constructor con un valor de argumento predeterminado.
Pruébalo.

El código da como salida:

objeto

None

Todo lo que hemos dicho sobre el manejo de los nombres también se aplica a los nombres de métodos,
un método cuyo nombre comienza con __ está (parcialmente) oculto.

El ejemplo muestra este efecto:

class conClase:

def visible(self):

print("visible")

def __oculto(self):

print("oculto")

obj = conClase()

obj.visible()

try:

obj.__oculto()

except:

print("fallido")
obj._conClase__oculto()

El código da como salida:

visible

fallido

oculto

Ejecuta el programa y pruébalo.

La vida interna de clases y objetos


Cada clase de Python y cada objeto de Python está pre-equipado con un conjunto de atributos útiles que
pueden usarse para examinar sus capacidades.

Ya conoces uno de estos: es la propiedad __dict__ .

Observemos cómo esta propiedad trata con los métodos: mira el código en el editor.

Ejecútalo para ver qué produce. Verifica el resultado.

Encuentra todos los métodos y atributos definidos. Localiza el contexto en el que existen: dentro del
objeto o dentro de la clase.

La vida interna de clases y objetos: continuación


__dict__ es un diccionario. Otra propiedad incorporada que vale la pena mencionar es una cadena
llamada __name__ .

La propiedad contiene el nombre de la clase. No es nada emocionante, es solo una cadena.

Nota: el atributo __name__ está ausente del objeto - existe solo dentro de las clases.

Si deseas encontrar la clase de un objeto en particular, puedes usar una función llamada type() , la
cual es capaz (entre otras cosas) de encontrar una clase que se haya utilizado para crear instancias de
cualquier objeto.

Mira el código en el editor, ejecútalo y compruébalo tu mismo.

La salida del código es:

conClase

conClase

Nota: algo como esto print(obj.__name__) causará un error.


La vida interna de clases y objetos: continuación
__module__ es una cadena, también almacena el nombre del módulo que contiene la definición
de la clase.

Vamos a comprobarlo: ejecuta el código en el editor.

La salida del código es:

__main__

__main__

Como sabes, cualquier módulo llamado __main__ en realidad no es un módulo, sino es el archivo
actualmente en ejecución.

La vida interna de clases y objetos: continuación


__bases__ es una tupla. La tupla contiene clases (no nombres de clases) que son superclases
directas para la clase.

El orden es el mismo que el utilizado dentro de la definición de clase.

Te mostraremos solo un ejemplo muy básico, ya que queremos resaltar como funciona la herencia.

Además, te mostraremos cómo usar este atributo cuando discutamos los aspectos orientados a objetos
de las excepciones.

Nota: solo las clases tienen este atributo - los objetos no.

Hemos definido una función llamada printBases() , diseñada para presentar claramente el contenido
de la tupla.

Observa el código en el editor. Ejecútalo. Su salida es:

( object )

( object )

( SuperUno SuperDos )

Nota: una clase sin superclases explícitas apunta al objeto (una clase de Python predefinida) como su
antecesor directo.

Reflexión e introspección
Todo esto permite que el programador de Python realice dos actividades importantes
específicas para muchos lenguajes objetivos. Las cuales son:

 Introspección, que es la capacidad de un programa para examinar el tipo o las


propiedades de un objeto en tiempo de ejecución.
 Reflexión, que va un paso más allá, y es la capacidad de un programa para
manipular los valores, propiedades y/o funciones de un objeto en tiempo de
ejecución.
En otras palabras, no tienes que conocer la definición completa de clase/objeto para
manipular el objeto, ya que el objeto y/o su clase contienen los metadatos que te permiten
reconocer sus características durante la ejecución del programa.

Investigando Clases
¿Qué puedes descubrir acerca de las clases en Python? La respuesta es simple: todo.

Tanto la reflexión como la introspección permiten al programador hacer cualquier cosa con cada objeto,
sin importar de dónde provenga.

Analiza el código en el editor.

La función llamada incIntsI() obtiene un objeto de cualquier clase, escanea su contenido para
encontrar todos los atributos enteros con nombres que comienzan con i, y los incrementa en uno.

¿Imposible? ¡De ningúna manera!

Así es como funciona:

 La línea 1: define una clase simple...


 Líneas 3 a la 10: ... la llenan con algunos atributos.
 Línea 12: ¡esta es nuestra función!
 Línea 13: escanea el atributo __dict__ , buscando todos los nombres de atributos.
 Línea 14: si un nombre comienza con i...
 Línea 15: ... utiliza la función getattr() para obtener su valor actual;
nota: getattr() toma dos argumentos: un objeto y su nombre de propiedad (como una
cadena) y devuelve el valor del atributo actual.
 Línea 16: comprueba si el valor es de tipo entero, emplea la función isinstance() para este
propósito (discutiremos esto más adelante).
 Línea 17: si la comprobación sale bien, incrementa el valor de la propiedad haciendo uso de la
función setattr() ; la función toma tres argumentos: un objeto, el nombre de la propiedad
(como una cadena) y el nuevo valor de la propiedad.

El código da como salida:

{'a': 1, 'b': 2, 'i': 3, 'ireal': 3.5, 'entero': 4, 'z': 5}


{'a': 1, 'b': 2, 'i': 4, 'ireal': 3.5, 'entero': 4, 'z': 5}
¡Eso es todo!

Herencia: ¿por qué y cómo?


Antes de comenzar a hablar sobre la herencia, queremos presentar un nuevo y práctico mecanismo
utilizado por las clases y los objetos de Python: es la forma en que el objeto puede presentarse a si
mismo.

Comencemos con un ejemplo. Observa el código en el editor.

El programa imprime solo una línea de texto, que en nuestro caso es:

<__main__.Estrella object at 0x7f377e552160>

Si ejecutas el mismo código en tu computadora, verás algo muy similar, aunque el número hexadecimal
(la subcadena que comienza con 0x) será diferente, ya que es solo un identificador de objeto interno
utilizado por Python, y es poco probable que aparezca igual cuando se ejecuta el mismo código en un
entorno diferente.

Como puedes ver, la impresión aquí no es realmente útil, y algo más específico, es preferible.

Afortunadamente, Python ofrece tal función.

Herencia: ¿por qué y cómo?


Cuando Python necesita que alguna clase u objeto deba ser presentado como una cadena (es
recomendable colocar el objeto como argumento en la invocación de la función print() ), intenta
invocar un método llamado __str__() del objeto y emplear la cadena que devuelve.

El método por default __str__() devuelve la cadena anterior: fea y poco informativa. Puedes
cambiarlo definiendo tu propio método del nombre.

Lo acabamos de hacer: observa el código en el editor.

El método nuevo __str__() genera una cadena que consiste en los nombres de la estrella y la
galaxia, nada especial, pero los resultados de impresión se ven mejor ahora, ¿no?

¿Puedes adivinar la salida? Ejecuta el código para verificar si tenías razón.

Herencia: ¿por qué y cómo?


El término herencia es más antiguo que la programación de computadoras, y describe la
práctica común de pasar diferentes bienes de una persona a otra después de la muerte de
esa persona. El término, cuando se relaciona con la programación de computadoras, tiene un
significado completamente diferente.
Definamos el término para nuestros propósitos:

La herencia es una práctica común (en la programación de objetos) de pasar atributos y


métodos de la superclase (definida y existente) a una clase recién creada, llamada
subclase.

En otras palabras, la herencia es una forma de construir una nueva clase, no desde
cero, sino utilizando un repertorio de rasgos ya definido. La nueva clase hereda (y
esta es la clave) todo el equipamiento ya existente, pero puedes agregar algo nuevo si es
necesario.

Gracias a eso, es posible construir clases más especializadas (más


concretas) utilizando algunos conjuntos de reglas y comportamientos generales
predefinidos.
El factor más importante del proceso es la relación entre la superclase y todas sus subclases
(nota: si B es una subclase de A y C es una subclase de B, esto también significa que C es
una subclase de A, ya que la relación es totalmente transitiva).

Aquí se presenta un ejemplo muy simple de herencia de dos niveles:

class Vehiculo:
pass

class VehiculoTerrestre(Vehiculo):
pass

class VehiculoOruga(VehiculoTerrestre):
pass

Todas las clases presentadas están vacías por ahora, ya que te mostraremos cómo funcionan
las relaciones mutuas entre las superclases y las subclases. Las llenaremos con contenido
pronto.

Podemos decir que:

 La clase Vehiculo es la superclase para


clases VehiculoTerrestre y VehiculoOruga .
 La clase VehiculoTerrestre es una subclase de Vehiculo y la superclase
de VehiculoOruga al mismo tiempo.
 La clase VehiculoOruga es una subclase tanto
de Vehiculo y VehiculoTerrestre .

El conocimiento anterior proviene de la lectura del código (en otras palabras, lo sabemos
porque podemos verlo).

¿Python sabe lo mismo? ¿Es posible preguntarle a Python al respecto? Sí lo es.

Herencia: issubclass()
Python ofrece una función que es capaz de identificar una relación entre dos clases, y aunque su
diagnóstico no es complejo, puede verificar si una clase particular es una subclase de cualquier otra
clase.

Así es como se ve:

issubclass(ClaseUno, ClaseDos)

La función devuelve True si ClaseUno es una subclase de ClaseDos , y False de lo contrario.

Vamos a verlo en acción, puede sorprenderte. Mira el código en el editor. Léelo cuidadosamente.

Hay dos bucles anidados. Su propósito es verificar todos los pares de clases ordenadas posibles y
que imprima los resultados de la verificación para determinar si el par coincide con la relación
subclase-superclase.

Ejecuta el código. El programa produce el siguiente resultado:


True False False

True True False

True True True

Hagamos que el resultado sea más legible:

↓ es una subclase de → Vehiculo VehiculoTerrestre VehiculoOruga

Vehiculo True False False

VehiculoTerrestre True True False

VehiculoOruga True True True

Existe una observación importante que hacer: cada clase se considera una subclase de sí misma.

Herencia: isinstance()
Como ya sabes, un objeto es la encarnación de una clase. Esto significa que el objeto es como un
pastel horneado usando una receta que se incluye dentro de la clase.

Esto puede generar algunos problemas.

Supongamos que tienes un pastel (por ejemplo, resultado de un argumento pasado a tu función). Deseas
saber qué receta se ha utilizado para prepararlo. ¿Por qué? Porque deseas saber qué esperar de él, por
ejemplo, si contiene nueces o no, lo cual es información crucial para ciertas personas.

Del mismo modo, puede ser crucial si el objeto tiene (o no tiene) ciertas características. En otras
palabras, si es un objeto de cierta clase o no.

Tal hecho podría ser detectado por la función llamada isinstance() :

isinstance(nombreObjeto, nombreClase)

La función devuelve True si el objeto es una instancia de la clase, o False de lo contrario.

Ser una instancia de una clase significa que el objeto (el pastel) se ha preparado utilizando una
receta contenida en la clase o en una de sus superclases.

No lo olvides: si una subclase contiene al menos las mismas caracteristicas que cualquiera de sus
superclases, significa que los objetos de la subclase pueden hacer lo mismo que los objetos derivados de
la superclase, por lo tanto, es una instancia de su clase de inicio y cualquiera de sus superclases.

Probémoslo. Analiza el código en el editor.

Hemos creado tres objetos, uno para cada una de las clases. Luego, usando dos bucles anidados,
verificamos todos los pares posibles de clase de objeto para averiguar si los objetos son instancias de
las clases.

Ejecuta el código.

Esto es lo que obtenemos:


True False False

True True False

True True True

Hagamos que el resultado sea más legible:

↓ es una instancia de → Vehiculo miVehiculoTerrestre VehiculoOruga

miVehiculo True False False

miVehiculoTerrestre True True False

VehiculoOruga True True True

¿La tabla confirma nuestras expectativas?

Herencia: el operador is
También existe un operador de Python que vale la pena mencionar, ya que se refiere directamente a los
objetos: aquí está:

objetoUno is objetoDos

El operador is verifica si dos variables (en este caso objetoUno y objetoDos ) se refieren al
mismo objeto.

No olvides que las variables no almacenan los objetos en sí, sino solo los identificadores que
apuntan a la memoria interna de Python.

Asignar un valor de una variable de objeto a otra variable no copia el objeto, sino solo su identificador. Es
por ello que un operador como is puede ser muy útil en ciertas circunstancias.

Echa un vistazo al código en el editor. Analicémoslo:

 Existe una clase muy simple equipada con un constructor simple, que crea una sola propiedad.
La clase se usa para instanciar dos objetos. El primero se asigna a otra variable, y su
propiedad val se incrementa en uno.
 Luego, el operador is se aplica tres veces para verificar todos los pares de objetos posibles, y
todos los valores de la propiedad val son mostrados en pantalla.
 La última parte del código lleva a cabo otro experimento. Después de tres tareas, ambas
cadenas contienen los mismos textos, pero estos textos se almacenan en diferentes objetos.

El código imprime:

False
False
True
1 2 1
True False
Los resultados prueban que ob1 y ob3 son en realidad los mismos objetos, mientras
que str1 y str2 no lo son, a pesar de que su contenido sea el mismo.

Cómo Python encuentra propiedades y métodos


Ahora veremos cómo Python trata con los métodos de herencia.

Echa un vistazo al ejemplo en el editor. Vamos a analizarlo:

 Existe una clase llamada Super , que define su propio constructor utilizado para asignar la
propiedad del objeto, llamada nombre .
 La clase también define el método __str__() , lo que permite que la clase pueda presentar su
identidad en forma de texto.
 La clase se usa luego como base para crear una subclase llamada Sub . La clase Sub define su
propio constructor, que invoca el de la superclase. Toma nota de cómo lo hemos
hecho: Super.__init__(self, nombre) .
 Hemos nombrado explícitamente la superclase y hemos apuntado al método para invocar
a __init__() , proporcionando todos los argumentos necesarios.
 Hemos instanciado un objeto de la clase Sub y lo hemos impreso.

El código da como salida:

Mi nombre es Andy.

Nota: Como no existe el método __str__() dentro de la clase Sub , la cadena a imprimir se producirá
dentro de la clase Super . Esto significa que el método __str__() ha sido heredado por la
clase Sub .

Cómo Python encuentra propiedades y métodos: continuación


Mira el código en el editor. Lo hemos modificado para mostrarte otro método de acceso a cualquier
entidad definida dentro de la superclase.

En el ejemplo anterior, nombramos explícitamente la superclase. En este ejemplo, hacemos uso de la


función super() , la cual accede a la superclase sin necesidad de conocer su nombre:

super().__init__(nombre)

La función super() crea un contexto en el que no tiene que (además, no debe) pasar el argumento
propio al método que se invoca; es por eso que es posible activar el constructor de la superclase
utilizando solo un argumento.

Nota: puedes usar este mecanismo no solo para invocar al constructor de la superclase, pero también
para obtener acceso a cualquiera de los recursos disponibles dentro de la superclase.

Cómo Python encuentra propiedades y métodos: continuación


Intentemos hacer algo similar, pero con propiedades (más precisamente con: variables de clase).

Observa el ejemplo en el editor.

Como puedes observar, la clase Super define una variable de clase llamada supVar , y la
clase Sub define una variable llamada subVar .
Ambas variables son visibles dentro del objeto de clase Sub - es por ello que el código da como salida:

Cómo Python encuentra propiedades y métodos: continuación


El mismo efecto se puede observar con variables de instancia - observa el segundo ejemplo en el editor.

El constructor de la clase Sub crea una variable de instancia llamada subVar , mientras que el
constructor de Super hace lo mismo con una variable de nombre supVar . Al igual que el ejemplo
anterior, ambas variables son accesibles desde el objeto de clase Sub .

La salida del programa es:

12

11

Nota: La existencia de la variable supVar obviamente está condicionada por la invocación del
constructor de la clase Super . Omitirlo daría como resultado la ausencia de la variable en el objeto
creado (pruébalo tu mismo).

Cómo Python encuentra propiedades y métodos: continuación


Ahora es posible formular una declaración general que describa el comportamiento de Python.

Cuando intentes acceder a una entidad de cualquier objeto, Python intentará (en este orden):

 Encontrarla dentro del objeto mismo.


 Encontrarla en todas las clases involucradas en la línea de herencia del objeto de abajo hacia
arriba.

Si ambos intentos fallan, una excepción ( AttributeError ) será lanzada.

La primera condición puede necesitar atención adicional. Como sabes, todos los objetos derivados de una
clase en particular pueden tener diferentes conjuntos de atributos, y algunos de los atributos pueden
agregarse al objeto mucho tiempo después de la creación del objeto.

El ejemplo en el editor resume esto en una línea de herencia de tres niveles. Analízalo cuidadosamente.

Todos los comentarios que hemos hecho hasta ahora están relacionados con casos de herencia única,
cuando una subclase tiene exactamente una superclase. Esta es la situación más común (y también la
recomendada).

Python, sin embargo, ofrece mucho más aquí. En las próximas lecciones te mostraremos algunos
ejemplos de herencia múltiple.

Cómo Python encuentra propiedades y métodos: continuación


La herencia múltiple ocurre cuando una clase tiene más de una superclase.
Sintácticamente, dicha herencia se presenta como una lista de superclases separadas por comas entre
paréntesis después del nombre de la nueva clase, al igual que aquí:

class SuperA:

varA = 10

def funA(self):

return 11

class SuperB:

varB = 20

def funB(self):

return 21

class Sub(SuperA, SuperB):

pass

obj = Sub()

print(obj.varA, obj.funA())

print(obj.varB, obj.funB())

La clase Sub tiene dos superclases: SuperA y SuperB . Esto significa que la clase Sub hereda todos
los bienes ofrecidos por ambas clases SuperA y SuperB .

El código imprime:

10 11

20 21

Ahora es el momento de introducir un nuevo término - overriding (anulación).

¿Qué crees que sucederá si más de una de las superclases define una entidad con un nombre en
particular?

Cómo Python encuentra propiedades y métodos: continuación


Analicemos el ejemplo en el editor.

Tanto la clase Nivel1 como la Nivel2 definen un método llamado fun() y una propiedad
llamada var . ¿Significará esto el objeto de la clase Nivel3 podrá acceder a dos copias de cada
entidad? De ningún modo.
La entidad definida después (en el sentido de herencia) anula la misma entidad definida
anteriormente. Es por eso que el código produce el siguiente resultado:

200 201

Como puedes ver, la variable de clase var y el método fun() de la clase Nivel2 anula las entidades
de los mismos nombres derivados de la clase Nivel1 .

Esta característica se puede usar intencionalmente para modificar el comportamiento predeterminado de


las clases (o definido previamente) cuando cualquiera de tus clases necesite actuar de manera diferente a
su ancestro.

También podemos decir que Python busca una entidad de abajo hacia arriba, y está completamente
satisfecho con la primera entidad del nombre deseado que encuentre.

¿Qué ocurre cuando una clase tiene dos ancestros que ofrecen la misma entidad y se encuentran en el
mismo nivel? En otras palabras, ¿Qué se debe esperar cuando surge una clase usando herencia
múltiple? Miremos lo siguiente.

Cómo Python encuentra propiedades y métodos: continuación


Echemos un vistazo al ejemplo en el editor.

La clase Sub hereda todos los bienes de dos superclases, Izquierda y Derecha (estos nombres
están destinados a ser significativos).

No hay duda de que la variable de clase varDerecha proviene de la clase Derecha , y la


variable varIzquierda proviene de la clase Izquierda respectivamente.

Esto es claro. Pero, ¿De donde proviene la variable var ? ¿Es posible adivinarlo? El mismo problema se
encuentra con el método fun() - ¿Será invocado desde Izquierda o desde Derecha ? Ejecutemos
el programa: la salida será:

I II DD Izquierda

Esto prueba que ambos casos poco claros tienen una solución dentro de la clase Izquierda . ¿Es esta
una premisa suficiente para formular una regla general? Sí lo es.

Podemos decir que Python busca componentes de objetos en el siguiente orden:

 Dentro del objeto mismo.


 En sus superclases, de abajo hacia arriba.
 Si hay más de una clase en una ruta de herencia, Python las escanea de izquierda a derecha.

¿Necesitas algo más? Simplemente haz una pequeña enmienda en el código - reemplaza: class
Sub(Izquierda, Derecha): con: class Sub(Derecha, Izquierda): , luego ejecuta el
programa nuevamente y observa qué sucede.

¿Qué ves ahora? Vemos:

D II DD Derecha

¿Ves lo mismo o algo diferente?


Cómo construir una jerarquía de clases
Construir una jerarquía de clases no es solo por amor al arte.

Si divides un problema entre las clases y decides cuál de ellas debe ubicarse en la parte superior y cuál
debe ubicarse en la parte inferior de la jerarquía, debes analizar cuidadosamente el problema, pero antes
de mostrarte cómo hacerlo (y cómo no hacerlo), queremos resaltar un efecto interesante. No es nada
extraordinario (es solo una consecuencia de las reglas generales presentadas anteriormente), pero
recordarlo puede ser clave para comprender cómo funcionan algunos códigos y cómo se puede usar este
efecto para construir un conjunto flexible de clases.

Echa un vistazo al código en el editor. Analicémoslo:

 Existen dos clases llamadas Uno y Dos , se entiende que Dos es derivada de Uno . Nada
especial. Sin embargo, algo es notable: el método doit() .
 El método doit() está definido dos veces: originalmente dentro de Uno y posteriormente
dentro de Dos . La esencia del ejemplo radica en el hecho de que es invocado solo una vez -
dentro de Uno .

La pregunta es: ¿cuál de los dos métodos será invocado por las dos últimas líneas del código?

La primera invocación parece ser simple, el invocar el método haz_algo() del objeto uno obviamente
activará el primero de los métodos.

La segunda invocación necesita algo de atención. También es simple si tienes en cuenta cómo Python
encuentra los componentes de la clase. La segunda invocación lanzará el método hazlo() en la forma
existente dentro de la clase Dos , independientemente del hecho de que la invocación se lleva a cabo
dentro de la clase Uno .

En efecto, el código genera el siguiente resultado:

hazlo de Uno
hazlo de Dos

Nota: la situación en la cual la subclase puede modificar el comportamiento de su superclase (como


en el ejemplo) se llama polimorfismo. La palabra proviene del griego (polys: "muchos, mucho" y
morphe, "forma, forma"), lo que significa que una misma clase puede tomar varias formas dependiendo de
las redefiniciones realizadas por cualquiera de sus subclases.

El método, redefinido en cualquiera de las superclases, que cambia el comportamiento de la superclase,


se llama virtual.

En otras palabras, ninguna clase se da por hecho. El comportamiento de cada clase puede ser modificado
en cualquier momento por cualquiera de sus subclases.

Te mostraremos cómo usar el polimorfismo para extender la flexibilidad de la clase.

Cómo construir una jerarquía de clases: continuación


Mira el ejemplo en el editor.

¿Se parece a algo? Sí, por supuesto que lo hace. Se refiere al ejemplo que se muestra al comienzo del
módulo cuando hablamos de los conceptos generales de la programación orientada a objetos.
Puede parecer extraño, pero no utilizamos herencia en este ejemplo, solo queríamos mostrarte que no
nos limita.

Definimos dos clases separadas capaces de producir dos tipos diferentes de vehículos terrestres. La
principal diferencia entre ellos está en cómo giran. Un vehículo con ruedas solo gira las ruedas delanteras
(generalmente). Un vehículo oruga tiene que detener una de las pistas.

¿Puedes seguir el código?

 Un vehículo oruga realiza un giro deteniéndose y moviéndose en una de sus pistas (esto lo hace
el método control_de_pista() , el cual se implementará más tarde).
 Un vehículo con ruedas gira cuando sus ruedas delanteras giran (esto lo hace el
método girar_ruedas_delanteras() ).
 El método girar() utiliza el método adecuado para cada vehículo en particular.

¿Puedes detectar el error del código?

Los métodos girar() son muy similares como para dejarlos en esta forma.

Vamos a reconstruir el código: vamos a presentar una superclase para reunir todos los aspectos similares
de los vehículos, trasladando todos los detalles a las subclases.

Cómo construir una jerarquía de clases: continuación


Mira el código en el editor nuevamente. Esto es lo que hemos hecho:

 Definimos una superclase llamada Vehiculo , la cual utiliza el método girar() para
implementar un esquema para poder girar, mientras que el giro en si es realizado
por cambiardireccion() ; nota: dicho método está vacío, ya que vamos a poner todos los
detalles en la subclase (dicho método a menudo se denomina método abstracto, ya que solo
demuestra alguna posibilidad que será instanciada más tarde).
 Definimos una subclase llamada VehiculoOruga (nota: es derivada de la clase Vehiculo )
la cual instancia el método cambiardireccion() utilizando el método
denominado control_de_pista().
 Respectivamente, la subclase llamada VehiculoTerrestre hace lo mismo, pero usa el
método girar_ruedas_delanteras() para obligar al vehículo a girar.

La ventaja más importante (omitiendo los problemas de legibilidad) es que esta forma de código te
permite implementar un nuevo algoritmo de giro simplemente modificando el método girar() , lo cual
se puede hacer en un solo lugar, ya que todos los vehículos lo obedecerán.

Así es como el el polimorfismo ayuda al desarrollador a mantener el código limpio y consistente.

Cómo construir una jerarquía de clases: continuación


La herencia no es la única forma de construir clases adaptables. Puedes lograr los mismos objetivos (no
siempre, pero muy a menudo) utilizando una técnica llamada composición.

La composición es el proceso de componer un objeto usando otros objetos diferentes. Los objetos
utilizados en la composición entregan un conjunto de rasgos deseados (propiedades y / o métodos),
podemos decir que actúan como bloques utilizados para construir una estructura más complicada.

Puede decirse que:

 La herencia extiende las capacidades de una clase agregando nuevos componentes y


modificando los existentes; en otras palabras, la receta completa está contenida dentro de la
clase misma y todos sus ancestros; el objeto toma todas las pertenencias de la clase y las usa.
 La composición proyecta una clase como contenedor capaz de almacenar y usar otros
objetos (derivados de otras clases) donde cada uno de los objetos implementa una parte del
comportamiento de una clase.

Permítenos ilustrar la diferencia usando los vehículos previamente definidos. El enfoque anterior nos
condujo a una jerarquía de clases en la que la clase más alta conocía las reglas generales utilizadas para
girar el vehículo, pero no sabía cómo controlar los componentes apropiados (ruedas o pistas).

Las subclases implementaron esta capacidad mediante la introducción de mecanismos especializados.


Hagamos (casi) lo mismo, pero usando composición. La clase, como en el ejemplo anterior, sabe cómo
girar el vehículo, pero el giro real lo realiza un objeto especializado almacenado en una propiedad
llamada controlador . El controlador es capaz de controlar el vehículo manipulando las partes
relevantes del vehículo.

Echa un vistazo al editor: así es como podría verse.

Existen dos clases llamadas Pistas y Ruedas - ellas saben cómo controlar la dirección del vehículo.
También hay una clase llamada Vehiculo que puede usar cualquiera de los controladores disponibles
(los dos ya definidos o cualquier otro definido en el futuro): el controlador se pasa a la clase durante
la inicialización.

De esta manera, la capacidad de giro del vehículo se compone de un objeto externo, no implementado
dentro de la clase Vehiculo .

En otras palabras, tenemos un vehículo universal y podemos instalar pistas o ruedas en él.

El código produce el siguiente resultado:

ruedas: True True


ruedas: True False
pistas: False True
pistas: False False

Herencia simple versus herencia múltiple


Como ya sabes, no hay obstáculos para usar la herencia múltiple en Python. Puedes derivar
cualquier clase nueva de más de una clase definida previamente.

Solo hay un "pero". El hecho de que puedas hacerlo no significa que tengas que hacerlo.

No olvides que:

 Una sola clase de herencia siempre es más simple, segura y fácil de entender y
mantener.

 La herencia múltiple siempre es arriesgada, ya que tienes muchas más


oportunidades de cometer un error al identificar estas partes de las superclases que
influirán efectivamente en la nueva clase.

 La herencia múltiple puede hacer que la anulación sea extremadamente difícil;


además, el emplear la función super() se vuelve ambiguo.
 La herencia múltiple viola el principio de responsabilidad única (mas detalles
aquí: https://en.wikipedia.org/wiki/Single_responsibility_principle) ya que forma una
nueva clase de dos (o más) clases que no saben nada una de la otra.

 Sugerimos encarecidamente la herencia múltiple como la última de todas las posibles


soluciones: si realmente necesitas las diferentes funcionalidades que ofrecen las
diferentes clases, la composición puede ser una mejor alternativa.
Diamantes y porque no los quieres
El espectro de problemas que posiblemente provienen de la herencia múltiple se ilustra
mediante un problema clásico denominado problema de diamantes. El nombre refleja la
forma del diagrama de herencia: echa un vistazo a la imagen.

 Existe la superclase superior nombrada A.


 Aquí hay dos subclases derivadas de A - B y C.
 Y también está la subclase inferior llamada D, derivada de B y C (o C y B, ya que
estas dos variantes significan cosas diferentes en Python).

¿Puedes ver el diamante allí?

A Python, sin embargo, no le gustan los diamantes, y no te permitirá implementar algo como
esto. Si intentas construir una jerarquía como esta:

class A:
pass

class B(A):

pass

class C(A):

pass

class D(A, B):

pass

d = D()

Obtendrás una excepción TypeError, junto con el siguiente mensaje:

Cannot create a consistent method resolution

order (MRO) for bases B, A

Donde MRO significa Method Resolution Order. Este es el algoritmo que Python utiliza para
buscar el árbol de herencia y encontrar los métodos necesarios.

Los diamantes son preciosos y valiosos ... pero no en la programación. Evítalos por tu propio
bien.

Más sobre excepciones


El discutir sobre la programación orientada a objetos ofrece una muy buena oportunidad para volver a las
excepciones. La naturaleza orientada a objetos de las excepciones de Python las convierte en una
herramienta muy flexible, capaz de adaptarse a necesidades específicas, incluso aquellas que aún no
conoces.

Antes de adentrarnos en el lado orientado a objetos de las excepciones, queremos mostrarte algunos
aspectos sintácticos y semánticos de la forma en que Python trata el bloque try-except, ya que ofrece un
poco más de lo que hemos presentado hasta ahora.

La primera característica que queremos analizar aquí es una rama adicional posible que se puede colocar
dentro (o más bien, directamente detrás) del bloque try-except: es la parte del código que comienza
con else - justo como el ejemplo en el editor.

Un código etiquetado de esta manera se ejecuta cuando (y solo cuando) no se ha generado ninguna
excepción dentro de la parte del try: . Podemos decir que esta rama se ejecuta después del try: - ya
sea el que comienza con except (no olvides que puede haber más de una rama de este tipo) o la que
comienza con else .
Nota: la rama else: debe ubicarse después de la última rama except .

El código de ejemplo produce el siguiente resultado:

Todo salió bien

0.5

División fallida

None

Más sobre excepciones


El bloque try-except se puede extender de una manera más: agregando una parte encabezada por la
palabra clave reservada finally (debe ser la última rama del código diseñada para manejar
excepciones).

Nota: estas dos variantes ( else y finally ) no son dependientes entre si, y pueden coexistir u ocurrir
de manera independiente.

El bloque finally siempre se ejecuta (finaliza la ejecución del bloque try-except, de ahí su nombre),
sin importar lo que sucedió antes, incluso cuando se genera o lanza una excepción, sin importar si esta se
ha manejado o no.

Mira el código en el editor. Su salida es:

Todo salió bien

Es el momento de decir adiós

0.5

División fallida

Es el momento de decir adiós

None

Las excepciones son clases


Los ejemplos anteriores se centraron en detectar un tipo específico de excepción y responder de manera
apropiada. Ahora vamos a profundizar más y mirar dentro de la excepción misma.

Probablemente no te sorprenderá saber que las excepciones son clases. Además, cuando se genera
una excepción, se crea una instancia de un objeto de la clase y pasa por todos los niveles de ejecución
del programa, buscando la rama "except" que está preparada para tratar con la excepción.

Tal objeto lleva información útil que puede ayudarte a identificar con precisión todos los aspectos de la
situación pendiente. Para lograr ese objetivo, Python ofrece una variante especial de la cláusula de
excepción: puedes encontrarla en el editor.

Como puedes ver, la sentencia except se extendió y contiene una frase adicional que comienza con la
palabra clave reservada as , seguida por un identificador. El identificador está diseñado para capturar la
excepción con el fin de analizar su naturaleza y sacar conclusiones adecuadas.

Nota: el alcance del identificador solo es dentro del except , y no va más allá.
El ejemplo presenta una forma muy simple de utilizar el objeto recibido: simplemente imprímelo (como
puedes ver, la salida es producida por el método del objeto __str__() ) y contiene un breve mensaje
que describe la razón.

Se imprimirá el mismo mensaje si no hay un bloque except en el código, y Python se verá obligado a
manejarlo por si mismo.

Las excepciones son clases


Todas las excepciones integradas de Python forman una jerarquía de clases.

Analiza el código en el editor.

Este programa muestra todas las clases de las excepciónes predefinidas en forma de árbol.

Como un árbol es un ejemplo perfecto de una estructura de datos recursiva, la recursión parece ser
la mejor manera de recorrerlo. La función printExcTree() toma dos argumentos:

 Un punto dentro del árbol desde el cual comenzamos a recorrerlo.


 Un nivel de anidación (lo usaremos para construir un dibujo simplificado de las ramas del árbol).

Comencemos desde la raíz del árbol: la raíz de las clases de excepciónes de Python es la
clase BaseException (es una superclase de todas las demás excepciones).

Para cada una de las clases encontradas, se realiza el mismo conjunto de operaciones:

 Imprimir su nombre, tomado de la propiedad __name__ .


 Iterar a través de la lista de subclases provistas por el método __subclasses__() , e invocar
recursivamente la función printExcTree() , incrementando el nivel de anidación
respectivamente.

Ten en cuenta cómo hemos dibujado las ramas. La impresión no está ordenada de alguna manera: si
deseas un desafío, puedes intentar ordenarla tu mismo. Además, hay algunas imprecisiones sutiles en la
forma en que se presentan algunas ramas. Eso también se puede arreglar, si lo deseas.

Así es como se ve:

BaseException
+---Exception
| +---TypeError
| +---StopAsyncIteration
| +---StopIteration
| +---ImportError
| | +---ModuleNotFoundError
| | +---ZipImportError
| +---OSError
| | +---ConnectionError
| | | +---BrokenPipeError
| | | +---ConnectionAbortedError
| | | +---ConnectionRefusedError
| | | +---ConnectionResetError
| | +---BlockingIOError
| | +---ChildProcessError
| | +---FileExistsError
| | +---FileNotFoundError
| | +---IsADirectoryError
| | +---NotADirectoryError
| | +---InterruptedError
| | +---PermissionError
| | +---ProcessLookupError
| | +---TimeoutError
| | +---UnsupportedOperation
| | +---herror
| | +---gaierror
| | +---timeout
| | +---Error
| | | +---SameFileError
| | +---SpecialFileError
| | +---ExecError
| | +---ReadError
| +---EOFError
| +---RuntimeError
| | +---RecursionError
| | +---NotImplementedError
| | +---_DeadlockError
| | +---BrokenBarrierError
| +---NameError
| | +---UnboundLocalError
| +---AttributeError
| +---SyntaxError
| | +---IndentationError
| | | +---TabError
| +---LookupError
| | +---IndexError
| | +---KeyError
| | +---CodecRegistryError
| +---ValueError
| | +---UnicodeError
| | | +---UnicodeEncodeError
| | | +---UnicodeDecodeError
| | | +---UnicodeTranslateError
| | +---UnsupportedOperation
| +---AssertionError
| +---ArithmeticError
| | +---FloatingPointError
| | +---OverflowError
| | +---ZeroDivisionError
| +---SystemError
| | +---CodecRegistryError
| +---ReferenceError
| +---BufferError
| +---MemoryError
| +---Warning
| | +---UserWarning
| | +---DeprecationWarning
| | +---PendingDeprecationWarning
| | +---SyntaxWarning
| | +---RuntimeWarning
| | +---FutureWarning
| | +---ImportWarning
| | +---UnicodeWarning
| | +---BytesWarning
| | +---ResourceWarning
| +---error
| +---Verbose
| +---Error
| +---TokenError
| +---StopTokenizing
| +---Empty
| +---Full
| +---_OptionError
| +---TclError
| +---SubprocessError
| | +---CalledProcessError
| | +---TimeoutExpired
| +---Error
| | +---NoSectionError
| | +---DuplicateSectionError
| | +---DuplicateOptionError
| | +---NoOptionError
| | +---InterpolationError
| | | +---InterpolationMissingOptionError
| | | +---InterpolationSyntaxError
| | | +---InterpolationDepthError
| | +---ParsingError
| | | +---MissingSectionHeaderError
| +---InvalidConfigType
| +---InvalidConfigSet
| +---InvalidFgBg
| +---InvalidTheme
| +---EndOfBlock
| +---BdbQuit
| +---error
| +---_Stop
| +---PickleError
| | +---PicklingError
| | +---UnpicklingError
| +---_GiveupOnSendfile
| +---error
| +---LZMAError
| +---RegistryError
| +---ErrorDuringImport
+---GeneratorExit
+---SystemExit
+---KeyboardInterrupt

Anatomía detallada de las excepciones


Echemos un vistazo más de cerca al objeto de la excepción, ya que hay algunos elementos realmente
interesantes aquí (volveremos al tema pronto cuando consideremos las técnicas base de entrada y salida
de Python, ya que su subsistema de excepción extiende un poco estos objetos).

La clase BaseException introduce una propiedad llamada args . Es una tupla diseñada para reunir
todos los argumentos pasados al constructor de la clase. Está vacío si la construcción se ha
invocado sin ningún argumento, o solo contiene un elemento cuando el constructor recibe un argumento
(no se considera el argumento self aquí), y así sucesivamente.

Hemos preparado una función simple para imprimir la propiedad args de una manera elegante, puedes
ver la función en el editor.

Hemos utilizado la función para imprimir el contenido de la propiedad args en tres casos diferentes,
donde la excepción de la clase Exception es lanzada de tres maneras distintas. Para hacerlo más
espectacular, también hemos impreso el objeto en sí, junto con el resultado de la
invocación __str__() .

El primer caso parece de rutina, solo hay el nombre Exception despues de la palabra clave
reservada raise . Esto significa que el objeto de esta clase se ha creado de la manera más rutinaria.

El segundo y el tercer caso pueden parecer un poco extraños a primera vista, pero no hay nada extraño,
son solo las invocaciones del constructor. En la segunda sentencia raise , el constructor se invoca con
un argumento, y en el tercero, con dos.

Como puedes ver, la salida del programa refleja esto, mostrando los contenidos apropiados de la
propiedad args :

: :

mi excepción : mi excepción : mi excepción

('mi', 'excepción') : ('mi', 'excepción') : ('mi', 'excepción')

Cómo crear tu propia excepción


La jerarquía de excepciones no está cerrada ni terminada, y siempre puedes ampliarla si deseas o
necesitas crear tu propio mundo poblado con tus propias excepciones.

Puede ser útil cuando se crea un módulo complejo que detecta errores y genera excepciones, y deseas
que las excepciones se distingan fácilmente de cualquier otra de Python.

Esto se puede hacer al definir tus propias excepciones como subclases derivadas de las
predefinidas.

Nota: si deseas crear una excepción que se utilizará como un caso especializado de cualquier excepción
incorporada, derivala solo de esta. Si deseas construir tu propia jerarquía, y no quieres que esté
estrechamente conectada al árbol de excepciones de Python, derivala de cualquiera de las clases de
excepción principales, tal como: Exception.

Imagina que has creado una aritmética completamente nueva, regida por sus propias leyes y teoremas.
Está claro que la división también se ha redefinido, y tiene que comportarse de una manera diferente a la
división de rutina. También está claro que esta nueva división debería plantear su propia excepción,
diferente de la incorporada ZeroDivisionError, pero es razonable suponer que, en algunas
circunstancias, tu (o el usuario de tu aritmética) pueden tratar todas las divisiones entre cero de la misma
manera.

Demandas como estas pueden cumplirse en la forma presentada en el editor. Mira el código y
analicémoslo:

 Hemos definido nuestra propia excepción, llamada MyZeroDivisionError , derivada de la


incorporada ZeroDivisionError . Como puedes ver, hemos decidido no agregar ningún
componente nuevo a la clase.

En efecto, una excepción de esta clase puede ser, dependiendo del punto de vista deseado,
tratada como una simple excepción ZeroDivisionError, o puede ser considerada por
separado.

 La función doTheDivision() lanza una


excepción MyZeroDivisionError o ZeroDivisionError , dependiendo del valor del
argumento.

La función se invoca cuatro veces en total, mientras que las dos primeras invocaciones se
manejan utilizando solo una rama except (la más general), las dos últimas invocan dos ramas
diferentes, capaces de distinguir las excepciones (no lo olvides: el orden de las ramas hace una
diferencia fundamental).

Cómo crear tu propia excepción: continuación


Cuando vas a construir un universo completamente nuevo lleno de criaturas completamente nuevas que
no tienen nada en común con todas las cosas familiares, es posible que desees construir tu propia
estructura de excepciones.

Por ejemplo, si trabajas en un gran sistema de simulación destinado a modelar las actividades de un
restaurante de pizza, puede ser conveniente formar una jerarquía de excepciones por separado.

Puedes comenzar a construirla definiendo una excepción general como una nueva clase base para
cualquier otra excepción especializada. Lo hemos hecho de la siguiente manera:

class PizzaError(Exception):
def __init__(self, pizza, mensaje):
Exception.__init__(mensaje)
self.pizza = pizza

Nota: vamos a recopilar más información específica aquí de lo que recopila una Excepción regular,
entonces nuestro constructor tomará dos argumentos:

 Uno que especifica una pizza como tema del proceso.


 Otro que contiene una descripción más o menos precisa del problema.

Como puedes ver, pasamos el segundo parámetro al constructor de la superclase y guardamos el primero
dentro de nuestra propiedad.

Un problema más específico (como un exceso de queso) puede requerir una excepción más específica.
Es posible derivar la nueva clase de la ya definida PizzaError , como hemos hecho aquí:

class DemasiadoQuesoError(PizzaError):
def __init__(self, pizza, queso, mensaje):
PizzaError._init__(self, pizza, mensaje)
self.queso = queso

La excepción DemasiadoQuesoError necesita más información que la excepción


regular PizzaError , así que lo agregamos al constructor, el nombre queso es entonces almacenado
para su posterior procesamiento.

Cómo crear tu propia excepción: continuación


Mira el código en el editor. Combinamos las dos excepciones previamente definidas y las aprovechamos
para que funcionen en un pequeño ejemplo.

Una de ellas es lanzada dentro de la función hacerPizza() cuando ocurra cualquiera de estas dos
situaciones erróneas: una solicitud de pizza incorrecta o una solicitud de una pizza con demasiado queso.

Nota:

 El remover la rama que comienza con except DemasiadoQuesoError hará que todas las
excepciones que aparecen se clasifiquen como PizzaError .
 El remover la rama que comienza con except PizzaError provocará que la
excepción DemasiadoQuesoError no pueda ser manejada, y hará que el programa finalice.

La solución anterior, aunque elegante y eficiente, tiene una debilidad importante. Debido a la manera algo
fácil de declarar los constructores, las nuevas excepciones no se pueden usar tal cual, sin una lista
completa de los argumentos requeridos.

Eliminaremos esta debilidad estableciendo valores predeterminados para todos los parámetros del
constructor. Observa:

class PizzaError(Exception):
def __init__(self, pizza='desconocida', mensaje=''):
Exception.__init__(self, mensaje)
self.pizza = pizza

class DemasiadoQuesoError(PizzaError):
def __init__(self, pizza='desconocida', queso='>100', mensaje=''):
PizzaError.__init__(self, pizza, mensaje)
self.queso = queso

def hacerPizza(pizza, queso):


if pizza not in ['margherita', 'capricciosa', 'calzone']:
raise PizzaError
if queso > 100:
raise DemasiadoQuesoError
print("¡Pizza lista!")

for (pz, ch) in [('calzone', 0), ('margherita', 110), ('mafia', 20)]:


try:
hacerPizza(pz, ch)
except DemasiadoQuesoError as tmce:
print(tmce, ':', tmce.queso)
except PizzaError as pe:
print(pe, ':', pe.pizza)

Ahora, si las circunstancias lo permiten, es posible usar unicamente los nombres de clase.

Generadores, dónde encontrarlos


Generador - ¿Con qué asocias esta palabra? Quizás se refiere a algún dispositivo electrónico. O tal vez
se refiere a una máquina pesada diseñada para producir energía eléctrica u otra cosa.

Un generador de Python es un fragmento de código especializado capaz de producir una serie de


valores y controlar el proceso de iteración. Esta es la razón por la cual los generadores a menudo se
llaman iteradores, y aunque hay quienes pueden encontrar una diferencia entre estos dos, aquí los
trataremos como uno mismo.

Puede que no te hayas dado cuenta, pero te has topado con generadores muchas, muchas veces antes.
Echa un vistazo al fragmento de código:

for i in range(5):
print(i)

La función range() es un generador, la cual también es un iterador.

¿Cuál es la diferencia?

Una función devuelve un valor bien definido, el cual, puede ser el resultado de una evaluación compleja,
por ejemplo, de un polinomio, y se invoca una vez, solo una vez.

Un generador devuelve una serie de valores, y en general, se invoca (implícitamente) más de una vez.

En el ejemplo, el generador range() se invoca seis veces, proporcionando cinco valores de cero a
cuatro.

El proceso anterior es completamente transparente. Vamos a arrojar algo de luz sobre el. Vamos a
mostrarte el protocolo iterador.

Generadores, dónde encontrarlos: continuación


El protocolo iterador es una forma en que un objeto debe comportarse para ajustarse a las reglas
impuestas por el contexto de las sentencias for e in . Un objeto conforme al protocolo iterador se
llama iterador.

Un iterador debe proporcionar dos métodos:

 __iter__() el cual debe devolver el objeto en sí y que se invoca una vez (es necesario
para que Python inicie con éxito la iteración).
 __next__() el cual debe devolver el siguiente valor (primero, segundo, etc.) de la serie
deseada: será invocado por las sentencias for / in para pasar a la siguiente iteración; si no hay
más valores a proporcionar, el método deberá lanzar la excepción StopIteration .

¿Suena extraño? De ningúna manera. Mira el ejemplo en el editor.

Hemos creado una clase capaz de iterar a través de los primeros n valores (donde n es un parámetro del
constructor) de los números de Fibonacci.

Permítenos recordarte: los números de Fibonacci(Fibi) se definen de la siguiente manera:

Fib1 = 1
Fib2 = 1
Fibi = Fibi-1 + Fibi-2

En otras palabras:

 Los primeros dos números de la serie Fibonacci son 1.


 Cualquier otro número de Fibonacci es la suma de los dos anteriores (por ejemplo, Fib3 = 2,
Fib4 = 3, Fib5 = 5, y así sucesivamente).

Vamos a ver el código:

 Líneas 2 a 6: el constructor de la clase imprime un mensaje (lo usaremos para rastrear el


comportamiento de la clase), se preparan algunas variables: ( __n para almacenar el límite de la
serie, __i para rastrear el número actual de la serie Fibonacci, y __p1 junto con __p2 para
guardar los dos números anteriores).
 Líneas 8 a 10: el método __iter__ está obligado a devolver el objeto iterador en sí mismo; su
propósito puede ser un poco ambiguo aquí, pero no hay misterio; trata de imaginar un objeto que
no sea un iterador (por ejemplo, es una colección de algunas entidades), pero uno de sus
componentes es un iterador capaz de escanear la colección; el
método __iter__ debe extraer el iterador y confiarle la ejecución del protocolo de
iteración; como puedes ver, el método comienza su acción imprimiendo un mensaje.

 Líneas 12 a 21: el método __next__ es responsable de crear la secuencia; es algo largo, pero
esto debería hacerlo más legible; primero, imprime un mensaje, luego actualiza el número de
valores deseados y, si llega al final de la secuencia, el método interrumpe la iteración al generar
la excepción StopIteration; el resto del código es simple y refleja con precisión la definición que
te mostramos anteriormente.

 Las líneas 23 y 24 hacen uso del iterador.

El código produce el siguiente resultado:

__init__
__iter__
__next__
1
__next__
1
__next__
2
__next__
3
__next__
5
__next__
8
__next__
13
__next__
21
__next__
34
__next__
55
__next__

Observa:

 El objeto iterador se instancia primero.


 Después, Python invoca el método __iter__ para acceder al iterador real.
 El método __next__ se invoca once veces: las primeras diez veces produce valores útiles,
mientras que la ultima finaliza la iteración.

Generadores, dónde encontrarlos: continuación


El ejemplo muestra una solución donde el objeto iterador es parte de una clase más compleja.

El código no es sofisticado, pero presenta el concepto de una manera clara.

Echa un vistazo al código en el editor.


Hemos puesto el iterador Fib dentro de otra clase (podemos decir que lo hemos compuesto dentro de la
clase Class ). Se instancia junto con el objeto de Class .

El objeto de la clase se puede usar como un iterador cuando (y solo cuando) responde positivamente a la
invocación __iter__ - esta clase puede hacerlo, y si se invoca de esta manera, proporciona un objeto
capaz de obedecer el protocolo de iteración.

Es por eso que la salida del código es la misma que anteriormente, aunque el objeto de la clase Fib no
se usa explícitamente dentro del contexto del bucle for .

La sentencia yield
El protocolo iterador no es difícil de entender y usar, pero también es indiscutible que el
protocolo es bastante inconveniente.

La principal molestia que tiene es que necesita guardar el estado de la iteración en las
invocaciones subsequentes de __iter__ .

Por ejemplo, el iterador Fib se ve obligado a almacenar con precisión el lugar en el que se
detuvo la última invocación (es decir, el número evaluado y los valores de los dos elementos
anteriores). Esto hace que el código sea más grande y menos comprensible.

Es por eso que Python ofrece una forma mucho más efectiva, conveniente y elegante de
escribir iteradores.

El concepto se basa fundamentalmente en un mecanismo muy específico proporcionado por


la palabra clave reservada yield .

Se puede ver a la palabra clave reservada yield como un hermano más inteligente de la
sentencia return , con una diferencia esencial.

Echa un vistazo a esta función:

def fun(n):

for i in range(n):

return i

Se ve extraño, ¿no? Está claro que el bucle for no tiene posibilidad de terminar su primera
ejecución, ya que el return lo romperá irrevocablemente.

Además, invocar la función no cambiará nada: el bucle for comenzará desde cero y se
romperá inmediatamente.

Podemos decir que dicha función no puede guardar y restaurar su estado en invocaciones
posteriores.

Esto también significa que una función como esta no se puede usar como generador.
Hemos reemplazado exactamente una palabra en el código, ¿puedes verla?

def fun(n):

for i in range(n):

yield i

Hemos puesto yield en lugar de return . Esta pequeña enmienda convierte la función
en un generador, y el ejecutar la sentencia yield tiene algunos efectos muy interesantes.

Primeramente, proporciona el valor de la expresión especificada después de la palabra clave


reservada yield , al igual que return , pero no pierde el estado de la función.

Todos los valores de las variables están congelados y esperan la próxima invocación, cuando
se reanuda la ejecución (no desde cero, como ocurre después de un return ).

Hay una limitación importante: dicha función no debe invocarse explícitamente ya que
no es una función; es un objeto generador.

La invocación devolverá el identificador del objeto, no la serie que esperamos del


generador.

Debido a las mismas razones, la función anterior (la que tiene el return ) solo se puede
invocar explícitamente y no se debe usar como generador.

Cómo construir un generador:


Permítenos mostrarte el nuevo generador en acción.

Así es como podemos usarlo:

def fun(n):

for i in range(n):

yield i

for v in fun(5):

print(v)

¿Puedes adivinar la salida?

Revisar

Cómo construir tu propio generador


¿Qué pasa si necesitas un generador para producir las primeras n potencias de 2 ?

Nada difícil. Solo mira el código en el editor.

¿Puedes adivinar la salida? Ejecuta el código para verificar tus conjeturas.


Los generadores también pueden usarse dentro de listas de comprensión, como aqui:

def potenciasDe2(n):

potencia = 1

for i in range(n):

yield potencia

potencia *= 2

t = [x for x in potenciasDe2(5)]

print(t)

Ejecuta el ejemplo y verifica la salida.

La función list() puede transformar una serie de invocaciones de generador subsequentes en una
lista real:

def potenciasDe2(n):

potencia = 1

for i in range(n):

yield potencia

potencia *= 2

t = list(potenciasDe2(3))

print(t)

Nuevamente, intenta predecir el resultado y ejecuta el código para verificar tus predicciones.

Además, el contexto creado por el operador in también te permite usar un generador.

El ejemplo muestra cómo hacerlo:

def potenciasDe2(n):

potencia= 1
for i in range(n):

yield potencia

potencia*= 2

for i in range(20):

if i in potenciasDe2(4):

print(i)

¿Cuál es la salida del código? Ejecuta el programa y verifica.

Ahora veamos un Generador de números de la serie Fibonacci implementando lo anterior.

Aquí está:

def Fib(n):

p = pp = 1

for i in range(n):

if i in [0, 1]:

yield 1

else:

n = p + pp

pp, p = p, n

yield n

fibs = list(Fib(10))

print(fibs)

Adivina la salida (una lista) producida por el generador y ejecuta el código para verificar si tenías razón.

Más sobre comprensión de listas


Debes poder recordar las reglas que rigen la creación y el uso de un fenómeno de Python
llamado comprensión de listas: una forma simple de crear listas y sus contenidos.

En caso de que lo necesites, te proporcionamos un recordatorio en el editor.

Existen dos partes dentro del código, ambas crean una lista que contiene algunas de las primeras
potencias naturales de diez.
La primer parte utiliza una forma rutinaria del bucle for , mientras que la segunda hace uso de la
comprensión de listas y construye la lista en el momento, sin necesidad de un bucle o cualquier otro
código.

Pareciera que la lista se crea dentro de sí misma; esto es falso, ya que Python tiene que realizar casi las
mismas operaciones que en la primera parte, pero el segundo formalismo es simplemente más elegante y
le evita al lector cualquier detalle innecesario.

El ejemplo genera dos líneas idénticas que contienen el siguiente texto:

[1, 10, 100, 1000, 10000, 100000]

Ejecuta el código para verificar si tenemos razón.

Más sobre comprensión de listas: continuación


Hay una sintaxis muy interesante que queremos mostrarte ahora. Su usabilidad no se limita a la
comprensión de listas.

Es una expresión condicional: una forma de seleccionar uno de dos valores diferentes en función
del resultado de una expresión booleana.

Observa :

expresión_uno if condición else expresión_dos

Puede parecer un poco sorprendente a primera vista, pero hay que tener en cuenta que no es una
instrucción condicional. Además, no es una instrucción en lo absoluto. Es un operador.

El valor que proporciona es expresión_uno cuando la condición es True (verdadero),


y expresión_dos cuando sea falso.

Un buen ejemplo te dirá más. Mira el código en el editor.

El código llena una lista con unos y ceros , si el índice de un elemento particular es impar, el elemento
se establece en 0 , y a 1 de lo contrario.

¿Simple? Quizás no a primera vista. ¿Elegante? Indiscutiblemente.

¿Se puede usar el mismo truco dentro de una lista de comprensión? Sí, por supuesto.

Más sobre comprensión de listas: continuación


Mira el ejemplo en el editor.

Compacidad y elegancia: estas dos palabras vienen a la mente al mirar el código.

Entonces, ¿qué tienen en común, generadores y listas de comprensión? ¿Hay alguna conexión entre
ellos? Sí. Una conexión algo suelta, pero inequívoca.

Solo un cambio puede convertir cualquier comprensión en un generador.


Ahora mira el código a continuación y ve si puedes encontrar el detalle que convierte una comprensión de
la lista en un generador:

lst = [1 if x % 2 == 0 else 0 for x in range(10)]

genr = (1 if x % 2 == 0 else 0 for x in range(10))

for v in lst:

print(v, end=" ")

print()

for v in genr:

print(v, end=" ")

print()

Son los paréntesis. Los corchetes hacen una comprensión, los paréntesis hacen un generador.

El código, cuando se ejecuta, produce dos líneas idénticas:

1 0 1 0 1 0 1 0 1 0

1 0 1 0 1 0 1 0 1 0

¿Cómo puedes saber que la segunda asignación crea un generador, no una lista?

Hay algunas pruebas que podemos mostrarte. Aplica la función len() a ambas entidades.

len(lst) dará como resultado 10 , claro y predecible, len(genr) provocará una excepción y verás
el siguiente mensaje:

TypeError: object of type 'generator' has no len()

Por supuesto, guardar la lista o el generador no es necesario; puedes crearlos exactamente en el lugar
donde los necesites, como aquí:

for v in [1 if x % 2 == 0 else 0 for x in range(10)]:

print(v, end=" ")

print()

for v in (1 if x % 2 == 0 else 0 for x in range(10)):

print(v, end=" ")

print()
Nota: la misma apariencia de la salida no significa que ambos bucles funcionen de la misma manera. En
el primer bucle, la lista se crea (y se itera) como un todo; en realidad, existe cuando se ejecuta el bucle.

En el segundo bucle, no hay ninguna lista, solo hay valores subsequentes producidos por el generador,
uno por uno.

Realiza tus propios experimentos.

La función lambda

La función lambda es un concepto tomado de las matemáticas, más específicamente, de


una parte llamada el cálculo Lambda, pero estos dos fenómenos no son iguales.

Los matemáticos usan el cálculo Lambda en sistemas formales conectados con: la lógica, la
recursividad o la demostrabilidad de teoremas. Los programadores usan la
función lambda para simplificar el código, hacerlo más claro y fácil de entender.

Una función lambda es una función sin nombre (también puedes llamarla una función
anónima). Por supuesto, tal afirmación plantea inmediatamente la pregunta: ¿cómo se usa
algo que no se puede identificar?

Afortunadamente, no es un problema, ya que se puede mandar llamar dicha función si


realmente se necesita, pero, en muchos casos la función lambda puede existir y funcionar
mientras permanece completamente de incógnito.

La declaración de la función lambda no se parece a una declaración de función normal;


compruébalo tu mismo:

lambda parámetros: expresión

Tal cláusula devuelve el valor de la expresión al tomar en cuenta el valor del


argumento lambda actual.

Como de costumbre, un ejemplo será útil. Nuestro ejemplo usa tres funciones lambda , pero
con nombres. Analizalo cuidadosamente:

dos = lambda : 2
cuadrado = lambda x : x * x
potencia = lambda x, y : x ** y

for a in range(-2, 3):


print(cuadrado(a), end=" ")
print(potencia(a, dos()))

Vamos a analizarlo:

 La primer lambda es una función anónima sin parametros que siempre devuelve
un 2 . Como se ha asignado a una variable llamada dos , podemos decir que la
función ya no es anónima, y se puede usar su nombre para invocarla.

 La segunda es una función anónima de un parámetro que devuelve el valor de su


argumento al cuadrado. Se ha nombrado también como tal.
 La tercer lambda toma dos parametros y devuelve el valor del primero elevado al
segundo. El nombre de la variable que lleva la lambda habla por si mismo.

El programa produce el siguiente resultado:

4 4
1 1
0 0
1 1
4 4

Este ejemplo es lo suficientemente claro como para mostrar cómo se declaran las
funciones lambda y cómo se comportan, pero no dice nada acerca de por qué son
necesarias y para qué se usan, ya que se pueden reemplazar con funciones de Python de
rutina.

¿Dónde está el beneficio?

¿Cómo usar lambdas y para qué?


La parte más interesante de usar lambdas aparece cuando puedes usarlas en su forma pura - como
partes anónimas de código destinadas a evaluar un resultado.

Imagina que necesitamos una función (la nombraremos imprimirfuncion ) que imprime los valores
de una (otra) función dada para un conjunto de argumentos seleccionados.

Queremos que imprimirfuncion sea universal - debería aceptar un conjunto de argumentos


incluidos en una lista y una función a ser evaluada, ambos como argumentos; no queremos codificar
nada.

Mira el ejemplo en el editor. Así es como hemos implementado la idea.

Analicémoslo. La función imprimirfuncion() toma dos parámetros:

 El primero, una lista de argumentos para los que queremos imprimir los resultados.
 El segundo, una función que debe invocarse tantas veces como el número de valores que se
recopilan dentro del primer parámetro.

Nota: También hemos definido una función llamada poli() - esta es la función cuyos valores vamos a
imprimir. El cálculo que realiza la función no es muy sofisticado: es el polinomio (de ahí su nombre) de la
forma:

f(x) = 2x2 - 4x + 2

El nombre de la función se pasa a imprimirfuncion() junto con un conjunto de cinco argumentos


diferentes: el conjunto está construido con una cláusula de comprensión de la lista.

El código imprime las siguientes líneas:

f(-2)=18
f(-1)=8
f(0)=2
f(1)=0
f(2)=2

¿Podemos evitar definir la función poli() , ya que no la vamos a usar más de una vez? Sí, podemos:
este es el beneficio que puede aportar una función lambda.

Mira el ejemplo de abajo. ¿Puedes ver la diferencia?

def imprimirfuncion(args, fun):


for x in args:
print('f(', x,')=', fun(x), sep='')

imprimirfuncion([x for x in range(-2, 3)], lambda x: 2 * x**2 - 4 * x + 2)

La función imprimirfuncion() se ha mantenido exactamente igual, pero no hay una


función poli() . Ya no la necesitamos, ya que el polinomio ahora está directamente dentro de la
invocación de la función imprimirfuncion() en forma de una lambda definida de la siguiente
manera: lambda x: 2 * x**2 - 4 * x + 2 .

El código se ha vuelto más corto, más claro y más legible.

Permítenos mostrarte otro lugar donde las lambdas pueden ser útiles. Comenzaremos con una
descripción de map() , una función de Python incorporada. Su nombre no es demasiado descriptivo, su
idea es simple y la función en sí es muy utilizable.

Lambdas y la función map()

En el más simple de todos los casos posibles, la función map() toma dos argumentos:

 Una función.
 Una lista.
map(función, lista)

La descripción anterior está extremadamente simplificada, ya que:

 El segundo argumento map() puede ser cualquier entidad que se pueda iterar (por ejemplo,
una tupla o un generador).
 map() puede aceptar más de dos argumentos.

La función map() aplica la función pasada por su primer argumento a todos los elementos de su
segundo argumento y devuelve un iterador que entrega todos los resultados de funciones
posteriores. Puedes usar el iterador resultante en un bucle o convertirlo en una lista usando la
función list() .

¿Puedes ver un papel para una lambda aquí?

Observa el código en el editor: hemos usado dos lambdas en él.

Esta es la explicación:

 Se construye la lista1 con valores del 0 al 4 .


 Después, se utiliza map junto con la primer lambda para crear una nueva lista en la que todos
los elementos han sido evaluados como 2 elevado a la potencia tomada del elemento
correspondiente de lista1 .
 lista2 es entonces impresa.
 En el siguiente paso, se usa nuevamente la función map() para hacer uso del generador que
devuelve, e imprimir directamente todos los valores que entrega; como puedes ver, hemos usado
el segundo lambda aquí - solo eleva al cuadrado cada elemento de lista2 .

Intenta imaginar el mismo código sin lambdas. ¿Sería mejor? Es improbable.

Lambdas y la función filter()


Otra función de Python que se puede embellecer significativamente mediante la aplicación de una lambda
es filter() .

Espera el mismo tipo de argumentos que map() , pero hace algo diferente - filtra su segundo
argumento mientras es guiado por direcciones que fluyen desde la función especificada en el
primer argumento (la función se invoca para cada elemento de la lista, al igual que en map() ).

Los elementos que regresan True de la función pasan el filtro - los otros son rechazados.

El ejemplo en el editor muestra la función filter() en acción.

Nota: hemos hecho uso del módulo random para inicializar el generador de números aleatorios (que no
debe confundirse con los generadores de los que acabamos de hablar) con la función seed() , para
producir cinco valores enteros aleatorios de -10 a 10 usando la función randint() .

Luego se filtra la lista y solo se aceptan los números que son pares y mayores que cero.

Por supuesto, no es probable que recibas los mismos resultados, pero así es como se veían nuestros
resultados:

[6, 3, 3, 2, -7]

[6, 2]

Una breve explicación de cierres


Comencemos con una definición: cierres es una técnica que permite almacenar valores a pesar de
que el contexto en el que se crearon ya no existe.. ¿Complicado? Un poco.

Analicemos un ejemplo simple:

def exterior(par):
loc = par

var = 1
exterior(var)

print(var)
print(loc)

El ejemplo es obviamente erróneo.


Las dos últimas líneas provocarán una excepción NameError - ni par ni loc son accesibles fuera de la
función. Ambas variables existen cuando y solo cuando la función exterior() esta siendo ejecutada.

Mira el ejemplo en el editor. Hemos modificado el código significativamente.

Hay un elemento completamente nuevo - una función (llamada interior ) dentro de otra función
(llamada exterior ).

¿Como funciona? Como cualquier otra función excepto por el hecho de que interior() solo se puede
invocar desde dentro de exterior() . Podemos decir que interior() es una herramienta privada
de exterior() , ninguna otra parte del código la puede acceder.

Observa cuidadosamente:

 La función interior() devuelve el valor de la variable accesible dentro de su alcance, ya


que interior() puede utilizar cualquiera de las entidades a disposición de exterior() .
 La función exterior() devuelve la función interior() por si misma; mejor dicho,
devuelve una copia de la función interior() al momento de la invocación de la
función exterior() ; la función congelada contiene su entorno completo, incluido el estado de
todas las variables locales, lo que también significa que el valor de loc se retiene con éxito,
aunque exterior() ya ha dejado de existir.

En efecto, el código es totalmente válido y genera:

La función devuelta durante la invocación de exterior() es un cierre.

Una breve explicación de cierres: continuación


Un cierre se debe invocar exactamente de la misma manera en que se ha declarado.

En el ejemplo anterior (vea el código a continuación):

def exterior(par):
loc = par
def interior():
return loc
return interior

var = 1
fun = exterior(var)
print(fun()))

La función interior() no tenía parámetros, por lo que tuvimos que invocarla sin argumentos.

Ahora mira el código en el editor. Es totalmente posible declarar un cierre equipado con un número
arbitrario de parámetros, por ejemplo, al igual que la función potencia() .

Esto significa que el cierre no solo utiliza el ambiente congelado, sino que también puede modificar su
comportamiento utilizando valores tomados del exterior.
Este ejemplo muestra una circunstancia más interesante: puedes crear tantos cierres como quieras
usando el mismo código. Esto se hace con una función llamada crearcierre() . Nota:

 El primer cierre obtenido de crearcierre() define una herramienta que eleva al cuadrado
su argumento.
 El segundo está diseñado para elevar el argumento al cubo.

Es por eso que el código produce el siguiente resultado:

0 0 0
1 1 1
2 4 8
3 9 27
4 16 64

Realiza tus propias pruebas.

Accediendo a archivos desde el código en Python


Uno de los problemas más comunes en el trabajo del desarrollador es procesar datos
almacenados en archivos que generalmente se almacenan físicamente utilizando
dispositivos de almacenamiento: discos duros, ópticos, de red o de estado sólido.

Es fácil imaginar un programa que clasifique 20 números, y es igualmente fácil imaginar que
el usuario de este programa ingrese estos veinte números directamente desde el teclado.

Es mucho más difícil imaginar la misma tarea cuando hay 20,000 números para ordenar, y no
existe un solo usuario que pueda ingresar estos números sin cometer un error.

Es mucho más fácil imaginar que estos números se almacenan en el archivo que lee el
programa. El programa clasifica los números y no los envía a la pantalla, sino que crea un
nuevo archivo y guarda la secuencia ordenada de números allí.

Si queremos implementar una base de datos simple, la única forma de almacenar la


información entre ejecuciones del programa es guardarla en un archivo (o archivos si tu base
de datos es más compleja).

Es un principio que cualquier problema de programación no simple se basa en el uso de


archivos, ya sea que procese imágenes (almacenadas en archivos), multiplique matrices
(almacenadas en archivos) o calcule salarios e impuestos (lectura de datos almacenados en
archivos).
Puedes preguntarte por qué hemos esperado hasta ahora para mostrarte esto.

La respuesta es muy simple: la forma en que Python accede y procesa los archivos se
implementa utilizando un conjunto consistente de objetos. No hay mejor momento para
hablar de esto.

Nombres de archivos
Los diferentes sistemas operativos pueden tratar a los archivos de diferentes maneras. Por
ejemplo, Windows usa una convención de nomenclatura diferente a la adoptada en los
sistemas Unix/Linux.

Si utilizamos la noción de un nombre de archivo canónico (un nombre que define de forma
exclusiva la ubicación del archivo, independientemente de su nivel en el árbol de directorios),
podemos darnos cuenta de que estos nombres se ven diferentes en Windows y en Unix/Linux:

Como puedes ver, los sistemas derivados de Unix/Linux no usan la letra de la unidad de disco
(p. Ejemplo, C: ) y todos los directorios crecen desde un directorio raíz llamado / , mientras
que los sistemas Windows reconocen el directorio raíz como \ .
Además, los nombres de archivo de sistemas Unix/Linux distinguen entre mayúsculas y
minúsculas. Los sistemas Windows almacenan mayúsculas y minúsculas en el nombre del
archivo, pero no distinguen entre ellas.

Esto significa que estas dos cadenas:

EsteEsElNombreDelArchivo
y
esteeselnombredelarchivo

describen dos archivos diferentes en sistemas Unix/Linux, pero tienen el mismo nombre para
un solo archivo en sistemas Windows.

La diferencia principal y más llamativa es que debes usar dos separadores diferentes
para los nombres de directorio: \ en Windows y / en Unix/Linux.

Esta diferencia no es muy importante para el usuario normal, pero es muy importante al
escribir programas en Python.

Para entender por qué, intenta recordar el papel muy específico que desempeña \ dentro de
las cadenas en Python.

Nombres de Archivo: continuación


Supongamos que estás interesado en un archivo en particular ubicado en el directorio dir, y
con el nombre de archivo.

Supongamos también que deseas asignar a una cadena el nombre del archivo.

En sistemas Unix/Linux, se ve de la siguiente manera:

nombre = "/dir/archivo"

Pero si intentas codificarlo para el sistema Windows:

nombre = "\dir\archivo"

obtendrás una sorpresa desagradable: Python generará un error o la ejecución del programa
se comportará de manera extraña, como si el nombre del archivo se hubiera distorsionado de
alguna manera.

De hecho, no es extraño en lo absoluto, pero es bastante obvio y natural. Python usa


la \ como un caracter de escape (como \n ).

Esto significa que los nombres de archivo de Windows deben escribirse de la siguiente
manera:

nombre = "\\dir\\archivo"

Afortunadamente, también hay una solución más. Python es lo suficientemente inteligente


como para poder convertir diagonales en diagonales invertidas cada vez que descubre que el
sistema operativo lo requiere.

Esto significa que cualquiera de las siguientes asignaciones:


nombre = "/dir/archivo"

nombre = "c:/dir/archivo"

funcionará también con Windows.

Cualquier programa escrito en Python (y no solo en Python, porque esa convención se aplica
a prácticamente todos los lenguajes de programación) no se comunica con los archivos
directamente, sino a través de algunas entidades abstractas que se nombran de manera
diferente en los distintos lenguajes o entornos, los términos más utilizados son handles (un
tipo de puntero inteligente) o streams (una especie de canal) (los usaremos como
sinónimos aquí).

El programador, que tiene un conjunto de funciones y métodos, puede realizar ciertas


operaciones en el stream, que afectan los archivos reales utilizando mecanismos contenidos
en el núcleo del sistema operativo.

De esta forma, puedes implementar el proceso de acceso a cualquier archivo, incluso cuando
el nombre del archivo es desconocido al momento de escribir el programa.
Las operaciones realizadas con el stream abstracto reflejan las actividades relacionadas con
el archivo físico.

Para conectar (vincular) el stream con el archivo, es necesario realizar una operación
explícita.

La operación de conectar un stream con un archivo es llamada abrir el archivo, mientras


que desconectar el enlace se denomina cerrar el archivo.

Por lo tanto, la conclusión es que la primera operación realizada en el stream es


siempre open (abrir) y la ultima es close (cerrar) . El programa, en efecto, es libre
de manipular el stream entre estos dos eventos y manejar el archivo asociado.

Esta libertad está limitada por las características físicas del archivo y la forma en que se abrió
el archivo.

Digamos nuevamente que la apertura del stream puede fallar, y puede ocurrir debido a
varias razones: la más común es la falta de un archivo con un nombre específico.
También puede suceder que el archivo físico exista, pero el programa no puede abrirlo.
También existe el riesgo de que el programa haya abierto demasiados streams, y el sistema
operativo específico puede no permitir la apertura simultánea de más de n archivos (por
ejemplo, 200).

Un programa bien escrito debe detectar estas aperturas fallidas y reaccionar en


consecuencia.

Streams para Archivos


La apertura del stream no solo está asociada con el archivo, sino que también se debe
declarar la manera en que se procesará el stream. Esta declaración se llama un open mode
(modo abierto).

Si la apertura es exitosa, el programa solo podrá realizar las operaciones que sean
consistentes con el modo abierto declarado.

Hay dos operaciones básicas a realizar con el stream:

 Lectura del stream: las porciones de los datos se recuperan del archivo y se colocan
en un área de memoria administrada por el programa (por ejemplo, una variable).
 Escritura del stream: Las porciones de los datos de la memoria (por ejemplo, una
variable) se transfieren al archivo.

Hay tres modos básicos utilizados para abrir un stream:

 Modo Lectura: un stream abierto en este modo permite solo operaciones de


lectura; intentar escribir en la transmisión provocará una excepción (la excepción se
llama UnsupportedOperation, la cual hereda el OSError y el ValueError, y proviene
del módulo io).
 Modo Escritura: un stream abierto en este modo permite solo operaciones de
escritura; intentar leer el stream provocará la excepción mencionada anteriormente.
 Modo Actualizar: un stream abierto en este modo permite tanto lectura como
escritura.

Antes de discutir cómo manipular los streams, te debemos una explicación. El stream se
comporta casi como una grabadora.

Cuando lees algo de un stream, un cabezal virtual se mueve sobre la transmisión de acuerdo
con el número de bytes transferidos desde el stream.

Cuando escribes algo en el stream el mismo cabezal se mueve a lo largo del stream
registrando los datos de la memoria.

Siempre que hablemos de leer y escribir en el stream, trata de imaginar esta analogía. Los
libros de programación se refieren a este mecanismo como la posición actual del archivo,
aquí también usaremos este término.
Ahora es necesario mostrarte el objeto responsable de representar los streams en los
programas.

Manejo de Archivos
Python supone que cada archivo está oculto detrás de un objeto de una clase
adecuada.

Por supuesto, es difícil no preguntar cómo interpretar la palabra adecuada.

Los archivos se pueden procesar de muchas maneras diferentes: algunos dependen del
contenido del archivo, otros de las intenciones del programador.

En cualquier caso, diferentes archivos pueden requerir diferentes conjuntos de operaciones y


comportarse de diferentes maneras.

Un objeto de una clase adecuada es creado cuando abres el archivo y lo aniquilas al


momento de cerrarlo.

Entre estos dos eventos, puedes usar el objeto para especificar qué operaciones se deben
realizar en un stream en particular. Las operaciones que puedes usar están impuestas por la
forma en que abriste el archivo.

En general, el objeto proviene de una de las clases que se muestran aquí:


Nota: nunca se utiliza el constructor para dar vida a estos objetos. La unica forma
de obtenerlos es invocar la función llamada open() .

La función analiza los argumentos proporcionados y crea automáticamente el objeto


requerido.

Si deseas deshacerte del objeto, invoca el método denominado close() .

La invocación cortará la conexión con el objeto y el archivo, y eliminará el objeto.

Para nuestros propósitos, solo nos ocuparemos de los streams representados por los
objetos BufferIOBase y TextIOBase . Entenderás por qué pronto.

Manejo de Archivos: continuación


Debido al tipo de contenido de los streams, todos se dividen en tipo texto y binario.

Los streams de texto están estructurados en líneas; es decir, contienen caracteres


tipográficos (letras, dígitos, signos de puntuación, etc.) dispuestos en filas (líneas), como se
ve a simple vista cuando se mira el contenido del archivo en el editor.

Este tipo de archivo es escrito (o leído) principalmente carácter por carácter, o línea por
línea.

Los streams binarios no contienen texto, sino una secuencia de bytes de cualquier valor. Esta
secuencia puede ser, por ejemplo, un programa ejecutable, una imagen, un audio o un
videoclip, un archivo de base de datos, etc.

Debido a que estos archivos no contienen líneas, las lecturas y escrituras se relacionan con
porciones de datos de cualquier tamaño. Por lo tanto, los datos se leen y escriben byte a
byte, o bloque a bloque, donde el tamaño del bloque generalmente varía de uno a un valor
elegido arbitrariamente.

Ahora viene un problema pequeño. En los sistemas Unix/Linux, los extremos de la línea están
marcados por un solo carácter llamado LF (código ASCII 10) designado en los programas de
Python como \n .

Otros sistemas operativos, especialmente los derivados del sistema prehistórico CP/M (que
también aplica a los sistemas de la familia Windows) utilizan una convención diferente: el
final de la línea está marcada por un par de caracteres, CR y LF (códigos ASCII 13 y 10) los
cuales se puede codificar como \r\n .
Esta ambigüedad puede causar varias consecuencias desagradables.

Si creas un programa responsable de procesar un archivo de texto y está escrito para


Windows, puedes reconocer los extremos de las líneas al encontrar los caracteres \r\n , pero
si el mismo programa se ejecuta en un entorno Unix/Linux será completamente inútil, y
viceversa: el programa escrito para sistemas Unix/Linux podría ser inútil en Windows.

Estas características indeseables del programa, que impiden o dificultan el uso del programa
en diferentes entornos, se denomina falta de portabilidad.

Del mismo modo, el rasgo del programa que permite la ejecución en diferentes entornos se
llama portabilidad. Un programa dotado de tal rasgo se llama programa portable.

Manejo de archivos: continuación


Dado que los problemas de portabilidad eran (y siguen siendo) muy graves, se tomó la
decisión de resolver definitivamente el problema de una manera que no atraiga mucho la
atención del desarrollador.

Se realizó a nivel de clases, que son responsables de leer y escribir caracteres hacia y desde
el stream. Funciona de la siguiente manera:
 Cuando el stream está abierto y se recomienda que los datos en el archivo asociado
se procesen como texto (o no existe tal aviso), se cambia al modo texto.

 Durante la lectura y escritura de líneas desde y hacia el archivo asociado, no ocurre


nada especial en el entorno Unix, pero cuando se realizan las mismas operaciones en
el entorno Windows, un proceso llamado traducción de caracteres de nueva
línea ocurre: cuando lees una línea del archivo, cada par de caracteres \r\n se
reemplaza con un solo caracter \n , y viceversa; durante las operaciones de
escritura, cada caracter \n se reemplaza con un par de caracteres \r\n .

 El mecanismo es completamente transparente para el programa, el cual puede


escribirse como si estuviera destinado a procesar archivos de texto Unix/Linux
solamente; el código fuente ejecutado en un entorno Windows también funcionará
correctamente.

 Cuando el stream está abierto, su contenido se toma tal cual es, sin ninguna
conversión - no se agregan, ni se omiten bytes.

Abriendo los streams


El abrir un stream se realiza mediante una función que se puede invocar de la siguiente
manera:

stream = open(file, mode = 'r', encoding = None)

Vamos a analizarlo:
 El nombre de la función ( open ) habla por si mismo; si la apertura es exitosa, la
función devuelve un objeto stream; de lo contrario, se genera una excepción (por
ejemplo, FileNotFoundError si el archivo que vas a leer no existe).
 El primer parámetro de la función ( file ) especifica el nombre del archivo que se
asociará al stream.
 El segundo parámetro ( mode ) especifica el modo de apertura utilizado para el
stream; es una cadena llena de una secuencia de caracteres, y cada uno de ellos
tiene su propio significado especial (más detalles pronto).
 El tercer parámetro ( encoding ) especifica el tipo de codificación (por ejemplo, UTF-8
cuando se trabaja con archivos de texto).
 La apertura debe ser la primera operación realizada en el stream.

Nota: el modo y los argumentos de codificación pueden omitirse; en dado caso, se tomarán
sus valores predeterminados. El modo de apertura predeterminado es leer en modo de texto,
mientras que la codificación predeterminada depende de la plataforma utilizada.

Permítenos ahora presentarte los modos de apertura más importantes y útiles. ¿Listo?

Abriendo los streams: modos

Modo de apertura r : lectura

 El stream será abierto en modo lectura.


 El archivo asociado con el stream debe existir y tiene que ser legible, de lo contrario
la función open() lanzará una excepción.

Modo de apertura w : escritura

 El stream será abierto en modo escritura.


 El archivo asociado con el stream no necesita existir. Si no existe, se creará; si
existe, se truncará a la longitud de cero (se borrá); si la creación no es posible (por
ejemplo, debido a los permisos del sistema) la función open() lanzará una
excepción.

Modo de apertura a : adjuntar

 El stream será abierto en modo adjuntar.


 El archivo asociado con el stream no necesita existir; si no existe, se creará; si
existe, el cabezal de grabación virtual se establecerá al final del archivo (el contenido
anterior del archivo permanece intacto).

Modo de apertura r+ : leer y actualizar

 El stream será abierto en modo leer y actualizar.


 El archivo asociado con el stream debe existir y tiene que ser escribible, de lo
contrario la función open() lanzará una excepción.
 Se permiten operaciones de lectura y escritura en el stream.

Modo de apertura w+ : escribir y actualizar

 El stream será abierto en modo escribir y actualizar.


 El archivo asociado con el stream no necesita existir; si no existe, se creará; el
contenido anterior del archivo permanece intacto.
 Se permiten operaciones de lectura y escritura en el stream.
Seleccionando modo de texto y modo binario

Si hay una letra b al final de la cadena del modo significa que el stream se debe abrir en
el modo binario.

Si la cadena del modo termina con una letra t el stream es abierto en modo texto.

El modo texto es el comportamiento predeterminado que se utiliza cuando no se especifica


ya sea modo binario o texto.

Finalmente, la apertura exitosa del archivo establecerá la posición actual del archivo (el
cabezal virtual de lectura/escritura) antes del primer byte del archivo si el modo no es a y
después del último byte del archivo si el modo es a .

Modo texto Modo binario Descripción


rt rb lectura
wt wb escritura
at ab adjuntar
r+t r+b leer y actualizar
w+t w+b escribir y actualizar

EXTRA

También puedes abrir un archivo para su creación exclusiva. Puedes hacer esto usando el
modo de apertura x . Si el archivo ya existe, la función open() lanzará una excepción.

Abriendo el stream por primera vez


Imagina que queremos desarrollar un programa que lea el contenido del archivo de texto
llamado: C:\Users\User\Desktop\file.txt.

¿Cómo abrir ese archivo para leerlo? Aquí está el fragmento del código:

try:

stream = open("C:\Users\User\Desktop\file.txt", "rt")

# aqui se procesa el archivo

stream.close()

except Exception as exc:

print("No se puede abrir el archivo:", exc)

¿Que está pasando aqui?


 Hemos abierto el bloque try-except ya que queremos manejar los errores de tiempo
de ejecución suavemente.
 Se emplea la función open() para intentar abrir el archivo especificado (ten en
cuenta la forma en que hemos especificado el nombre del archivo).
 El modo de apertura se define como texto para leer (como texto es la
configuración predeterminada, podemos omitir la t en la cadena de modo).
 En caso de éxito obtenemos un objeto de la función open() y lo asignamos a la
variable del stream.
 Si open() falla, manejamos la excepción imprimiendo la información completa del
error (es bueno saber qué sucedió exactamente).
Streams pre-abiertos
Dijimos anteriormente que cualquier operación del stream debe estar precedida por la
invocación de la función open() . Hay tres excepciones bien definidas a esta regla.

Cuando comienza nuestro programa, los tres streams ya están abiertos y no requieren
ninguna preparación adicional. Además, tu programa puede usar estos streams
explícitamente si tienes cuidado de importar el módulo sys :

import sys

Porque ahí es donde se coloca la declaración de estos streams.

Los nombres de los streams son: sys.stdin , sys.stdout y sys.stderr .

Vamos a analizarlos:

 sys.stdin
o stdin (significa entrada estándar).
o El stream stdin normalmente se asocia con el teclado, se abre previamente
para la lectura y se considera como la fuente de datos principal para los
programas en ejecución.
o La función bien conocida input() lee datos de stdin por default.

 sys.stdout
o stdout (significa salida estándar).
o El stream stdout normalmente está asociado con la pantalla, preabierta
para escritura, considerada como el objetivo principal para la salida de datos
por el programa en ejecución.
o La función bien conocida print() envía los datos al stream stdout .

 sys.stderr
o stderr (significa salida de error estándar).
o El stream stderr normalmente está asociado con la pantalla, preabierta
para escribir, considerada como el lugar principal donde el programa en
ejecución debe enviar información sobre los errores encontrados durante su
trabajo.
o No hemos presentado ningún método para enviar datos a este stream (lo
haremos pronto, lo prometemos).
o La separación de stdout (resultados útiles producidos por el programa)
de stderr (mensajes de error, indudablemente útiles pero no proporcionan
resultados) ofrece la posibilidad de redirigir estos dos tipos de información a
los diferentes objetivos. Una discusión más extensa sobre este tema está
más allá del alcance de nuestro curso. El manual del sistema operativo
proporcionará más información sobre estos temas.

Cerrando streams

La última operación realizada en un stream (esto no incluye a los streams stdin , stdout ,
y stderr pues no lo requieren) debe ser cerrarlo.

Esa acción se realiza mediante un método invocado desde dentro del objeto del
stream: stream.close() .

 El nombre de la función es fácil de entender close() , es decir cerrar.


 La función no espera argumentos; el stream no necesita estar abierto.
 La función no devuelve nada pero lanza una excepción IOError en caso de un error.
 La mayoría de los desarrolladores creen que la función close() siempre tiene éxito
y, por lo tanto, no hay necesidad de verificar si ha realizado su tarea correctamente.

Esta creencia está solo parcialmente justificada. Si el stream se abrió para escribir y
luego se realizó una serie de operaciones de escritura, puede ocurrir que los datos
enviados al stream aún no se hayan transferido al dispositivo físico (debido a los
mecanismos de cache o buffer). Dado que el cierre del stream obliga a los búferes a
descargarse, es posible que dichas descargas fallen y, por lo tanto, close() falle
también.

Ya hemos mencionado fallas causadas por funciones que operan con los streams, pero no
mencionamos nada sobre cómo podemos identificar exactamente la causa de la falla.

La posibilidad de hacer un diagnóstico existe y es proporcionada por uno de los componentes


de excepción de los streams.

Diagnosticando problemas con los streams

El objeto IOError está equipado con una propiedad llamada errno (el nombre viene de la
frase error number, número de error) y puedes accederla de la siguiente manera:

try:

# operaciones con streams

except IOError as exc:

print(exc.errno)

El valor del atributo errno se puede comparar con una de las constantes simbólicas
predefinidas en módulo errno .
Echemos un vistazo a algunas constantes seleccionadas útiles para detectar errores
de flujo:

errno.EACCES → Permiso denegado

El error se produce cuando intentas, por ejemplo, abrir un archivo con atributos de solo
lectura para abrirlo.

errno.EBADF → Número de archivo incorrecto

El error se produce cuando intentas, por ejemplo, operar un stream sin abrirlo.

errno.EEXIST → Archivo existente

El error se produce cuando intentas, por ejemplo, cambiar el nombre de un archivo con su
nombre anterior.

errno.EFBIG → Archivo demasiado grande

El error ocurre cuando intentas crear un archivo que es más grande que el máximo permitido
por el sistema operativo.

errno.EISDIR → Es un directorio

El error se produce cuando intentas tratar un nombre de directorio como el nombre de un


archivo ordinario.

errno.EMFILE → Demasiados archivos abiertos

El error se produce cuando intentas abrir simultáneamente más streams de los aceptables
para el sistema operativo.

errno.ENOENT → El archivo o directorio no existe

El error se produce cuando intentas acceder a un archivo o directorio inexistente.


errno.ENOSPC → no queda espacio en el dispositivo

El error ocurre cuando no hay espacio libre en el dispositivo.

La lista completa es mucho más larga (incluye también algunos códigos de error no
relacionados con el procesamiento del stream).

Diagnosticando problemas con los streams: continuación


Si eres un programador muy cuidadoso, puedes sentir la necesidad de usar una secuencia de
sentencias similar a la que se presenta a continuación:

import errno

try:

s = open("c:/users/user/Desktop/file.txt", "rt")

# el procesamiento va aquí

s.close()

except Exception as exc:

if exc.errno == errno.ENOENT:

print("El archivo no existe.")

elif exc.errno == errno.EMFILE:

print("Has abierto demasiados archivos.")

else:

printf("El número de error es:", exc.errno)

Afortunadamente, existe una función que puede simplificar el código de manejo de


errores. Su nombre es strerror() , y proviene del módulo os y espera solo un
argumento: un número de error.

Su función es simple: proporciona un número de error y una cadena que describe el


significado del error.

Nota: Si pasas un código de error inexistente (un número que no está vinculado a ningún
error real), la función lanzará una excepción ValueError.
Ahora podemos simplificar nuestro código de la siguiente manera:

from os import strerror

try:

s = open("c:/users/user/Desktop/file.txt", "rt")

# el procesamiento va aquí

s.close()

except Exception as exc:

print("El archivo no se pudo abrir:", strerror(exc.errno));

Bueno. Ahora es el momento de tratar con archivos de texto y familiarizarse con algunas
técnicas básicas que puedes utilizar para procesarlos.

Procesamiento de archivos de texto


En esta lección vamos a preparar un archivo de texto simple con contenido breve y simple.

Te mostraremos algunas técnicas básicas que puedes utilizar para leer el contenido del archivo para
poder procesarlo.

El procesamiento será muy simple: vas a copiar el contenido del archivo a la consola y contarás todos los
caracteres que el programa ha leído.

Pero recuerda: nuestra comprensión de un archivo de texto es muy estricta. Es un archivo de texto sin
formato: puede contener solo texto, sin decoraciones adicionales (formato, diferentes fuentes, etc.).

Es por eso que debes evitar crear el archivo utilizando un procesador de texto avanzado como MS Word,
LibreOffice Writer o algo así. Utiliza los conceptos básicos que ofrece tu sistema operativo: Bloc de notas,
vim, gedit, etc.

Si tus archivos de texto contienen algunos caracteres nacionales no cubiertos por el juego de caracteres
ASCII estándar, es posible que necesites un paso adicional. La invocación de tu función open() puede
requerir un argumento que denote una codificación específica del texto.

Por ejemplo, si estás utilizando un sistema operativo Unix/Linux configurado para usar UTF-8 como una
configuración de todo el sistema, la función open() puede verse de la siguiente manera:

stream = open('file.txt', 'rt', encoding='utf-8')

Donde el argumento de codificación debe establecerse en un valor dentro de una cadena que representa
la codificación de texto adecuada (UTF-8, en este caso).

Consulta la documentación de tu sistema operativo para encontrar el nombre de codificación adecuado


para tu entorno.

INFORMACIÓN
A los fines de nuestros experimentos con el procesamiento de archivos que se llevan a cabo en esta
sección, vamos a utilizar un conjunto de archivos precargados (p. Ej., los archivos tzop.txt,
o text.txt) con los cuales podrás trabajar. Si deseas trabajar con tus propios archivos localmente en tu
máquina, te recomendamos que lo hagas y que utilices un Entorno de Desarrollo para llevar a cabo tus
propias pruebas.

Procesamiento de archivos de texto: continuación


La lectura del contenido de un archivo de texto se puede realizar utilizando diferentes métodos; ninguno
de ellos es mejor o peor que otro. Depende de ti cuál de ellos prefieres y te gusta.

Algunos de ellos serán a veces más prácticos y otros más problemáticos. Se flexible. No tengas miedo de
cambiar tus preferencias.

El más básico de estos métodos es el que ofrece la función read() , la cual pudiste ver en acción en la
lección anterior.

Si se aplica a un archivo de texto, la función es capaz de:

 Leer un número determinado de caracteres (incluso solo uno) del archivo y devolverlos como
una cadena.
 Leer todo el contenido del archivo y devolverlo como una cadena.
 Si no hay nada más que leer (el cabezal de lectura virtual llega al final del archivo), la función
devuelve una cadena vacía.

Comenzaremos con la variante más simple y usaremos un archivo llamado text.txt . El archivo
contiene lo siguiente:

Lo hermoso es mejor que lo feo.


Explícito es mejor que implícito.
Simple es mejor que complejo.
Complejo es mejor que complicado.

Ahora observa el código en el editor y analicémoslo.

La rutina es bastante simple:

 Se usa el mecanismo try-except y se abre el archivo con el nombre (text.txt en este caso).
 Intenta leer el primer caracter del archivo ( ch = s.read(1) ).
 Si tienes éxito (esto se demuestra por el resultado positivo de la condición while ), se muestra
el caracter (nota el argumento end= ,¡es importante! ¡No querrás saltar a una nueva línea
después de cada caracter!).
 Se actualiza el contador ( cnt ).
 Intenta leer el siguiente carácter y el proceso se repite.

Procesamiento de archivos de texto: continuación


Si estás absolutamente seguro de que la longitud del archivo es segura y puedes leer todo el archivo en la
memoria de una vez, puedes hacerlo: la función read() , invocada sin ningún argumento o con un
argumento que se evalúa a None , hará el trabajo por ti.

Recuerda - el leer un archivo muy grande (en terabytes) usando este método puede dañar tu
sistema operativo.
No esperes milagros: la memoria de la computadora no se puede extender.

Observa el código en el editor. ¿Que piensas de el?

Vamos a analizarlo:

 Abre el archivo, como anteriormente se hizo.


 Lee el contenido mediante una invocación de la función read() .
 Despues, se procesa el texto, iterando con un bucle for su contenido, y se actualiza el valor del
contador en cada vuelta del bucle.

El resultado será exactamente el mismo que en el ejemplo anterior.

Procesando archivos de texto: readline()


Si deseas manejar el contenido del archivo como un conjunto de líneas, no como un montón de
caracteres, el método readline() te ayudará con eso.

El método intenta leer una línea completa de texto del archivo, y la devuelve como una cadena en caso
de éxito. De lo contrario, devuelve una cadena vacía.

Esto abre nuevas oportunidades: ahora también puedes contar líneas fácilmente, no solo caracteres.

Hagámos uso de ello. Observa el código en el editor.

Como puedes ver, la idea general es exactamente la misma que en los dos ejemplos anteriores.

Procesando archivos de texto: readlines()


Otro método, que maneja el archivo de texto como un conjunto de líneas, no como caracteres,
es readlines() .

Cuando el método readlines() , se invoca sin argumentos, intenta leer todo el contenido del
archivo y devuelve una lista de cadenas, un elemento por línea del archivo.

Si no estás seguro de si el tamaño del archivo es lo suficientemente pequeño y no deseas probar el


sistema operativo, puedes convencer al método readlines() de leer no más de un número
especificado de bytes a la vez (el valor de retorno sigue siendo el mismo, es una lista de una cadena).

Siéntete libre de experimentar con este código de ejemplo para entender cómo funciona el
método readlines() .

El tamaño máximo del búfer de entrada aceptado se pasa al método como argumento.

Puedes esperar que readlines() procese el contenido del archivo de manera más efectiva
que readline() , ya que puede ser invocado menos veces.

Nota: cuando no hay nada que leer del archivo, el método devuelve una lista vacía. Úsalo para detectar el
final del archivo.

Puedes esperar que al aumentar el tamaño del búfer mejore el rendimiento de entrada, pero no hay una
regla de oro para ello: intenta encontrar los valores óptimos por ti mismo.
Observa el código en el editor. Lo hemos modificado para mostrarte cómo usar readlines() .

Hemos decidido usar un búfer de 15 bytes de longitud. No pienses que es una recomendación.

Hemos utilizado ese valor para evitar la situación en la que la primera invocación
de readlines() consuma todo el archivo.

Queremos que el método se vea obligado a trabajar más duro y que demuestre sus capacidades.

Existen dos bucles anidados en el código: el exterior emplea el resultado de readlines() para
iterar a través de él, mientras que el interno imprime las líneas carácter por carácter.

Procesando archivos de texto: continuación


El último ejemplo que queremos presentar muestra un rasgo muy interesante del objeto devuelto por la
función open() en modo de texto.

Creemos que puede sorprenderte - el objeto es una instancia de la clase iterable.

¿Extraño? De ningúna manera. ¿Usable? Si, por supuesto.

El protocolo de iteración definido para el objeto del archivo es muy simple: su


método __next__ solo devuelve la siguiente línea leída del archivo.

Además, puedes esperar que el objeto invoque automáticamente a close() cuando cualquiera de las
lecturas del archivo lleguen al final del archivo.

Mira el editor y ve cuán simple y claro se ha vuelto el código.

Manejando archivos de texto: write()


Escribir archivos de texto parece ser más simple, ya que hay un método que puede usarse para realizar
dicha tarea.

El método se llama write() y espera solo un argumento: una cadena que se transferirá a un archivo
abierto (no lo olvides), el modo de apertura debe reflejar la forma en que se transfieren los datos
- escribir en un archivo abierto en modo de lectura no tendrá éxito).

No se agrega carácter de nueva línea al argumento de write() , por lo que debes agregarlo tu mismo si
deseas que el archivo se complete con varias líneas.

El ejemplo en el editor muestra un código muy simple que crea un archivo llamado newtext.txt (nota:
el modo de apertura w asegura que el archivo se creará desde cero, incluso si existe y contiene datos)
y luego pone diez líneas en él.

La cadena que se grabará consta de la palabra línea, seguida del número de línea. Hemos decidido
escribir el contenido de la cadena carácter por carácter (esto lo hace el bucle interno for ) pero no estás
obligado a hacerlo de esta manera.

Solo queríamos mostrarte que write() puede operar con caracteres individuales.

El código crea un archivo con el siguiente texto:

línea #1

línea #2
línea #3

línea #4

línea #5

línea #6

línea #7

línea #8

línea #9

línea #10

¿Puedes imprimir el contenido del archivo en la consola?

Te alentamos a que pruebes el comportamiento del método write() localmente en tu máquina.

Manejando archivos de texto: continuación


Mira el ejemplo en el editor. Hemos modificado el código anterior para escribir líneas enteras en el archivo
de texto.

El contenido del archivo recién creado es el mismo.

Nota: puedes usar el mismo método para escribir en el stream stderr , pero no intentes abrirlo, ya que
siempre está abierto implícitamente.

Por ejemplo, si deseas enviar un mensaje de tipo cadena a stderr para distinguirlo de la salida normal
del programa, puede verse así:

import sys

sys.stderr.write("Mensaje de Error")

¿Qué es un bytearray?
Antes de comenzar a hablar sobre archivos binarios, tenemos que informarte sobre una de
las clases especializadas que Python usa para almacenar datos amorfos.

Los datos amorfos son datos que no tienen forma específica - son solo una serie de
bytes.

Esto no significa que estos bytes no puedan tener su propio significado o que no puedan
representar ningún objeto útil, por ejemplo, gráficos de mapa de bits.

Los datos amorfos no pueden almacenarse utilizando ninguno de los medios presentados
anteriormente: no son cadenas ni listas.

Debe haber un contenedor especial capaz de manejar dichos datos.


Python tiene más de un contenedor, uno de ellos es una clase especializada llamada
bytearray - como su nombre indica, es un arreglo que contiene bytes (amorfos).

Si deseas tener dicho contenedor, por ejemplo, para leer una imagen de mapa de bits y
procesarla de alguna manera, debes crearlo explícitamente, utilizando uno de los
constructores disponibles.

Observa:

data = bytearray(100)

Tal invocación crea un objeto bytearray capaz de almacenar diez bytes.

Nota: dicho constructor llena todo el arreglo con ceros.

Bytearrays: continuación
Bytearrays se asemejan a listas en muchos aspectos. Por ejemplo, son mutables, son suceptibles a la
función len() , y puedes acceder a cualquiera de sus elementos usando indexación convencional.

Existe una limitación importante - no debes establecer ningún elemento del arreglo de bytes con un
valor que no sea un entero (violar esta regla causará una excepción TypeError) y tampoco está
permitido asignar un valor fuera del rango de 0 a 255 (a menos que quieras provocar una
excepción ValueError).

Puedes tratar cualquier elemento del arreglo de bytes como un valor entero - al igual que en el
ejemplo en el editor.

Nota: hemos utilizado dos métodos para iterar el arreglo de bytes, y hemos utilizado la
función hex() para ver los elementos impresos como valores hexadecimales.

Ahora te vamos a mostrar cómo escribir un arreglo de bytes en un archivo binario, como no
queremos guardar su representación legible, queremos escribir una copia uno a uno del contenido de la
memoria física, byte a byte.

Bytearrays: continuación
Entonces, ¿cómo escribimos un arreglo de bytes en un archivo binario?

Observa el código en el editor. Analicémoslo:

 Primero, inicializamos bytearray con valores a partir de 10 ; si deseas que el contenido del
archivo sea claramente legible, reemplaza el 10 con algo como ord('a') , esto producirá
bytes que contienen valores correspondientes a la parte alfabética del código ASCII (no pienses
que harás que el archivo sea un archivo de texto; sigue siendo binario, ya que se creó con un
indicador - bandera wb ).
 Después, creamos el archivo usando la función open() , la única diferencia en comparación
con las variantes anteriores es que el modo de apertura contiene el indicador b .
 El método write() toma su argumento ( bytearray ) y lo envía (como un todo) al archivo.
 El stream se cierra de forma rutinaria.

El método write() devuelve la cantidad de bytes escritos correctamente.


Si los valores difieren de la longitud de los argumentos del método, puede significar que hay algunos
errores de escritura.

En este caso, no hemos utilizado el resultado; esto puede no ser apropiado en todos los casos.

Intenta ejecutar el código y analiza el contenido del archivo recién creado.

Lo vas a usar en el siguiente paso.

Cómo leer bytes de un stream

La lectura de un archivo binario requiere el uso de un método especializado llamado readinto() , ya


que el método no crea un nuevo objeto del arreglo de bytes, sino que llena uno creado previamente con
los valores tomados del archivo binario.

Nota:

 El método devuelve el número de bytes leídos con éxito.


 El método intenta llenar todo el espacio disponible dentro de su argumento; si existen más datos
en el archivo que espacio en el argumento, la operación de lectura se detendrá antes del final del
archivo; el resultado del método puede indicar que el arreglo de bytes solo se ha llenado de
manera fragmentaria (el resultado también lo mostrará y la parte del arreglo que no está siendo
utilizada por los contenidos recién leídos permanece intacta).

Mira el código completo a continuación:

from os import strerror

data = bytearray(10)

try:

bf = open('file.bin', 'rb')

bf.readinto(data)

bf.close()

for b in data:

print(hex(b), end=' ')

except IOError as e:

print("Se produjo un error de E/S: ", strerr(e.errno))

Analicémoslo:

 Primero, abrimos el archivo (el que se creó usando el código anterior) con el modo descrito
como rb .
 Luego, leemos su contenido en el arreglo de bytes llamado data , con un tamaño de diez bytes.
 Finalmente, imprimimos el contenido del arreglo de bytes: ¿Son los mismos que esperabas?

Ejecuta el código y verifica si funciona.

Cómo leer bytes de un stream


Se ofrece una forma alternativa de leer el contenido de un archivo binario mediante el método
denominado read() .

Invocado sin argumentos, intenta leer todo el contenido del archivo en la memoria, haciéndolo parte
de un objeto recién creado de la clase bytes.

Esta clase tiene algunas similitudes con bytearray , con la excepción de una diferencia significativa:
es immutable.

Afortunadamente, no hay obstáculos para crear un arreglo de bytes tomando su valor inicial directamente
del objeto de bytes, como aquí:

from os import strerror

try:

bf = open('file.bin', 'rb')

data = bytearray(bf.read())

bf.close()

for b in data:

print(hex(b), end=' ')

except IOError as e:

print("Se produjo un error de E/S: ", strerr(e.errno))

Ten cuidado - no utilices este tipo de lectura si no estás seguro de que el contenido del archivo se
ajuste a la memoria disponible.

Cómo leer bytes de un stream: continuación


Si el método read() se invoca con un argumento, se especifica el número máximo de bytes a leer.

El método intenta leer la cantidad deseada de bytes del archivo, y la longitud del objeto devuelto puede
usarse para determinar la cantidad de bytes realmente leídos.

Puedes usar el método como aquí:

try:

bf = open('file.bin', 'rb')

data = bytearray(bf.read(5))
bf.close()

for b in data:

print(hex(b), end=' ')

except IOError as e:

print("Se produjo un error de E/S:", strerr(e.errno))

Nota: los primeros cinco bytes del archivo han sido leídos por el código; los siguientes cinco todavía están
esperando ser procesados.

Copiando archivos: una herramienta simple y funcional


Ahora vas a juntar todo este nuevo conocimiento, agregarle algunos elementos nuevos y usarlo para
escribir un código real que pueda copiar el contenido de un archivo.

Por supuesto, el propósito no es crear un reemplazo para los comandos como copy de (MS Windows)
o cp de (Unix/Linux) pero para ver una forma posible de crear una herramienta de trabajo, incluso si nadie
quiere usarla.

Observa el código en el editor. Analicémoslo:

 Las líneas 3 a la 8: solicitan al usuario el nombre del archivo a copiar e intentan abrirlo para
leerlo; se termina la ejecución del programa si falla la apertura; nota: emplea la
función exit() para detener la ejecución del programa y pasar el código de finalización al
sistema operativo; cualquier código de finalización que no sea 0 significa que el programa ha
encontrado algunos problemas; se debe utilizar el valor errno para especificar la naturaleza
del problema.
 Las líneas 9 a la 15: repiten casi la misma acción, pero esta vez para el archivo de salida.
 La línea 17: prepara una parte de memoria para transferir datos del archivo fuente al destino; Tal
área de transferencia a menudo se llama un búfer, de ahí el nombre de la variable; el tamaño del
búfer es arbitrario; en este caso, decidimos usar 64 kilobytes; técnicamente, un búfer más grande
es más rápido al copiar elementos, ya que un búfer más grande significa menos operaciones de
E/S; en realidad, siempre hay un límite, cuyo cruce no genera más ventajas; pruébalo tú mismo
si quieres.
 Línea 18: cuenta los bytes copiados: este es el contador y su valor inicial.
 Línea 20: intenta llenar el búfer por primera vez.
 Línea 21: mientras se obtenga un número de bytes distinto de cero, repite las mismas acciones.
 Línea 22: escribe el contenido del búfer en el archivo de salida (nota: hemos usado un segmento
para limitar la cantidad de bytes que se escriben, ya que write() siempre prefiero escribir
todo el búfer).
 Línea 23: actualiza el contador.
 Línea 24: lee el siguiente fragmento de archivo.
 Las líneas 29 a la 31: limpieza final: el trabajo está hecho.
LABORATORIO

Tiempo Estimado
30 minutos
Nivel de dificultad
Medio

Objetivos
 Mejorar las habilidades del estudiante al operar con la lectura archivos.
 Utilizar colecciones de datos para contar datos numerosos.
Escenario
Un archivo de texto contiene algo de texto (nada inusual) pero necesitamos saber con qué frecuencia
aparece cada letra en el texto. Tal análisis puede ser útil en criptografía, por lo que queremos poder
hacerlo en referencia al alfabeto latino.

Tu tarea es escribir un programa que:

 Pida al usuario el nombre del archivo de entrada.


 Lea el archivo (si es posible) y cuente todas las letras latinas (las letras mayúsculas y minúsculas
se tratan como iguales).
 Imprima un histograma simple en orden alfabético (solo se deben presentar recuentos distintos
de cero).

Crea un archivo de prueba para tu código y verifica si tu histograma contiene resultados válidos.

Suponiendo que el archivo de prueba contiene solo una línea con:

aBc

el resultado esperado debería verse de la siguiente manera:a -> 1

b -> 1

c -> 1

Tip:

Creemos que un diccionario es un medio perfecto de recopilación de datos para almacenar los recuentos.
Las letras pueden ser las claves mientras que los contadores pueden ser los valores.

LABORATORIO

Tiempo Estimado
15-20 minutos

Nivel de dificultad
Medio

Prerrequisitos
05_9.15.1
Objetivos
 Mejorar las habilidades del estudiante para operar con archivos en modo (lectura/escritura).
 Emplear lambdas para cambiar el ordenamiento.
Escenario
El código anterior necesita ser mejorado. Está bien, pero tiene que ser mejor.

Tu tarea es hacer algunas enmiendas, que generen los siguientes resultados:

 El histograma de salida se ordenará en función de la frecuencia de los caracteres (el contador


más grande debe presentarse primero).
 El histograma debe enviarse a un archivo con el mismo nombre que el de entrada, pero con la
extensión '.hist' (debe concatenarse con el nombre original).

Suponiendo que el archivo de prueba contiene solo una línea con:

cBabAa

El resultado esperado debería verse de la siguiente manera:a -> 3

b -> 2

c -> 1

Tip:

Emplea una lambda para cambiar el ordenamiento.

LABORATORIO

Tiempo Estimado
30 minutos

Nivel de dificultad
Medio

Objetivos
 Mejorar las habilidades del alumno para operar con archivos en modo lectura.
 Perfeccionar las habilidades del estudiante para definir y usar excepciones y diccionarios
autodefinidos.
Escenario
El profesor Jekyll dirige clases con estudiantes y regularmente toma notas en un archivo de texto. Cada
línea del archivo contiene 3 elementos: el nombre del alumno, el apellido del alumno y el número de
puntos que el alumno recibió durante ciertas clases.

Los elementos están separados con espacios en blanco. Cada estudiante puede aparecer más de una
vez dentro del archivo del profesor Jekyll.

El archivo puede tener el siguiente aspecto:


John Smith 5

Anna Boleyn 4.5

John Smith 2

Anna Boleyn 11

Andrew Cox 1.5

Tu tarea es escribir un programa que:

 Pida al usuario el nombre del archivo del profesor Jekyll.


 Lea el contenido del archivo y cuenta la suma de los puntos recibidos para cada estudiante.
 Imprima un informe simple (pero ordenado), como este:
Andrew Cox 1.5

Anna Boleyn 15.5

John Smith 7.0

Nota:

 Tu programa debe estar completamente protegido contra todas las fallas posibles: la inexistencia
del archivo, el vacío del archivo o cualquier falla en los datos de entrada; encontrar cualquier
error de datos debería causar la terminación inmediata del programa, y lo erróneo deberá
presentarse al usuario.
 Implementa y usa tu propia jerarquía de excepciones: la presentamos en el editor; la segunda
excepción se debe generar cuando se detecta una línea incorrecta y la tercera cuando el archivo
fuente existe pero está vacío.

Tip:

Emplea un diccionario para almacenar los datos de los estudiantes.

También podría gustarte