×
Preliminares
1 Primeros pasos
2 El mundo desde un ordenador
2.1 Todo son números
2.1.1 Sistemas de numeración
2.1.2 Diccionarios
2.1.3 Profundizando en la clase str
2.1.4 Secuencias de bytes: bytes y bytearray
2.1.5 Imágenes en forma de números: pixels
Pillow
turtle
2.1.6 Operaciones a nivel de bits
2.1.7 Python como herramienta científica
numpy
matplotlib
2.2 El ordenador y Python
2.2.1 El símbolo de sistema de Windows
2.2.2 El sistema de archivos desde Python
2.2.3 Streams. Entrada y salida estándares
2.2.4 Profundizando en la clase list
2.2.5 Conjuntos: set y frozenset
2.2.6 Gestión de errores
3 Ampliando horizontes
4 Teoría y teoremas
5 Jugando con Python

Aprender Python:

2 El mundo desde un ordenador

Para programar en cualquier lenguaje es importante tener unos conocimientos lo más amplios posible sobre cómo funcionan los ordenadores. En esta sección continuaremos profundizando en los mecanismos de programación con Python mientras entendemos algunos conceptos importantes. El principal es:

Para un ordenador TODO SON NÚMEROS

2.1 Todo son números

O más concretamente, todo son bits. Dentro del ordenador fluyen señales eléctricas que representan ceros y unos, y con eso la máquina es capaz de construir todo tipo de datos e interpretarlos. El ordenador propiamente dicho está formado por el procesador y la memoria, todo lo demás son "accesorios". El procesador necesita la memoria para mantener los datos y poder emplearlos, y ahí está todo su universo. Disponer de un sistema de almacenamiento masivo y permanente le permite sencillamente manejar más cantidad de datos. Los datos en el sistema de almacenamiento (usualmente uno o más discos duros) no están a disposición del procesador, este necesita primero cargarlos en la memoria y ahí si que puede trabajar con ellos.

A continuación están los "sentidos" del ordenador, los sistemas de entrada/salida (E/S), que permiten recibir y enviar datos desde diversas fuentes y hacia también diversos destinos. El sistema de almacenamiento que hemos mencionado pertenece a esta misma categoría, los mecanismos que el procesador emplea para comunicarse con un teclado no son diferentes de los que usa para leer el disco duro. En la segunda mitad de esta sección abordaremos la cuestión de los ficheros y sistemas E/S.

2.1.1 Sistemas de numeración

En el mundo de bits del ordenador, todos los números están formados por dos posibles dígitos: cero y uno. Es lo que se conoce como sistema binario de numeración, y es el sistema natural para el procesador. Cuando vemos o introducimos valores en nuestro familiar sistema decimal los programas hacen malabares para convertirlos desde y hacia el binario. En realidad, de las bases de numeración que se emplean en informática (binario, octal, decimal y hexadecimal) la menos natural es la decimal. Como hemos mencionado, el sistema natural es el binario, y la agrupación básica es el byte de 8 bits, pero para las personas es difícil de leer y de recordar una secuencia de unos y ceros. El sistema octal (en base 8) comprime tres bits por dígito, pero la estrella del mundo de la programación es el sistema hexadecimal, en base 16, que empaqueta perfectamente un byte en dos dígitos. Como programador conviene familiarizarse rápidamente con dicho sistema.

Vamos a mirar con más detalle los valores numéricos en Python y los sistemas que nos permiten cambiar de base. Empecemos por un sencillo código:

# Cambio de base

num=[None for i in range(4)]

while True:
if (entrada:=input("Un número natural, please: "))==):
break

num[2]=eval(entrada)
num[0]=bin(num[2])
num[1]=oct(num[2])
num[3]=hex(num[2])

print("="*40)
for i in num:
print(f"{i:>18} {type(i)}")

print(f"{num[2]:>18b} binario")
print(f"{num[2]:>18o} octal")
print(f"{num[2]:>18} decimal")
print(f"{num[2]:>18X} hexadecimal"

Veamos las novedades, en la línea 3 creamos una lista de 4 elementos, y los inicializamos con un nuevo valor, None, que como su propio nombre indica significa ninguno. En Python existe un objeto para representar la inexistencia de un valor, y es este. Solicitamos valores hasta que se introduzca una cadena vacía en la sentencia input() de la línea 6, en cuyo caso terminamos el bucle y el programa. A continuación procesamos el valor y lo guardamos en la lista num en los cuatro formatos: binario, octal, decimal y hexadecimal (y en este orden). Observa las funciones de conversión, no son difíciles de recordar.

En la línea 14 imprimimos una línea de separación y luego mostramos los cuatro valores y sus tipos. A continuación volvemos a presentarlos pero usando las capacidades de conversión de una cadena con formato. Ejecutamos el programa con valores estratégicos y obtenemos la siguiente salida:

============= RESTART: C:/Users/User/Documents/Python/Cambio de base.py =============
Un número natural, please: 32
========================================
0b100000 <class 'str'>
0o40 <class 'str'>
32 <class 'int'>
0x20 <class 'str'>
100000 binario
40 octal
32 decimal
20 hexadecimal
Un número natural, please: 127
========================================
0b1111111 <class 'str'>
0o177 <class 'str'>
127 <class 'int'>
0x7f <class 'str'>
1111111 binario
177 octal
127 decimal
7F hexadecimal
Un número natural, please: 255
========================================
0b11111111 <class 'str'>
0o377 <class 'str'>
255 <class 'int'>
0xff <class 'str'>
11111111 binario
377 octal
255 decimal
FF hexadecimal
Un número natural, please: 65535
========================================
0b1111111111111111 <class 'str'>
0o177777 <class 'str'>
65535 <class 'int'>
0xffff <class 'str'>
  1111111111111111 binario
177777 octal
65535 decimal
FFFF hexadecimal
Un número natural, please:
>>>

Lo primero que te hago notar es que las funciones bin(), oct() y hex() producen representaciones en forma de texto, e incluyen los prefijos que se utilizan para introducir valores numéricos en sus respectivas bases. En cambio, los especificadores de formato no incluyen los prefijos.

El primer valor corresponde al primer caracter imprimible, el espacio. Los otros son el máximo valor que podemos usar con 7 bits (es importante porque muchas veces el octavo bit se usa como bit de signo) y los valores máximos para un byte y una palabra de 16 bits. Fíjate en que con números hexadecimales podemos expresar de forma muy compacta tanto los bytes como las palabras de dos o más bytes.

Veamos un nuevo código acerca de las bases de numeración:

La función int()

n=int(input("Un número decimal: "))
print(n)

b=int(input("Un número binario: "),2)
print(f"{b:b} {b}")

o=int(input("Un número octal: "),8)
print(f"{o:o} {o}")

h=int(input("Un número hexadecimal: "),16)
print(f"{h:x} {h}")

Asegúrate de utilizar los dígitos correctos para cada base. Este es un ejemplo del resultado:

============= RESTART: C:/Users/User/Documents/Python/int().py =============
Un número decimal: 50
50
Un número binario: 10101
10101 21
Un número octal: 567
567 375
Un número hexadecimal: A0
a0 160
>>>

Como puedes ver, la función int() (de hecho el constructor int(), ya lo veremos cuando hablemos de las clases) acepta un segundo parámetro que corresponde con la base de numeración, por defecto es diez. Podríamos utilizar los prefijos, pero para cada base solo se acepta el prefijo correspondiente.

============= RESTART: C:/Users/User/Documents/Python/int().py =============
Un número decimal: 100
100
Un número binario: 0b100
100 4
Un número octal: 0o100
100 64
Un número hexadecimal: 0x100
100 256
>>>

Hasta aquí (de momento) lo relativo a números en diferentes bases. A partir de ahora, cuando sea conveniente usaremos preferentemente valores hexadecimales.

2.1.2 Diccionarios

Vamos a hacer un paréntesis para introducir otro tipo de datos predefinido, los diccionarios. Los diccionarios contienen múltiples valores, como listas y tuplas, pero no pertenecen al conjunto de tipos secuencia, sino al conjunto mapa (mapping). Son el único tipo de dicho conjunto. Un diccionario consiste en un conjunto de pares clave: valor, de forma que podemos acceder a los valores usando las claves. Veámoslo en el modo interactivo:

>>> d={1:"uno","2","dos"}
>>> d
{1: 'uno', '2': 'dos'}
>>> type(d)
<class 'dict'>
>>> d[1]
'uno'
>>> d[2]
Traceback (most recent call last):
File "<pyshell#9>", line 1, in <module>
d[2]
KeyError: 2
>>> d["2"]
'dos'
>>>

Los diccionarios van encerrados entre llaves "{}", y cada entrada clave: valor consiste en una clave que puede ser de cualquier tipo que no sea mutable, y no pueden repetirse, separada por dos puntos del valor que puede ser cualquier cosa, números, textos, listas e incluso otros diccionarios. Las claves deben utilizarse literalmente, en el primer caso hemos usado el valor entero 1, y eso nos hace tener la falsa ilusión de acceder mediante un índice, como con las listas, pero al intentarlo con el número 2 vemos que no es así. Los diccionarios pertenecen a la clase 'dict'.

Podemos usar el nombre de la clase como función para crear un diccionario:

>>> dict()
{}
>>> dict(uno=1,dos=2)
{'uno': 1, 'dos': 2}
>>> dict(1="uno",2="dos")
SyntaxError: expression cannot contain assignment, perhaps you meant "=="?
>>> dict([(1,"uno"),("2", "dos")])
{1: 'uno', '2': 'dos'}
>>> dict({1:"uno","dos":2})
{1: 'uno', 'dos': 2}
>>> d=_
>>> len(d)
2
>>> list(d)
[1, 'dos']
>>> for i in d:
print(i,d[i])


1 uno
dos 2
>>>

Si usamos dict() vemos que el modo de asignar los pares es diferente, clave=valor, no podemos usar comillas para las claves pero se consideran cadenas. No podemos utilizar números como claves, a no ser que usemos una lista con tuplas (clave,valor) o la forma anteriormente vista entre llaves. Nuestra conocida función len() nos devuelve el número de parejas, y si usamos list() obtenemos una lista con las claves. Si iteramos un diccionario obtenemos consecutivamente las claves. Vamos a ver algún programa con un diccionario.

# Diccionario

d=dict(Lunes="Maandag",Martes="Dinsdag",Miércoles="Woensdag",
Jueves="Donderdag",Viernes="Vrijdag",Sábado="Zaterdag",
Domingo="Zondag")

print()
for i in d:
print(i)

print()
for i in d:
print(d[i])

print()
print("Viernes" in d)
print("Maandag" in d)

d["Maandag"]="Lunes"
del d["Lunes"]
d["Martes"]=None

print()
for i in d.items():
print(i)

Este es el resultado del programa:

============= RESTART: C:/Users/User/Documents/Python/Diccionario.py =============

Lunes
Martes
Miércoles
Jueves
Viernes
Sábado
Domingo

Maandag
Dinsdag
Woensdag
Donderdag
Vrijdag
Zaterdag
Zondag

True
False

('Martes', None)
('Miércoles', 'Woensdag')
('Jueves', 'Donderdag')
('Viernes', 'Vrijdag')
('Sábado', 'Zaterdag')
('Domingo', 'Zondag')
('Maandag', 'Lunes')

Los diccionarios son mutables, en la línea 19 creamos un nuevo valor al usar una asignación con una clave que no existe ("Maandag" es un valor asignado a la clave "Lunes"), y en la línea 21 cambiamos el valor correspondiente a la clave "Martes".

El operador in que usamos en las líneas 16 y 17 devuelve True si el valor anterior al operador está incluído en la estructura de datos posterior. En el caso de un diccionario se comprueban solo las claves, pero podemos usarlo en secuencias de cualquier clase, incluyendo cadenas de caracteres.

En la línea 20 aparece por primera vez en este curso la sentencia del (por delete, borrar), que sirve para eliminar objetos. En este caso borramos la entrada correspondiente a la clave "Lunes", pero podríamos haber borrado el diccionario entero con del d.

Para finalizar con el código, en la línea 24 el método dict.items() de los objetos tipo diccionario devuelve tuplas conteniendo cada par clave:valor. Hay dos métodos más que proporcionan tan solo las claves: dict.keys() y solo los valores: dict.values(). Los valores que guardamos en un diccionario pueden ser de cualquier clase, numéricos, cadenas, secuencias e incluso otros diccionarios. Como en Python todo son objetos, podemos guardar incluso funciones.

>>> d={"a":print,"b":type}
>>> d
{'a': <built-in function print>, 'b': <class 'type'>}
>>> d["a"]("Hola")
Hola
>>> d["b"]("Hola")
<class 'str'>
>>> del d
>>> d
Traceback (most recent call last):
File "<pyshell#20>", line 1, in <module>
d
NameError: name 'd' is not defined

>>>

Observa que usamos el identificador de la función sin los paréntesis. Si utilizásemos los paréntesis la función se ejecutaría y lo que se almacenaría sería el valor de retorno.

>>> d={1:print(),2:type(1)}
>>> d
{1: None, 2: <class 'int'>}
>>>

De momento eso es todo en cuanto a los diccionarios, vamos a ver más cosas acerca de lo que un ordenador puede codificar con números.

2.1.3 Profundizando en la clase str

Recapitulando lo visto acerca de cadenas de texto, cualquier conjunto de caracteres encerrado entre comillas dobles, sencillas o triples es considerado por el intérprete de Python un literal de texto, esto es, de la clase str. Podemos usar el operador + para concatenar textos, y el operador * para repetirlos un número entero de veces, podemos usar la función len() para conocer su longitud, y podemos usar índices y porciones para obtener partes del texto. Los textos en Python están codificados mediante el sistema unicode, y podemos usar secuencias de escape para representar caracteres que no podemos teclear. Un texto es iterable y va devolviendo cada uno de los caracteres que lo componen. Tambien podemos obtener el código numérico de un caracter con ord(ch) o el caracter correspondiente a un valor numérico con chr(int). Para terminar, conocemos el método str.isdecimal(), que es uno de los muchos métodos de la clase str. Repasemos lo visto:

Los textos definidos mediante comillas triples (una comilla sencilla repetida tres veces) se extienden a lo largo de varias líneas, e incluyen cualquier espacio dentro de la línea. Vemos que los saltos de línea y un tabulador que hemos introducido también están incluídos.

>>> "Esto es un texto"
'Esto es un texto'
>>> 'Esto   también'
'Esto   también'
>>> '''Este texto
puede extenderse
entre líneas'''
'Este texto\npuede extenderse\n\tentre líneas'
>>>

Podemos usar el operador + para concatenar textos y tambien podemos escribir literales de texto que estén separados solo por espacio vacío y serán concatenados.

>>> "Esto es"+"un texto"
'Esto esun texto'
>>> "Esto" "también"
'Estotambién'
>>>

Mediante el constructor str() podemos crear objetos de texto a partir de cualquier otro tipo de objeto. Todos los objetos predefinidos de Python tienen una representación textual incorporada, y eso es lo que proporcionan al invocar str(objeto).

>>> str()
''
>>> n=3.14
>>> str(n)
'3.14'
>>> l=[False,True]
>>> str(l)
'[False,True]'
>>> d={1:"uno",2:"dos"}
>>> str(d)
"{1: 'uno', 2: 'dos'}"
>>> f=print
>>> str(f)
'<built-in function print>'
>>>

Accedemos a los elementos de una cadena de texto mediante los índices o porciones. Podemos efectuar bucles sobre los caracteres de una cadena, y usar secuencias de escape y la función char() para obtener caracteres especiales. Podemos medir la longitud de una cadena y saber si una subcadena forma parte de otra con la expresión subcadena in cadena.

>>> texto="Los ramanes lo hacen todo por triplicado."
>>> "triplicado"*3
'triplicadotriplicadotriplicado'
>>> "todo" in texto
True
>>> len(texto)
41
>>> texto[5]
'a'
>>> texto[4:11]
'ramanes'
>>> for i in texto[-11:]:
print(i,end=".")

t.r.i.p.l.i.c.a.d.o...
>>> texto="\"Hola\nMundo\" "+chr(0x1F30D)
>>> print(texto)
"Hola
Mundo" 🌍
>>>

Dada la importancia que tienen las cadenas de texto, la clase str en Python está profusamente surtida de métodos para efectuar todo tipo de procesos. La lista a continuación no es completa, pero si contiene los métodos más útiles.

Métodos de la clase str
str.capitalize() Devuelve una copia de str con el primer caracter en mayúsculas y el resto en minúsculas
str.center(ancho,char=" ") Devuelve una cadena de ancho caracteres con str en el centro
Admite un segundo argumento con el caracter de relleno
str.count(sub,start=0,end=∞) Cuenta el número de apariciones de la subcadena sub en str
Opcionalmente podemos indicar comienzo o comienzo y final de la porción
str.endswith(sufijo,start=0,end=∞) Devuelve True si str termina con sufijo
Opcionalmente podemos indicar comienzo o comienzo y final de la porción
sufijo puede ser una tupla con varias posibilidades
str.find(sub,start=0,end=∞) Devuelve el índice de la primera aparición de sub en str
Opcionalmente podemos indicar comienzo o comienzo y final de la porción
Si no se encuentra devuelve -1
str.format(*args) Crea una cadena con formateo de datos, igual que las cadenas f""
Cada campo de reemplazo {formato} en str se asigna a un argumento
mediante un índice, por posición o por nombre del argumento
str.index(sub,start=0,end=∞) Igual que str.find() pero produce un error si no se encuentra sub
str.isalnum() Devuelve True si todos los caracteres de str son alfanuméricos
y hay al menos un caracter. Devuelve False en caso contrario
str.isalpha() Devuelve True si todos los caracteres de str son alfabéticos
y hay al menos un caracter. Devuelve False en caso contrario
str.isascii() Devuelve True si todos los caracteres de str son ASCII
y hay al menos un caracter. Devuelve False en caso contrario Los caracteres ASCII son aquellos entre los códigos 0-127 (0x00-0x7f)
str.isdecimal() Devuelve True si todos los caracteres de str son decimales
y hay al menos un caracter. Devuelve False en caso contrario
str.isidentifier() Devuelve True si str cumple con los requisitos
para ser un identificador. Devuelve False en caso contrario
str.islower() Devuelve True si todos los caracteres de str son minúsculas
y hay al menos un caracter. Devuelve False en caso contrario
str.isupper() Devuelve True si todos los caracteres de str son mayúsculas
y hay al menos un caracter. Devuelve False en caso contrario
str.join(iterable) Devuelve una cadena formada por cada uno de los elementos de iterable
separados por la cadena str
str.ljust(ancho,char=" ") Devuelve una cadena de longitud ancho con str justificada a la izquierda
Admite como opción el caracter de relleno
str.lower() Devuelve una copia de str en minúsculas
str.lstrip(chars=<espacios>) Elimina los caracteres en blanco a la izquierda de la cadena
Podemos especificar los caracteres a eliminar
str.partition(sep) Devuelve un tupla con tres partes de la cadena str:
Desde el principio hasta la primera aparición del separador sep,
el propio separador y el resto de la cadena hasta el final
Si no se encuentra sep devuelve la cadena entera y dos cadenas vacías
str.replace(old,new,count=∞) Devuelve una copia de str con todas las apariciones de old
reemplazadas por new. El argumento opcional count indica el
número de ocurrencias que serán reemplazadas
str.rfind(sub,start=0,end=∞) Devuelve el índice de la última aparición de sub en str
Opcionalmente podemos indicar comienzo o comienzo y final de la porción
Si no se encuentra devuelve -1
str.rindex(sub,start=0,end=∞) Igual que str.rfind() pero produce un error si no se encuentra sub
str.rjust(ancho,char=" ") Devuelve una cadena de longitud ancho con str justificada a la derecha
Admite como opción el caracter de relleno
str.rpartition(sep) Devuelve un tupla con tres partes de la cadena str:
Desde después de la última aparición del separador sep hasta el final,
el propio separador y el comienzo de la cadena hasta sep
Si no se encuentra sep devuelve dos cadenas vacías más la cadena entera
str.rsplit(sep=None,max=-1) Devuelve una lista de las palabras en str, de derecha a izquierda
Podemos especificar el separador y con max el número de palabras,
el resto se mantendrá unido
str.rstrip(chars=<espacios>) Elimina los caracteres en blanco a la derecha de la cadena
Podemos especificar los caracteres a eliminar
str.split(sep=None,max=-1) Devuelve una lista de las palabras en str, de izquierda a derecha
Podemos especificar el separador y con max el número de palabras,
el resto se mantendrá unido
str.splitlines(flag) Devuelve una lista con str dividida en líneas
Si flag=True incluye los separadores
str.startswith(prefijo,start=0,end=∞) Devuelve True si str comienza con prefijo
Opcionalmente podemos indicar comienzo o comienzo y final de la porción
prefijo puede ser una tupla con varias posibilidades
str.strip(chars=<espacios>) Elimina los caracteres en blanco a iquierda y derecha de la cadena
Podemos especificar los caracteres a eliminar
str.swapcase() Devuelve una copia de str con las minúsculas
convertidas en mayúsculas y viceversa
str.title() Devuelve una copia de str en la que cada palabra empieza por mayúscula
y continúa por minúsculas
str.upper() Devuelve una copia de str con todos los caracteres en mayúsculas

Pongámonos a trabajar para ver en funcionamiento estos métodos, los agruparemos en varias secciones en función del tipo de proceso que realizan, en primer lugar los que modifican aspectos como mayúsculas y minúsculas:

>>> c="elemental, querido Watson"
>>> c.capitalize()
'Elemental, querido Watson'
>>> c.lower()
'elemental, querido watson'
>>> c.upper()
'ELEMENTAL, QUERIDO WATSON'
>>> c.swapcase()
z 'ELEMENTAL, QUERIDO wATSON'
>>> c.title()
'Elemental, Querido Watson'
>>>

Se trata de métodos sencillos de entender, no requieren ningún parámetro y afectan a la grafía en mayúsculas o minúsculas de las letras. capitalize() se limita a poner la primera letra en mayúsculas. Si la cadena comenzara por una mayúscula o un caracter no alfabético no hace nada. upper() y lower() ponen toda la cadena en mayúsculas o minúsculas, respectivamente. swapcase() cambia las minúsculas por mayúsculas y las mayúsculas por minúsculas. title() pone cada inicial de palabra en mayúscula. Todos los métodos de la clase str devuelven una nueva cadena, recuerda que las cadenas son inmutables. Podemos aplicar los métodos sobre cualquier objeto de tipo cadena, incluso literales o sobre el valor que devuelva otra función.

>>> "hola, mundo".title().swapcase()
'hOLA, mUNDO'
>>>

A continuación los métodos que evalúan el contenido de la cadena para saber si pertenece a una cierta categoría:

>>> c.isalnum()
False
>>> "Watson007".isalnum()
True
>>> "Viñé Cárdaba.isalpha()
False
>>> "Cárdaba".isalpha()
True
>>> "Watson007.isalpha()
False
>>> "МоскваGroßräschenΑθήνα".isalpha()
True
>>> c.isascii()
True
>>> "Viñé".isascii()
False
>>> "10.10".isdecimal()
False
>>> "-10".isdecimal()
False
>>> "10".isdecimal()
True
>>> "__init_007__".isidentifier()
True
>>> "007_init".isidentifier()
False
>>> "viñé cárdaba".islower()
True
>>> "__[viñé.007]".islower()
True
>>> "VIÑÉ\n\tCÁRDABA".isupper()
True
>>>

Estos métodos son también bastante sencillos de entender. isalnum(), isalpha(), isascii() e isdecimal() requieren que todos los caracteres de la cadena sean del tipo adecuado, si intercalamos espacios o signos de puntuación devolverán False. isalnum() acepta tanto letras como números de cualquier clase (el criterio se basa en el tipo de caracter que corresponde a cada código en unicode. Corresponde al valor devuelto por el método ud.category() de la biblioteca unicodedata). isalnum() engloba la unión de los conjuntos que producirían isalpha() e isdecimal().

isalpha() da por válidos todo tipo de caracteres alfabéticos, no solo los acentuados y la "ñ" del español, sino numerosos caracteres de otros idiomas, en cambio isdecimal() solo acepta dígitos del 0 al 9.

isidentifier() nos indica si una cadena puede usarse como nombre de identificador, lo cual no implica que tal identificador haya sido asignado o no. Por último, isupper() e islower() evalúan los caracteres alfabéticos ignorando la presencia de espacios o símbolos de puntuación, y devuelven True si todas las letras en la cadena son mayúsculas o minúsculas.

Veamos las funciones que producen una salida formateada:

>>> c.center(40,"-")
'-------elemental, querido Watson--------'
>>> c.ljust(40,"*")
'elemental, querido Watson***************'
>>> c.rjust(40," ")
'elemental, querido Watson'
>>> c.rjust(40,"<>")
Traceback (most recent call last):
File "<pyshell#6>", line 1, in <module>
c.rjust(40,"<>")
TypeError: The fill character must be exactly one character long

>>>

Estos tres métodos son similares, se usan para presentar la cadena dentro de un espacio determinado (Si el espacio fuese igual o menor a la longitud de la cadena inicial, se devolvería una copia de dicha cadena). También podemos seleccionar un único caracter de relleno para el espacio añadido.

>>> print("Nombre: {}\nEdad: {}\nEstado civil: {}".format("Watson", 29, "Soltero"))
Nombre: Watson
Edad: 29
Estado civil: Soltero
>>> v=97
>>> "{0:b}b {2:o}o {3:d} {1:X}h {0:c}".format(v, v//2, -v, v+100)
'1100001b -141o 197 30h a'
>>> "{v:#x} {0}".format(False, v=99)
'0x63 False'
>>> "{0:d} {num:04X}h {3:>010b}b {1:s}".format(100, c[19:], 0, 15, num=257)
'100 0101h 0000001111b Watson'
>>>

El método format() se aplica sobre una cadena con la misma sintaxis que las cadenas de formato, y presenta sus argumentos tal y como especifique la cadena. Podemos referirnos a tales argumentos según sus posiciones correlativas como en el primer ejemplo, por un índice numérico como en el segundo , o por nombres. Podemos mezclar índices y nombres como en los ejemplos tercero y cuarto, pero los argumentos con nombre deben ir a continuación de aquellos que no lo tienen. Si usamos índices o nombres no podemos usar el modo posicional por defecto.

>>> ".".join("elemental")
'e.l.e.m.e.n.t.a.l'
>>> "><".join("0123456789")
'0><1><2><3><4><5><6><7><8><9'
>>> " - ".join(["Lunes","Martes","Miércoles"])
'Lunes - Martes - Miércoles'
>>>

El método join() se aplica a una cadena que actúa como separador de los valores de su argumento. Si el argumento consiste en una cadena, separa sus caracteres, pero su principal aplicación es sobre secuencias de cadenas (listas o tuplas), que presenta intercalando el o los caracteres del separaror entre cada item de la secuencia.

Hay seis métodos para localizar subcadenas dentro de otra:

>>> c.find("Watson")
19
>>> c.index(",")
9
>>> c.find("Holmes")
-1
>>> c.index("Holmes")
Traceback (most recent call last):
  File "<pyshell#48>", line 1, in <module>
c.index("Holmes")
ValueError: substring not found

>>> c.rfind("e")
13
>>> c.rindex("n")
24
>>> if not c.endswith("."): c+="."

>>> c
'elemental, querido Watson.'
>>> c="www.miguelangelviñé.com"
>>> c.endswith((".es",".org",".com"))
True
>>> c.startswith("www.")
True
>>>

find() y rfind() buscan la aparición de una subcadena desde la izquierda y la derecha respectivamente. La diferencia frente a los métodos index() y rindex() es que aquellos devuelven el valor -1 si no se encuentra la subcadena y estos producen un error en tal caso. startswith() y endswith() buscan en los extremos de la cadena y devuelven True si encuentran una coindicencia. Como argumento podemos pasar una única subcadena o una secuencia de subcadenas, en el último caso si encuentra cualquiera de ellas devuelve True.

Hay cinco métodos que dividen la cadena en fragmentos, dos de ellos usan un separador para partir la cadena en tres fragmentos, otros dos separan por palabras y el quinto por líneas:

>>> c="e_mail@miguelangel_viñé.com"
>>> c.partition("@")
('e_mail', '@', 'miguelangel_viñé.com')
>>> c.partition("_")
('e', '_', 'mail@miguelangel_viñé.com')
>>> c.rpartition("_")
('e_mail@miguelangel', '_', 'viñé.com')
>>>
>>> c="Volverán las oscuras golondrinas"
>>> c.split()
['Volverán', 'las', 'oscuras', 'golondrinas']
>>> c.rsplit()
['Volverán', 'las', 'oscuras', 'golondrinas']
>>> c.split(None,2)
['Volverán', 'las', 'oscuras golondrinas']
>>> c.rsplit(None,2)
['Volverán las', 'oscuras', 'golondrinas']
>>> c.split("o")
['V', 'lverán las ', 'scuras g', 'l', 'ndrinas']
>>>
>>> c='''y pues es quien hace iguales
al rico y al pordiosero,
poderoso caballero
es don Dinero.'''

>>>c
'y pues es quien hace iguales\nal rico y al pordiosero,\npoderoso caballero\nes don Dinero.'
>>> c.splitlines()
['y pues es quien hace iguales', 'al rico y al pordiosero,', 'poderoso caballero', 'es don Dinero.']
>>> c.splitlines(1)
['y pues es quien hace iguales\n', 'al rico y al pordiosero\n,', 'poderoso caballero\n', 'es don Dinero.']
>>>

partition() divide una cadena en tres partes usando una sección como "bisagra". La versión normal usa la primera aparicion del separador y rpartition() emplea la última. De no encontrarse el fragmento elegido como separador partition() devuelve una tupla con la cadena original y dos cadenas vacías, y rpartition() devuelve una tupla con dos cadenas vacías y la cadena original en último lugar.

split() y rsplit() dividen una cadena en una lista de palabras, sin argumentos funcionan exáctamente igual. Si indicamos un primer argumento distinto de None utilizan el caracter o grupo indicado como separador de palabras, si no se indican argumentos o el primero es None se parten las palabras en los espacios en blanco. El segundo argumento indica el número máximo de palabras a separar, el resto de la frase queda en una sola cadena.

splitlines() divide un texto en líneas. Si añadimos un argumento que se evalúe como True mantiene los caracteres de salto de línea en las cadenas resultantes.

A continuación tenemos tres funciones que eliminan espacios en blanco al pricipio y/o al final de la cadena, y otra que reemplaza unas subcadenas por otras nuevas:

>>> c=10*" "+"¡Ferpectamente!"+10*" "
'¡Ferpectamente!'
>>> c.lstrip()
'¡Ferpectamente!'
>>> c.strip()
'¡Ferpectamente!'
>>> c.rstrip()
'¡Ferpectamente!'
>>> c.replace(" ","")
'¡Ferpectamente!'
>>> c=c.replace(" ",".",10)
>>> c
'..........¡Ferpectamente!'
>> c.strip(" .")
'¡Ferpectamente!'
>>>

El método strip() y sus variantes lstrip() y rstrip() eliminan los espacios al principio o final de la cadena, o en ambos extremos. En realidad podemos especificar una cadena como argumento y se eliminará cualquier caracter que esté en dicha cadena. EL método replace() sustituye las subcadenas que coincidan con el primer argumento por la especificada en el segundo argumento, pudiendo añadir un tercero con el número máximo de sustituciones a efectuar.

El último método a comprobar es count() que cuenta el número de apariciones de una subcadena.

>>> c="Es bueno el deleitarse con el bello cielo"
>>> c.count("el")
5
>>>

Vemos que no repara si la subcadena es una palabra entera o parte de una, si hubiésemos querido contar las apariciones del artículo determinado masculino tendríamos que haber tecleado:

>>> c.split().count("el")
2
>>>

Fïjate en que el resultado del método split() es una lista de cadenas. El método count() puede aplicarse también a las listas y a los demás tipos de secuencias. Esto ocurre con una cierta cantidad de los métodos que acabamos de ver. Más adelante miraremos más en detalle los otros tipos de secuencia, ahora vamos a mirar un método de str que genera un nuevo tipo de datos que hasta ahora no habíamos incorporado a nuestro repertorio, y que consisten en la transformación directa entre textos y su representación numérica.

>>> c="Abecedario"
>>> b=c.encode()
>>> b
b'Abecedario'
>>> type(b)
<class 'bytes'>
>>>

Acabamos de presentar en sociedad la clase bytes.

2.1.4 Secuencias de bytes: bytes y bytearray

Los dos tipos de secuencias de bytes son similares, se diferencian en que bytes es un tipo inmutable y bytearray es mutable. Ambos corresponden a cadenas de números de 0 a 0xFF (0-255, los valores que se pueden representar con 8 bits). Como los valores 0x20 a 0x7F (32-127) corresponden al antiguo juego de caracteres ASCII, podemos asimilar las secuencias de bytes o bytearray con cadenas de texto, siempre que no usemos caracteres fuera del rango 32-127. Los literales de tipo bytes se escriben como una cadena de texto con el prefijo b.

Veamos unos ejemplos de código, como habitualmente, para entender un poco más:

>>> bytes()
b''
>>> bytes(4)
b'\x00\x00\x00\x00'
>>> bytes(range(4))
b'\x00\x01\x02\x03'
>>> bytes("Hola","utf-8")
b'Hola'
>>>
>>> bytearray()
bytearray(b'')
>>> bytearray(4)
bytearray(b'\x00\x00\x00\x00')
>>> bytearray(range(4))
bytearray(b'\x00\x01\x02\x03')
>>> bytearray("Hola","utf-8")
bytearray(b'Hola')
>>> type(_)
<class 'bytearray'>
>>>

Mientras que la clase bytes dispone de una forma de expresar literales la clase bytearray no la tiene, se representa mediante el nombre de la clase y una secuencia de tipo bytes entre paréntesis. Vemos que para usar un texto tenemos que emplear un segundo parámetro con la codificación empleada. Esto se debe a que existen varios sistemas para codificar el juego de símbolos de unicode. De momento es suficiente con usar el valor "utf-8", que es el que usa Python. Si descargáramos datos desde un fichero de texto tendríamos que usar la misma codificación que haya empleado el programa que haya creado el texto.

Vemos que podemos construir las secuencias de bytes de cuatro formas básicas:

En este caso el objeto era una cadena de texto, ya veremos más cosas sobre objetos según avancemos en el aprendizaje del lenguaje.

Estas clases de secuencias son importantes porque como dijimos al comienzo de la sección, absolutamente todo lo que maneja el ordenador está formado por secuencias de bytes. bytes puede contener la representación de cualquier dato, no solo textos sino imágenes, sonidos, etc.

Como los bytes se representan mediante dos dígitos hexadecimales, la forma natural de presentar estos valores (cuando no se correspondan con caracteres ASCII imprimibles) es mediante valores hexadecimales. También existe un método de clase (bytes.fromhex()) que convierte cadenas con pares de dígitos hexadecimales en cadenas de bytes. Un método de clase se caracteriza porque no se aplica sobre un objeto de la clase, sino que hay que usar el propio nombre de clase a la izquierda del nombre del método.

>>> hexstr="2EF0 F1F2"
>>> b=bytes.fromhex(hexstr)
>>> b
b'.\xf0\xf1\xf2'
>>> b.hex()
'2ef0f1f2'
>>> b.hex("-")
'2e-f0-f1-f2'
>>> b.hex(".",2)
'2ef0.f1f2'
>>>

Los espacios dentro de la cadena hexadecimal se ignoran, así como saltos de línea y demás. El método opuesto es .hex(), que devuelve una cadena con los valores hexadecimales de la secuencia de bytes. Podemos usar un argumento con un caracter de separación entre bytes, y un segundo valor indicando cada cuantos bytes se intercala el separador.

Podemos convertir una cadena de bytes en una lista con los valores decimales correpondientes mediante el uso de list():

>>> list(b)
[46, 240, 241, 242]
>>>

También podemos usar índices y porciones como en otros tipos de secuencias:

>>> b[3]
242
>>> type(_)
<class 'int'>
>>> b[2:]
b'\xf1\xf2'
>>> type(_)
<class 'bytes'>
>>>

Un índice nos devuelve un valor de tipo entero, mientras que una porción nos devuelve un objeto de tipo bytes. La clase bytearray admite los mismos métodos que hemos visto para su contrapartida inmutable.

Las operaciones que son comunes a todos los tipos de secuencias se detallan a continuación:

OperaciónResultado
x in s True si algún elemento de s es igual a x
False en caso contrario
(En secuencias de caracteres o bytes busca subsecuencias)
x not in s False si algún elemento de s es igual a x
True en caso contrario
(En secuencias de caracteres o bytes busca subsecuencias)
s1 + s2 Concatenación de las secuencias s1 y s2
(Si las secuencias son inmutables devuelve una nueva secuencia)
s * n
n * s
Concatenación de s consigo misma n veces
(Si las secuencias son inmutables devuelve una nueva secuencia)
(Si n es menor o igual a cero devuelve una secuencia vacía del mismo tipo)
s[i] Elemento de índice i de s, contando desde 0
Índices negativos cuentan desde el final
(Error si el índice está fuera de rango)
s[i:j] Porción de los elementos entre i y j
Si j es mayor que i devuelve una secuencia vacía
s[i:j:k] Porción de los elementos entre i y j
tomados cada k elementos
len(s) Longitud (número de items) de s
min(s) Menor de los elementos de s
max(s) Mayor de los elementos de s
s.index(x)
s.index(x,i)
s.index(x,i,j)
Índice de la primera aparición de x dentro de s
o dentro de una porción de s
(En secuencias de caracteres o bytes busca subsecuencias)
(Error si no se encuentra x)
s.count(x) Número de veces que aparece x en s
(En secuencias de caracteres o bytes busca subsecuencias)

Los tipos byte y bytearray son la forma natural de leer y escribir ficheros, independientemente de su contenido, puesto que todo en el fondo es reducido a secuencias de bytes. Vamos a hacer nuestra primera incursión con ficheros:

# File2bytes
# Programa para leer un fichero

ANCHO=16
FICHERO=input("Fichero: ")

with open(FICHERO,"rb") as f:
b=f.read()

i=0
while i<len(b):
for j in range(min(ANCHO,len(b)-i)):
print(f"{b[i+j]:02X}",end="")
print((ANCHO-j-1)*2*" "+" - ",end="")
for j in range(min(ANCHO,len(b)-i)):
ch=b[i+j]
if ch<32:
print(chr(ch+0x2400),end="")
elif ch<127:
print(f"{ch:c}",end="")
else:
print(chr(0x25b7),end="")
i+=j+1
print()

Una somera explicación del programa: Se trata de leer el contenido de un fichero y de mostrar en dos columnas el contenido en valores numéricos (hexadecimales) a la izquierda y la representación textual a la derecha. La variable ANCHO en la línea 4 indica cuantos bytes/caracteres se muestran en cada línea, es decir, la anchura de las columnas.

Introducimos en la línea 5 el nombre del fichero a leer, y a continuación usamos una estructura nueva, el bloque with. Sin entrar en muchos detalles de momento, cuando hacemos operaciones con ficheros se requieren tres pasos básicos:

Si el primer paso falla, simplemente no conseguiremos abrir ningún fichero, pero si en el segundo paso se producen errores, el fichero quedaría marcado por el sistema operativo con el estado de "abierto" y eso bloquearía el fichero para cualquier cosa que queramos hacer a continuación. Tendríamos que reiniciar el sistema para corregirlo, y eso no es la mejor alternativa. La sentencia with garantiza que si se produce algún error, antes de salir del programa se realizarán las necesarias tareas de limpieza, en este caso cerrar el fichero. En general, siempre que manejemos ficheros usaremos un bloque with como mecanismo de seguridad.

La función open() recibe un nombre de fichero y opcionalmente una cadena con el modo de apertura. Nosotros lo abrimos solo para lectura ("r" de read) y en modo binario ("b"). En la segunda parte de esta sección trabajaremos más con ficheros. En la línea 8 leemos el fichero entero en la variable b, como hemos elegido un modo binario la variable adoptará el tipo bytes. En el momento en que abandonemos el bloque with Python realiza el cierre del fichero.

A continuación presentamos el fichero. Usamos la variable i como indice para ir recorriendo el fichero hasta el final mediante el bucle while de la línea 11. Para cada posición, mostramos mediante sendos bucles los valores hexadecimales y el texto. Si nos quedan menos de ANCHO bytes para terminar, mostramos solo los restantes usando la función min(), que selecciona el menor de una serie de valores.

En la línea 14 imprimimos un guión entre espacios para separar las columnas, nos aseguramos de alinear la segunda columna por si hemos impreso menos de ANCHO bytes, usando la expresión (ANCHO-j-1)*2*" "

Dentro del bucle que muestra el contenido como texto comprobamos primero si el código del caracter es menor de 32, y en tal caso imprimimos el caracter unicode que representa el código de control correspondiente. Si estamos en el rango imprimible mostramos el caracter, y en otro caso mostramos el caracter "▷"

En las líneas 23 y 24 actualizamos el índice e imprimimos un salto de línea.

Antes de ejecutar el programa crea un archivo con el bloc de notas (notepad) con el siguiente contenido:

En un lugar de la mancha
de cuyo nombre no quiero acordarme

Guárdalo con la codificación: UTF-8 dándole como nombre: Quijote8.txt, luego (usando la opción Archivo->Guardar como...) guárdalo con el nombre QuijoteLE.txt y codificación UTF-16 LE y también como QuijoteBE.txt con la codificación UTF-16 BE. Luego ejecuta el programa recién tecleado tres veces introduciendo los tres nombres de fichero que acabamos de crear, sin cerrar la ventana de salida de IDLE (Pyhton Shell). Obtendrás lo siguiente:

============= RESTART: C:/Users/User/Documents/Python/File2bytes.py =============
Fichero: quijote8.txt
456E20756E206C75676172206465206C - En un lugar de l
61204D616E6368610D0A646520637579 - a Mancha␍␊de cuy
6F206E6F6D627265206E6F2071756965 - o nombre no quie
726F2061636F726461726D65 - ro acordarme

>>>
============= RESTART: C:/Users/User/Documents/Python/File2bytes.py =============
Fichero: quijotele.txt
FFFE45006E00200075006E0020006C00 - ▷▷E␀n␀ ␀u␀n␀ ␀l␀
75006700610072002000640065002000 - u␀g␀a␀r␀ ␀d␀e␀ ␀
6C00610020004D0061006E0063006800 - l␀a␀ ␀M␀a␀n␀c␀h␀
61000D000A0064006500200063007500 - a␀␍␀␊␀d␀e␀ ␀c␀u␀
79006F0020006E006F006D0062007200 - y␀o␀ ␀n␀o␀m␀b␀r␀
650020006E006F002000710075006900 - e␀ ␀n␀o␀ ␀q␀u␀i␀
650072006F002000610063006F007200 - e␀r␀o␀ ␀a␀c␀o␀r␀
6400610072006D006500 - d␀a␀r␀m␀e␀

>>>
============= RESTART: C:/Users/User/Documents/Python/File2bytes.py =============
Fichero: quijotebe.txt
FEFF0045006E00200075006E0020006C - ▷▷␀E␀n␀ ␀u␀n␀ ␀l
00750067006100720020006400650020 - ␀u␀g␀a␀r␀ ␀d␀e␀
006C00610020004D0061006E00630068 - ␀l␀a␀ ␀M␀a␀n␀c␀h
0061000D000A00640065002000630075 - ␀a␀␍␀␊␀d␀e␀ ␀c␀u
0079006F0020006E006F006D00620072 - ␀y␀o␀ ␀n␀o␀m␀b␀r
00650020006E006F0020007100750069 - ␀e␀ ␀n␀o␀ ␀q␀u␀i
00650072006F002000610063006F0072 - ␀e␀r␀o␀ ␀a␀c␀o␀r
006400610072006D0065 - ␀d␀a␀r␀m␀e

>>>

La primera observación es que los ficheros codificados en los modos UTF-16 son el doble de largos, cada caracter ocupa 16 bits, es decir, dos bytes. Como estamos en el rango ascii el byte extra siempre es 0 (El caracter ascii NULL). También se ve rápidamente que el salto de línea se convierte en la secuencia CR-LF de caracteres de control. CR por retorno de carro (carriage return) y LF por Alimentación de línea (line feed).

El modo UTF-8 permite codificar el juego de caracteres unicode empleando tan solo un byte cuando es posible. Mientras usemos texto normal esto será así, con lo cual vemos que el fichero quijote8.txt contiene exáctamente los caracteres del texto como si fuera una cadena de Python. Los otros modos se diferencian en el orden de los bytes que forman cada palabra de 16 bits. Básicamente existen dos sistemas para agrupar valores de más de un byte: little-endian y big-endian (se corresponden con los sufijos LE y BE de los modos de codificación). El primer sistema ordena los bytes del menos significativo al más significativo, y el otro en orden inverso.

Simplemente, es importante conocer este orden cuando procesemos secuencias de bytes o recibiremos muchas sorpresas.

2.1.5 Imágenes en forma de números: pixels

Vemos que codificar texto es relativamente sencillo, pero hay muchos más datos que podemos representar mediante convenciones numéricas. En el mundo de interfaces gráficos de los sistemas operativos actuales uno de los principales son las imágenes.

A continuación tienes una imagen para descargar en distintos formatos. En Firefox usa click derecho y la opción Guardar imagen como... para guardarlas en tu directorio de los programas Python.

Una vez guardadas vamos a modificar nuestro programa de lectura de ficheros para ver su contenido binario

# File2hex
# Programa para visualizar el contenido de ficheros

ANCHO=16
EXT=("gif","png","jpg")
FILENAME="Gradiente."

for s in EXT:

with open(FILENAME+s,"rb") as f:
bytefile=f.read()

print("Fichero:",FILENAME+s)
print(len(bytefile),"bytes")
print(64*"═")

i=0
while i<len(bytefile):
binario=bytefile[i:i+ANCHO].hex()
texto=""
for ch in bytefile[i:i+ANCHO]:
if ch<32:
texto+=chr(0x25c1)
elif ch<127:
texto+=chr(ch)
else:
texto+=chr(0x25b7)

print(binario+(ANCHO*2-len(binario))*" "+" - "+texto)
i+=ANCHO

print()

En este caso el nombre del fichero y las tres extensiones están incorporados en las líneas 5 y 6. Hacemos un bucle para cada extensión (es decir, para cada fichero) concatenando el nombre y la extensión para obtener los nombres completos. En las líneas 10-11 abrimos y leemos el fichero, descargándolo en la variable bytefile. Imprimimos un encabezamiento con el nombre del fichero, su longitud y una línea de separación.

El bucle while de la línea 10 hasta el final muestra cada uno de los ficheros. Usamos un índice pero esta vez guardamos los valores hexadecimales en la cadena binario y la representación en forma de caracteres en la variable texto, así no necesitamos un segundo bucle. En la línea 29 imprimimos las dos columnas y en la 30 incrementamos el índice. Entre fichero y fichero dejamos una línea de separación gracias a la línea 32. Esta vez la salida es bastante más extensa. Ejecútalo y lo comprobarás.

La imagen que se repite en cada formato de fichero es un degradado de arriba hacia abajo entre azul y blanco. Las dimensiones en pixels son 32 x 32 y la profundidad de color en el GIF es de 8 bit y en los demás de 24 bits. Esto representa unos tamaños de:

FormatoDimensionesBits por pixelTamaño imagenTamaño fichero
GIF 32 x 32 8 bpp 32*32=1024 1655 bytes
PNG 32 x 32 24 bpp 32*32*3=3072 755 bytes
JPG 32 x 32 24 bpp 32*32*3=3072 659 bytes

- El formato GIF es ya antiguo, y está limitado a una profundidad de color de 256 valores. Vemos que ocupa más el fichero de lo que ocupan los datos de la imagen. La ventaja del GIF es que permite crear imágenes animadas.

- El formato PNG es moderno y tiene una buena tasa de compresión. Vemos que el fichero ocupa mucho menos que la imagen. Las ventajas es que es un formato libre de uso y que permite almacenar un canal alfa con la transparencia de la imagen

- El formato JPG es posiblemente el más extendido actualmente y tiene una muy buena tasa de compresión. No es un formato libre

Respecto a la salida del programa, vemos que los tres ficheros tienen al principio una zona en la que podemos distinguir algún segmento de texto, corresponde con la cabecera del fichero, donde se indican datos acerca de la resolución y profundidad de color, tipo de codificación, algoritmo de compresión, etc. A continuación los datos de la imagen no tienen ningún sentido contemplados como texto.

Este tipo de programa que hemos utilizado se denomina Visor hexadecimal. Una progreso sería poder modificar los valores y guardar una nueva versión del fichero, entonces sería un Editor hexadecimal. Sabiendo lo que se hace podemos modificar incluso programas ejecutables.

La biblioteca PILLOW

Vamos a descargar y emplear una biblioteca que maneja imágenes. Para instalarla usa el símbolo del sistema y teclea el comando:

pip install Pillow

Si todo es correcto se instalará la biblioteca y la tendremos a partir de ahora disponible para nuestros programas.

Volvamos al fichero gradiente.png, esta vez vamos a abordarlo de un modo gráfico:

#Pillow_1

from PIL import Image

# Cargamos la imagen desde el fichero
im = Image.open("Gradiente.png")
#Mostramos el formato, el tamaño y el modo de color
print(im.format, im.size, im.mode)
# Mostramos la imagen
im.show()

#Redimensionamos la imagen
im_big = im.resize((256, 256))
im_big.show()

#Rotamos la imagen redimensionada
im_rot = im_big.rotate(45)
im_rot.show()

#Rotamos la imagen redimensionada, expandiendo el tamaño
im_rot = im_big.rotate(45, expand=1)
im_rot.show()

La biblioteca pillow manipula imágenes, y para mostrarlas utiliza el visor que tengamos configurado por defecto en el sistema. En mi caso utilizo IrfanView, un programa libre. Al ejecutar el programa anterior obtenemos un resultado en la consola, como consecuencia de la función print() de la línea 8, y cuatro ventanas con imágenes por cada método imagen.show(). Hay que hacer notar que cada vez que mostramos una imagen el programa se detiene, podemos cerrar el símbolo de sistema que también se abrirá para que continúe, manteniendo abiertas las cuatro ventanas con gráficos.

============= RESTART: C:/Users/User/Documents/Python/Pillow_1.py =============
PNG (32, 32) RGB
>>>

El método de clase:   Image.open(), con un nombre de fichero como argumento, abre dicho fichero y delvuelve un objeto de clase Image. En la línea 8 empleamos tres propiedades del objeto, que nos indican el formato del fichero, su resolución y modo de color. A continuación el método .show() que, aplicado a un objeto de la clase Image abre el visualizador del sistema con la imagen.

Como muestra de las posibilidades de la clase hemos usado los métodos .resize(), que recibe una tupla con las nuevas dimensiones (ancho, alto), y .rotate(), que gira la imagen en sentido trigonométrico los grados indicados por el primer argumento. Si no indicamos el argumento con nombre expand se mantiene el tamaño, con lo cual partes de la imagen pueden salirse del cuadro. Si indicamos cualquier valor distinto de False se redimensiona la imagen para contener las esquinas.

En realidad no necesitamos un fichero para crear la imagen del gradiente.

#Pillow_2

from PIL import Image

im0=Image.new("RGB", (256, 256), "#0044FF")
im1=Image.linear_gradient("L")
im1=im1.convert("RGB")

im=Image.blend(im0, im1, 0.5)
im.show()

Ejecuta el programa y obtendrás la siguiente imagen:

En este caso hemos empleado tres nuevos métodos de clase:

Image.new() sirve para crear una nueva imagen. Los parámetros son una cadena indicando el modo, una tupla indicando las dimensiones y, opcionalmente, el color. Hemos empleado una cadena especificando los tres componentes de color RGB, correspondiendo el valor 00 al rojo, 0x44 al verde y 0xFF al azul.

Image.linear_gradient() crea una imagen de 256x256 pixels con un gradiente en tonos de gris desde el negro en la parte superior al blanco en la inferior y 8 bits por pixel. Este último modo es el que hemos indicado con el argumento "L". Si intentamos usar modos RGB produce un error.

Image.blend() mezcla dos imágenes que indicaremos en los dos primeros argumentos empleando un canal alfa (un canal en el que los valores indican grados de opacidad, desde 1, que es la máxima, a 0, que es total transparencia). Las dos imágenes deben ser del mismo tamaño y modo de color.

El método convert() se aplica sobre un objeto de la clase Image y sirve para cambiar el modo de color de una imagen. Convertimos im1 de tonos de gris a modo de color RGB para que coincida con im0.

Ya que tenemos que manejar clases, objetos y métodos, vamos a aclarar algún concepto relativo a la OOP (Programación orientada a objetos, object-oriented programming). En la programación procedural, manejamos por una parte los datos (aquello que solemos guardar en variables) y el código para manipular dichos datos (solemos emplear funciones y código en general).

La OOP define clases, a partir de las cuales podemos crear objetos. La definición de una clase tiene paralelismos con la de una función, pero en este caso la clase define atributos (que son datos) y métodos (que son código) asociados a cada objeto o a la propia clase. Una vez definida la clase, utilizamos su nombre como función para crear objetos (instancias) de dicha clase. Una función que crea objetos es un constructor, y al hecho de crear un objeto lo denominamos instanciación.

Dicho esto, hay atributos que son compartidos por toda la clase, y métodos que se invocan con el nombre de la clase y no sobre uno de los objetos, esto es los que llamamos atributos o métodos de clase, contraponiéndose a los atributos y métodos de los objetos.

Vamos a ver más posibilidades de la biblioteca Pillow. EL gradiente anterior resulta más oscuro que el original, dado que consiste en un fondo azul oscurecido por matices de gris. Podemos mezclar dos imágenes con azul y blanco y usar una máscara para pasar de uno a otro color:

#Pillow_3

from PIL import Image

im0=Image.new("RGB", (256, 256), "#0044FF") #Fondo azul
im1=Image.new("RGB", (256, 256), "#FFFFFF") #Fondo blanco
im2=Image.linear_gradient("L") #Máscara

im=Image.composite(im0, im1, im2.rotate(180))
im.show()

En método de clase Image.composite() se parece a Image.blend() que empleamos antes, pero en este caso usamos un fichero como máscara, y no un valor constante de transparencia. El fichero puede ser monocromo, en escala de grises o RGB. La imagen obtenida en este caso es la siguiente:

Mientras el módulo Image define una clase para contener imágenes con métodos para abrir o crear ficheros y modificar sus características, el módulo ImageDraw define su propia clase, que a su vez permite utilizar primitivas de dibujo para pintar sobre ella.

#Pillow_4

ANCHO=256
ALTO=256

def lineH(y, color, grosor): #Línea a lo ancho de la imagen
imD.line((0, y, ANCHO, y), color, grosor)

from PIL import Image, ImageDraw

im=Image.new("RGB", (ANCHO, ALTO))
imD=ImageDraw.Draw(im)
for y in range(257):#Creamos un gradiente vertical de arriba abajo
color=f"#{y:02X}{y:02X}FF"#modificando los componentes rojo y verde
lineH(y, color, 1)#mientras trazamos líneas horizontales

im.show()

Este programa produce una imagen idéntica a las del primer ejemplo, vamos a hacer un par de ejemplos más de gradientes procedurales (esto es, que se crean mediante la ejecución de un código.

#Pillow_5

ANCHO=256
ALTO=256

from PIL import Image, ImageDraw

#Doble gradiente diagonal
im=Image.new("RGB", (ANCHO, ALTO))
imD=ImageDraw.Draw(im)
for i in range(256):
color=f"#{255-i:02X}FF{255-i:02X}"
imD.line((i, 0, 255, 255-i), color, 1)
imD.line((0, i, 255-i, 255), color, 1)
im.show()

#Gradiente en esquina
im=Image.new("RGB", (ANCHO, ALTO))
imD=ImageDraw.Draw(im)
for i in range(256):
color=f"#FF{i:02X}{i:02X}"
imD.rectangle((i, i, 255, 255), color)
im.show()

#Gradiente circular
im0=Image.new("RGB", (ANCHO, ALTO), "#00FFFF")
im1=Image.new("RGB", (ANCHO, ALTO), "#FFFF00")
im2=Image.radial_gradient("L")
im=Image.composite(im0, im1, im2)
im.show()

Para el gradiente en esquina hemos empleado un nuevo método: ImD.rectangle(), siendo imD un objeto de la clase ImageDraw. Este método pinta un rectángulo entre dos puntos que se indican por sus cuatro coordenadas (x0, y0, x1, y1) seguidas por un color de relleno. En el caso del gradiente circular hemos aprovechado el método de clase Image.radial_gradient, que genera una imagen de 256x256 pixels con un gradiente en tonos de gris. Hemos repetido el método anterior de usar dicho gradiente como máscara para mexclar unas imágenes en amarillo y cian. El resultado son estas tres imágenes:

Más adelante veremos con mayor detalle las posibilidades de Pillow.

La biblioteca TURTLE

El módulo turtle, perteneciente a la biblioteca estándar, está dedicado al trazado de imágenes. Está basado en un lenguaje de programación llamado Logo. El Logo es un lenguaje que genera gráficos basados en el movimiento de un punto (representado normalmente por una tortuga) a través de un plano trazando líneas.

turtle tiene una característica que nos gusta mucho para el aprendizaje, podemos usarlo desde el intérprete interactivo y ver el resultado de nuestras órdenes al instante. No necesitamos usar PiP para descargar el módulo, puesto que es parte de la distribución de Python, dentro de la biblioteca estándar.

>>> import turtle as t
>>> t.color("red", "yellow")
>>>

Al introducir la segunda línea se abre la ventana de turtle con un lienzo en blanco, y el cursor (por defecto un triángulo) en el centro con los colores seleccionados: primer plano rojo, fondo amarillo.

Podemos cambiar el aspecto con la función turtle.shape(), que acepta una cadena de caracteres con la forma elegida.

>>> t.shape("turtle")
>>> # Opciones: arrow, turtle, circle, square, triangle, classic
>>>

Ahora hay una pequeña tortuga perfilada en rojo y rellena de amarillo en el centro del lienzo. La mayoría de las funciones que veremos a continuación devuelven el valor de la característica correspondiente si se invocan sin argumentos, o lo modifican si se invocan con argumentos. Las características que nos interesan del pincel (pen) representado por la tortuga son:

CaracterísticaFunciónDescripción
color turtle.color()
turtle.pencolor()
turtle.fillcolor()
Obtiene o modifica los colores de trazo y fondo
Obtiene o modifica el color del trazo
Obtiene o modifica el color del relleno
grosor turtle.pensize()
turtle.width()
Obtiene o modifica el grosor del trazo
dirección turtle.heading()
turtle.setheading()
Obtiene el valor del ángulo hacia el que apunta el cursor
Configura el ángulo hacia el que apunta el cursor
velocidad turtle.speed() Obtiene o modifica la velocidad de movimiento del cursor
1...10 = lento...rápido, 0 = máximo
estado
de
trazado
turtle.isdown()
turtle.pendown()
turtle.penup()
Devuelve True si la pluma está bajada, False en caso contrario
Baja la pluma, también: turtle.down() o turtle.pd()
Sube la pluma, también: turtle.up() o turtle.pu()
relleno turtle.filling()
turtle.begin_fill()
turtle.end_fill()
Devuelve True si el modo de relleno está activado
Se invoca antes de trazar una forma para activar el relleno
Se invoca al finalizar las formas para que se realize el relleno
posición turtle.position()
turtle.pos()
Devuelve las coordenadas del cursor

Vamos a ver cómo reacciona nuestra pequeña tortuga a diferentes órdenes:

>>> t.speed(7)
>>> t.forward(100)
>>> t.left(90)
>>> t.backward(100)
>>> t.towards(0,0)
135.0
>>> t.setheading(_)
>>> t.distance(0,0)
141.4213562373095
>>> t.fd(_)
>>>

La ventana de turtle mostrará esto:

turtle.forward (turtle.fd) mueve la tortuga hacia delante. Cuando realizamos movimientos se pintará un trazo del color activo si la pluma está bajada. De la misma manera, turtle.backward (turtle.back o turtle.bk) mueve hacia atrás. turtle.left (turtle.lt) gira a la izquierda mientras que turtle.right (turtle.rt) gira hacia la derecha. Te habrás fijado en que existen siempre formas alternativas o abreviadas para cada orden.

turtle.towards nos indica el ángulo que hay hacia las coordenadas que indiquemos como argumentos. Empleamos el resultado para apuntar nuesta tortuga. turtle.distance devuelve la distancia que hay hacia esas coordenadas. Podríamos haber resuelto más fácilmente el último tramos de la siguiente forma:

>>> t.undo()
>>> t.goto(0,0)

turtle.undo deshace el último paso realizado y turtle.goto (turtle.setposition o turtle.setpos) mueve la tortuga hasta el punto indicado.

Vamos a ver las otras primitivas de dibujo de que disponemos.

>>> t.dot(10,"green")
>>> t.up()
>>> t.fd(100)
>>> uno=t.stamp()
>>> t.setheading(0)
>>> t.fd(100)
>>> t.clearstamp(uno)
>>> t.down()
>>> t.circle(100,180)

En realidad, además de las órdenes de movimiento disponemos de pocas instrucciones para pintar; turtle.dot traza un punto del diámetro especificado en el primer argumento, o de max(turtle.pensize()+4, 2*turtle.pensize()) si el argumento es None, y del color indicado en el segundo argumento o del color del trazo si no se indica.

turtle.stamp() estampa una copia del cursor en la posición actual, devuelve un identificador que podemos almacenar. turtle.clearstamp(id) borra el estampado indicado por el identificador del argumento mientras que turtle.clearstamps(n) borra todos los estampados si no indicamos ningún argumento, los primeros n estampados si es positivo y los últimos si es negativo.

Para finalizar, turtle.circle(radio) traza un arco de círculo del radio indicado. El centro del círculo está a la distancia radio a la izquierda del cursor (o sea, a 90 grados de la dirección a la que apunte). Podemos indicar un segundo argumento con la extensión en grados que será trazada, o un tercer argumento con el número de pasos empleados. Podemos emplear el identificador steps para referirnos a este último argumento, y nos permite trazar polígonos.

>>> t.home()
>>> t.clear()
>>> t.circle(100,steps=3)
>>> t.pencolor("green")
>>> t.circle(100,steps=4)
>>> t.pencolor("blue")
>>> t.circle(100,steps=5)
>>> t.pencolor("gray")
>>> t.circle(100,steps=6)
>>> t.pencolor("#40F0F0")
>>> t.circle(100,steps=7)

Con este ejemplo aprendemos turtle.home(), que mueve el cursor al punto de origen y turtle.clear(), que borra el lienzo. También vemos que, como casi siempre en Python, podemos indicar nombres de colores o cadenas con los componentes RGB. Estas líneas producen el siguiente resultado:

El interés de turtle es que con tan pocas primitivas podemos crear formas sorprendentes con un poco de código.

#turtle demo

import turtle as t
import math

t.hideturtle()
t.speed(0)

#Espiral
t.up()
t.color("blue")
t.pensize(2)
t.goto(250, -10)
t.down()
for i in reversed(range(250)):
t.forward(i/10)
t.left(5)

#Espiral áurea
radio=250
PHI=(1+math.sqrt(5))/2
t.up()
t.color("green")
t.pensize(6)
t.goto(-500, 300)
t.right(90)
t.down()
for i in range(12):
t.circle(radio, 90)
radio/=PHI

#Estrella
t.up()
t.goto(-500, -300)
t.setheading(0)
t.color("red", "yellow")
t.width(1)
t.down()
t.begin_fill()
for i in range(50):
t.fd(400)
t.left(141)
t.end_fill()

#Texto
t.up()
t.goto(100,-300)
t.color("red")
t.write("EJEMPLOS\nde turtle", font=("Times", 48, "bold"))

t.done()

Ejecuta el código y obtendrás la siguiente salida:

2.1.6 Operaciones a nivel de bits

Vamos a añadir a nuestro repertorio de operaciones un conjunto que realiza combinaciones a nivel de bits, entre ellos hay tres que implementan las operaciones lógicas (AND, OR y XOR) a nivel de bits y dos que realizan desplazamientos.

Operadores a nivel de bits
Desplazamiento expr1 >> expr2
expr1 << expr2
Desplazamiento hacia la derecha
Desplazamiento hacia la izquierda
Operaciones lógicas
a nivel de bits
expr1 & expr2
expr1 | expr2
expr1 ^ expr2
AND a nivel de bits
OR a nivel de bits
XOR (OR exclusive) a nivel de bits

Los operandos deben ser siempre valores enteros, si se trata de expresiones deben devolver valores enteros. Los operadores de desplazamiento mueven cada bit del primer argumento a la derecha o izquierda el número de veces que indique el segundo argumento. Se introducen ceros ocupando las posiciones desplazadas.

bit
9
bit
8
bit
7
bit
6
bit
5
bit
4
bit
3
bit
2
bit
1
bit
0
>> 2 bit
9
bit
8
bit
7
bit
6
bit
5
bit
4
bit
3
bit
2
1011001001 10110010
bit
7
bit
6
bit
5
bit
4
bit
3
bit
2
bit
1
bit
0
<< 2 bit
7
bit
6
bit
5
bit
4
bit
3
bit
2
bit
1
bit
0
--
11001001 1100100100

El efecto de un desplazamiento de x a la izquierda n veces equivale a la operación: n * (2 ** n)
Un desplazamiento similar a la derecha equivale a: n / (2 ** n)

Las operaciones booleanas a nivel de bit combinan los bits en idénticas posiciones de sus argumentos de acuerdo con las siguientes tablas:

0 & 0 = 00 | 0 = 00 ^ 0 = 0
010011011
100101101
111111110

La correspondencia con las operaciones lógicas and y or es exacta: & (AND a nivel de bits) devuelve 0 si alguno de los bits es cero, | (OR a nivel de bits) devuelve 1 si alguno de los bits es uno. El operador ^ (XOR, OR exclusivo) es diferente; un cero mantiene el valor del otro operando, mientras que un uno invierte el valor. En la práctica, con dos operandos, devuelve 0 si ambos son iguales y 1 si son diferentes.

Apliquemos estos conocimientos en el intérprete de Python:

>>> x, y, z = 0xAA, 0xCC, 0xF0
>>> bin(x)
'0b10101010'
>>> bin(y)
'0b11001100'
>>> bin(z)
'0b11110000'
>>>
>>> bin(x & y)
'0b10001000'
>>> bin(x & z)
'0b10100000'
>>> bin(y & z)
'0b11000000'
>>> bin(x & y & z)
'0b10000000'
>>>
>>> bin(x | y)
'0b11101110'
>>> bin(x | z)
'0b11111010'
>>> bin(y | z)
'0b11111100'
>>> bin(x | y | z)
'0b11111110'
>>>
>>> bin(x ^ y)
'0b1100110'
>>> bin(x ^ z)
'0b1011010'
>>> bin(y ^ z)
'0b111100'
>>> bin(x ^ y ^ z)
'0b10010110'
>>>

Usamos tres configuraciones alternando unos y ceros con distintas frecuencias: x alterna cada bit, y en grupos de dos bits y z en grupos de cuatro bits. El operador AND <&> conserva los unos comunes, el operador OR <|> conserva los ceros comunes, y el operador XOR <^> produce ceros en los bits idénticos y unos en los bits diferentes (ojo, que la función bin() elimina los ceros por la izquierda, y eso puede resultar confuso).

Veamos funcionar los operadores de desplazamiento de bits:

# Operabits. Operadores de desplazamiento de bits

def reprbin(exp):
s = eval("f\"{"+ exp + ":016b}\"")
print(s + " " + exp + "\t"+ str(int(eval("f\"{" + exp + ":b}\""), 2)))

x=100
for i in range(8, 0, -1):
reprbin(str(x)+ " << " +str(i))
print(50 * "─")
reprbin(str(x))
print(50 * "─")
for i in range(1, 9):
reprbin(str(x)+ " >> " +str(i))

La función reprbin() recibe una expresión en forma de cadena de texto y la evalúa convirtiéndola a binario. Presenta el resultado binario, la expresión, y el valor decimal. En la línea 5 ajustamos los espacios antes del tabulador para que la útlima columna quede alineada cuando llamemos a la función desde la línea 11 con x como expresión.

Empleamos dos bucles para presentar todos los estados de desplazamiento de x desde 8 bits a la izquierda a 8 bits a la derecha. El valor de referencia se resalta con líneas encima y debajo. El resultado será el siguiente:

============= RESTART: C:/Users/User/Documents/Python/Operabits.py =============
0110010000000000 100 << 8 25600
0011001000000000 100 << 7 12800
0001100100000000 100 << 6 6400
0000110010000000 100 << 5 3200
0000011001000000 100 << 4 1600
0000001100100000 100 << 3 800
0000000110010000 100 << 2 400
0000000011001000 100 << 1 200
──────────────────────────────────────────────────
0000000001100100 100 100
──────────────────────────────────────────────────
0000000000110010 100 >> 1 50
0000000000011001 100 >> 2 25
0000000000001100 100 >> 3 12
0000000000000110 100 >> 4 6
0000000000000011 100 >> 5 3
0000000000000001 100 >> 6 1
0000000000000000 100 >> 7 0
0000000000000000 100 >> 8 0
>>>

Observa que los desplazamientos a la izquierda equivalen a multiplicar cada vez por 2, y a la derecha a hacer una división entera también entre 2. En realidad es lo mismo que ocurre si desplazamos a la izquierda o derecha los dígitos decimales, en esta caso sería multiplicando por 10 o dividiendo entre 10.

Las operaciones a nivel de bits son profusamente usadas, por ejemplo, en proceso de imágenes. De momento amplían nuestro repertorio para construir expresiones en Python.

2.1.7 Python como herramienta científica

Uno de los principales usos del lenguaje Python es como herramienta para la comunidad científica. Su alto nivel, las estupendas librerías de que dispone, y la posibilidad de usarlo en modo interactivo, lo convierten en un lenguaje especialmente apto para su uso en pequeñas o no tan pequeñas aplicaciones auxiliares para el desarrollo científico. Concretamente, las grandes capacidades de Python para el análisis de datos y la relativa sencillez de su sintaxis lo hacen muy atractivo para estas funciones.

Vamos a ver por encima dos de las librerías que componen el ecosistema SciPy, un entorno especialmente adaptado para las matemáticas, la ingeniería y la investigación científica en general. Se trata de numpy, que proporciona capacidades para manejar matrices a partir de un nuevo tipo de datos que amplía considerablemente las funcionalidades de las listas de Python, y de matplotlib, que propociona enormes capacidades de representación gráfica de datos. Como matplotlib está especialmente diseñado para trabajar con las matrices de numpy, veremos este en primer lugar.

La biblioteca NUMPY

Como con todas las librerías externas, la forma de instalar numpy es desde el símbolo de sistema utilizando la orden:

pip install numpy

Si todo es normal, después del proceso de descarga e instalación podremos emplear numpy desde nuestra instalación de Python. Para comprobarlo utiliza en IDLE la siguiente sentencia:

>>> import numpy as np
>>>

Si no obtienes ningún resultado es una buena señal, puesto que las sentencias de importación solo muestran alguna salida cuando se produce un error, normalmente la ausencia de la biblioteca o errores de sintaxis al teclear los nombres de paquetes, módulos o funciones. La forma habitual de importación es la que te acabo de mostrar. Siempre usaremos el alias np. Sigamos haciendo alguna comprobación.

>>> a = np.arange(10)
>>> type(a)
<class 'numpy.ndarray'>
>>> print(a)
[0 1 2 3 4 5 6 7 8 9]
>>>

Acabamos de crear el tipo básico de numpy, el tipo numpy.ndarray. La biblioteca numpy incluye muchas funciones similares a las que ya conocemos pero adaptadas para aplicarlas sobre arrays. Por ejemplo, la función numpy.arange() funciona igual que la función range(), pero genera un array con los valores producidos (y los valores no tienen por que ser necesariamente enteros). Vemos que la forma de presentar el array con la función print() es más elegante al prescindir de las comas, y ya veremos que nos permite ver con perfecta claridad las matrices bidimensionales.

Un array de numpy consiste en una colección de datos organizados en una parilla multidimensional. Podemos indexar dichos datos de múltiples formas, y los datos deben ser del mismo tipo.

Veamos las propiedades básicas de un array a partir del que acabamos de crear:

>>> a.ndim
1
>>> a.shape
(10,)
>>> a.size
10
>>> a.dtype
dtype('int32')
>>>

El atributo ndarray.ndim indica las dimensiones, o número de ejes del array, en este caso tenemos un array lineal o de una sola dimensión. ndarray.shape devuelve una tupla con la longitud para cada dimensión. ndarray.size devuelve el tamaño del array, es decir el número de elementos, que viene a ser el producto del tamaño de cada dimensión, o sea, de todos los valores devueltos por shape. En el próximo ejemplo crearemos arrays de varias dimensiones. Por último, ndarray.dtype nos indica el tipo de datos que contiene el array. Como hemos mencionado, todos los datos de un array son del mismo tipo, a diferencia de las listas de Python.

>>> a=np.array([[1,2,3],[4,5,6],[7,8,9]])
>>> b=np.array([[[1,2],[3,4]],[[5,6],[7,8]]],np.float64)
>>> print(a)
[[1 2 3]
 [4 5 6]
 [7 8 9]]
>>> print(b)
[[[1. 2.]
  [3. 4.]]

 [[5. 6.]]
  [7. 8.]]]

>>> a.ndim
2
>>> a.shape
(3, 3)
>>> a.size
9
>>> a.dtype
dtype('int32')
>>> b.ndim
3
>>> b.shape
(2, 2, 2)
>>> b.size
8
>>> b.dtype
dtype('float64')
>>>

Hemos creado arrays de dos y tres dimensiones. Si no especificamos el tipo de valores como segundo argumento, numpy elige el tipo en función de los valores que escribamos. Si todos son enteros creará un array de enteros. En el caso de b hemos seleccionado como segundo argumento el tipo np.float64, que es el tipo flotante de numpy. Vemos que dimensiones, forma (shape) y tamaño son las que esperamos: a tiene dos dimensiones, una forma de 3x3 y un tamaño de 9. b tiene 3 dimensiones, una forma de 2x2x2 y un tamaño de 8.

Veamos otras formas de creación de arrays de numpy:

>>> np.zeros(5)
array([0., 0., 0., 0., 0.])
>>> np.ones((2,2),np.int64)
array([[1, 1],
[1, 1]], dtype=int64)

>>> np.empty(2)
array([1.49168667e+020, 6.01346953e-154])
>>> np.arange(1,10,2)
array([1, 3, 5, 7, 9])
>>> np.linspace(0,10,5)
array([ 0. , 2.5, 5. , 7.5, 10. ])
>>>

Existen funciones para crear arrays con ceros (numpy.zeros), con unos (numpy.ones) o con elementos vacíos (numpy.empty). En todas ellas especificaremos una tupla con el tamaño de cada eje, o un único valor para arrays unidimensionales, y podemos indicar el tipo de datos como segundo argumento. En el caso del array "vacío" numpy no inicializa los datos, con lo cual recibimos una sierta sorpresa al observar que salen unos valores misteriosos. Esto depende de lo que contenga la memoria en la que es alojado el array. Si creamos un array de elementos vacíos, debemos asegurarnos más adelante de inicializarlos con un valor conocido. Ya conocemos la función numpy.arange, y hemos usado numpy.linspace, que divide el intervalo entre los argumentos primero y segundo para devolvernos tantos valores equidistantes como indiquemos en el tercer argumento.

>>> np.linspace(0,1,11)
array([0. , 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1. ])
>>> np.linspace(0,100,2)
array([  0., 100.])
>>>

El número de segmentos resultantes será igual al tercer argumento menos 1.

Podemos añadir elementos a un array con la función numpy.append(). El primer argumento será el array al que añadir los datos, el segundo argumento un array (o lista) con los datos a añadir, y un tercer argumento opcional indica el eje en el cual añadir los datos. En el caso de un array bidimensional el eje 0 son las filas y el 1 las columnas. Si no especificamos ningún eje, los dos conjuntos son aplanados, es decir, reducidos a un solo eje, y luego concatenados. Si especificamos un eje, el tamaño en ese eje de los datos a añadir debe coincidir con el del array inicial.

>>> a=np.array([[1,2],[3,4]])
>>> print(a)
[[1 2]
 [3 4]]

>>> np.append(a,(5,6))
array([1, 2, 3, 4, 5, 6])
>>> np.append(a,[[5,6]],0)
array([[1, 2],
[3, 4],
[5, 6]])

>>> b=np.append(a,[[5],[6]],1)
>>> b
array([[1, 2, 5],
[3, 4, 6]])

>>> b.reshape(3,2)
array([[1, 2],
[5, 3],
[4, 6]])

>>>

Vemos en el primer ejemplo que al no especificar un eje los arrays se han reducido a una única dimensión y luego concatenado. En el segundo caso añadimos una fila de dos elementos al array y en el tercero añadimos una columna tambien de dos elementos. En el útimo ejemplo hemos usado el método ndarray.reshape() que sirve para cambiar la forma de un array, siempre que el tamaño del array resultante sea el mismo, es decir, que el producto de las dimensiones finales debe ser igual al de las dimensiones iniciales.

#reshape

import numpy as np

a=np.random.randint(0,11,(3,4))

for t in ((12,),(2,6),(3,4),(4,3),(6,2),(12,1)):
b=a.reshape(t)
print(b)
print(40*"=")

print("a")
print(a)
print(40*"=")

print("a.T")
print(a.T)
print(40*"=")

print("a.ravel()")
print(a.ravel())

numpy incluye un módulo de funciones aleatorias: numpy.random. La función numpy.random.randint acepta tres argumentos, los valores mínimo (inclusive) y máximo (exclusive) entre los que devolverá valores, y la forma del array que devuelve. De este modo en la línea 5 creamos un array de 3x4 relleno aleatoriamente con valores del 0 al 10.

El bucle de las líneas 7-10 nos muestra todas las combinaciones de formas bidimensionales del array, teniendo en cuenta que el producto de todos los valores debe ser siempre 12. Queda como ejercicio para el lector añadir las formas tridimensionales posibles.

A continuación imprimimos el array original (que no ha sido alterado por las operaciones a.reshape() anteriores y le aplicamos dos transformaciones de forma: el atributo ndarray.T, que devuelve la matriz tranpuesta del array (cambiando filas por columnas), y el método ndarray.ravel() que devuelve el array aplanado (lo mismo que conseguiríamos con ndarray.reshape(ndarray.size))

La salida del programa será similar a esta (teniendo en cuenta que los valores serán distintos cada vez que lo ejecutemos):

============= RESTART: C:/Users/User/Documents/Python/reshape.py =============
[7 0 6 9 7 2 5 2 4 3 7 7]
========================================
[[7 0 6 9 7 2]
 [5 2 4 3 7 7]]
========================================
[[7 0 6 9]
 [7 2 5 2]
 [4 3 7 7]]
========================================
[[7 0 6]
 [9 7 2]
 [5 2 4]
 [3 7 7]]
========================================
[[7 0]
 [6 9]
 [7 2]
 [5 2]
 [4 3]
 [7 7]]
========================================
[[7]
 [0]
 [6]
 [9]
 [7]
 [2]
 [5]
 [2]
 [4]
 [3]
 [7]
 [7]]
========================================
a
[[7 0 6 9]
 [7 2 5 2]
 [4 3 7 7]]
========================================
a.T
[[7 7 4]
 [0 2 3]
 [6 5 7]
 [9 2 7]]
========================================
a.ravel()
[7 0 6 9 7 2 5 2 4 3 7 7]

>>>

Si mantenemos abierta la ventana de los resultados, podemos comprobar que el array sigue sin haber sido modificado, dado que no hemos asignado los valores de las diferentes transformaciones. El método ndarray.resize(), con la misma sintaxis que .reshape(), modifica el array in situ:

>>> a
array([[7, 0, 6, 9],
[7, 2, 5, 2],
[4, 3, 7, 7]])

>>> a.resize(2,6)
>>> a
array([[7, 0, 6, 9, 7, 2],
[5, 2, 4, 3, 7, 7]])

>>>

Tanto con los métodos .reshape() como .resize(), podemos dejar que la máquina recalcule una o varias dimensiones indicando el valor -1:

>>> a.resape(4,-1)
array([[7, 0, 6],
[9, 7, 2],
[5, 2, 4],
[3, 7, 7]])

>>> a.reshape(-1,3,2)
array([[[7, 0],
[6, 9],
[7, 2]],

[[5, 2],
[4, 3],
[7, 7]]])

>>>

Para acceder a los elementos de un array usamos índices y porciones, teniendo en cuenta que si hay más de una dimensión el primer índice corresponde a las filas, el segundo a las columnas, etc.

>>> a.resize(4,3)
>>> a
array([[7, 0, 6],
[9, 7, 2],
[5, 2, 4],
[3, 7, 7]])

>>> a[0]
array([7, 0, 6])
>>> a[1::2]
array([[9, 7, 2],
[3, 7, 7]])

>>> a[1][1]=-10
>>> a
array([[  7,   0,   6],
[  9, -10,   2],
[  5,   2,   4],
[  3,   7,   7]])

>>>

Para terminar (por el momento) con numpy, las operaciones aritméticas efectuadas con arrays se realizan elemento a elemento.

>>> a=np.array([[10,20],[30,40]])
>>> print(a)
[[10 20]
 [30 40]]

>>> print(a+5)
[[15 25]
 [35 45]]

>>> print(a*10)
[[100 200]
 [300 400]]

>>> print(a*a)
[[ 100  400]
 [ 900 1600]]

>>>

Vemos que si multiplicamos un array por otro se realizan las multiplicaciones entre los elementos en las mismas posiciones. Podemos obtener el producto matricial de ambos arrays usando el operador @

>>> a=np.arange(4).reshape((2,2))
>>> b=a.T()
>>> a
array([[0, 1],
[2, 3]])

>>> b
array([[0, 2],
[1, 3]])

>>> a * b
array([[0, 2],
[2, 9]])

>>> a @ b
array([[ 1, 3],
[ 3, 13]])

>>>

Dejamos de momento numpy con este esbozo (se trata de una biblioteca de enorme extensión y posibilidades) y vamos a pasar a otra librería que sirve para representar gráficamente tablas de valores como las que maneja numpy.

La biblioteca MATPLOTLIB

matplotlib es una de las bibliotecas que se encuentran dentro del gran repositorio The Python Package Index (PyPI). Para descargarla y disponer de ella desde nuestro intérprete, debemos usar el símbolo de sistema, como es ya habitual, tecleando la instrucción:

pip install matplotlib

Si no se producen errores ya disponemos de nuestra nueva biblioteca. Comenzemos a teclear para ver las posibilidades de este paquete.

>>> import matplotlib.pyplot as plt
>>> x=[n for n in range(-10,11)]
>>> y=[n for n in x]
>>> plt.plot(x,y)
[<matplotlib.lines.Line2D object at 0x0000027E287A6040>]
>>> plt.show()
>>>

En este momento nos aparece la ventana gráfica de matplotlib con la representación de los datos. En el eje x valores entre -10 y 10, en el eje y sus cuadrados.

La función matplotlib.pyplot.show() muestra el resultado de las gráficas que hayamos configurado anteriormente y no devuelve el control a Python hasta que cerremos la ventana. Podemos guardar nuestras gráficas antes de hacerlo. También podemos interrumpir el programa pulsando <CTRL>+<C>, con lo cual la ventana gráfica se mantendrá abierta.

El módulo pyplot (a partir de ahora usaremos para el código el alias plt) incluye funciones que emulan un famoso programa matemático llamado MATLAB. Comenzamos por él dado que es uno de los más sencillos de uso de matplotlib, que al igual que numpy es una biblioteca enorme y de grandes posibilidades.

La función plt.plot, recibe dos series de valores de idéntica longitud, la primera corresponde al eje x y la segunda al eje y. Sin especificar más el programa gestiona la presentación de los ejes y de la gráfica de una forma que en principio funciona perfectamente.

Podemos añadir un tercer argumento con una cadena de formato, que configura el color y aspecto de la gráfica. Continuémos con la misma sesión del intérprete.

>>> import numpy as np
>>> a=np.arange(-10,10,0.1)
>>> plt.subplot(131)
<AxesSubplot:>
>>> plt.plot(a,a**2,".r")
[<matplotlib.lines.Line2D object at 0x0000027E2B599F70>]
>>> plt.subplot(132)
<AxesSubplot:>
>>> plt.plot(a,a**3,",b")
[<matplotlib.lines.Line2D object at 0x0000027E2B5D5790>]
>>> plt.subplot(133)
<AxesSubplot:>
>>> plt.plot(a,100-a**2,"*g")
[<matplotlib.lines.Line2D object at 0x0000027E2B6136D0>]
>>> plt.show()
>>>

Esta vez obtenemos este resultado:

Hemos empleado un array de numpy porque nos da la opción de realizar las funciones matemáticas directamente sobre él. Esta vez hemos usado la función plt.subplot() cuyo argumento son las dimensiones en filas y columnas de la rejilla de salida, y la posición del gráfico en esa rejilla. Así hemos especificado una rejilla de una fila con tres columnas y sucesivamente hemos pintado en cada columna (por cierto, aquí se empieza a contar desde 1, no es lo habitual en Python). Al llamar a plt.plot() hemos usado una cadena para indicar el formato de forma rápida. Hemos usado los colores "r" (red), "b" (blue) y "g" (green), y puntos (.), pixels (,) y estrellas (*) para la gráfica.

Vamos a pasar a programar para explorar más comodamente otras características:

#matplotlib_test

import matplotlib.pyplot as plt
import numpy as np

a=np.deg2rad(np.arange(-180,181))

plt.xlabel("radianes")
plt.title("Funciones trigonométricas")
plt.axis([-3.5,3.5,-2,2])
plt.plot(a,np.sin(a))
plt.plot(a,np.cos(a))
plt.plot(a,np.tan(a),".",markersize=1)
plt.show()

Como dijimos acerca de numpy, este incluye numerosas funciones matemáticas que se aplican sobre arrays enteros. En la línea 6 creamos un array con valores entre -180° y 180° convertidos a radianes, para luego poder usarlos en las funciones trigonométricas. Añadimos dos funciones nuevas en las líneas 9 y 10 que crean una etiqueta para el eje x y un título para la gráfica. La función plt.axis() de la línea 11 indica las coordenadas (x0, x1, y0, y1) que marcan el límite de representación de los ejes. Prueba a añadir una marca de comentario a esa línea y verás que puesto que la tangente tiende a infinito en los 90° y -90°, el resultado será extraño. Así, hemos limitado el eje x entre un poco más de -π y +π que son los valores en radianes de -180° y 180°, y el eje y entre -2 y 2.

Las gráficas en sí son contruidas con las instrucciones de las líneas 11-13, primero representamos la función seno, luego coseno y por último la tangente. Como esta última tiene puntos de discontinuidad en π/2 y 3π/2 utilizamos puntos mediante el indicador de formato ".", y limitamos su tamaño con el parámetro con nombre markersize. La función plot incluye numerosos parámetros que podemos utilizar para ajustar el resultado de la representación. Poco a poco iremos conociendo algunos. La gráfica resultante es:

Con una pequeña modificación del código anterior vamos a añadir una leyenda a la gráfica.

plt.plot(a,np.sin(a),label="seno")
plt.plot(a,np.cos(a),label="coseno")
plt.plot(a,np.tan(a),".",markersize=1,label="tangente")
plt.legend()
plt.show()

Es tan sencillo como poner nombre a cada línea usando el parámetro label dentro de la función plot, y luego usar la función legend() antes de mostrar la gráfica.

matplotlib puede mostrar muchos más tipos de gráficos, vamos a ver un ejemplo de la función plt.scatter(), que muestra magnitudes distribuídas en un plano representadas por marcas de tamaño proporcional a dichas magnitudes. Veámoslo funcionando.

#matplotlib_test scatter

import matplotlib.pyplot as plt
import numpy as np

#Fijando la "semilla" del generador de pseudoaleatoriedad
#obtenemos siempre los mismos resultados
np.random.seed(28210553)

N=50
x=np.random.rand(N)
y=np.random.rand(N)
area=(30*np.random.rand(N))**2

plt.title("pyplot.scatter()")
plt.scatter(x,y,s=area,alpha=.7)
plt.show()

Y este es el resultado del programa.

Hemos usado la función numpy.random.seed() para fijar el generador de pseudoaleatoriedad de forma que siempre se repitan los mismos valores. Si eliminamos la línea 8 cada ejecución producirá un resultado diferente. En la línea 11 definimos la cantidad de valores a mostrar y creamos dos arrays con la función numpy.random.rand(N) que genera un array de N valores entre 0 y 1. En el caso del tercer array incluímos un poco de cálculo adicional para producir valores un poco mayores para que resulten visibles las diferencias de tamaño. La función scatter recibe los datos de coordenadas x e y, el de tamaños con el parámetro con nombre s, y la transparencia mediante el parámetro alpha. Podríamos incluir otro array con los colores para los puntos, mediante el parámetro c.

x=np.random.rand(N)
y=np.random.rand(N)
color=np.random.rand(N)
area=(30*np.random.rand(N))**2

plt.title("pyplot.scatter()")
plt.scatter(x,y,c=color,s=area,marker="h",alpha=.7)
plt.show()

Ya que estamos, hemos usado el parámetro marker para representar hexágonos.

Otra posibilidad es usar etiquetas en vez de valores.

#matplotlib_test categorías

import matplotlib.pyplot as plt

nombres=["A","B","C"]
valores=[40,80,25]
magnitudes=[1000,500,3000]
color=["#FF6622","22FF66","#6622FF"]

plt.figure(figsize=(9,4.5),facecolor="#FFFFEE")

plt.subplot(131)
plt.title("plt.bar()")
plt.bar(nombres,valores,color=color)
plt.subplot(132)
plt.axis([-1,3,-1,100])
plt.title("plt.scatter()")
plt.scatter(nombres,valores,s=magnitudes,c=color)
plt.subplot(133)
plt.grid(True,linestyle=":")
plt.title("plt.plot()")
plt.plot(nombres,valores,"r--")

plt.suptitle("Gráfica de categorías",fontweight=800)
plt.show()

Esta vez usamos cuatro listas normales de Python, con las etiquetas para el eje x, los valores para el eje y>, magnitudes para usar con scatter() y colores para cada categoría. En la línea 9 usamos la función plt.figure(), que define una serie de parámetros para la figura que es la estructura principal de matplotlib, todo aquello que vemos reflejado en la ventana de salida. Hemos definido dos parámetros, el tamaño (ancho x alto) en pulgadas y el color del fondo.

A continuación creamos tres gráficas usando subplot, poniéndoles un título a cada una. EL primer gráfico usa una función nueva: plt.bar(), que presenta gráficos de barras. El segundo gráfico usa las magnitudes para la función scatter, y antes hemos ajustado los límites de los ejes para que los círculos queden dentro de la gráfica. El tercer gráfico es el de líneas normal con plot(), hemos añadido una rejilla con el estilo de línea de puntos y especificaciones de formato con el color (rojo) y el estilo de línea del gráfico.

Por último, en la línea 24 usamos otra función para añadir un título a toda la figura. Hemos indicado como argumentos el título y el "peso" (Hemos indicado 800 de un rango 0-1000). La gráfica producida por este ejemplo es:

Terminamos este vistazo de matplotlib con otro ejemplo de las muchas posibilidades que nos ofrece:

#matplotlib_otras

import matplotlib.pyplot as plt
import numpy as np

nombres=["A","B","C"]
valores=[40,80,25]
colores=["#FF4400","#00FF44","#4400FF"]

#Barras horizontales
plt.subplot(3,5,(1,3))
plt.title("Barras horizontales",style="italic")
plt.barh(nombres,valores,color=colores)

#Gráficos de tarta
plt.subplot(3,5,(4,10))
plt.title("Gráfico\nde\ntarta",size=10)
explode=[0,0,.1]
plt.pie(valores,labels=nombres,colors=colores,explode=explode,shadow=True)

#Coordenadas polares
a=np.arange(0,10,.1)
b=10-a
plt.subplot(3,5,(6,12),projection="polar")
plt.title("Proyección polar",y=-0.3)
plt.axis([None,None,0,10])
plt.polar(a,b)

#Histograma
x=np.random.randn(1000)
plt.subplot(3,5,(14,15))
plt.title("Histograma",c="blue")
plt.hist(x,100)

plt.show()

Creamos cuatro ejemplos de gráficas en una rejilla de 3x5. Observa que al usar plt.subplot estamos indicando una tupla como tercer argumento, cada gráfico se extiende aproximadamente por la zona de la rejilla delimitada por las celdas inicial y final de la tupla. Hemos creado títulos para cada trazado y modificado una propiedad en cada uno: En el primer caso el estilo del texto, en el segundo el tamaño (observa que hemos incluído saltos de línea en el título y se han respetado), en el tercero la posición vertical y en el cuarto el color.

La función plt.barh es exáctamente igual que plt.bar pero produce barras horizontales en vez de verticales. plt.pie crea gráficos de tarta a partir de una lista de valores, cada sector ocupa un espacio proporcional al valor en relación con la suma de todos los valores. El argumento explode es un array con valores de desplazamiento radial para cada sector, de forma que podemos resaltar los que queramos, y shadow indica si habrá una sombra en el borde del gráfico.

Las coordenadas polares son un sistema de proyección en que cada punto se define por un ángulo y un radio. Al seleccionar el área de trazado (axis en la terminología de matplotlib) con subplot hemos indicado con el argumento projection que vamos a usar coordenadas polares. Con la instrucción plt.axis limitamos la extensión del eje y (el radial) manteniendo el eje x en 360°

Por último, hemos usado una función nueva de numpy: np.random.randn(), que genera un array con la forma indicada por el argumento con valores aleatorios en una distribución normal, con forma de campana en torno a un centro. El segundo argumento define número de divisiones verticales en que dividimos el rango.

Y aquí dejamos por el momento la librería matplotlib, esperando que te hayas hecho una idea de lo que puedes hacer con ella.

2.2 El ordenador y Python

Vamos a ver con cierto detalle la simbiosis entre nuestro intérprete de Python y nuestro ordenador, o más exáctamente con nuestro sistema operativo, que es quién condiciona como nos relacionamos con nuestra máquina.

El ordenador es una estructura formada por dos grandes niveles o capas principales, hardware: la parte física, los componentes electrónicos, y software: la parte lógica, el código que controla la parte física, creado a base de instrucciones similares a las de nuestros programas de Python. El hardware condiciona en alguna medida las posibilidades del ordenador, especialmente en lo relativo a potencia de cálculo, y en suma a la velocidad a la que trabaja. Hay procesos que pueden ser inabordables en tiempo si el ordenador no posee la potencia suficiente, pero lo que realmente constituye el "alma" de la máquina es el software, los programas que la controlan.

En los ordenadores de tipo PC hay una "capa" de software que viene incorporada en la máquina, la BIOS o Sistema básico de entrada-salida (Basic input-output system). La BIOS cumple dos funciones principales, por una parte proporciona una serie de funciones que controlan a un nivel muy elemental los dispositivos incorporados como discos, teclado, pantalla, etc. Por otra, efectúa el chequeo de arranque del ordenador (POST - Power-on self-test) y a continuación carga el sistema operativo.

El sistema operativo es lo que hace al ordenador ser como es. Complementa y amplía las funciones de la BIOS para manejar todo aquello que podamos conectar en la máquina y gestiona la entrada/salida, la interacción con el mundo exterior. Este concepto (entrada/salida), que ya habremos atisbado anteriormente es el núcleo de lo que hace la máquina: recibe datos y luego nos devuelve otros datos obtenidos a partir de aquellos recibidos.

El sistema operativo gestiona un aspecto clave para la utilidad del ordenador que es el sistema de archivos, el modo de organizar la información en forma de ficheros en un soporte electrónico para poder disponer de ella en cualquier momento, y por información no solo entendemos los Datos sino también los Programas.

2.2.1 El símbolo de sistema de Windows

Hace años no existían pantallas con las capacidades gráficas actuales, ni máquinas con el rendimiento necesario para gestionarlas, y los entornos de trabajo con computadores eran a través de terminales o consolas en modo texto. El concepto de terminal engloba la pantalla y el teclado que usamos para enviar las órdenes y recibir sus resultados. Windows evolucionó como entorno gráfico de ventanas sobre el antiguo MS-DOS, que funcionaba de una manera similar al actual símbolo de sistema de Windows. La familia de sistemas operativos UNIX también funciona como una consola de texto sobre la cual se ejecuta un entorno de ventanas si así lo deseamos. En PC el representante de dicha familia es LINUX.

Centrándonos en la consola de Windows, ya has tenido encuentros con ella para usar pip, el instalador de paquetes de Python, o para ejecutar programas que no "corren" en IDLE, como los de colorama. Ahora vamos a tratar de conocer un poco más la forma de trabajo desde el símbolo de sistema. Para arrancar el símbolo de sistema debemos ejecutar un programa llamado cmd.exe que en la instalación normal de Windows reside en la carpeta o directorio: %windir%\system32\, donde %windir% se refiere a la unidad (tipicamente C:) y el directorio (típicamente \Windows) de instalación del propio Windows.

Hay diferentes maneras de ejecutar el programa:

La última forma es mi favorita, a partir de ahí solo tienes que hacer doble click sobre el icono del escritorio. Eso si, mantén tu escritorio ordenado, no dejes que Windows amontone los iconos como quiera ;-)

Una vez en el símbolo del sistema aparece la conocida pantalla:

Tenemos la consola de texto, un encabezamiento indicando la versión de Windows y el promt que nos indica que está esperando recibir nuestras órdenes. El prompt nos da información sobre la unidad y el directorio actual.

Hemos dicho que una de las principales misiones del sistema operativo es gestionar el sistema de archivos, y el concepto de directorios es una pieza angular de dicho sistema. Estamos familiarizados con las unidades de discos y las carpetas de Windows, que corresponden a lo que llamamos directorios en la consola. Se trata de una estructura en forma de árbol, que parte para cada unidad de un nodo principal que se llama directorio raíz. En cada directorio podemos poner ficheros u otros directorios en cualquier cantidad, y así a partir del directorio raíz se desarrolla la estructura de cada disco.

En Windows cada unidad de disco tiene una letra asociada, por motivos históricos las dos primeras (A: y B:) correspondían a las unidades de discos flexibles, y las unidades de discos duros comienzan a partir de la tercera unidad, la C:, que corresponde a la unidad de instalación del sistema operativo.

Podemos seleccionar la unidad activa desde la consola indicando su letra seguida por el caracter dos puntos y pulsando INTRO para validar la orden.

Al cambiar de unidad aparecemos en el directorio activo para dicha unidad, por defecto en el directorio raíz, indicado por una barra invertida después de los dos puntos de la unidad. La unidad F: en mi ordenador es una unidad de disco extraíble que en ese momento no contiene ningún disco. Vemos que no hemos salido de la unidad E:. La unidad L: no existe. Al volver a la unidad C: volvemos al directorio en el que estábamos inicialmente.

El entorno de texto de una consola de texto a primera vista es menos amigable que un entorno gráfico. Lo primero que hemos de aprender es a orientarnos y sobre todo, a obtener ayuda. El comando >help nos muestra un sumario de los comandos disponibles, si añadimos el nombre de algún comando como argumento nos muestra ayuda específica para dicho comando. Por otra parte, si usamos el argumento /? con cualquier orden nos mostrará también la ayuda disponible. Puedes observar que por lo general usamos el caracter "/" para indicar modificadores sobre un comando. En las trayectorias (paths) de directorios usaremos el caracter barra invertida "\" para separar los diferentes directorios.

Para navegar a lo largo de un disco usamos la orden cd (de change directory, cambiar directorio). La sintaxis es sencilla: cd nuevo_directorio, donde nuevo_directorio es la ruta a la que queremos movernos. Hay tres especificaciones con un significado especial:

Necesitamos ver donde estamos, que posibles caminos podemos tomar, para poder navegar. La siguiente orden que debemos conocer es aquella que muestra el contenido de los directorios. Se trata del comando dir

Vemos que nos muestra columnas con la fecha y hora de creación, a continuación nos indica si se trata de un directorio, depues el tamaño (solo en el caso de ficheros) y el nombre. Podemos seleccionar el orden:

Usamos el modificador /o: y a continuación hemos usado g (directorios primero) y n (orden alfabético por nombres). Podemos tambien elegir que clase de elementos son mostrados:

El modificador /a: se refiere a los atributos de cada entrada. Hemos seleccionado que solamente muestre directorios. Ahora bien, si buscamos algo concreto podemos delimitar la búsqueda indicando el nombre o las partes conocidas del nombre y usando caracteres comodín. Hay dos caracteres comodín: el símbolo de cierre de interrogación ? representa un único caracter que puede ser cualquiera. El asterisco representa una serie variable de caracteres desconocidos. Si queremos buscar nuestros programas Python, buscaríamos:

Si queremos buscar una parte del nombre:

O aquellos cuya extensión comienza por m:

No dudes en usar el argumento /? para obtener toda la información de uso del comando.

La consola de Windows nos da unas facilidades que no existían en el MS-DOS original, por ejemplo, si tecleamos parte de una ruta y pulsamos la tecla <TAB> se autocompleta el nombre siempre que exista una coincidencia. Si hubiera varias posibilidades pulsado de nuevo el tabulador iríamos rotando entre todas ellas. Otra facilidad es la disponibilidad del portapapeles, esto es, de las opciones de copiar y pegar desde el símbolo de sistema o desde otro programa hacia este. Por ejemplo, puedes abrir el símbolo de sistema, luego en una ventana de Windows abres tu directorio de Python y copias la dirección. En la ventana de comandos escribes cd, un espacio y usas la combinación de teclas <MAYUSC>+<INSERT> para pegar la dirección que has copiado. Luego basta con pulsar INTRO para moverte hasta tu directorio de programas Python. Otra posibilidad es la de desplazar la pantalla hacia arriba para ver el resultado de salidas largas, o de las órdenes anteriores. Por último, si usas las teclas de cursor arriba puedes recuperar las últimas instrucciones que hayas tecleado.

Para terminar este vistazo del símbolo del sistema, vamos a explicar el concepto de ficheros ejecutables. Windows reconoce los archivos ejecutables primeramente por su extensión, los caracteres que van despues del punto al final del nombre de archivo. Actualmente las extensiones ejecutables que se reconocen son:

Los ficheros .exe son código compilado que la máquina puede ejecutar directamente. Los ficheros .bat son ficheros de texto con secuencias de comandos de consola que se ejecutan en orden y nos permiten automatizar tareas habituales de una forma muy sencilla. Para ejecutar estos fichero basta teclear el nombre (sin la extensión) en el símbolo del sistema.

Windows reconoce muchas otras extensiones de archivo y las asocia con programas para manejarlas. En este caso si tecleamos el nombre con la extensión se llamará al programa adecuado. Por ejemplo, si tecleamos el nombre de un programa Python con la extensión .py, se invocará al intérprete de Python para ejecutar el programa, o si tecleamos el nombre de un fichero con extensión .png se invocará al programa predeterminado para mostrar imágenes.

Cuando tecleamos una orden en el símbolo del sistema, lo primero que se hace es comprobar si se trata de un comando interno, incorporado en la propia consola, de no ser así se busca un ejecutable con el nombre indicado en el directorio actual, y si no se encuentra se busca en cada uno de los directorios guardados en la variable de entorno PATH. Si no se encuentra es cuando se produce un error.

Podemos ver las variables de entorno (y modificarlas) con el comando set.

Como hemos dicho, la variable PATH guarda las rutas de búsqueda de ejecutables, al instalar Python debemos haber activado su inclusión en PATH para que podamos arrancar el intérprete desde cualquier ventana del símbolo del sistema. Una variable curiosa es PROMPT, que indica mediante una serie de códigos el prompt de sistema. Por ejemplo, si tecleamos: set prompt=$C$T$F obtendremos la hora entre paréntesis. Tambien podemos simplemente teclear prompt $C$T$F.

Los cambios que efectuemos en el entorno de esta manera solo estarán activos para la sesión actual. En el momento en que cerremos el símbolo de sistema se pierden. De momento está bien así, para evitar problemas. Las variables de entorno son importantes para que el símbolo de sistema funcione correctamente, y cualquier cambio hecho sin conocimiento puede dejarlo inutilizado. Te habrás dado cuenta a estas alturas de que la consola no distingue entre mayúsculas y minúsculas.

Nos despedimos con un útimo comando: cls (clear screen) borra la pantalla.

2.2.2 El sistema de archivos desde Python

Ya hemos tenido un encuentro con ficheros cuando explorabamos los tipos bytes y bytearray. Ahora vamos a añadir nuevos conocimientos para poder movernos por el árbol de directorios y manejar ficheros.

Las operaciones de escritura sobre ficheros deben hacerse siempre con mucho cuidado para no dañar archivos importantes, con la consiguiente pérdida de datos o, lo que es peor, alterando el funcionamiento del propio sistema operativo o de algún programa.

Empezaremos explorando directorios:

>>> import os
>>> current=os.getcwd()
>>> print(current)
C:\Users\Miguel\Documents\Desarrollo\Python
>>> print(os.path.dirname(current))
C:\Users\Miguel\Documents\Desarrollo
>>> print(os.path.basename(current))
Python
>>>

La biblioteca  os  es parte de la biblioteca estándar, y proporciona funciones para manejar aspectos del sistema operativo. Esto incluye entre otras cosas el proceso de rutas de directorios. Las funciones que acabamos de utilizar nos indican el directorio activo (os.getcwd()), y nos permiten dividirlo en partes. Como en diferentes sistemas se utilizan diferentes sintaxis para separar directorios, si usamos estas funciones la propia librería tendrá en cuenta el sistema en que se ejecuta el programa y resolverá las rutas sin provocar errores.

Podemos hacer una función para dividir completamente una ruta.

# os test

import os

def splittray(tray):
l=[]
while tray!=os.path.dirname(tray):
l.insert(0,os.path.basename(tray))
tray=os.path.dirname(tray):
l.insert(0,tray)
return l

print(splittray(os.getcwd()))

Como novedad estamos usando el método lista.insert() de la clase list. Dicho método añade un elemento (el segundo argumento) a la lista en la posición indicada por el primer argumento. Así como el método append() siempre añade nuevos elementos al final de la lista con insert() podemos colocarlos donde queramos. En este caso vamos añadiendo siempre los elementos al principio de la lista, dado que vamos extrayendo los subdirectorios de derecha a izquierda, y así quedan ordenados. El resultado será:

============= RESTART: C:/Users/User/Documents/Python/os test.py =============
['C:\\', 'Users', 'Miguel', 'Documents', 'Desarrollo', 'Python']
>>>

La función que usaremos habitualmente para comprobar el contenido de un directorio será:   .os.scandir().   Esta función devuelve un iterador que contiene objetos de tipo os.DirEntry, con información sobre cada elemento.

Propiedades de os.DirEntry
.name El nombre de cada entrada de directorio
.path La ruta completa de cada entrada
.is_dir() Devuelve True si la entrada es un directorio
.is_file() Devuelve True si la entrada es un fichero

Con esta información vamos a crear una función para ver los directorios y poder navegar por ellos.

# dirs - Define una función para ver directorios

import os

def dirs(path="."):
curr=os.scandir(path)
l=[]
for f in curr:
if f.is_dir():
l.append(f.name)
cadena=f"Directorio de {os.path.abspath(path)}:"
print(cadena)
print("─"*len(cadena))
for d in sorted(l):
print("   ",d)

La función acepta como argumento una ruta de directorio, por defecto usamos el directorio actual. Almacenamos en curr el retorno de la función os.scandir() y lo iteramos para extraer los nombres de aquellas entradas que son directorios y guardarlos en la lista l. A continuación escribimos un encabezamiento con el directorio explorado (lo convertimos en una trayectoria absoluta con la función os.abspath()) y un separador, y luego la lista de nombres ordenados alfabéticamente por la función sorted(). Al ejecutar el programa no notamos nada especial, pero ahora disponemos de la función dirs() desde el intérprete:

============= RESTART: C:/Users/User/Documents/Python/dirs.py =============
>>> dirs()
Directorio de C:\Users\Miguel\Documents\Desarrollo\Python:
──────────────────────────────────────────────────────────
Arcade prácticas
Arcade tutorial
__pycache__

>>> dirs("Arcade prácticas")
Directorio de C:\Users\Miguel\Documents\Desarrollo\Python\Arcade prácticas:
───────────────────────────────────────────────────────────────────────────
Fuentes
Imágenes
Materiales
Sonidos

>>> dirs("..")
Directorio de C:\Users\Miguel\Documents\Desarrollo:
───────────────────────────────────────────────────
.vscode
C++
Documentación Python
HTML
Python

>>> os.chdir("..")
>>> os.getcwd()
'C:\\Users\\Miguel\\Documents\\Desarrollo'
>>>

Podemos ver el árbol de directorios desde un punto dado. Para movernos por él usaremos la función os.chdir(nuevo_directorio), que funciona exáctamente igual que la orden cd del símbolo de sistema.

En el mismo programa dirs.py vamos a añadir otra función similar para ver los ficheros que contiene un determinado directorio


def fdir(path="."):
curr=os.scandir(path)
l=[]
for f in curr:
if f.is_file():
l.append(f.name)
cadena=f"Ficheros en {os.path.abspath(path)}:"
print(cadena)
print("─"*len(cadena))
for d in sorted(l):
print("   ",d)

Puedes copiar y pegar toda la función dirs y reemplazar tres cosas, el nombre de la función en la línea 17, en la 21 usar el método .is_file() en lugar de .is_dir() y en la 23 cambiar ligeramente el encabezamiento, esto último ni siquiera es realmente necesario, pero es una forma de marcar diferencias. De nuevo ejecuta el programa y no cierres la ventana Python 3.x.x Shell

>>> fdir("Arcade tutorial")
Ficheros en C:\Users\Miguel\Documents\Desarrollo\Python\Arcade tutorial:
────────────────────────────────────────────────────────────────────────
01_open_window.py
02_draw_sprites.py
03_user_control.py
04_add_gravity.py
05_scrolling.py
06_coins_and_sound.py
07_score.py
08_load_map.py
09_endgame.py
Test mapa.tmx
map.tmx
map2_level_1 - copia.tmx
map2_level_1.tmx
map2_level_2.tmx
tmw_desert_spacing.tsx

>>>

Ambas funciones tienen una carencia de cierta importancia, solo podemos indicar un directorio, no podemos usar filtros mediante caracteres comodín ni buscar un nombre en concreto. Vamos a "redondear la faena" diseñando una función que permita listar tanto directorios como ficheros, y que permita filtrar los nombres usando comodines como la orden dir del símbolo del sistema. Añade aún las siguientes líneas a tu programa:


import subprocess as sp:

def mydir(arg=""):
midir=sp.run("cmd /C dir "+arg, capture_output=True)
print(midir.stdout.decode("cp850"))

La biblioteca subprocess proporciona funciones para gestionar el lanzamiento de procesos, programas independientes del programa principal que podemos ejecutar en paralelo con este. Se trata de temas avanzados, pero la función subprocess.run() nos permite ejecutar cualquier programa y esperar a que termine, y además podemos redirigir la entrada estándar y la salida estándar o capturar esta última como hemos hecho con el argumento capture_output=True, de forma que estamos haciendo una pequeña "trampa". Estamos invocando el comando cmd de Windows, que en realidad es ¡el símbolo del sistema!, el parámetro /C indica que se ejecute la orden a continuación y luego se cierre la ventana, y el comando, por supuesto, es el propio comando dir de la consola, al que pasamos los argumentos adicionales que nosotros incluyamos en la llamada a mydir(). Es lo mismo que conseguiríamos desde la consola del sistema.

El ejemplo nos sirve para aprender una nueva función, pero en realidad desde un programa sería más eficaz usar la función os.scandir() y luego filtrar la salida por medio de código extra. Por cierto, la salida capturada por run() está en el atributo .stdout del objeto devuelto y tiene el tipo bytes. Aplicando la función bytes.decode(encoding) la transformamos en una cadena de texto. Como ves, la codificación del texto de la consola no es UTF-8 ni nada parecido. Si te apetece comprobar la importancia de elegir la codificación correcta, prueba a alterar el valor por "utf-8", "utf-16le" o "utf-16be".

El siguiente paso, despues de poder saber qué ficheros hay en un directorio y movernos por la estructura de directorios, es emplear los ficheros. Podemos leer su contenido o modificarlo, cambiar su nombre, copiarlos o moverlos y eliminarlos. Todas aquellas actividades que implican modificar ficheros debemos hacerlas siempre teniendo en cuenta que no sea sobre ficheros importantes o que queremos conservar. Por cierto, una práctica saludable es hacer copias de seguridad con frecuencia.

Vamos a crear nuestro propio fichero para jugar con él sin cargarnos nada:

# write file

import os

while True:
fname=input("Nombre de fichero: ")
if os.path.isfile(fname):
if input(f"{fname} ya existe. ¿Sobreescribir? (S/N) ").upper()!="S":
continue
break

txt=input("Texto para guardar: ")

with open(fname,"w",encoding="utf-8") as f:
f.write(txt)
f.close()

El bucle de las líneas 5-10 tiene por objeto obtener un nombre de fichero, y que si dicho nombre ya existe el usuario tenga que confirmar su modificación. La comprobación de la existencia del fichero la hacemos en la línea 7 mediante la función os.path.isfile(path), que devuelve True si path existe y es un fichero. En realidad hemos dejado un "fleco" suelto, si introducimos un nombre de un directorio existente, no se comprueba, y se produciría un error al tratar de crear un fichero con ese mismo nombre. En cualquier caso no habría peligro de dañar el directorio, puesto que las funciones que gestionan ficheros y directorios son completamente diferentes.

Una vez obtenido un nombre de fichero, la línea 12 nos pide un texto para guardar en el fichero, y en la línea 14 abrimos el fichero en modo escritura mediante el argumento "w" en segunda posición. Si el fichero no existe se crea y queda abierto en modo de escritura como texto (esa es la opción por defecto). Si el fichero existe, se trunca a 0, el contenido del fichero se borra y queda abierto para escritura. Hemos seleccionado la codificación UTF-8 que es la que Python usa por defecto. En las líneas 15-16 volcamos el texto en el fichero y a continuación cerramos este.

============= RESTART: C:/Users/User/Documents/Python/Write file.py =============
Nombre de fichero: Write file.py
Write file.py ya existe. ¿Sobreescribir? (S/N) B
Nombre de fichero: Test.txt
Texto para guardar: Con cien cañones por banda,\nviento en popa, a toda vela,\nno corta el mar sino vuela\nun velero bergantín.
>>>

El programa no es muy amable, no obtenemos ninguna indicación de que haya funcionado, salvo la ausencia de errores. Puedes comprobar que la respuesta a la pregunta ¿sobreescribir? se trata con mucha libertad, en realidad cualquier cosa que no sea una "s" se considera una negativa. Si abrimos nuestro directorio de Python y abrimos el fichero Text.txt que habrá aparecido allí, obtendremos lo siguiente:

Por supuesto, input() lee cadenas literalmente, no sustituye las secuencias de escape. Para eso modifiquemos la línea 12 de la siguiente forma:

txt=eval(chr(34)+input("Texto para guardar: ")+chr(34))

Si volvemos a ejecutar el programa ocurrirá lo siguiente:

============= RESTART: C:/Users/User/Documents/Python/Write file.py =============
Nombre de fichero: Test.txt
Test.txt ya existe. ¿Sobreescribir? (S/N) s
Texto para guardar: Con cien cañones por banda,\nviento en popa, a toda vela,\nno corta el mar sino vuela\nun velero bergantín.
>>>

Esta vez el contenido del fichero Test.txt será:

Creemos otro programa para comprobar el modo append; añadir contenido a un fichero:

# append file

import os

while True:
fname=input("Nombre de fichero: ")
if os.path.isfile(fname):
if input(f"{fname} ya existe. ¿Modificar? (S/N) ").upper()!="S":
continue
break

txt=eval(chr(34)+input("Texto para añadir: ")+chr(34))

with open(fname,"a",encoding="utf-8") as f:
f.write("\n"+txt+"\n")
f.close()

Hemos cambiado mínimamente los textos de las líneas 8 y 12, el modo de apertura de la función open() de la línea 14 y hemos añadido saltos de línea en la línea 15, antes y después del texto. El modo de apertura indicado por "a" implica que todo aquello que escribamos en el fichero se añadirá al final de este, no podemos alterar el contenido anterior de ninguna forma, es un modo seguro si lo que nos interesa hacer es precisamente eso. La ejecución del nuevo programa puede ser como la siguiente:

============= RESTART: C:/Users/User/Documents/Python/Append file.py =============
Nombre de fichero: Test.txt
Test.txt ya existe. ¿Modificar? (S/N) s
Texto para añadir: \nBajel pirata que llaman\npor su bravura \"El Temido\",\nen todo el mar conocido\ndel uno al otro confín.
>>>

Y habremos obtenido el siguiente fichero de texto:

Vamos a dar un importante salto para incorporar facultades de edición a nuestro programa. Agárrate fuerte:

#Read and write

import os

#Función para contar el número de líneas
def getfilelines(f):
f.seek(0)
return len(f.readlines())

#Función para elegir un rango de líneas
def selectfilelines(f):
maxlin=getfilelines(f)
while True:
start=eval(input(f"Desde línea: (0-{maxlin}): "))
end=eval(input(f"Hasta línea: ({start+1}-{maxlin}): "))
if 0 <= start <= end <= maxlin
return start,end

#Función para reescribir el fichero con nuevo contenido
def rewrite(f,start,end,text):
f.seek(0)
old=f.readlines()
new=""
for i in range(0,start):
new+=old[i]
new+=text
for i in range(end,len(old)):
new+=old[i]
f.seek(0)
f.write(new)
f.truncate()
f.flush()

################ PROGRAMA PRINCIPAL ################

#Seleccionamos un nombre de fichero
while True:
fname=input("Nombre de fichero: ")
if os.path.isfile(fname):
if input(f"{fname} ya existe. ¿Modificar? (S/N) ").upper()=="S":
break
else:
open(fname,"x").close()
break

#Abrimos el fichero en modo lectura-escritura
whith open(fname,"r+",encoding="utf-8") as f:
#Bucle principal
while True:
#Selección de opciones
while (opt:=input("\n\u24CBer \u24B7orrar "+
"\u24BEnsertar \u24B6ñadir "+
"\u24b8ancelar ").upper()) not in "VBIAC":
pass
if opt=="V" #Ver el fichero
f.seek(0)
for line,text in enumerate(f):
print(f"{line:4d} {text}",end="")
elif opt=="B" #Borrar líneas
start,end=selectfilelines(f)
rewrite(f,start,end,"")
elif opt=="I" #Insertar líneas
start,end=selectfilelines(f)
text=eval(chr(34)+input("Texto a insertar: ")+chr(34))
rewrite(f,start,end,text)
elif opt=="A" #Añadir texto
start=end=getfilelines(f)
text=eval(chr(34)+input("Texto a añadir: ")+chr(34))
rewrite(f,start,end,text)
elif opt=="C"#Cancelar
break

print("Terminado")

Esta vez se trata de un programa de cierta entidad, vamos a abordarlo progresivamente.

En primer lugar observa la estructura general. En las primeras líneas del programa (despues del comentario con el nombre del programa) están las bibliotecas importadas. En este caso la biblioteca os. A continuación definimos las funciones que vamos a emplear y por fín tenemos el código principal. Aquí es el punto en el que hay que comenzar a estudiar el flujo del programa.

Dentro del programa hay dos secciones claras, en forma de dos bucles infinitos. Las líneas 37-44 insistirán hasta conseguir un nombre de fichero, si el fichero existe se nos pide confirmación para emplearlo, en caso contrario en la línea 43 usamos el modificador "x" en la llamada a open(), que indica que el fichero se abre exclusivamente para ser creado. En la misma sentencia usamos el método .close() sobre el objeto fichero devuelto por open() para volver a cerrar el fichero recién creado.

El cuerpo principal se encuentra entre las líneas 47-71. Abrimos el fichero dentro de un bloque width para evitar problemas en caso de error y lanzamos el bucle principal.

Ya hemos comentado que la mayor parte de los programas funcionan dentro de un bucle en el que se gestiona todo el trabajo. El nuestro incluye un bucle anidado de entrada en las líneas 51-54 que nos muestra un menú de opciones y acepta una tecla para indicar la opción elegida. Por último, una vez conseguida una opción válida el bloque condicional 55-71 procesa cada una de las posibilidades y de vuelta al comienzo del bucle principal.

Una vez comprendida la estructura general, vamos a ver detalladamente cada pequeña sección de código

#Función para contar el número de líneas
def getfilelines(f):
f.seek(0)
return len(f.readlines())

Como explica el comentario, queremos saber el número de líneas de texto del fichero. La función necesita el objeto fichero (el que hemos obtenido en la línea 47 mediante la función open() y asignado al identificador f). Empleamos para el parámetro el mismo nombre, pero no es necesario, el valor f de la función getfilelines() es un valor que solamente se reconoce dentro de dicha función. En cualquier caso emplear los mismos nombres resulta más coherente y hace el código más sencillo de entender.

Empleamos el método .seek() del objeto fichero para mover el puntero al comienzo del mismo. Esto es muy importante porque todas las instrucciones de lectura o escritura se producen a partir del punto en que esté situado el puntero del fichero y modifican este según la cantidad de valores escritos o leídos.

El método .readlines() devuelve en forma de lista cada una de las líneas de texto, solamente lo usamos para obtener el número de elementos de la lista, que es el de líneas en el fichero. Como leemos todo el fichero el puntero queda colocado al final del mismo.

#Función para elegir un rango de líneas
def selectfilelines(f):
maxlin=getfilelines(f)
while True:
start=eval(input(f"Desde línea: (0-{maxlin}): "))
end=eval(input(f"Hasta línea: ({start+1}-{maxlin}): "))
if 0 <= start <= end <= maxlin
return start,end

Aquí tenemos una función de entrada, que recaba información al usuario. Se trata de saber en qué linea queremos comenzar y hasta que línea queremos llegar en las operaciones de borrado e inserción.

Lo primero que hacemos es obtener mediante la función anterior el número total de líneas del fichero para establecer un márgen para la entrada. Para ello necesitamos el argumento con la referencia al fichero. A continuación empleamos de nuevo un bucle infinito del cual solo saldremos cuando la entrada sea satisfactoria.

Guardamos en la variable start el valor de la primera línea, usando un prompt que nos indica el rango aceptable. A continuación hacemos lo mismo para la última línea, guardándola en la variable end. Por fín comprobamos en la línea 16 que el rango de valores es válido y en caso afirmativo devolvemos ambas variables.

#Función para reescribir el fichero con nuevo contenido
def rewrite(f,start,end,text):
f.seek(0)
old=f.readlines()
new=""
for i in range(0,start):
new+=old[i]
new+=text
for i in range(end,len(old)):
new+=old[i]
f.seek(0)
f.write(new)
f.truncate()
f.flush()

Esta función es la encargada de llevar a cabo tres de las opciones del programa: borrar, insertar o añadir líneas. Recibe cuatro argumentos con la referencia del fichero, los números de línea entre los cuales insertar texto, y el texto a insertar.

Como siempre, colocamos el puntero al comienzo del fichero y esta vez guardamos la lista con las líneas de texto que nos devuelve .readlines(). Este es el contenido original del fichero que se guarda en old. En new en cambio guardamos el texto que contendrá el fichero una vez modificado. De momento una cadena vacía.

Ahora usamos un bucle para añadir a new las líneas del fichero anteriores a la línea de comienzo de la inserción, luego añadimos el texto a insertar y mediante otro bucle las líneas del fichero original posteriores a la línea seleccionada como punto final de inserción. Fíjate en que ambos bucles son idénticos pero usan distintos valores en la función range().

Atento a las líneas a partir de la 29: Volvemos al comienzo del fichero y escribimos el nuevo contenido. Como es posible que dicho contenido sea más corto que el original truncamos el fichero. El método .truncate() acepta un argumento con la longitud en la que se trunca el fichero, si no indicamos nada se realiza en la posición actual del puntero. Como acabamos de escribir, el puntero está justo al final de lo escrito.

Para terminar, el método .flush() vuelva todos los buffers del fichero en el disco. Ten en cuenta que los sistemas de gestión de archivos emplean un almacenamiento en memoria tanto en la lectura como en la escritura de los datos, dado que el acceso a los discos está en un rango de velocidad unos tres dígitos menor que el acceso a memoria. El contenido de esos almacenamientos que llamamos buffers va siendo poco a poco sincronizado con el disco. Para evitar incongruencias después de haber modificado el fichero nos aseguramos de que su contenido quede completamente actualizado llamando a .flush().

#Seleccionamos un nombre de fichero
while True:
fname=input("Nombre de fichero: ")
if os.path.isfile(fname):
if input(f"{fname} ya existe. ¿Modificar? (S/N) ").upper()=="S":
break
else:
open(fname,"x").close()
break

Este en nuestro punto de entrada al programa. Un bucle nos pide un nombre de fichero, comprueba si se trata de un fichero existente y en caso afirmativo nos pide confirmación para acceder a él. Ahora la responsabilidad de cualquier daño es del usuario por aceptar ;-)

Si el fichero no existe la orden open con el modificador "x" crea un fichero nuevo (si existiera el fichero fname se produciría un error, pero ese aspecto ya lo hemos comprobado antes. Según obtenemos la referencia del fichero, sin necesidad de almacenarla en ninguna variable, usamos el método .close() para cerrarlo.

#Abrimos el fichero en modo lectura-escritura
whith open(fname,"r+",encoding="utf-8") as f:

Tanto si el fichero existía de antemano como si acabamos de crearlo ahora lo abrimos con el modificador "r+". El caracter + añadido a "w" o a "r", amplía las posibilidades para que el fichero pueda ser tanto leído como escrito, pero es muy diferente usar "r+" de "w+". En el primer caso el fichero se abre para lectura/escritura, con el puntero colocado al comienzo del mismo. Si el fichero no existe se produce un error, por eso hemos tenido que crearlo antes y que volver a cerrarlo. Si hubiéramos usado "w+", el fichero se crearía para escritura/lectura en caso de no existir, pero si existiese se abriría de la misma manera truncando su longitud a 0 y perderíamos todo el contenido.

#Selección de opciones
while (opt:=input("\n\u24CBer \u24B7orrar "+
"\u24BEnsertar \u24B6ñadir "+
"\u24b8ancelar ").upper()) not in "VBIAC":
pass

Nuestro menú de opciones tiene algunas particularidades simpáticas. Utilizamos el operador de asignación como expresión para guardar el valor en opt a la vez que lo evaluamos. El prompt comienza por un salto de línea para evitar que una visualización anterior del fichero pueda encabalgarse con él. Empleamos secuencias de escape unicode para resaltar las letras que constituyen las entradas del menú. Así es el aspecto:

Ⓥer Ⓑorrar Ⓘnsertar Ⓐñadir Ⓒancelar

Convertimos la entrada a mayúsculas con el método .upper() y comprobamos si están en la cadena "VBIAC". El operador in sobre una cadena comprueba subcadenas de cualquier longitud, en este caso nos interesan los caracteres individuales. Si no hemos introducido ninguno de esos caracteres el bucle ejecuta la sentencia pass que sencillamente no hace nada. Hemos de emplearla porque debe haber alguna sentencia en el bloque del bucle, igual que en cualquier bloque de código.

if opt=="V" #Ver el fichero
f.seek(0)
for line,text in enumerate(f):
print(f"{line:4d} {text}",end="")
elif opt=="B" #Borrar líneas
start,end=selectfilelines(f)
rewrite(f,start,end,"")
elif opt=="I" #Insertar líneas
start,end=selectfilelines(f)
text=eval(chr(34)+input("Texto a insertar: ")+chr(34))
rewrite(f,start,end,text)
elif opt=="A" #Añadir texto
start=end=getfilelines(f)
text=eval(chr(34)+input("Texto a añadir: ")+chr(34))
rewrite(f,start,end,text)
elif opt=="C"#Cancelar
break

Aquí tenemos el "núcleo duro" de nuestro programa. Una cascada de condiciones comprueba cada una de las letras del menú y si encuentra una coincidencia lanza la opción seleccionada.

La opción Ⓥer coloca el puntero al comienzo, puesto que de no hacerlo es muy posible que no se devolviese ningún resultado. Luego iteramos el fichero con la función enumerate() para obtener tanto los números de línea como el texto de cada una. El resultado de iterar un fichero es que va devolviendo línea por línea el contenido (a partir siempre del puntero actual) y desplazando dicho puntero.

La opción Ⓑorrar usa la función selectfilelines() para determinar la porción a borrar. El funcionamiento es como las porciones de una secuencia, o la función range(). Se comienza en la primera línea (start) incluída, y se termina en la última (end) excluída. Sencillamente llamamos a rewrite() con una cadena vacía como texto a incluír.

La opción Ⓘnsertar es idéntica salvo porque nos pide una cadena nueva para insertar. La procesamos con eval() de la misma manera que lo hicimos en los programas Write file.py y Append file.py. De este modo podemos incluir secuencias de escape como saltos de línea, comillas o tabuladores.

Ⓐñadir nos pide también una cadena pero no necesita posición. Usa la última línea obtenida mediante getfilelines() para "insertar" el texto al final del fichero.

Por último Ⓒancelar interrumpe el bucle principal mediante una sentencia break y finaliza el programa.

Ejecuta el programa para ponerlo a prueba:

============= RESTART: C:/Users/User/Documents/Python/Read and write.py =============
Nombre de fichero: test.txt
test.txt ya existe. ¿Modificar? (S/N) s

Ⓥer Ⓑorrar Ⓘnsertar Ⓐñadir Ⓒancelar v
0 Con cien cañones por banda,
1 viento en popa, a toda vela,
2 no corta el mar sino vuela
3 un velero bergantín.
4
5 Bajel pirata que llaman
6 por su bravura "El Temido",
7 en todo el mar conocido
8 del uno al otro confín.

Ⓥer Ⓑorrar Ⓘnsertar Ⓐñadir Ⓒancelar i
Desde línea: (0-9): 0
Hasta línea (1-9): 0
Texto a insertar: Canción del pirata\nJosé de Espronceda\n\n

Ⓥer Ⓑorrar Ⓘnsertar Ⓐñadir Ⓒancelar v
0 Canción del pirata
1 José de Espronceda
2
3 Con cien cañones por banda,
4 viento en popa, a toda vela,
5 no corta el mar sino vuela
6 un velero bergantín.
7
8 Bajel pirata que llaman
9 por su bravura "El Temido",
10 en todo el mar conocido
11 del uno al otro confín.

Usamos el fichero creado en las anteriores prácticas. Al no tener un entorno visual amigable conviene usar muy a menudo la opción Ⓥer para orientarnos. Vemos que la opción Ⓘnsertar ha funcionado correctamente, y nos permite añadir al principio del fichero el título y el nombre del autor. Como curiosidad, el resaltado de sintaxis de IDLE resulta un tanto ridículo fuera de contexto.

Ⓥer Ⓑorrar Ⓘnsertar Ⓐñadir Ⓒancelar i
Desde línea: (0-9): 3
Hasta línea (1-9): 4
Texto a insertar: Con cien pendones por borda\n

Ⓥer Ⓑorrar Ⓘnsertar Ⓐñadir Ⓒancelar v
0 Canción del pirata
1 José de Espronceda
2
3 Con cien pendones por borda
4 viento en popa, a toda vela,
5 no corta el mar sino vuela
6 un velero bergantín.
7
8 Bajel pirata que llaman
9 por su bravura "El Temido",
10 en todo el mar conocido
11 del uno al otro confín.

Esta vez usamos de nuevo la opción Ⓘnsertar para reemplazar la primera línea del poema, con permiso de Espronceda.

Ⓥer Ⓑorrar Ⓘnsertar Ⓐñadir Ⓒancelar b
Desde línea: (0-9): 4
Hasta línea (1-9): 5

Ⓥer Ⓑorrar Ⓘnsertar Ⓐñadir Ⓒancelar v
0 Canción del pirata
1 José de Espronceda
2
3 Con cien pendones por borda
4 no corta el mar sino vuela
5 un velero bergantín.
6
7 Bajel pirata que llaman
8 por su bravura "El Temido",
9 en todo el mar conocido
10 del uno al otro confín.

Ⓑorrar ha funcionado correctamente y hemos eliminado la línea 4. Solo nos falta la opción Ⓐñadir.

Ⓥer Ⓑorrar Ⓘnsertar Ⓐñadir Ⓒancelar a
Texto a añadir: \ny colorín colorado, este cuento se ha acabado

Ⓥer Ⓑorrar Ⓘnsertar Ⓐñadir Ⓒancelar v
0 Canción del pirata
1 José de Espronceda
2
3 Con cien pendones por borda
4 no corta el mar sino vuela
5 un velero bergantín.
6
7 Bajel pirata que llaman
8 por su bravura "El Temido",
9 en todo el mar conocido
10 del uno al otro confín.
11
12 y colorín colorado, este cuento se ha acabado
Ⓥer Ⓑorrar Ⓘnsertar Ⓐñadir Ⓒancelar c
Terminado
>>>

De este modo hemos programado un pequeño editor de texto en modo consola. Por supuesto que no es un programa realmente práctico, es complicado controlar lo que hacemos, y está orientado solamente a líneas, no podemos editar una palabra o caracter sueltos. Además deberíamos incorporar bastante código para evitar errores que podemos provocar.

Ya en la introducción del nombre de fichero pueden pasar varias cosas:

Cada una de estas circunstancias produce un error diferente:

============= RESTART: C:/Users/User/Documents/Python/Read and write.py =============
Nombre de fichero:
Traceback (most recent call last):
File "C:\Users\Miguel\Documents\Desarrollo\Python\Read and write.py", line 43, in <module>
open(fname,"x").close()
FileNotFoundError: [Errno 2] No such file or directory: ''

============= RESTART: C:/Users/User/Documents/Python/Read and write.py =============
Nombre de fichero: Arcade tutorial
Traceback (most recent call last):
File "C:\Users\Miguel\Documents\Desarrollo\Python\Read and write.py", line 43, in <module>
open(fname,"x").close()
PermissionError: [Errno 13] Permission denied: 'Arcade tutorial'

============= RESTART: C:/Users/User/Documents/Python/Read and write.py =============
Nombre de fichero: ¿ni idea?
Traceback (most recent call last):
File "C:\Users\Miguel\Documents\Desarrollo\Python\Read and write.py", line 43, in <module>
open(fname,"x").close()
OSError: [Errno 22] Invalid argument: '¿Ni idea?'

Fíjate en que la mayor parte de los errores provienen de las entradas del usuario. Las líneas 14-15 en la función selectfilelines() son otra potencial fuente de errores, fundamentalmente que se introduzca algo que no se corresponda con un valor numérico.

El último punto en el que podemos causar errores es el input gemelo de las líneas 64 y 68, en las opciones Ⓘnsertar y Ⓐñadir.

Ⓥer Ⓑorrar Ⓘnsertar Ⓐñadir Ⓒancelar a
Texto a añadir: es "ligeramente" absurdo
Traceback (most recent call last):
File "C:\Users\Miguel\Documents\Desarrollo\Python\Read and write.py", line 68, in <module>
text=eval(chr(34)+input("Texto a añadir: ")+chr(34))
File "<string>", line 1
"es "ligeramente" absurdo"
^
SyntaxError: invalid syntax
Ⓥer Ⓑorrar Ⓘnsertar Ⓐñadir Ⓒancelar a
Texto a añadir: \un amigo
Traceback (most recent call last):
File "C:\Users\Miguel\Documents\Desarrollo\Python\Read and write.py", line 68, in <module>
text=eval(chr(34)+input("Texto a añadir: ")+chr(34))
File "<string>", line 1
SyntaxError: (unicode error) 'unicodeescape' codec can't decode bytes in position 0-1: truncated \uXXXX escape
Ⓥer Ⓑorrar Ⓘnsertar Ⓐñadir Ⓒancelar i
Desde línea: (0-2): 0
Hasta línea (1-2): 2
Texto a insertar: "+variable_inexistente+"
Traceback (most recent call last):
File "C:\Users\Miguel\Documents\Desarrollo\Python\Read and write.py", line 64, in <module>
text=eval(chr(34)+input("Texto a insertar: ")+chr(34))
File "<string>", line 1
NameError: name 'variable_inexistente' is not defined

Las comillas del último caso están colocadas para mantener una expresión sintácticamente correcta para la función eval(), pero entre ellas podemos meter expresiones que produzcan errores, como hemos hecho.

Resumiendo, si haces un programa que deba ser utilizado por otras personas debes mirar con lupa cada sentencia input() y analizar lo que el usuario introduzca antes de ponerte a trabajar con ello. Por otra parte, es enormemente divertido pensar en formas de noquear a tu propio programa. 💥

Terminamos de momento con los ficheros viendo cuatro funciones adicionales. Vamos a añadir esta vez la biblioteca shutil que es también parte de la biblioteca estándar e incluye funciones avanzadas para manejar ficheros.

>>> import os
>>> import shutil
>>>
>>> #Cambiar el nombre de un fichero
>>> os.rename("test.txt","Espronceda.txt")
>>>
>>> #Copiar un fichero
>>> shutil.copy("Espronceda.txt","test.txt")
'test.txt'
>>>
>>> #Mover un fichero
>>> shutil.move("Espronceda.txt","..\\test.txt")
'..\\test.txt'
>>>
>>> #Borrar un fichero
>>> os.remove("..\\test.txt")
>>>

Aunque un poco lacónica, shutil al menos nos da una pista de lo que ha ocurrido. os ni siquiera hace eso. Vemos que con las funciones shutil.copy() y shutil.move() no solo podemos cambiar el directorio de destino sino también renombrar el fichero al mismo tiempo. Después de este código hemos dejado las cosas en el mismo punto de partida. Mientras lo tecleas comprueba en tu carpeta de Python y en la superior el resultado de tus instrucciones.

2.2.3 Streams. Entrada y salida estándares

Hemos mencionado en varias ocasiones la salida estándar y la entrada estándar. Puede que recuerdes que al comienzo de esta larga sección hemos mencionado que para el ordenador el acceso a los periféricos de entrada/salida como el teclado y la pantalla no es diferente en absoluto del acceso a los sistemas de almacenamiento, es decir, los discos duros. Por supuesto que la gestión de los sistemas de ficheros es muy diferente de la del teclado, pero el sistema operativo, en un alarde de flexibilidad, está diseñado para gestionar cualquier canal (stream) de transmisión de datos de la misma manera que un fichero. Las diferencias están en los procesos para conseguir la conexión, pero una vez efectuada se emplean prácticamente los mismos mecanismos de lectura/escritura. De hecho se puede emplear el mismo tipo de objeto-fichero para el teclado, la pantalla, un archivo en disco o una conexión remota.

Cuando trabajamos en modo consola se mantienen abiertos tres canales de datos:

NombreDescripciónPor defecto
stdin Entrada estándar (Standard Input) Teclado
stdout Salida estándar (Standard Output) Pantalla
stderr Error estándar (Standard Error) Pantalla

En la tabla vemos que, por defecto, estos canales están asignados al teclado como entrada y a la pantalla como salida. El caso es que podemos cambiarlos y redirigirlos a otro canal, incluídos ficheros. Ya hemos visto ejecutando programas en IDLE que a veces la salida no corresponde con la que obtenemos del mismo programa en el símbolo del sistema (lo vimos con los caracteres de control y con colorama). Esto de debe a que IDLE intercepta la salida estándar y procesa los códigos de otra manera.

También hemos visto, sin ahondar en ello, como al utilizar subprocess.run() capturábamos la salida del comando DIR de la consola. Vamos a volver al símbolo de sistema para aprender más sobre la salida y entrada estándares. Si abres la consola e introduces la orden:

dir /w

Obtendrás el listado del directorio en formato ancho:

Pero si lo introduces de la siguiente forma:

dir /w > dir.txt

No obtienes ninguna salida, en apariencia. Si observas desde una ventana del explorador de archivos la misma carpeta, verás que ha aparecido un fichero con el nombre dir.txt. Lo que ha ocurrido es que hemos usado el indicador de redirección de la salida estándar, y hemos dirigido esta al fichero en lugar de a la pantalla. Abre el nuevo fichero haciendo doble click en él.

¿Observas una anomalía en lo que debería ser una vocal acentuada? Esto se debe a que el bloc que notas ha asumido una codificación ANSI (lo pone abajo a la derecha) y esta no es la misma que usa la consola. En cualquier caso lo que nos interesa ahora es que podemos desviar los datos que un programa envía a la pantalla y guardarlos en un fichero. También podemos hacer lo siguiente:

dir /b >> dir.txt

Abre de nuevo el fichero dir.txt y observarás que se ha conservado el contenido anterior y se ha escrito la nueva salida (un listado del directorio en formato simple) a continuación.

En el símbolo de sistema podemos usar los siguentes comandos de redirección:

> Redirecciona la salida estándar (stdout)
Sobreescribe el contenido
>> Redirecciona la salida estándar
Añade la nueva salida al contenido anterior
2> Redirecciona la salida de error estándar (stderr)
< Redirecciona la entrada estándar
| Tubería (pipe): Conecta la salida de un programa
con la entrada de otro

Con estos comandos podemos redirigir desde y hacia un fichero, un dispositivo (device) o un programa. ¿Recuerdas el programa if.py que creamos en la primera sección? Vamos al directorio de nuestros archivos Python desde la consola. Una vez allí, imita mis pasos. Si has guardado los programas con otros nombres deberás usar los nombres que tú hayas empleado.

Acabamos de usar nuestro último programa para crear un fichero con el contenido "16\n" y llamado miedad. Vemos que si asignamos dicho fichero a la entrada para el programa if.py, el programa lee los datos desde allí como si alguien los hubiera tecleado. Por cierto, si queremos hacer programas para la consola hay que tener en cuenta que los caracteres unicode no funcionan.

En este punto, podemos programar un filtro para la consola, esto es, un programa que recibe datos por la entrada y los devuelve transformados por la salida. Por ejemplo podemos diseñar un filtro para transformar la codificación de salida de la consola en utf-8.

# FILTRO: cp850-unicode

import sys

#Tabla de translación cp850 --> unicode
CP850={0x01:0x263A,0x02:0x263B,0x03:0x2665,0x04:0x2666,
0x05:0x2663,0x06:0x2660,0x07:0x2022,0x08:0x25D8,
0x09:0x25CB,0x0A:0x25D9,0x0B:0x2642,0x0C:0x2640,
0x0D:0x266A,0x0E:0x266B,0x0F:0x263C,0x10:0x25BA,
0x11:0x25C4,0x12:0x2195,0x13:0x203C,0x14:0x00B6,
0x15:0x00A7,0x16:0x25AC,0x17:0x21A8,0x18:0x2191,
0x19:0x2193,0x1A:0x2192,0x1B:0x2190,0x1C:0x221F,
0x1D:0x2194,0x1E:0x25B2,0x1F:0x25BC,0x7F:0x2302,
0x80:0x00C7,0x81:0x00FC,0x82:0x00E9,0x83:0x00E2,
0x84:0x00E4,0x85:0x00E0,0x86:0x00E5,0x87:0x00E7,
0x88:0x00EA,0x89:0x00EB,0x8A:0x00E8,0x8B:0x00EF,
0x8C:0x00EE,0x8D:0x00EC,0x8E:0x00C4,0x8F:0x00C5,
0x90:0x00C9,0x91:0x00E6,0x92:0x00C6,0x93:0x00F4,
0x94:0x00F6,0x95:0x00F2,0x96:0x00FB,0x97:0x00F9,
0x98:0x00FF,0x99:0x00D6,0x9A:0x00DC,0x9B:0x00F8,
0x9C:0x00A3,0x9D:0x00D8,0x9E:0x00D7,0x9F:0x0192,
0xA0:0x00E1,0xA1:0x00ED,0xA2:0x00F3,0xA3:0x00FA,
0xA4:0x00F1,0xA5:0x00D1,0xA6:0x00AA,0xA7:0x00BA,
0xA8:0x00BF,0xA9:0x00AE,0xAA:0x00AC,0xAB:0x00BD,
0xAC:0x00BC,0xAD:0x00A1,0xAE:0x00AB,0xAF:0x00BB,
0xB0:0x2591,0xB1:0x2592,0xB2:0x2593,0xB3:0x2502,
0xB4:0x2524,0xB5:0x00C1,0xB6:0x00C2,0xB7:0x00C0,
0xB8:0x00A9,0xB9:0x2563,0xBA:0x2551,0xBB:0x2557,
0xBC:0x255D,0xBD:0x00A2,0xBE:0x00A5,0xBF:0x2510,
0xC0:0x1514,0xC1:0x2534,0xC2:0x252C,0xC3:0x251C,
0xC4:0x2500,0xC5:0x253C,0xC6:0x00E3,0xC7:0x00C3,
0xC8:0x255A,0xC9:0x2554,0xCA:0x2569,0xCB:0x2566,
0xCC:0x2560,0xCD:0x2550,0xCE:0x256C,0xCF:0x00A4,
0xD0:0x00F0,0xD1:0x00D0,0xD2:0x00CA,0xD3:0x00CB,
0xD4:0x00C8,0xD5:0x0131,0xD6:0x00CD,0xD7:0x00CE,
0xD8:0x00CF,0xD9:0x2518,0xDA:0x250C,0xDB:0x2588,
0xDC:0x2584,0xDD:0x00A6,0xDE:0x00CC,0xDF:0x2580,
0xE0:0x00D3,0xE1:0x00DF,0xE2:0x00D4,0xE3:0x00D2,
0xE4:0x00F5,0xE5:0x00D5,0xE6:0x00B5,0xE7:0x00FE,
0xE8:0x00DE,0xE9:0x00DA,0xEA:0x00DB,0xEB:0x00D9,
0xEC:0x00FD,0xED:0x00DD,0xEE:0x00AF,0xEF:0x00B4,
0xF0:0x00AB,0xF1:0x00B1,0xF2:0x2017,0xF3:0x00BE,
0xF4:0x00B6,0xF5:0x00A7,0xF6:0x00F7,0xF7:0x00B8,
0xF8:0x00B0,0xF9:0x00A8,0xFA:0x00B7,0xFB:0x00B9,
0xFC:0x00B3,0xFD:0x00B2,0xFE:0x25A0,0xFF:0x00A0}

#Guardamos la salida estándar
my_stdout=sys.stdout

#Redirigimos la salida a un fichero con el parámetro /f:
if len(sys.argv)>1:
if sys.argv[1].upper().startswit("/F:"):
f=open(sys.argv[1][3:],"a",encoding="utf-8")
sys.stdout=f

#Leemos la entrada estándar y la volcamos en la salida estándar
while True:
try:
entrada=input()
except EOFError:
break
print(entrada.translate(CP850))

#Recuperamos la salida estándar
sys.stdout=my_stdout

Confeccionar la tabla de translación ha sido trabajoso, pero como muchas cosas en el mundo de la programación, una vez hecha ya la tenemos disponible siempre. Vamos a explicar el programa, que viene cargado de novedades.

Como podrás observar, la tabla de translación es un diccionario, que asocia unas claves numéricas en el rango de 00-FF (0-256) que es la extensión de la codificación de la consola, con valores hexadecimales que se corresponden con códigos unicode. Solamente ha sido preciso incluir los valores que no coinciden.

El código en sí es sencillo. La propiedad sys.stdout almacena la referencia de la salida estándar, la guardamos en una variable por si necesitamos redirigirla.

A continuación viene la sección más novedosa. Cuando invocamos un comando con argumentos desde el Símbolo de sistema, se crea una variable que contiene los argumentos en forma de cadenas. En Python esta variable se guarda en sys.argv, que es una lista de cadenas. El primer valor (sys.argv[0]) siempre es el nombre del programa, al que se incorpora la trayectoria completa. Este es un modo sencillo para un programa de saber en qué directorio está situado, que puede ser diferente del directorio actual. A partir de ahí, cada grupo de caracteres separado por espacios se considera un argumento y se guarda en posiciones consecutivas de la lista. De este modo, sys.argv[1] es el primer argumento.

Comprobamos si este primer argumento comienza por "/f:" (usamos el método .upper() para reconocerlo tanto en minúsculas como en mayúsculas) y en caso positivo, la continuación de la cadena después de este principio (a partir del caracter 3) debe ser un nombre de fichero. Abrimos el fichero en modo append con codificación UTF-8 y asignamos la referencia del fichero abierto que nos devuelve la función open() a la salida estándar. A partir de aquí cualquier intento de escribir en la salida estándar se dirigirá a nuestro fichero.

Llegamos al bucle principal. Aquí también hay varias novedades importantes. Básicamente leemos la entrada estándar mediante input() y la escribimos en la salida estándar mediante print(). Usamos un bucle infinito, y dentro un nuevo bloque: try-except.

Esta es una estructura para gestionar los errores: try define un bloque de instrucciones que se ejecutan. Si todo va bien, se saltan las cláusulas except que pueda haber y la ejecución sigue normalmente, en este caso se escribe en la salida y se continúa con el bucle.

En caso de producirse un error, se interrumpe el bloque de try y, si hay alguna sentencia except que gestione dicho error, se ejecuta esta. Si no la hay Python interrumpe el programa y nos muestra esos bonitos mensajes en rojo que tan acostumbrados estamos a ver a estas alturas. Nosotros interceptamos el error EOFError, que quiere decir que se ha terminado el fichero (o la entrada en general). EOF significa End of File. Esto quiere decir que input() ha agotado el material que tenía para leer, y por tanto detenemos el bucle y terminamos el programa restituyendo la salida estándar a su valor original. En la próxima sección diseccionaremos la gestión de errores más profundamente.

No podemos dejar pasar el método más importante, el que hace el trabajo del programa que era convertir los caracteres de la code page 850 que usa la consola a unicode. En su momento no vimos el método cadena.translate() porque usa un diccionario como argumento y aún no conocíamos la clase dict. Como sabes, un diccionario contiene pares clave:valor, de forma que podemos emplear las claves para obtener los valores. .translate() utiliza un diccionario cuyas claves son números con códigos de caracteres y los valores pueden ser:

  1. Un código unicode
  2. Una cadena de varios caracteres
  3. None

En los dos primeros casos se reemplaza el carácter cuyo código es la clave por que corresponde al código o por la cadena. En el tercer caso se suprime.

Haz la siguiente prueba desde el símbolo de sistema:

He creado un fichero batch con órdenes de la consola para moverme con una sola orden al directorio de Python. Una vez allí obtenemos un listado de los ficheros .py del directorio redirigiéndolo al fichero dir1.txt. A continuación empleamos el filtro que acabamos de crear, usamos una "tubería" para dirigir la salida de dir hacia nuestro programa (hay que escribir también la extensión para que sepa ejecutarlo) y en la línea de comandos añadimos el argumento /f:dir2.txt que como dijimos al observar el funcionamiento del programa le indica a este un fichero para redirigir su propia salida.

Para terminar, ejecutamos directamente los ficheros de texto tecleando sus nombres y extensión. Esto equivale a hacer doble click desde una ventana de Windows. Como la extensión está asociada con un programa (el bloc de notas) se abre este con el fichero. Es lo mismo que hacemos al ejecutar nuestros programas Python directamente en la consola. El resultado es el siguiente, la primera versión interpreta mal los caracteres acentuados o especiales, pero nuestro filtro ha resuelto el problema.

 ...ha resuelto el problema en parte 🤥, la é acentuada del fichero "cadenas del revés.py" no ha salido como debería, aparentemente sale el mismo caracter (una coma). Dejaremos el misterio de momento, el programa está ahí y su función didáctica también.

Por si quieres usar un comando para activar el directorio de Python, aquí te dejo las instrucciones que debes teclear en el cuaderno de notas:

@ECHO OFF
C:
cd \Users\Miguel\Documents\Desarrollo\Python

Ten en cuenta que en la segunda línea debes poner la unidad donde tengas tu directorio Python y en la orden cd debes poner el nombre de tu propio directorio de Python, incluyendo la primera barra atrás para que sea una trayectoria absoluta. Guárdalo como pydir.bat y acabarás de crear un fichero de órdenes por lotes o fichero batch. Es como un programa Python pero contiene órdenes de la consola que esta ejecuta de forma similar. Si ahora desde el explorador de Windows mueves el fichero al directorio C:\Windows, lo tendrás disponible siempre que estés en la consola tecleando pydir.

El programa hace lo siguiente:

@ECHO OFF Desactiva el eco, esto es, la repetición de la órden en la pantalla
El caracter @ desactiva el eco para la orden en la misma línea
C: Cambia la unidad activa a la C:
cd \trayectoria Nos mueve al directorio especificado por trayectoria
La barra atrás implica que la trayectoria se calcula desde el directorio raíz

2.2.4 Profundizando en la clase list

Ya conocimos los datos de tipo secuencia en la primera sección, y hemos visto las operaciones comunes para todos los tipos sentencia cuando vimos los tipos Bytes y Bytearray. Vamos ahora a ampliar nuestro conocimiento de la clase list.

Las tuplas y las listas son muy similares pero las tuplas son inmutables. Vamos a ver un ejemplo:

>>> l=[10,20,30]
>>> t=tuple(l)
>>> l2=l
>>> t2=t
>>> l is l2
True
>>> t is t2
True
>>> l2.append(40)
>>> t2+=(40,)
>>> l is l2
True
>>> t is t2
False
>>> l
[10, 20, 30, 40]
>>> l2
[10, 20, 30, 40]
>>> t
(10, 20, 30)
>>> t2
(10, 20, 30, 40)
>>>

El operador is implica una identidad mayor que la de ==, no solo se trata de que dos objetos tengan contenidos idénticos, sino de que sean el mismo objeto. Vemos que hemos creado una lista y a partir de ella una tupla, luego hemos guardado ambas en otras dos variables, e inicialmente las copias son el mismo objeto que el original. A continuación añadimos un elemento a la segunda lista con el método .append() y a la tupla mediante una asignación con adición, y comprobamos que las listas siguen siendo la misma pero las tuplas ya no lo son.

Si lo recuerdas, cuando hablamos de ello, dijimos que es posible hacer estas operaciones sobre tuplas o cadenas, pero que lo que obtenemos es una nueva tupla o cadena con los nuevos datos, no el original modificado. En cambio al modificar l2 hemos modificado también l, que es el mismo objeto.

Veamos aún más curiosidades acerca de esto:

>>> l1=[]
>>> l2=[]
>>> l3=l1
>>> l1==l2==l3
True
>>> l1 is l2
False
>>> l1 is l3
True
>>> l3+=l2
>>> l1==l2==l3
True
>>> l1 is l3
True
>>> l3=l3+l2
>>> l1==l2==l3
True
>>> l1 is l3
False
>>> l2 is l3
False
>>>

Aquí tenemos tres listas, las dos primeras se han inicializado con una lista vacía, pero son objetos distintos, la tercera se ha inicializado con l1. Las tres tienen el mismo valor, la primera y la tercera se refieren al mismo objeto.

Al usar el operador de asignación con suma, vemos que sigue todo igual, nada se ha modificado respecto al estado anterior, pero cuando empleamos la expresión l3=l3+l2 hay un cambio, el valor de las tres sigue siendo idéntico (tres listas vacías) pero l3 ya no se refiere al mismo objeto que l1, y tampoco que l2, se ha convertido en un tercer objeto diferente. Veamos lo que ocurre en una llamada de función:

# Variable mutables en argumentos

num=40

def muta(lista):
global num
lista.append(num)
num+=10

lista=[]
milista=[10,20,30]

print(milista)
for i in range(3):
muta(milista)
print(milista)

print(lista)

La función muta recibe una lista como argumento y usa el método .append() sobre esa lista, no devuelve ningún valor dado que no hemos incluído una sentencia return. En la línea 6 indicamos que num se refiere a una variable global, con lo cual usamos la variable num que hemos definido en la línea 3. Hemos creado la variable lista en la línea 10 para que entiendas que el nombre del argumento de muta() no tiene nada que ver fuera del ámbito de la propia función, es una variable local por contraposición a la variable global. El resultado es:

============= RESTART: C:/Users/User/Documents/Python/argmuta.py =============
[10, 20, 30]
[10, 20, 30, 40]
[10, 20, 30, 40, 50]
[10, 20, 30, 40, 50, 60]
[]
>>>

La lista que ha cambiado ha sido milista al ser enviada a la función muta. lista no ha sido modificada. Esto quiere decir que cuando enviamos variables mutables como argumentos de función enviamos una referencia a la propia variable, y si es modificada en el curso de la función resultará modificada también fuera de ella. Si queremos usar una lista pero no cambiarla es mejor enviar copias al llamar a las funciones.

Métodos de la clase list
list(iter) Constructor de la clase: Crea un objeto de clase list
Añade elementos a partir del iterable iter
x in l True si algún elemento de l es igual a x
False en caso contrario
x not in l False si algún elemento de l es igual a x
True en caso contrario
l1 + l2 Concatenación de las listas l1 y l2
l * n
n * l
Concatenación de l consigo misma n veces
(Si n es menor o igual a cero devuelve una lista vacía)
l[i] Elemento de índice i de l, contando desde 0
Índices negativos cuentan desde el final
(Error si el índice está fuera de rango)
l[i:j] Porción de los elementos entre i y j
Si j es mayor que i devuelve una lista vacía
l[i:j:k] Porción de los elementos entre i y j
tomados cada k elementos
len(l) Longitud (número de items) de l
min(l) Menor de los elementos de l
(Error si los tipos no soportan la comparación)
max(l) Mayor de los elementos de l
(Error si los tipos no soportan la comparación)
l.index(x)
l.index(x,i)
l.index(x,i,j)
Índice de la primera aparición de x dentro de l
o dentro de una porción de l
(Error si no se encuentra x)
s.count(l) Número de veces que aparece x en l
l[i]=x Reemplaza el elemento i por el valor x
l[i:j]=iter Reemplaza la porción entre i y j por el iterable iter
del l[i:j] Elimina la porción entre i y j
Igual que l[i:j]=[]
l[i:k:k]=iter Reemplaza los elementos entre i y j de k en k
por el iterable iter, que debe tener la misma longitud
del l[i:j:k] Elimina los elementos entre i y j cada k
l.append(x) Añade x al final de la lista
l.clear() Borra el contenido de la lista
Igual que del l[:]
l.copy() Devuelve una lista diferente idéntica a la lista
l.extend(iter)
l+=iter
Extiende la lista con el contenido del iterable
l*=n Extiende la lista n veces consigo misma
n debe ser un valor entero, si es cero o negativo borra el contenido
l.insert(i,x) Inserta el valor x en la posición i
l.pop(i=-1) Nos devuelve el valor del elemento i a la vez que lo elimina
Sin argumento lo hace sobre el último elemento
l.remove(x) Elimina la primera aparición del valor x en la lista
Si x no está en la lista produce un error
l.reverse() Invierte el orden de los elementos de la lista
No devuelve ningún valor
l.sort(key=None,reverse=False) Ordena la lista. Los parámetros con nombre son opcionales
key indica una función que procesa los valores a ordenar
reverse indica un criterio de ordenación descendente

Podemos crear una lista de varios modos:

Los valores de una lista pueden ser de cualquier tipo, y podemos mezclarlos de cualquier modo

#Test de listas

import math
import numpy as np

lista=[1, 1.0, 1+1j, None, True, "A", b"0x01",
bytearray(b"0x01"), (1,), [1], {1:"uno"},
tuple, print, math, np, np.array(1),
np.array(1).reshape]

for i,val in enumerate(lista):
print(f"{i:2d} {val}\n{type(val)}\n{30*'='}")

Hemos mezclado un montón de tipos distintos en esta lista de Babel:

============= RESTART: C:/Users/User/Documents/Python/Test_listas_1.py =============
0 1
<class 'int'>
==============================
1 1.0
<class 'float'>
==============================
2 (1+1j)
<class 'complex'>
==============================
3 None
<class 'NoneType'>
==============================
4 True
<class 'bool'>
==============================
5 A
<class 'str'>
==============================
6 b'0x01'
<class 'bytes'>
==============================
7 bytearray(b'0x01')
<class 'bytearray'>
==============================
8 (1,)
<class 'tuple'>
==============================
9 [1]
<class 'list'>
==============================
10 {1: 'uno'}
<class 'dict'>
==============================
11 <class 'tuple'>
<class 'type'>
==============================
12 <built-in function print>
<class 'builtin_function_or_method'>
==============================
13 <module 'math' (built-in)>
<class 'module'>
==============================
14 <module 'numpy' from 'C:\\Users\\Miguel\\AppData\\Local\\Programs\\Python\\Python38\\lib\\site-packages\\numpy\\__init__.py'>
<class 'module'>
==============================
15 1
<class 'numpy.ndarray'>
==============================
16 <built-in method reshape of numpy.ndarray object at 0x000001E7AB975F30>
<class 'builtin_function_or_method'>
==============================

>>>

Además, si los objetos de una lista son a su vez secuencias podemos crear listas multidimensionales, un poco al estilo de los arrays de numpy.

>>> l=[[1,2,3],["a","b","c"]
>>> l[0]
[1, 2, 3]
>>> l[1]
['a', 'b', 'c']
>>> l[1][1]
'b'
>>> l.append(["\u0391","\u0392","\u0393"])
>>> l
[[1, 2, 3], ['a', 'b', 'c'], ['Α', 'Β', 'Γ']]

El acceso a elementos o secciones de una lista multidimensional resulta bastante más compejo que con los arrays de numpy, pero si necesitamos mezclar tipos es la opción adecuada. En este caso es posible que no se puedan comparar entre si para ordenar la lista, con lo cual obtendríamos un error si lo intentamos.

>>> l=[1,"a",None]
>>> l.sort()
Traceback (most recent call last):
File "<pyshell#4>", line 1, in <module>
l.sort()
TypeError: '<' not supported between instances of 'str' and 'int'

>>>

Las listas también soportan los operadores de comparación, pero como en el caso anterior, siempre que los elementos la soporten. Como las comparaciones se hacen elemento a elemento, si cada par en el mismo índice es de tipos compatibles puede funcionar. En el momento en que se encuentre una diferencia se resuelve la comparación y el resto de elementos no son evaluados.

>>> l=[1,2,3]
>>> m=[0,"a","b"]
>>> n=[1,2]
>>> o=[2]
>>> l>m
True
>>> l>n
True
>>> l>o
False

Por otra parte, si una lista tiene menos elementos que otra, siempre que la comparación resulte idéntica hasta el último elemento de la lista más corta, se considera mayor la más larga. Si los elementos son compuestos, se comparan a su vez de la misma manera:

>>> l=[[1,2,3],["a","b","c"]]
>>> m=[[1,2,3],["a","b","c","d"]]
>>> l>m
False

Los métodos de las listas que trabajan por elementos lo hacen en una única dimensión. Si las listas son multidimensionales puede que no nos proporcionen el resultado esperado.

# Test de listas 2

def lcount(lista,elemento):
cuenta=0
for i in lista:
if type(i)==list or type(i)==tuple
cuenta+=lcount(i,elemento)
else:
if i==elemento:
cuenta+=1
return cuenta

# Creamos una lista bidimensional
lista=[[1,2,3],
[2,3,4],
[5,1,3]]

print(lista.count(1))
print(lcount(lista,1))
print(lcount(lista,3))

El resultado es:

============= RESTART: C:/Users/User/Documents/Python/Test_listas_2.py =============
0
2
3
>>>

El método .count() no ha devuelto el valor correcto porque ha comparado los elementos lista[0], lista[1] y lista[2] que son listas con el entero 1. En cambio la función lcount que hemos escrito funciona correctamente. Recorre cada elemento de la lista, y si este es una lista o tupla, vuelve a recorrerlo llamándose a si mismo. Es lo que se conoce como recursividad en programación. Se trata de un recurso eficaz para casos como este, en el que estamos explorando a través de listas dentro de otras listas con un esquema indefinido. No importa cuantas dimensiones tenga nuestra lista, ni cuantos elementos en cada sub-lista, la recursión los explorará todos. Si encontramos un elemento no compuesto realizamos la comparación e incrementamos la variable cuenta si resulta una igualdad.

Podríamos haber prescindido del condicional de las líneas 9-10 y haber escrito:

cuenta+=i==elemento

Dado que el operador de comparación == tiene más prioridad que la asignación aditiva, se efectúa antes y devuelve True o False, que empleados como valores numéricos equivalen a 1 y 0 respectivamente. Hemos empleado el método del condicional porque resulta más claro, pero hay muchas maneras de "comprimir" la sintaxis y ganar en eficiencia con este tipo de sustituciones.

Vamos a repasar en la práctica los métodos que acabamos de enumerar en la teoría.

# Test de listas 3

import random

# Función para crear una lista de enteros aleatorios
def randomlist(num,start,end=None,step=1):
lista=[]
if end==None:
start,end=0,start
for i in range(num):
lista.append(random.randrange(start,end,step))
return lista

# Función para marcar las apariciones de valor en lista
def apunta(lista,valor):
cadena=" "
for item in lista:
if item==valor:
cadena+="^"*len(str(item))
else:
cadena+=" "*len(str(item))
cadena+="  "
return cadena

maxim=random.randrange(5,21)
lista=randomlist(random.randrange(3,21),maxim)
while True:
num=eval(input(f"Dime un entero de 0 a {maxim-1}: "))
if num in lista:
break
print(f"Lo siento, {num} no está en la lista")

print(f"lista original: {lista}")
print(" "*16+apunta(lista,num))
print(f"{num} in lista = {num in lista}")
print(f"lista.count({num}) = {lista.count(num)}")

La función randomlist() que definimos en las líneas 6-16 crea una lista con valores aleatorios. Los argumentos que tenemos que poner son la longitud de la lista y los valores de la misma forma que en la función range. Podemos poner uno, dos o tres valores y para ello hemos usado un pequeño truco. Damos valores por defecto a end y step para que no sea obligatorio ponerlos, pero a end le damos el valor None, y así podemos comprobar si hemos especificado un solo argumento, en cuyo caso asignamos a start el valor 0 y usamos el argumento como valor para end. Una vez fijados los valores, mediante un bucle llenamos la lista de enteros aleatorios en el rango escogido.

La función apunta() crea una cadena con indicadores (acentos circunflejos) en las posiciones en las que aparezca valor en la lista. Para ello comenzamos con un espacio (por el corchete inicial) e iteramos cada elemento comparándolo con valor. LLenamos el espacio del elemento con espacios o acentos según la comparación resulte falsa o cierta, y añadimos dos espacios por el espacio y coma entre elementos.

A partir de la línea 25 empieza el programa en si, buscamos un valor entre 5 y 20 para la longitud de la lista y creamos esta, luego usamos un bucle infinito (ya has visto la utilidad de esta estructura) hasta obtener un valor que forme parte de la lista. En ese punto, imprimimos la lista, en la línea de debajo la cadena que devuelve apunta() para marcar las apariciones del valor elegido, y a continuación el enunciado y el resultado de la expresión num in lista y del método lista.count(num). Cada ejecución será diferente, este es un ejemplo de la salida:

============= RESTART: C:/Users/User/Documents/Python/Test_listas_3.py =============
Dime un entero de 0 a 10: 4
Lo siento, 4 no está en la lista
Dime un entero de 0 a 10: 6
lista original: [6, 6, 5, 1, 7]
^  ^
6 in lista = True
lista.count(6) = 2

Veamos algunos métodos más:

# Test de listas 4

import random

# Función para crear una lista de enteros aleatorios
def randomlist(num,start,end=None,step=1):
lista=[]
if end==None:
start,end=0,start
for i in range(num):
lista.append(random.randrange(start,end,step))
return lista

# Función para marcar las apariciones de valor en lista
def apunta(lista,valor):
cadena=" "
for item in lista:
if item==valor:
cadena+="^"*len(str(item))
else:
cadena+=" "*len(str(item))
cadena+="  "
return cadena

# Función que imprime la lista y la cadena apuntadora
def apuntada(lista,valor):
print(lista)
print(apunta(lista,valor))

maxim=random.randrange(5,21)
lista=randomlist(random.randrange(3,21),maxim)

for i in range(min(lista),max(lista)+1):
if i in lista:
n=lista.count(i)
v="vez" if n==1 else "veces"
print(f"{i} está {n} {v} en la lista")
apuntada(lista,i)
else:
print(f"{i} no está en la lista")

Hemos reutilizado buena parte del código anterior; añade la función apuntada() y las líneas 33-40. Esta vez comprobamos los valores presentes en la lista, para acotar el proceso usamos las funciones min() y max(). Para cada valor entre el mínimo y el máximo indicamos el número de apariciones y las apuntamos. En la línea 36 usamos una expresión condicional en la asignación para diferenciar el singular del plural.

============= RESTART: C:/Users/User/Documents/Python/Test_listas_4.py =============
0 está 2 veces en la lista
[0, 6, 9, 9, 6, 6, 9, 2, 5, 0, 7]
 ^^
1 no está en la lista
2 está 1 vez en la lista
[0, 6, 9, 9, 6, 6, 9, 2, 5, 0, 7]
^
3 no está en la lista
4 no está en la lista
5 está 1 vez en la lista
[0, 6, 9, 9, 6, 6, 9, 2, 5, 0, 7]
^
6 está 3 veces en la lista
[0, 6, 9, 9, 6, 6, 9, 2, 5, 0, 7]
^^  ^
7 está 1 vez en la lista
[0, 6, 9, 9, 6, 6, 9, 2, 5, 0, 7]
^
8 no está en la lista
9 está 3 veces en la lista
[0, 6, 9, 9, 6, 6, 9, 2, 5, 0, 7]
^  ^^

>>>

Vamos a emplear el método .index() para detectar las posiciones de múltiples apariciones de un elemento.

# Test de listas 5

import random

# Función para crear una lista de enteros aleatorios
def randomlist(num,start,end=None,step=1):
lista=[]
if end==None:
start,end=0,start
for i in range(num):
lista.append(random.randrange(start,end,step))
return lista

def multindex(lista,valor)
index=[]
if valor in lista:
index.append(lista.index(valor))
for i in range(lista.count(valor)-1):
index.append(lista.index(valor,index[-1]+1))
return index

maxim=random.randrange(21)
lista=ramdomlist(10,maxim)
print(lista)
valor=eval(input("Dime un valor de la lista:"))
index=multindex(lista,valor)
print(f"{valor} aparece en las posiciones {index}")

La función multindex() emplea los métodos .count e .index() para localizar todas las apariciones de un valor en la lista. Inicializamos una lista que también llamamos index, para el intérprete no hay conflicto de nombres con el método del mismo nombre, pero nosotros hemos de fijarnos muy bien en cuando nos referimos a la lista index o al método. Por ejemplo, en la línea 19 el primer index es la lista, el segundo el método y el tercero otra vez la lista. Es intencionado para que entendamos que los identificadores tienen sus ámbitos, y fuera de ellos podemos utilizar los mismos nombres sin ningún problema. En la línea 26 usamos una nueva variable index para recibir el resultado de la función multindex().

Si el valor no aparece en la lista, devolvemos la lista vacía que acabamos de crear. Si el valor aparece en la lista, guardamos la posición de la primera aparición en la línea 17, y hacemos un bucle a partir de ahí para todas las apariciones (lista.count(valor)) menos la que ya hemos apuntado, por eso restamos 1.

Esto lo hacemos para tener un primer valor en la lista index al entrar en el bucle de la línea 18 y poder usar la sentencia de la línea 19, que busca desde el índice siguiente a la última aparición de valor en la lista (que apuntamos en index[-1]). Si la lista estuviera vacía, la referencia index[-1] provocaría un error. Si solo hay una aparición el bucle se definirá para un range(0), es decir, no se realizará.

Un ejemplo de ejecución se muestra a continuación:

============= RESTART: C:/Users/User/Documents/Python/Test_listas_5.py =============
[2, 2, 2, 7, 3, 4, 7, 2, 5, 0]
Dime un valor de la lista: 7
7 aparece en las posiciones [3, 6]
>>>

Vemos que el segundo argumento del método .index() se refiere a la posición de comienzo de la porción de la lista, y que se extiende hata el final. Si indicamos un tercer argumento se referirá a la posición de finalización de la porción (como siempre, se termina en la posición inmediatamente anterior). En cualquier caso, aunque la búsqueda sea en una porción el índice devuelto se refiere a la lista completa.

Vamos a completar este repaso sobre el empleo de las listas:

>>> l=[1,1.0,True]
>>> l[0]==l[1]==l[2]
True
>>> l*2
[1, 1.0, True, 1, 1.0, True]
>>> reversed(l)
<list_reverseiterator object at 0x000001C7BBCC17F0>
>>> list(_)
[True, 1.0, 1]
>>> l
[1, 1.0, True]
>>> l.reverse()
>>> l
[True, 1.0, 1]

Creamos una lista con tres valores de tipos distintos, pero con el mismo valor (todos equivalen a 1). Vemos como funciona la multiplicación, nos devuelve la lista añadida a sí misma tantas veceS como el factor por el que la multipliquemos. Sin embargo, siempre que una operación, método o función devuelva un valor, si no lo almacenamos se pierde. Las operaciones que modifican la lista original no devuelven nada, tenemos que comprobar la lista para observar los cambios.

La función reversed() devuelve un iterador con los elementos al revés, podemos extraerlos empleando la función list() que como recordamos construye una lista a partir de un iterable. Seguimos sin modificar la lista original. En cambio, el método .reverse() no devuelve nada pero ha colocado los elementos en orden inverso.

>>> l.insert(2,3)
>>> l
[True, 1.0, 3, 1]
>>> l.sort()
>>> l
[True, 1.0, 1, 3]
>>> l[0]=1+1j
>>> l
[(1+1j), 1.0, 1, 3]
>>> l.sort()
Traceback (most recent call last):
File "<pyshell#72>", line 1, in <module>
l.sort()
TypeError: '<' not supported between instances of 'float' and 'complex'

>>> l.sort(key=abs)
>>> l
[1, 1.0, (1+1j), 3]

EL método .insert() tampoco devuelve nada, pero ha añadido el valor en la posición indicada. El método sort ordena el contenido de la lista comparando los elementos entre sí, si asignamos un valor a una posición concreta reemplazamos el contenido original. Al volver a efectuar la ordenación se produce un error porque no se pueden comparar elementos de diferentes tipos. Sin embargo tanto el método .sort() como la función sorted() pueden recibir el argumento key con una función que procesa los datos para ser comparados. La función abs (utilizamos solo el identificador, que contiene el objeto función. Si pusiesemos los paréntesis invocaríamos la función en ese punto, que no es lo que queremos) transforma el valor complejo en real, y así podemos ordenar la lista.

>>> l.pop(2)
(1+1j)
>>> l
[1, 1.0, 3]
>>> l.extend(range(5))
>>> l
[1, 1.0, 3, 0, 1, 2, 3, 4]
>>> l[0::3]
[1, 0, 3]
>>> l[0::3]=["A","B","C"]
>>> l
['A', 1.0, 3, 'B', 1, 2, 'C', 4]
>>> l.remove("B")
>>> l
['A', 1.0, 3, 1, 2, 'C', 4]
>>> l.remove("a")
Traceback (most recent call last):
File "<pyshell#11>", line 1, in <module>
l.remove("a")
ValueError: list.remove(x): x not in list

>>>

El método .pop() tiene el mismo efecto que la sentencia del para un solo item, pero devuelve el item borrado. .extend() funciona como append() pero permite añadir varios elementos a la vez. Ya hemos visto como funcionan las porciones, no solo podemos obtenerlas sino reemplazar los valores en una porción determinada. .remove() elimina un elemento, al igual que .pop o del, pero aquí seleccionamos el valor del elemento a eliminar, y no su posición. Si el elemento no está en la lista se produce un error.

Vamos a incorporar a nuestro repertorio otra de las funciones incorporadas al intérprete que está especialmente diseñada para trabajar con listas, la función zip().

>>> l1=[1,2,3]
>>> l2=["a","b","c"]
>>> zip(l1,l2)
<zip object at 0x000001B42514BD00>
>>> list(_)
[(1, 'a'), (2, 'b'), (3, 'c')]
>>>

La función zip() toma varios iterables como argumentos y agrupa los elementos en tuplas en orden de índices. Devuelve un objeto zip que es un iterador y podemos convertirlo en lista. Las listas o iterables empleados como argumentos no resultan modificados.

>>> l3=[True,False]
>>> list(zip(l1,l2,l3))
[(1, 'a', True), (2, 'b', False)]
>>> a,b,c=zip(*_)
>>> a
(1, 2)
>>> b
('a', 'b')
>>> c
(True, False)
>>>

Si los argumentos de zip() tienen distinta longitud, zip devuelve un iterador de la longitud del más corto de sus argumentos. Si empleamos un asterisco antes del identificador del argumento que está empaquetado, desempaqueta este. Vamos a explicar detalladamente el funcionamiento de un mecanismo relacionado con las listas, las List Comprehensions, en la información sobre Python en Español las llaman listas por comprensión. Se trata de una forma compacta y fácil de entender de crear el contenido de una lista.

>>> l=[x for x in range(5)]
>>> l
[0, 1, 2, 3, 4]
>>>

Este caso es demasiado sencillo, podríamos haber creado l mediante:

>>> l=list(range(5))

En cualquier caso, el código equivalente sería:

>>> l=[]
>>> for x in range(5):
l.append(x)


>>>

Frente a esto si es más compacto. Vamos a ampliar las posibilidades, no tenemos que limitarnos a usar la variable del bucle tal cual, podemos usar cualquier expresión antes del for:

>>> l=[x**2 for x in range(5)]
>>> l
[0, 1, 4, 9, 16]
>>>

Ahora empezamos a atisbar las posibilidades de las listas por comprensión. Además podemos filtrar los resultados del bucle mediante un condicional:

>>> l=[x for x in range(5) if x%3!=0]
>>> l
[1, 2, 4, 5, 7, 8, 10, 11, 13, 14]
>>>

Esto es más interesante, hemos eliminado los múltiplos de 3. Vamos a ver más posibilidades:

>>> l=[(x,chr(x)) for x in range(97,102)]
>>> l
[(97, 'a'), (98, 'b'), (99, 'c'), (100, 'd'), (101, 'e')]
>>> l=[(x,y) for x in [1,2,3] for y in [1,2,3] if x!=y]
>>> l
[(1, 2), (1, 3), (2, 1), (2, 3), (3, 1), (3, 2)]
>>> l=[(x,y) for x in [1,2,3] for y in range(x)]
>>> l
[(1, 0), (2, 0), (2, 1), (3, 0), (3, 1), (3, 2)]
>>>

Podemos generar varios valores (en tuplas) para cada vuelta del bucle. Podemos anidar bucles y podemos emplear las variables de los bucles externos dentro del cálculo de los bucles internos. Por supuesto siempre podemos usar condiciones. El límite es lo que podamos imaginar.

2.2.5 Conjuntos: set y frozenset

Estudiaremos ahora dos nuevos tipos incorporados, de tipo colección (con múltiples elementos) pero no secuencia, dado que se trata de colecciones sin ordenar (no podemos usar índices para acceder a ellas) y cuyos elementos nunca se repiten. Se trata del tipo set y su contrapartida inmutable frozenset.

Vamos a hablar del tipo set, el tipo frozenset es idéntico excepto que no puede ser modificado. Podemos crear un objeto de tipo set empleando el constructor set() o declarando un literal entre llaves con elementos sencillos separados por comas (a diferencia del tipo dict cuyos elementos son pares clave:valor). Para declarar un conjunto vacío hemos de emplear el constructor, puesto que unas llaves vacías indican un DICCIONARIO vacío.

Como de costumbre, aprendemos mejor con la práctica:

>>> a={"Abracadabra"}
>>> a
{'Abracadabra'}
>>> type(a)
<class 'set'>
>>> a=set("Abracadabra")
>>> a
{'A', 'r', 'a', 'c', 'b', 'd'}
>>>

El constructor set() requiere como argumento un iterable, ya sabemos que una cadena lo es y devuelve sus caracteres. Observa que en el conjunto a de la última línea están todos los caracteres diferentes de la palabra "Abracadabra", y que no están en orden. Ya hemos dicho que un conjunto es una colección desordenada y que sus elementos no se repiten. Para obtener el resultado del primer conjunto con el constructor deberíamos haber usado la siguiente sintaxis:

>>> a=set(["Abracadabra"])
>>> a
>>>
{'Abracadabra'}
>>>

Veamos las operaciones que podemos emplear tanto en los tipos set como frozenset

Operaciones sobre conjuntos en general
set y frozenset
len(conj) Devuelve el número de elementos (longitud) del conjunto
x in conj Devuelve True si el valor x está dentro del conjunto
False en caso contrario
x not in conj Devuelve False si el valor x está dentro del conjunto
True en caso contrario
conj1.isdisjoint(conj2) Devuelve True si ambos conjuntos son disjuntos,
es decir, si no tienen elementos en común
False en caso contrario
conj.issubset(iter)
conj1 <= conj2
conj1 < conj2
Devuelve True si cada elemento de conj está también en conj2
conj.issuperset(iter)
conj1 >= conj2
conj1 > conj2

Devuelve True si cada elemento de conj2 está también en conj
conj.union(iter1, ... itern)
conj1 | conj2 | ... conjn
Devuelve un nuevo conjunto con los elementos diferentes de todos
conj.intersection(iter1, ... itern)
conj1 & conj2 & ... conjn
Devuelve un nuevo conjunto con los elementos comunes de todos
conj.difference(iter1, ... itern)
conj1 - conj2 - ... conjn
Devuelve un nuevo conjunto con los elementos que solo están en conj1
conj.symmetric_difference(iter)
conj1 ^ conj2
Devuelve un nuevo conjunto con los elementos que están en un u otro
pero no en ambos conjuntos
conj.copy() Devuelve un conjunto diferente con el mismo contenido

Los métodos .union(), .intersection(), difference(), simmetric_difference(), issubset() e issuperset() aceptan cualquier iterable entre sus argumentos, no solo conjuntos. Por supuesto, los conjuntos son iterables, podemos emplear bucles para obtener sus elementos. El hecho de que podamos usar operadores de comparación con conjuntos no implica que podamos establecer un orden entre ellos. Por ejemplo, si tenemos una lista de conjuntos y usamos el método lista_de_conjuntos.sort() el resultado será imprevisible.

Operaciones sobre conjuntos mutables: set
set.update(iter1, ... itern)
set1 |= set2 | ... setn
Actualiza el conjunto añadiendo los elementos diferentes del resto
set.intersection_update(iter1, ... itern)
set1 &= set2 & ... setn
Actualiza el conjunto manteniendo solo los elementos comunes
set.difference_update(iter1, ... itern)
set1 -= set2 | ... set n
Actualiza el conjunto eliminando los elementos
que estén en cualquier otro
set.simmetric_difference_update(iter)
set1 ^ set2
Actualiza el conjunto dejando solo aquellos elementos
que están en uno u otro pero no en ambos
set.add(elem) Añade el elemento al conjunto si no estaba ya antes
set.remove(elem) Elimina el elemento del conjunto
Produce un error si el elemento no se encuentra
set.discard(elem) Elimina el elemento si está presente
set.pop() Elimina y devuelve un elemento al azar
Produce un error si el conjunto está vacío
set.clear() Vacía el conjunto

Vamos a usar un conjunto para una de las funciones para las que es perfecto, que es obtener una muestra de cada uno de los elementos diferentes en una secuencia, en este caso en una cadena de caracteres. Una vez conseguida esta información podemos conocer el número de veces que está presente cada caracter en el texto, y el porcentaje sobre el total.

# Set test 1

import math

# Una función que limita el número de decimales
def trunc(x,decs=2):
return math.trunc(x*10**decs)/10**decs

# Una función que calcula porcentajes
def percent(x,total):
return x*100/total

# Una función que cuenta todos los caracteres diferentes de un texto
def chcount(text)
l=sorted(list(set(text)))
result=[]
for ch in l:
result.append((ch,text.count(ch),trunc(percent(text.count(ch),len(text)))))
return result

texto="A set object is an unordered collection of distinct hashable objects. "+\
"Common uses include membership testing, removing duplicates from a sequence, "+\
"and computing mathematical operations such as intersection, union, difference, "+\
"and symmetric difference. (For other containers see the built-in dict, list, "+\
"and tuple classes, and the collections module.)"

for i in chcount(texto):
print(f"{i[0]} - {i[1]:3d} ({i[2]:5}%)")

La función trunc() nos permite recortar el número de decimales de un valor float. Multiplicamos por diez elevado al número de decimales que queremos conservar, lo que equivale a desplazar el punto decimal a la derecha ese número de posiciones. Usamos la función math.trunc() que elimina todos los decimales y volvemos a colocar el punto decimal en su sitio dividiendo por diez elevado al mismo valor del principio. Por defecto mantiene dos dígitos decimales.

La función percent() nos devuelve el porcentaje de x sobre el valor de total y no tiene ningúm misterio. La función chcount() si requiere alguna explicación. En la línea 15 obtenemos todos los caracteres diferentes del texto (construyendo un set a partir de él), a su vez convertimos el set en lista para poder ordenarlo. Usamos esa lista, que contiene en orden todos los caracteres diferentes de texto, para llenar la lista result con tuplas que contienen; el caracter, las veces que aparece en el texto y el porcentaje sobre el texto total.

Hemos usado un texto de la sección de documentación de la web: python.org acerca del tipo set. El programa es sí se ejecuta en las líneas 27 y 28, en las que invocamos la función chcount() e iteramos el resultado para presentarlo en columnas. Este es el resultado:

============= RESTART: C:/Users/User/Documents/Python/Set_test_1py =============
  -  47 (13.42%)
( -   1 ( 0.28%)
) -   1 ( 0.28%)
, -   8 ( 2.28%)
- -   1 ( 0.28%)
. -   3 ( 0.85%)
A -   1 ( 0.28%)
C -   1 ( 0.28%)
F -   1 ( 0.28%)
a -  16 ( 4.57%)
b -   5 ( 1.42%)
c -  20 ( 5.71%)
d -  13 ( 3.71%)
e -  38 (10.85%)
f -   6 ( 1.71%)
g -   3 ( 0.85%)
h -   8 ( 2.28%)
i -  24 ( 6.85%)
j -   2 ( 0.57%)
l -  13 ( 3.71%)
m -  12 ( 3.42%)
n -  25 ( 7.14%)
o -  21 (  6.0%)
p -   5 ( 1.42%)
q -   1 ( 0.28%)
r -  13 ( 3.71%)
s -  23 ( 6.57%)
t -  25 ( 7.14%)
u -  11 ( 3.14%)
v -   1 ( 0.28%)
y -   1 ( 0.28%)
>>>

Con esto dejamos este esbozo de los conjuntos y pasamos a otros temas.

2.2.6 Gestión de errores

Ya hemos visto en la sección sobre streams que existe un bloque especializado en gestión de errores, el bloque try-except. Vamos a mirar en detalle los mecanismos de gestion de errores mediante este bloque y su sintaxis completa.

La forma más sencilla es un bloque try: con código y a continuación uno o más bloques except:. En cada sentencia except especificamos el error que dicha sentencia procesa, o un grupo de errores en forma de tupla.

try:
algunas
sentencias
que pueden
dar errores
except ErrorClass1:
Aquí gestionamos
el error de clase 1
except (ErrorClass2,ErrorClass3):
Aquí gestionamos
los errores de clase
2 y 3

Si no se produce ningún error el bloque try se ejecuta íntegramente y no se ejecuta ninguno de los bloque except. Si se produce un error se interrumpe en ese punto la ejecución del bloque try y se ejecuta el primer bloque except que especifique ese error si lo hay, en caso contrario se produce la habitual parada del programa con la información sobre lo sucedido en color rojo. Si un bloque except intercepta un error cuando termine prosigue la ejecución después de toda la estructura try-except, no se ejecuta ningún otro bloque except.

Si el último de los bloques except no indica ningún error concreto será ejecutado para cualquier error que no hayan interceptado los bloques anteriores.

Aunque se produzca otro error en algún bloque except no será procesado por el programa, sino que el intérprete interrumpirá la ejecución (a no ser que dentro del bloque except creemos otra estructura try-except).

Podemos añadir una cláusula else después de todos los except y se ejecutará después del bloque try si no se han producido errores. También podemos cerrar todo el conjunto con una cláusula finally:, que se ejecutará siempre después de terminar el bloque, tanto si no se producen errores como si se producen, y en este caso tanto si son procesados por alguna cláusula except como si no. Un bloque finally suele emplearse para realizar "trabajos de limpieza", como cerrar ficheros o conexiones de red, destruir objetos innecesarios, etc.

try:
algunas
sentencias
que pueden
dar errores
except ErrorClass1:
Aquí gestionamos
el error de clase 1
except (ErrorClass2,ErrorClass3):
Aquí gestionamos
los errores de clase
2 y 3
...
except:
Aquí gestionamos
el resto de los errores
else:
Aquí terminamos las tareas
si no se han producido errores
finally:
Aquí realizamos
labores de limpieza

Esta es la estructura de un bloque try-except con todas sus posibilidades. Vamos a ver un programa para ir entendiendo el concepto.

# Manejo de excepciones

import os,sys
from colorama import init,Fore,Back,Style
init()

def inIDLE():
return("idlelib" in sys.modules)

def entrada():
print(verde+"Expresión:"+reset,end=" ")
return exec(input())

def infinito():
return infinito()

if inIDLE():
verde=azul=rojo=negro=amarillo=cyan=magenta=gris=reset=""
else:
verde=Fore.GREEN
azul=Fore.BLUE
rojo=Fore.RED
negro=Fore.BLACK
amarillo=Fore.YELLOW
cyan=Fore.CYAN
magenta=Fore.MAGENTA
gris=Fore.LIGHTBLACK_EX
reset=Style.RESET_ALL

lista=[x for x in range(1,11)]
iterador=(x for x in range(2))
for i in iterador:
pass

Flag=True
while Flag:
try:
x=entrada()
except ZeroDivisionError:
print(rojo+"Hasta el infinito...")
except NameError:
print(rojo+"A mi que me registren...")
except SyntaxError:
print(rojo+"La letra con sangre entra")
except FileNotFoundError:
print(rojo+"No busques donde no hay")
except TypeError:
print(rojo+"No mezcles churras con merinas")
except AttributeError:
print(rojo+"De eso no me queda")
except IndexError:
print(rojo+"Estás que te sales")
except OverflowError:
print(rojo+"¡Cuidado con explotar!")
except RecursionError:
print(rojo+"No le des más vueltas")
except StopIteration:
print(rojo+"Estoy agotado...")
except SystemExit:
print(rojo+"\nVuelta al mundo real")
Flag=False:
except KeyboardInterrupt:
print(rojo+"\nQue poco dura el amor")
Flag=False:
except:
print(rojo+"Aquí me has pillado...")
finally:
if Flag:
print(azul+"Prueba otra vez >>> ",end="")

Las líneas 1-34 son solo preparativos. Importamos las librerías que necesitamos, incluyendo colorama, e inicializamos esta. Usamos la función inIDLE() que ya vimos en su día para saber si estamos en el entorno de ejecución de IDLE, en cuyo caso no emplearemos códigos de color. La función entrada presenta un prompt en color (si estamos en modo consola) y devuelve la expresión que introduzcamos ejecutándola. La función infinito() se llama a si misma sin ningún mecanismo de parada, con lo cual provoca una recursión infinita.

En las líneas 17-28 inicializamos los códigos de color, si estamos en IDLE serán cadenas vacías. Por último, creamos una lista de 10 elementos y un iterador de solo 2 y agotamos este último. Ya tenemos el escenario preparado para el bucle principal.

Este discurre entre las líneas 35 y el final del programa. La variable Flag controla la salida del bucle. Llamamos a la función entrada dentro de la cláusula try, de forma que si se producen errores serán procesados por la batería de cláusulas except a continuación. El bloque finally se limita a animarnos a repetir si no hemos provocado la terminación del programa.

Ya hemos mencionado que la mayor parte de los errores vienen de las entradas de datos por parte del usuario. De hecho emplear una función eval() o exec() sobre un texto introducido por el usuario es un tanto temerario, damos a este la posibilidad de vulnerar nuestro sistema. En este caso vamos a provocar todos los errores de la lista de excepts desde la consola.

Cuando definimos un tipo de excepción en la cláusula except, eso implica interceptar tanto esa excepción propiamente como todas aquellas que se derivan de ella (En la próxima sección abordaremos la programación orientada a objetos). Como todo el sistema de clases, existen unas clases básicas y a partir de ellas se crean subclases. La clase básica de las excepciones es BaseException, y de ella derivan todas las demás. Como las cláusulas except se comprueban en orden, si interceptamos una clase base, nunca se llegará a interceptar ninguna clase derivada. Por ejemplo:

# Excepciones 2

import sys

def entrada():
return exec(input("Expresión: "))

while True:
try:
x=entrada()
except KeyboardInterrupt:
break
except BaseException:
print(sys.exc_info()[:2])
except ZeroDivisionError:
print("Error de división por cero")

Como el error KeyboardInterrupt está en primer puesto, si pulsamos CTRL+C se ejecutará la orden break y terminaremos el programa, pero el error de división por cero nunca llegará a la línea 15 porque será procesado por el anterior except.

============= RESTART: C:/Users/User/Documents/Python/Excepciones_2.py =============
Expresión: hola
(<class 'NameError'>, NameError("name 'hola' is not defined"))
Expresión: 1/0
(<class 'ZeroDivisionError'>, ZeroDivisionError('division by zero'))
Expresión:
>>>

Algunos ejemplos de las excepciones que podemos controlar son:

Clases de excepciones
BaseException La clase básica. Si la interceptamos procesaremos todas
AttributeError Ha fallado una referencia a un atributo de un módulo, clase u objeto
Queremos utilizar una propiedad o método inexistente
EOFError Se produce cuando una función input() deja de recibir datos
y encuentra una señal EOF (End of File)
IndexError Estamos usando un índice fuera de rango para una secuencia
KeyError Estamos usando una clave que no existe en un diccionario
KeyboardInterrupt Hemos pulsado la combinación de teclas CTRL+C
NameError El intérprete no encuentra un identificador
Queremos usar una variable o función que no existen
OSError Una llamada al SO (Sistema operativo) ha producido un error
OverflowError Hemos excedido la capacidad de representación para números
Con enteros no se produce nunca
RecursionError Hemos excedido el límite de recursividad
StopIteration Un iterador ha alcanzado el final
Esto es lo que produce la detención de un bucle for
SyntaxError Se ha encontrado un error sintáctico
SystemExit Se ha invocado la función sys.exit()
TypeError Una expresión o función ha recibido valores de tipos incorrectos
UnicodeError Se ha producido un error de codificación/decodificación de caracteres
ValueError Una expresión o función ha recibido valores de tipo correcto
pero de magnitud inadecuada
ZeroDivisionError Hemos intentado realizar una división por cero

Gestionando los puntos en que se pueden producir errores podemos reaccionar a ellos y tener un programa más robusto y más elegante. Hemos visto que la función sys.exc_info() devuelve información relativa al último error producido, concretamente devuelve una tupla con tres valores: tipo de excepción, valor e información de trazado. Por trazado se entiende el seguir el rastro del programa a través de las instrucciones que han producido el error, es una de los mecanismos que empleamos en depuración, que es como se denomina al proceso de buscar errores dentro de un programa para corregir el código.

Si quieres ver el árbol completo de las clases de excepciones aquí lo tienes. Y con esto damos por terminada esta segunda y extensa sección. En este momento dispones de herramientas suficientes para elaborar casi cualquier tipo de programa.