Actualización del libro PYTHON A FONDO a la versión 3.10 Óscar Ramírez Jiménez Extracto de Python a fondo Primera edición, 2021 Primera reimpresión, 2021 Segunda reimpresión actualizada, 2022 © 2021 Óscar Ramírez Jiménez © 2021 MARCOMBO, S. L. www.marcombo.com Diseño de cubierta: ENEDENÚ DISEÑO GRÁFICO Revisor técnico: Ferran Fábregas Corrección: Haizea Beitia y Manel Fernández Maquetación: D. Márquez Directora de producción: M.a Rosa Castillo «Cualquier forma de reproducción, distribución, comunicación pública o transformación de esta obra solo puede ser realizada con la autorización de sus titulares, salvo excepción prevista por la ley. Diríjase a CEDRO (Centro Español de Derechos Reprográficos, www.cedro.org) si necesita fotocopiar algún fragmento de esta obra». ISBN: 978-84-267-3227-9 D.L.: B 19897-2020 Impreso en Servicepoint Printed in Spain Python a fondo • Versión 3.8 (octubre del 2019): introducción del operador walrus, se añaden los parámetros solo-posicionales usando /en las funciones y el soporte de = para los f-strings, para autodocumentar expresiones y ayudar a depurar. • Versión 3.9 (octubre del 2020): se añade el paquete zoneinfo para facilitar el uso de zonas horarias en fechas, se añade el operador de unión (|) para diccionarios, se permite el uso de expresiones en decoradores, se añaden los métodos removesuffix y removeprefix para cadenas de caracteres, se permite usar tipos del builting para definir hints sin necesidad de importar la librería y se añade Annotated a typing para mejorar la integración de ambas, entre otros muchos cambios. Cabe destacar que en esta versión se han borrado muchas funciones que estaban presentes por retrocompatibilidad con la versión 2, y que en las siguientes versiones se borrarán más. • Versión 3.10 (octubre del 2021): Se mejoran considerablemente algunos mensajes de error siendo mucho más descriptivos y ofreciendo alternativas a errores comunes, se añade una nueva sentencia para el control de flujo basado en patrones de coincidencia llamada Structural Pattern Matching, se aplican multiples mejoras en el sistema de sugerencias de tipado, además de añadir nuevas funcionalidades y optimizaciones en los tipos str, bytes y bytesarray entre otros cambios. Para más información sobre cada una de las versiones y los cambios entre una y otra es recomendable revisar con frecuencia la web oficial de lenguaje de programación Python: https://www.python.org/doc/versions/. 1.2 CARACTERÍSTICAS PRINCIPALES DE LOS LENGUAJES DE PROGRAMACIÓN Los humanos nos comunicamos por medio de un lenguaje (mayoritariamente verbal). De forma similar, para comunicarnos con las máquinas hemos diseñado diferentes formas de comunicación denominadas lenguajes de programación. Al igual que los lenguajes utilizados entre humanos, los lenguajes de programación tienen diferentes características; están orientados a satisfacer las necesidades por las que han sido creados y los gustos de sus creadores y desarrolladores principales. Las principales características por las que un lenguaje de programación se puede caracterizar son la generación a la que pertenece, el nivel de abstracción del lenguaje, el tipo de tipado de variables, los paradigmas de programación que soporta y el propósito que tiene el lenguaje, como se verá en los próximos apartados. 4 Python 2ªED.indb 4 10/1/22 13:40 Capítulo 1 · Introducción al lenguaje Python 1.10.2 Instalación en Windows Es altamente improbable que en un sistema Windows ya se encuentre instalada una versión de Python por defecto, pero aún más que sea la última versión o la versión que se desee utilizar. Por tanto, en esta sección se expone cómo instalar Python en Windows. La forma más rápida y efectiva es descargar el instalador de la página oficial de Python: https://www.python.org/downloads/. Es importante tener en cuenta que la versión del sistema operativo coincida con la del instalador, 32 o 64 bits. Figura 1.4 Instalar Python en Windows. Como se puede ver en la Figura 1.4, el instalador de Python es igual que cualquier instalador estándar de Windows, con la peculiaridad de que permite seleccionar dónde instalar la versión de Python. Es importante que se seleccione la opción de añadir Python al PATH (Add Python 3.10 to PATH en la imagen), dado que, así, en cualquier terminal de Windows se podrá ejecutar cualquier programa Python llamando al intérprete como se muestra a continuación: $ python <nombre_del_fichero.py> Si se utiliza Windows 10, se puede instalar el subsistema de Windows para Linux (WSL), el cual permite ejecutar una instalación de Ubuntu Linux en el sistema para disponer de herramientas Unix (https://docs.microsoft.com/es-es/windows/wsl/). Otra alternativa interesante es el uso de Cygwin (https://www.cygwin.com/) si WSL no está disponible en su sistema. 37 Python 2ªED.indb 37 10/1/22 13:40 Capítulo 1 · Introducción al lenguaje Python Figura 1.5 Instalador gráfico de Python para Mac OS X. Una vez terminado el proceso de instalación, se puede ver que en las aplicaciones instaladas hay dos nuevos programas, IDLE y Python Launcher. IDLE es un editor de desarrollo de código Python e intérprete de desarrollo que se estudiará en profundidad más adelante en este libro. Por otro lado, Python Launcher es un lanzador de aplicaciones de Python. Es la aplicación que se puede configurar para abrir por defecto cualquier archivo con extensión de python o, si simplemente se arrastra un fichero python hacia el icono del launcher, este lo lanzará en el destino que esté configurado, por defecto, en una consola. 1.11 DISTRIBUCIONES DE PYTHON Existen distribuciones de Python que pretenden unir diferentes paquetes de librerías comúnmente utilizados en un ámbito específico con la finalidad de facilitar la instalación de todos ellos a la vez, para así comenzar a utilizar las herramientas lo antes posible sin dedicar tiempo a instalar cada componente por separado. Normalmente, estas distribuciones tienen más herramientas de las que un principiante, o incluso un experto, necesitaría, pero así se intenta cubrir el máximo número de casos de uso, aunque suponga tener que hacer una distribución de mayor tamaño. A continuación, se muestran algunas de las distribuciones de Python más conocidas. 39 Python 2ªED.indb 39 10/1/22 13:40 Python a fondo por ejemplo, sumas, multiplicaciones o elevar un número a otro, tanto si son números complejos como si es una combinación de complejos con enteros o reales. >>> complex('1+4j') (1+4j) >>> 5 + 2.34 + 5J - 12.4563 (-5.116300000000001+5j) >>> 1j ** 2 (-1+0j) >>> pow(3j, 2) (-9+0j) Las operaciones disponibles para este tipo de dato se pueden obtener haciendo uso de la función dir aplicada a una instancia de esta clase: >>> dir(complex()) # similar a dir(4 + 5J) por ejemplo ['__abs__', '__add__', '__bool__', '__class__', '__ delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__ lt__', '__mul__', '__ne__', '__neg__', '__new__', '__pos__', '__pow__', '__radd__', '__reduce__', '__reduce_ex__', '__ repr__', '__rmul__', '__rpow__', '__rsub__', '__rtruediv__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__ subclasshook__', '__truediv__', 'conjugate', 'imag', 'real'] # Python 3.10 • complex.real: devuelve la parte real del número complejo asociado. • complex.imag: devuelve la parte imaginaria del número complejo asociado. • complex.conjugate(): devuelve el número complejo conjugado del número asociado. A continuación, se muestran ejemplos de uso de esas operaciones: >>> x = 4 - 2j >>> x.conjugate() (4+2j) >>> x.imag -2.0 >>> x.real 4.0 100 Python 2ªED.indb 100 10/1/22 13:40 Capítulo 2 · Variables y tipos de datos >>> a.hex('_', 2) '5c78_3739_5c78_3230' >>> b'ñ' File "<stdin>", line 1 b'ñ' ^ SyntaxError: bytes can only contain ASCII literal characters 9 CONJUNTOS (SET Y FROZENSET) En Python existe un tipo de dato básico que representa una colección no ordenada de elementos únicos. Este tipo de dato se denomina set o frozenset dependiendiendo de sus características. Sus caractecterísticas principales son que puede contener cualquier tipo de dato y mezclarlo sin problemas y que, además, se encarga de tener un, y solo un, elemento igual. Por lo tanto, es de mucha utilidad en multitud de situaciones. La mayor restricción es que todos sus elementos deben ser hasheables. Un objeto es hasheable automáticamente si nunca cambia durante su tiempo de vida. La mayoría de los objetos inmutables son hasheables, y las tuplas o los frozensets son hasheables si todos sus elementos los son. Las listas son un tipo de dato que no es hasheable. Técnicamente, si una clase implementa la función __hash__(), todos los objetos de esa clase son hasheables (los detalles sobre clases e instancias se verán más adelante en este libro). La principal diferencia entre frozenset y set es que un tipo es mutable y el otro no. Además, los frozenset, aparte de ser inmutables, son hasheables, mientras que los set, no. Los constructores de estos tipos de datos tienen el mismo nombre que los tipos en sí: frozenset y set. Se pueden instanciar con objetos si se añade un argumento iterable al constructor, de lo contrario, se crearán como vacíos. Alternativamente, para la creación de set se puede usar una versión simplificada que consta de un iterador rodeado de los caracteres '{' '}'. >>> set(), frozenset() # Crea un conjunto vacío (set(), frozenset()) >>> set([1,2,3,4]) {1, 2, 3, 4} >>> {1,2,3,4} {1, 2, 3, 4} 163 Python 2ªED.indb 163 10/1/22 13:40 Python a fondo centinela, será elevada una excepción del tipo StopIteration, de lo contrario, devolverá el valor de la llamada. • enumerate(iterable, start=0): devuelve un objeto enumerate, el cual es una tupla de dos elementos en la que el primero es un entero que determina el índice (comenzando por start), y el segundo elemento es el elemento en la posición del índice del iterable. El objeto usado como iterable puede ser tanto una secuencia como un iterador o cualquier objeto que soporte iteraciones. • any(iterable): devuelve True si alguno de los elementos del iterable es verdadero. Si el iterable está vacío, devuelve Falso. • all(iterable): devuelve True si todos de los elementos del iterable son verdaderos o si el iterable está vacío. • map(function, iterable, …): esta función devuelve un iterador, que irá aplicando la función que se le pasa como primer parámetro, a cada elemento de iterable, y devolverá uno a uno los resultados. Adicionalmente, se pueden añadir más iterables y hacer que cada elemento de cada iterable se le pase como parámetro a la función de forma paralela, hasta que alguno de los iterables se quede sin elementos. Por tanto, el número de elementos totales devueltos será igual a la longitud del menor iterable. • list([iterable]): crea una lista con los valores obtenidos tras iterar un iterable (iterable) hasta llegar al final del mismo. También se usa en iteradores con el mismo fin, dado que ayuda a no tener que manejar las excepciones StopIterator. • reversed(secuencia): devuelve un iterador en orden inverso desde la secuencia que se le pasa como parámetro. El objeto puede no ser una secuencia, pero debe implementar el método mágico __reversed__ o los del protocolo de las secuencias (__len__ y __getitem__). • zip(*iterables, strict=False): genera un iterador como agregación de cada elemento en la misma posición de cada iterable que se le pase como parámetro soportando mínimo 2 iterables y devolviendo tuplas de tantos elementos como iterables se usen (uno por cada iterable). La longitud final del iterador será igual a la menor longitud de todos los iterables. El parámetro strict se añadió en Python 3.10 y si es True, comprueba si la longitud de ambos iterables es la misma, de lo contrario eleva una excepción tipo ValueError. − Nota: cuando se usa esta función es común utilizar el operador *, el cual permite descomponer una secuencia de elementos en argumentos para una función o una asignación y así poder concatenar varias funciones zip. 184 Python 2ªED.indb 184 10/1/22 13:40 Capítulo 3 · Fundamentos del lenguaje >>> if x < 0: ... print('Número negativo') ... print('El número es cero') ... print('Número positivo') ... elif x == 0: ... else: ... Número positivo Como se puede ver en el ejemplo, para comenzar a controlar el flujo se utiliza la cláusula if seguida de una expresión. Si se pretende comprobar otra expresión, cuando la anterior o anteriores han resultado falsas, se utilizan tantas clausulas elif como sean necesarias y al final de la comprobación se añade opcionalmente una clausula else, que el programa seguirá ejecutando si todas las anteriores expresiones se evaluaron como falsas. 2.2 IMPLEMENTACIONES DE SWITCH CASE EN PYTHON Un control de flujo muy utilizado en los lenguajes de programación es el switch (o case dependiendo del lenguaje) con el que se pretende encauzar la ejecución de código dependiendo del valor específico de una variable que se utiliza como operador. Python implementa una sentencia de control similar a partir de la versión 3.10, aunque si se utiliza una versión anterior, es muy simple de emular utilizando sentencias if o un simple diccionario como se puede ver en los siguientes ejemplos: >>> tipo = 'coche' >>> if tipo == 'coche': ... ruedas = 4 ... ruedas = 2 ... ruedas = 6 ... ruedas = -1 ... elif tipo == 'bicicleta': ... elif tipo == 'camión': ... else: ... >>> ruedas 4 201 Python 2ªED.indb 201 10/1/22 13:40 Python a fondo >>> tipo_a_ruedas = {'coche': 4, 'bicicleta': 2, 'camión': 6} >>> ruedas2 = tipo_a_ruedas.get(tipo, -1) >>> ruedas2 4 2.3 STRUCTURAL PATTERN MATCHING En la versión 3.10 se introdujo la sentencia de control que permite hacer comparaciones estructurales de patrones. Es una sentencia de control similar a la sentencia switch que existe en otros lenguajes de programación, pero muchisimo más potente como se puede ver en esta sección. La definición de esta sentencia formalmente es la siguiente: match subject: case <pattern_1>: <action_1> case <pattern_2>: <action_2> case <pattern_3>: <action_3> case _: <action_wildcard> Donde match y case son palabras reservadas, subject es la variable que será utilizada para comparar, pattern_x son todos los patrones a evaluar, action_x es cada una de las acciones a ejecutar y por defecto se puede utilizar _ que será ejecutado si ninguno de los patrones anteriores coincide. El ejemplo más simple es el control de ejecución dependiendo del valor que tenga una variable como por ejemplo comprobar si una variable es exactamente un valor: >>> def comprobar_nombre(nombre): ... ... ... match nombre: case 'Oscar': print('Correcto!') ... case 'El Pythonista': ... case _: ... print('Ha estado cerca!') 202 Python 2ªED.indb 202 10/1/22 13:40 Capítulo 3 · Fundamentos del lenguaje print('No es correcto') ... ... >>> comprobar_nombre('Juan') No es correcto >>> comprobar_nombre('El Pythonista') Ha estado cerca! >>> comprobar_nombre('Oscar') Correcto! Los patrones utilizados pueden tener sentencias if para comprobar algunas si la variable cumple alguna condición especial, pueden ser concatenados para intentar coincidir con varios patrones usando | o directamente cumplir ciertos patrones estructurales como ser instancia de un tipo en concreto (str en el siguiente ejemplo): >>> from enum import Enum >>> class Color(Enum): ... ... ... ... Blue = '#0000FF' Red = '#FF0000' Yellow = '#FFFF00' >>> def temperatura_color(color): ... ... match color: ... case 'gris': return 'Neutro' ... case Color.Yellow | Color.Red: ... case Color.Blue: ... case str(x) if int(x[1:], base=16) > 256: ... ... ... ... return 'Cálido' return 'Frío' return 'Cálido' >>> temperatura_color('gris') Neutro >>> print(temperatura_color(Color.Blue)) Frío >>> temperatura_color('#F0F022') 'Cálido' 203 Python 2ªED.indb 203 10/1/22 13:40 Python a fondo El ejemplo más simple es el control de ejecución dependiendo del valor que tenga una variable como por ejemplo comprobar si una variable es exactamente un valor: Para más información y casos de uso, se recomienda revisar el tutorial oficial de PEP 636 https://www.python.org/dev/peps/pep-0636/ 2.4 SENTENCIA if TERNARIA En Python existe una opción simplificada para utilizar la sentencia if cuando se desea usar en una sola sentencia, denominada ternaria, que se utiliza solamente en casos en los que queda claramente expresada la lógica inherente: >>> edad = 55 >>> categoria = 'Cadete' if edad < 15 else 'Adulto' >>> categoria 'Adulto' Como se puede ver en el ejemplo anterior, la forma ternaria solo necesita de las sentencias if y else en la parte derecha de una asignación. Se aconseja usarla solo cuando la lógica a utilizar quede clara y de forma concisa. Como forma general se puede definir de la siguiente manera: V = A if expression == True else B 3 FLUJO DE EJECUCIÓN CON BUCLES A menudo, el flujo de un algoritmo se mantiene realizando operaciones hasta que una expresión lógica cambie de valor u ocurra algún acontecimiento esperado. Para este tipo de ejecuciones se implementan los bucles, que son trozos de código que deben ser ejecutados hasta que una expresión determinada ocurra. 3.1 ANALIZANDO BUCLES while El primer ejemplo de creación de bucles que se va a estudiar en esta sección es la sentencia while ("mientras" en inglés). Esta sentencia permite permanecer iterando sobre un bloque del código continuamente "mientras" una expresión sea verdadera. La sintaxis básica es la siguiente: while_stmt ::= "while" expression ":" suite ["else" ":" suite] 204 Python 2ªED.indb 204 10/1/22 13:40 Python a fondo El siguiente código está guardado en un fichero y se ejecuta desde consola para ver cómo se representan los errores cuando se utilizan ficheros: def capitalizar(elem): return elem.capitalize() def formatea(elem): limpio = elem.trim() capitalizado = capitalizar(limpio) return capitalizado def formateador(elementos): resultado = [] for elem in elementos: resultado.append(formatea(elem)) if __name__ == '__main__': print(formateador(' Jose ')) print(formateador(2)) # python excepciones.py Traceback (most recent call last): File "/ruta/hasta/archivo/excepciones.py", line 34, in <module> print(formateador(' Jose ')) File ""/ruta/hasta/archivo/excepciones.py", line 30, in formateador resultado.append(formatea(elem)) File "/ruta/hasta/archivo/excepciones.py", line 22, in formatea limpio = elem.trim() AttributeError: 'str' object has no attribute 'trim'. Did you mean: 'strip'? En este ejemplo se puede ver cómo es una traza de error cuando ocurre en ficheros. Para obtener un objeto Traceback se puede hacer uso de sys.exc_ info() dentro del contexto de la excepción que se ha elevado, como en el siguiente ejemplo: 256 Python 2ªED.indb 256 10/1/22 13:40 Python a fondo >>> entero = 2 >>> entero.upper() Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'int' object has no attribute 'upper' >>> cadena = 'Parque', 'florido' >>> cadena.lower() Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'tuple' object has no attribute 'lower' >>> cadena ('Parque', 'florido') >>> info = dict(color='Verde', tipo='Coche') >>> modelo = info.get('modelo') >>> modelo.upper() Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'NoneType' object has no attribute 'upper' >>> info.items = 'Pelota' Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'dict' object attribute 'items' is read-only >>> # desde 3.10 >>> 'hola'.trim Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'str' object has no attribute 'trim'. Did you mean: 'strip'? >>> info.item Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'dict' object has no attribute 'item'. Did you mean: 'items'? Como se puede ver en los ejemplos, es muy fácil identificar la causa del error, dado que en el mensaje de la excepción aparece el tipo del objeto sobre el que se ha intentado llamar al atributo y el atributo inexistente en el objeto. 260 Python 2ªED.indb 260 10/1/22 13:40 Python a fondo NameError Esta excepción se eleva cuando un nombre local o global no es encontrado. Por tanto, suele ocurrir cuando no se ha inicializado una variable antes de ser usada o cuando se escribe de forma errónea: >>> mi_variable Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'mi_variable' is not defined >>> color = 'Amarillo' >>> print(clor) Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'clor' is not defined. Did you mean: 'color'? >>> animal = 'gato' >>> animall Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'animall' is not defined. Did you mean: 'animal'? Como se puede ver en el ejemplo, cuando se define la función foo no se eleva la excepción, sino que ocurre cuando se intenta usar la función. SyntaxError Esta excepción se eleva cuando hay un error de sintaxis y el parseador de código de Python lo encuentra. Las causas pueden ser múltiples, desde un identificador de variable no apropiado hasta una definición de función errónea, pasando por otros muchos casos: >>> a-4 = 4 File "<stdin>", line 1 a-4 = 4 ^^^ SyntaxError: cannot assign to expression here. Maybe you meant '==' instead of '='? >>> def foo File "<stdin>", line 1 264 Python 2ªED.indb 264 10/1/22 13:40 Capítulo 3 · Fundamentos del lenguaje def foo ^ SyntaxError: invalid syntax >>> mi_dict = { ... 'color': 'rojo', ... 'numero': 2 ... 'tipo': 'triangulo', File "<stdin>", line 3 'numero': 2 ^ SyntaxError: invalid syntax. Perhaps you forgot a comma? >>> try: ... 1 / 0 ... random = 3 File "<stdin>", line 3 random = 3 ^^^^^^ SyntaxError: expected 'except' or 'finally' block Este tipo de excepción no es tan descriptivo como otros, pero, a veces, como marca el punto exacto donde se ha encontrado el error, se puede adivinar fácilmente cuál es la causa. La gran ventaja de que exista esta excepción es que el error se da en tiempo de compilación y no en tiempo de ejecución, por lo que se puede arreglar antes de lanzar la aplicación. TypeError Esta excepción se eleva cuando se intenta aplicar una operación o una función sobre un objeto inapropiado. Algunos ejemplos son la suma o resta de números con cadenas de caracteres o listas, la aplicación de funciones numéricas como abs sobre objetos no numéricos u operaciones pensadas para operar sobre secuencias aplicadas a elementos únicos. Veamos los siguientes ejemplos: >>> abs('hola') Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: bad operand type for abs(): 'str' >>> max(1) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'int' object is not iterable 265 Python 2ªED.indb 265 10/1/22 13:40 Python a fondo ... ... ... self.nombre = nombre self.nombres.append(nombre) >>> garfield = Gato('Garfield') >>> >>> bigotes = Gato('Bigotes') >>> garfield.num_patas, bigotes.num_patas (4, 4) >>> bigotes.orejas = 1 >>> bigotes.orejas, garfield.orejas (1, 2) Como se puede ver en el ejemplo, todos los gatos tienen un número de patas (num_patas) y un número de orejas (orejas) con los valores por defecto 4 y 2, especificado en la clase Gato, pero cuando se instancian los objetos garfield y bigotes, se pueden modificar los valores de cada instancia. Al añadir atributos de clase, no solo se establecen los valores por defecto para cada instancia, sino que se puede acceder a esos atributos por medio de la clase sin instanciar ningún objeto: >>> vars(Gato) mappingproxy({'__module__': '__main__', 'num_patas': 4, 'orejas': 2, 'nombres': ['Garfield', 'Bigotes'], '__init__': <function Gato.__init__ at 0x10b16edc0>, '__dict__': <attribute '__dict__' of 'Gato' objects>, '__weakref__': <attribute '__weakref__' of 'Gato' objects>, '__doc__': None}) >>> Gato.num_patas 4 >>> Gato.orejas 2 >>> Gato.nombre Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: type object 'Gato' has no attribute 'nombre'. Did you mean: 'nombres'? Como se muestra en el ejemplo, como los atributos num_patas y orejas están definidos a nivel de clase, se pueden consultar esos valores sin tener que crear una instancia de Gato. Sin embargo, si se intenta acceder al 278 Python 2ªED.indb 278 10/1/22 13:40 Capítulo 4 · Programación orientada a objetos >>> print(dir(f)) ['_Foo__atributo_cls_privado', '_Foo__x', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_atributo_ cls_protegido', '_x', 'obtener_x', 'obtener_x_privada', 'obtener_x_protegida', 'x'] >>> print(f.x) 2 >>> print(f._x) 4 >>> print(f.__x) Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'Foo' object has no attribute '__x'. Did you mean: '_x'? >>> print(Foo.__atributo_cls_privado) Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: type object 'Foo' has no attribute '__atributo_ cls_privado'. Did you mean: '_Foo__atributo_cls_privado'? >>> print(f.__x) Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'Foo' object has no attribute '__x'. Did you mean: '_x'? >>> print(f._Foo__x) 6 >>> print(Foo._atributo_cls_protegido) 0 >>> print(Foo.__atributo_cls_privado) Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: type object 'Foo' has no attribute '__atributo_ cls_privado'. Did you mean: '_Foo__atributo_cls_privado'? >>> print(Foo._Foo__atributo_cls_privado) 0 281 Python 2ªED.indb 281 10/1/22 13:40 Capítulo 4 · Programación orientada a objetos 8 C ONTROLAR EL ESPACIO DE ATRIBUTOS CON slots Por defecto, en Python todos los atributos de una clase y de las instancias se guardan dentro de las variables __dict__ o __weakref__, pero existe un método con el que se pueden controlar los atributos que deben ser guardados (o no). Asimismo, ese mismo método define qué atributos son accesibles desde el exterior. El método consiste en usar __slots__. La definición de __slots__ se hace a nivel de clase y puede contener una cadena de caracteres, un iterable o una secuencia de cadenas de caracteres con los nombres de los atributos usados por las instancias. Al definir __slots__ se reserva espacio para las variables y se previene la creación automática de __dict__ o __weakref__, además de bloquear la creación de nuevos atributos dinámicamente. A continuación, se puede ver un ejemplo de cómo se definen y los usos habituales: >>> class Punto3D: ... __slots__ = ['x', 'y', 'z'] ... def __init__(self, x, y, z): ... ... ... ... ... self.x = x self.y = y self.z = z >>> p = Punto3D(1, 2, 3) >>> p.x, p.y, p.z (1, 2, 3) >>> p.nuevo_atributo = 89 Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'Punto3D' object has no attribute 'nuevo_atributo' >>> p.__dict__ Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'Punto3D' object has no attribute '__ dict__'. Did you mean: '__dir__'? 313 Python 2ªED.indb 313 10/1/22 13:41 Python a fondo Sin embargo, gracias al uso de dataclasses se podría hacer una clase similar así: @dataclass(order=True) class MiNumeroDC: valor: float = 0 Como se puede ver la cantidad de código es dramáticamente menor, pero este es solo uno de los ejemplos más simples del uso de esta librería. A continuación, se muestra cómo utilizar el decorador y todas sus propiedades: • @dataclasses.dataclass(*, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False, match_args=True, kw_ only=False, slots=False): es el decorador que se utiliza por defecto para decorar cualquier clase y la definición de los parámetros es la siguiente: − *: define que todos los argumentos hay que pasarlos usando clave valor. − init: define si el método __init__() será generado o no. − repr: define si el método __repr__() será generado o no. El método por defecto genera una cadena del tipo f’<nombre_clase>(<attr1>=<valor_attr1>, <attr2=<valor_attr2>…)’. Si se define manualmente el método __repr__() el parámetro se ignora. − eq: define si el método __eq__(). Este método solo acepta comparaciones entre dos instancias idénticas. − order: define los métodos __lt__(), __le__(), __gt__() y __ge__() que comparan solo instancias idénticas y comparando atributo por atributo en ambas instancias. − unsafe_hash: define si debería de intentar añadir un método __ hash__() automáticamente o no. El uso de este parámetro a True puede desembocar fácilmente en errores y se recomienda consultar la documentación oficial para hacer uso de este parámetro. − frozen: si este parámetro se usa como True impedirá que se modifique cualquier atributo o que se añadan nuevos. − match_args: si este parámetro se usa como True (por defecto), crea la tupla __match_args__ usando la lista de parámetros usados en el método __init__() (desde 3.10). − kw_only: si este parámetro se usa como True, se especifica que todos los parámetros deben de pasarse como clave-valor para inicializar la clase (desde 3.10). − slots: si este parámetro se usa como True, se definirá una nueva clase que contiene __slots__ (desde 3.10). ea el os la va A continuación, se introducen algunos de los demás métodos incluidos en el módulo dataclases que son útiles para la definición de clases: 320 Python 2ªED.indb 320 10/1/22 13:41 Capítulo 4 · Programación orientada a objetos • dataclasses.field(*, default=MISSING, default_factory=MISSING, repr=True, hash=None, init=True, compare=True, metadata=None, match_args=True, kw_only=False, slots=False): este método define como debería de comportarse un atributo de la clase y los parámetros se usan como sigue: − *: define que todos los argumentos hay que pasarlos usando clave valor. − default: define el valor por defecto de este atributo. − default_factory: cuando un atributo debe de ser inicializado con objetos mutables como listas o diccionarios es necesario utilizar este parámetro para que se creen objetos diferentes en cada instancia. − repr: define si este atributo deberá de aparecer o no en el método por defecto __repr__. − hash: define si este atributo deberá de aparecer o no en el método por defecto __hash__. − init: define si este atributo deberá de aparecer o no en el método por defecto __init__. − compare: define si este atributo debería de estar presente en los métodos de comparación. − metadata: si este parámetro puede ser un diccionario o None y será envuelto en un objeto tipo MappingProxyType. − match_args: si este parámetro se usa como True (por defecto), crea la tupla __match_args__ usando la lista de parámetros usados en el método __init__() (desde 3.10). − kw_only: si este parámetro se usa como True, se especifica que todos los parámetros deben de pasarse como clave-valor para inicializar la clase (desde 3.10). − slots: si este parámetro se usa como True, se definirá una nueva clase usando que contiene __slots__ (desde 3.10). • dataclasses.fields(clase_o_instancia): devuelve una tupla con los campos definidos en la clase o instancia. • dataclasses.asdict(instancia, *, dict_factory=dict): convierte la instancia a un diccionario con los atributos como clave y sus valores convertidos utilizando la función dict_factory de forma recursiva. • dataclasses.astuple(instancia, *, tuple_factory=dict): convierte la instancia a una tupla de pares con los atributos y sus valores convertidos utilizando la función tuple_factory de forma recursiva. 321 Python 2ªED.indb 321 10/1/22 13:41 Python a fondo • statistics: permite utilizar funciones estadísticas tales como medias, medianas, modas o cuartiles en objetos numéricos como enteros, fracciones o puntos flotantes. Desde la versión 3.10, también están disponibles las funciones de cálculo de covarianza, correlación de Pearson y la regresión lineal. Módulos de programación funcional • itertools: permite utilizar funciones iterativas eficientes. El uso de este módulo es recomendable para generar secuencias de iteradores, como combinaciones de iteradores, agrupaciones de elementos o series repetitivas, entre otras. • functools: permite crear funciones de orden superior en las que se usan o se devuelven funciones. Se pueden utilizar para crear y cachear métodos y para crear funciones o incluso métodos de clases parciales. • operator: permite utilizar los operadores intrínsecos de Python como funciones sin necesidad de instanciar las variables (por ejemplo __lt__, __le__, __mod__, etc). Se suele utilizar con otras funciones como map, reduce o algunas de las del módulo functools para declarar las funciones que realizar. Módulos de acceso a ficheros y directorios • pathlib: permite trabajar con las direcciones del sistema de ficheros utilizando objetos. Contiene gran variedad de funciones para determinar las direcciones, detectar el tipo de los ficheros y crear direcciones específicas para Windows o POSIX, entre otros usos. Algunas funcionalidades se solapan con las que se encuentran en el módulo os. • os.path: permite operar con el sistema de ficheros a un nivel más bajo que pathlib y añade algunas funcionalidades extra, como saber la última modificación o tiempo de creación de ficheros, entre otras. • fileinput: permite leer de varios flujos de entrada a la vez. Estos flujos (streams) pueden ser múltiples ficheros o salidas estándar del sistema. Para leer ficheros simples es mejor usar open(). • stat: contiene constantes y funciones para interpretar la salida de los comandos stat, fstat, lstat y fstatat. Estos comandos permiten conocer el estado de ficheros, y con este módulo se puede conocer la información desde Python con facilidad. • filecmp: permite comparar ficheros y directorios que parezcan similares. Se pueden definir parámetros como tiempos de creación o similitudes. Para comparar el contenido de dos ficheros se recomienda usar difflib. 364 Python 2ªED.indb 364 10/1/22 13:41