PCAP - Programming Essentials in Python 2 - Modulo 06
PCAP - Programming Essentials in Python 2 - Modulo 06
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.
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.
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.
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.
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.
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).
Vehículos Terrestres.
Vehículos Acuáticos.
Vehículos Aéreos.
Vehículos Espaciales.
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.
Podemos decir que todos los animales (nuestra clase de nivel superior) se puede dividir en
cinco subclases:
Mamíferos.
Reptiles.
Pájaros.
Peces.
Anfibios.
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.
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.
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.
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:
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
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.
Para hacer esto, debes asignar una variable para almacenar el objeto recién creado de esa
clase y crear un objeto al mismo tiempo.
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.
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.
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.
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.
pila = []
Estamos listos para definir una función que pone un valor en la pila. Aquí están las presuposiciones
para ello:
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:
def pop():
val = pila[-1]
del pila[-1]
return val
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.
1
2
3
Pruébalo.
Pero cuanto más la uses, más desventajas encontrarás. Éstas son algunas de ellas:
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:
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.
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.
class Pila:
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:
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.
class Pila:
def __init__(self):
print("¡Hola!")
objetoPila = Pila()
¡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.
Ahora agreguemos solo una propiedad al nuevo objeto - una lista para la pila. La
nombraremos listaPila .
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?
Esto no es lo que queremos de la pila. Nosotros queremos que listaPila este escondida del mundo
exterior. ¿Es eso posible?
class Pila:
def __init__(self):
self.__listaPila = []
objetoPila = Pila()
print(len(objetoPila.__listaPila))
¿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.
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.
class Pila:
def __init__(self):
self.__listaPila = []
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.
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 .
La declaración de la clase está completa y se han enumerado todos sus componentes. La clase está lista
para usarse.
Analiza el código:
class Pila:
def __init__(self):
self.__listaPila = []
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.
class Pila:
def __init__(self):
self.__listaPila = []
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())
La nueva clase debería poder evaluar la suma de todos los elementos almacenados actualmente en
la pila.
El primer paso es fácil: solo define una nueva subclase que apunte a la clase que se usará como
superclase.
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.
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.
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.
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 .
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.
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.
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.
class ClaseEjemplo:
def __init__(self, val = 1):
self.primera = 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.
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.
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.
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:
print(objetoEjemplo1._ClaseEjemplo__primera)
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.
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:
{'_ClaseEjemplo__primera': 1} 3
{'_ClaseEjemplo__primera': 2} 3
{'_ClaseEjemplo__primera': 4} 3
Ejecuta el programa y verifica si tus predicciones fueron correctas. Todo funciona como se esperaba,
¿no?
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.
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:
print(objetoEjemplo.b)
Como puedes ver, acceder a un atributo de objeto (clase) no existente provoca una
excepción AttributeError.
Como puedes ver, esta acción no es muy sofisticada. Esencialmente, acabamos de barrer el tema debajo
de la alfombra.
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:
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)
¿Puedes adivinar la salida del código? Ejecútalo para verificar tus conjeturas.
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'))
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.
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.
class conClase:
def metodo(self, par):
print("método:", par)
obj = conClase()
obj.metodo(1)
obj.metodo(2)
obj.metodo(3)
método: 1
método: 2
método: 3
class conClase:
varia = 2
def metodo(self):
print(self.varia, self.var)
obj = conClase()
obj.var = 3
obj.metodo()
2 3
El parámetro self también se usa para invocar otros métodos desde dentro de la clase.
class conClase():
def otro(self):
print("otro")
def metodo(self):
print("método")
self.otro()
obj = conClase()
obj.metodo()
método
otro
Si una clase tiene un constructor, este se invoca automática e implícitamente cuando se instancia el
objeto de la clase.
El constructor:
Observa el código en el editor. El ejemplo muestra un constructor muy simple pero funcional.
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).
El ejemplo en el editor muestra cómo definir un constructor con un valor de argumento predeterminado.
Pruébalo.
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.
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()
visible
fallido
oculto
Observemos cómo esta propiedad trata con los métodos: mira el código en el editor.
Encuentra todos los métodos y atributos definidos. Localiza el contexto en el que existen: dentro del
objeto o dentro de la clase.
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.
conClase
conClase
__main__
__main__
Como sabes, cualquier módulo llamado __main__ en realidad no es un módulo, sino es el archivo
actualmente en ejecución.
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.
( 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:
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.
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.
El programa imprime solo una línea de texto, que en nuestro caso es:
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.
El método por default __str__() devuelve la cadena anterior: fea y poco informativa. Puedes
cambiarlo definiendo tu propio método del nombre.
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?
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.
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.
El conocimiento anterior proviene de la lectura del código (en otras palabras, lo sabemos
porque podemos verlo).
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.
issubclass(ClaseUno, ClaseDos)
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.
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.
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.
isinstance(nombreObjeto, nombreClase)
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.
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.
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.
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.
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.
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 .
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.
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:
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 .
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).
Cuando intentes acceder a una entidad de cualquier objeto, Python intentará (en este orden):
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.
class SuperA:
varA = 10
def funA(self):
return 11
class SuperB:
varB = 20
def funB(self):
return 21
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
¿Qué crees que sucederá si más de una de las superclases define una entidad con un nombre en
particular?
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 .
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.
La clase Sub hereda todos los bienes de dos superclases, Izquierda y Derecha (estos nombres
están destinados a ser significativos).
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.
¿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.
D II DD Derecha
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.
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 .
hazlo de Uno
hazlo de Dos
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.
¿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.
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.
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.
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.
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.
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).
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.
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.
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
pass
d = D()
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.
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 .
0.5
División fallida
None
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.
0.5
División fallida
None
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.
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:
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:
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.
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
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 :
: :
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:
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 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).
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:
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
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
Ahora, si las circunstancias lo permiten, es posible usar unicamente los nombres de clase.
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)
¿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.
__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 .
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.
Fib1 = 1
Fib2 = 1
Fibi = Fibi-1 + Fibi-2
En otras palabras:
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.
__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 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.
Se puede ver a la palabra clave reservada yield como un hermano más inteligente de la
sentencia return , con una diferencia esencial.
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.
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.
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.
def fun(n):
for i in range(n):
yield i
for v in fun(5):
print(v)
Revisar
def potenciasDe2(n):
potencia = 1
for i in range(n):
yield potencia
potencia *= 2
t = [x for x in potenciasDe2(5)]
print(t)
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.
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)
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.
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.
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 :
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 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.
¿Se puede usar el mismo truco dentro de una lista de comprensión? Sí, por supuesto.
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.
for v in lst:
print()
for v in genr:
print()
Son los paréntesis. Los corchetes hacen una comprensión, los paréntesis hacen un generador.
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:
Por supuesto, guardar la lista o el generador no es necesario; puedes crearlos exactamente en el lugar
donde los necesites, como aquí:
print()
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.
La función lambda
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?
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
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.
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.
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.
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
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.
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.
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)
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() .
Esta es la explicación:
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.
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]
def exterior(par):
loc = par
var = 1
exterior(var)
print(var)
print(loc)
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:
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.
0 0 0
1 1 1
2 4 8
3 9 27
4 16 64
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í.
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.
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.
Supongamos también que deseas asignar a una cadena el nombre del archivo.
nombre = "/dir/archivo"
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.
Esto significa que los nombres de archivo de Windows deben escribirse de la siguiente
manera:
nombre = "\\dir\\archivo"
nombre = "c:/dir/archivo"
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í).
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.
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).
Si la apertura es exitosa, el programa solo podrá realizar las operaciones que sean
consistentes con el modo abierto declarado.
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.
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.
Los archivos se pueden procesar de muchas maneras diferentes: algunos dependen del
contenido del archivo, otros de las intenciones del programador.
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.
Para nuestros propósitos, solo nos ocuparemos de los streams representados por los
objetos BufferIOBase y TextIOBase . Entenderás por qué pronto.
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.
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.
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.
Cuando el stream está abierto, su contenido se toma tal cual es, sin ninguna
conversión - no se agregan, ni se omiten bytes.
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?
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.
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 .
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.
¿Cómo abrir ese archivo para leerlo? Aquí está el fragmento del código:
try:
stream.close()
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
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() .
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.
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:
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:
El error se produce cuando intentas, por ejemplo, abrir un archivo con atributos de solo
lectura para abrirlo.
El error se produce cuando intentas, por ejemplo, operar un stream sin abrirlo.
El error se produce cuando intentas, por ejemplo, cambiar el nombre de un archivo con su
nombre anterior.
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 abrir simultáneamente más streams de los aceptables
para el sistema operativo.
La lista completa es mucho más larga (incluye también algunos códigos de error no
relacionados con el procesamiento del stream).
import errno
try:
s = open("c:/users/user/Desktop/file.txt", "rt")
# el procesamiento va aquí
s.close()
if exc.errno == errno.ENOENT:
else:
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:
try:
s = open("c:/users/user/Desktop/file.txt", "rt")
# el procesamiento va aquí
s.close()
Bueno. Ahora es el momento de tratar con archivos de texto y familiarizarse con algunas
técnicas básicas que puedes utilizar para procesarlos.
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:
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).
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.
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.
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:
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.
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.
Vamos a analizarlo:
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.
Como puedes ver, la idea general es exactamente la misma que en los dos ejemplos anteriores.
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é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.
Además, puedes esperar que el objeto invoque automáticamente a close() cuando cualquiera de las
lecturas del archivo lleguen al final del archivo.
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.
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
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.
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)
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?
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.
En este caso, no hemos utilizado el resultado; esto puede no ser apropiado en todos los casos.
Nota:
data = bytearray(10)
try:
bf = open('file.bin', 'rb')
bf.readinto(data)
bf.close()
for b in data:
except IOError as e:
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?
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í:
try:
bf = open('file.bin', 'rb')
data = bytearray(bf.read())
bf.close()
for b in data:
except IOError as e:
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.
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.
try:
bf = open('file.bin', 'rb')
data = bytearray(bf.read(5))
bf.close()
for b in data:
except IOError as e:
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.
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.
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.
Crea un archivo de prueba para tu código y verifica si tu histograma contiene resultados válidos.
aBc
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.
cBabAa
b -> 2
c -> 1
Tip:
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.
John Smith 2
Anna Boleyn 11
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: