From d5f9b89f78bd849c82c6b4cd9f68fea6486f4e66 Mon Sep 17 00:00:00 2001 From: ELC Date: Sun, 24 Oct 2021 20:35:28 -0300 Subject: [PATCH 1/5] Add Bitwise Operations --- chapter1.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/chapter1.py b/chapter1.py index fdda570..6ba93ac 100644 --- a/chapter1.py +++ b/chapter1.py @@ -190,3 +190,15 @@ ord("A") # => 65 ord("¿") # => 191 ord("€") # => 8364 + + +#################################################### +# 1.8 Operaciones de bits +#################################################### + +~10 # => not 1010 = -1011 = -11 +10 & 9 # => 1010 * 1001 = 1000 = 8 +10 | 9 # => 1010 + 1001 = 1011 = 11 +10 ^ 9 # => 1010 ^ 1001 = 0011 = 3 +10 >> 1 # => 1010 >> 1 = 101 = 5 +10 << 1 # => 1010 << 1 = 10100 = 20 From c8dbaee38cfff1851280030ab3da6e04c652093f Mon Sep 17 00:00:00 2001 From: ELC Date: Sat, 16 Apr 2022 02:54:48 -0300 Subject: [PATCH 2/5] =?UTF-8?q?Actualizar=20Cap=C3=ADtulo=20de=20POO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter5.py | 475 ++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 365 insertions(+), 110 deletions(-) diff --git a/chapter5.py b/chapter5.py index 2909e67..d7acd8e 100644 --- a/chapter5.py +++ b/chapter5.py @@ -4,10 +4,9 @@ #################################################### -# 5.1 Constructor y Métodos de instancia +# 5.1 Inicializador y Métodos de instancia #################################################### - from __future__ import annotations @@ -65,24 +64,34 @@ def _get_next_id(cls): # 5.3 Dataclasses #################################################### -from dataclasses import dataclass # Biblioteca Estándar + +from typing import ClassVar +from dataclasses import dataclass, field +import uuid class Persona: - def __init__(self, nombre: str, edad: int, sexo: str, - peso: float, altura: float) -> None: - self.nombre: str = nombre - self.edad: int = edad - self.sexo: str = sexo - self.peso: float = peso - self.altura: float = altura + _dni: int = 0 + + def __init__(self, nombre: str, edad: int, altura: float, propiedades: Optional[List[str]] = None) -> None: + self.nombre = nombre + self.edad = edad + self.altura = altura + self.propiedades = propiedades or [] + self.id_socio = f"{nombre[0].upper()}-{str(uuid.uuid4())[:8]}" + self.dni = str(Persona._get_next_dni()).zfill(8) def es_mayor_edad(self) -> bool: return self.edad >= 17 + @classmethod + def _get_next_dni(cls) -> int: + cls._dni += 1 + return cls._dni + -juan: Persona = Persona("Juan", 18, "H", 85, 175.9) +juan: Persona = Persona("Juan", 18, 175.9) juan.es_mayor_edad() # => True -Persona("Julia", 16, "M", 65, 162.4).es_mayor_edad() # => False +Persona("Julia", 16, 162.4).es_mayor_edad() # => False print(juan) # => <__main__.Persona object at 0x000001C90BBF8688> @@ -93,30 +102,47 @@ class PersonaDataClass: sexo: str peso: float altura: float + propiedades: List[str] = field(default_factory=list) + id_socio: str = field(init=False) + dni: str = field(init=False) + _dni: ClassVar[int] = 0 + + def __post_init__(self): + self.id_socio: str = f"{self.nombre[0].upper()}-{str(uuid.uuid4())[:8]}" + self.dni = str(PersonaDataClass._get_next_dni()).zfill(8) def es_mayor_edad(self) -> bool: return self.edad >= 18 + @classmethod + def _get_next_dni(cls) -> int: + cls._dni += 1 + return cls._dni + pedro: PersonaDataClass = PersonaDataClass("Pedro", 18, "H", 85, 175.9) pedro.es_mayor_edad() # => True PersonaDataClass("Julia", 16, "M", 65, 162.4).es_mayor_edad() # => False -print(pedro) # => PersonaDataClass(nombre='Pedro', edad=18, sexo='H', peso=85, altura=175.9) +print(pedro) # => PersonaDataClass(nombre='Pedro', edad=18, sexo='H', peso=85, altura=175.9, propiedades=[], id_socio='P-f642581c', dni='00000001') #################################################### # 5.4 Sobrecarga de Operadores #################################################### + # Referencia: https://docs.python.org/3/reference/datamodel.html#basic-customization -from typing import List, Optional # Biblioteca Estándar +from typing import List, Optional +@dataclass class Article: - def __init__(self, name: str) -> None: - self.name = name + name: str + + def __eq__(self, other: object) -> bool: + if not isinstance(other, Article): + raise NotImplementedError() - def __eq__(self, other: Article) -> bool: return self.name == other.name def __hash__(self) -> int: @@ -129,36 +155,28 @@ def __repr__(self) -> str: return f"Article('{self.name}')" +@dataclass class ShoppingCart: - def __init__(self, articles: List[Article] = None) -> None: - if articles is None: - self.articles = [] - else: - self.articles = articles + articles: List[Article] = field(default_factory=list) def add(self, article: Article) -> ShoppingCart: self.articles.append(article) return self def remove(self, remove_article: Article) -> ShoppingCart: - new_articles = [] - - for article in self.articles: - if article != remove_article: - new_articles.append(article) - - self.articles = new_articles - + self.articles = [article for article in self.articles if article != remove_article] return self - def __eq__(self, other: ShoppingCart) -> bool: + def __eq__(self, other: object) -> bool: + if not isinstance(other, ShoppingCart): + raise NotImplementedError() return set(self.articles) == set(other.articles) def __str__(self) -> str: - return str([str(i) for i in self.articles]) + return str(self.articles) def __repr__(self) -> str: - return f"ShoppingCart({[art for art in self.articles]})" + return f"ShoppingCart({self.articles})" def __add__(self, other: ShoppingCart) -> ShoppingCart: return ShoppingCart(self.articles + other.articles) @@ -173,34 +191,79 @@ def __add__(self, other: ShoppingCart) -> ShoppingCart: # Test de reproducibilidad carrito = ShoppingCart().add(manzana).add(pera) -carrito == eval(repr(carrito)) # => True +assert carrito == eval(repr(carrito)) print(repr(carrito)) # => ShoppingCart([Article('Manzana'), Article('Pera')]) # Test de igualdad -ShoppingCart().add(manzana) == ShoppingCart().add(manzana) # => True +assert ShoppingCart().add(manzana) == ShoppingCart().add(manzana) # => True print(ShoppingCart().add(manzana)) # => ['Manzana'] # Test de remover objeto -ShoppingCart().add(tv).add(pera).remove(tv) == ShoppingCart().add(pera) # => True +assert ShoppingCart().add(tv).add(pera).remove(tv) == ShoppingCart().add(pera) # => True print(ShoppingCart().add(tv).add(pera).remove(tv)) # => ['Pera'] # Test de igualdad con distinto orden -ShoppingCart().add(tv).add(pera) == ShoppingCart().add(pera).add(tv) # => True +assert ShoppingCart().add(tv).add(pera) == ShoppingCart().add(pera).add(tv) # => True print(ShoppingCart().add(tv).add(pera)) # => ['Television', 'Pera'] # Test de suma combinado = ShoppingCart().add(manzana) + ShoppingCart().add(pera) -combinado == ShoppingCart().add(manzana).add(pera) # => True +assert combinado == ShoppingCart().add(manzana).add(pera) # => True print(combinado) # => ['Manzana', 'Pera'] #################################################### -# 5.5 Propiedades y Copia Profunda +# 5.5 Instancias como Functiones (__call__) +#################################################### + + +@dataclass +class Acumulador: + valor_inicial: Union[int, float] = 0 + valor: Union[int, float] = field(init=False) + + def __post_init__(self): + self.valor = self.valor_inicial + + def incrementar(self, valor: Union[int, float]) -> None: + self.valor += valor + + +acumulador_1 = Acumulador() +acumulador_1.incrementar(5) +acumulador_1.incrementar(10) +acumulador_1.incrementar(-2) + +assert acumulador_1.valor == 13 + +@dataclass +class AcumuladorAlternativo: + valor_inicial: Union[int, float] = 0 + valor: Union[int, float] = field(init=False) + + def __post_init__(self): + self.valor = self.valor_inicial + + def __call__(self, valor: Union[int, float]) -> None: + self.valor += valor + +acumulador_2 = AcumuladorAlternativo() +acumulador_2(5) +acumulador_2(10) +acumulador_2(-2) + +assert acumulador_2.valor == 13 + + +#################################################### +# 5.6 Propiedades y Copia Profunda #################################################### +# Referencia: https://docs.python.org/3/library/copy.html + @dataclass -class Articulo: +class Producto: _nombre: str _precio: float @@ -223,31 +286,31 @@ def precio(self, value: float) -> None: from copy import deepcopy # Biblioteca Estandar -def actualizar_precio(articulos: List[Articulo], porcentaje_aumento: float) -> List[Articulo]: - nuevos = [] - for articulo in deepcopy(articulos): - articulo.precio *= 1 + porcentaje_aumento / 100 - nuevos.append(articulo) +def actualizar_precio(productos: List[Producto], porcentaje_aumento: float) -> List[Producto]: + nuevos: List[Producto] = [] + for producto in deepcopy(productos): + producto.precio *= 1 + porcentaje_aumento / 100 + nuevos.append(producto) return nuevos nombres = ["sábana", "parlante", "computadora", "tasa", "botella", "celular"] precios = [10.25, 5.258, 350.159, 25.99, 18.759, 215.231] -articulos = [Articulo(nombre, precio) +productos = [Producto(nombre, precio) for nombre, precio in zip(nombres, precios)] porcentaje_aumento = 10 -articulos_actualizados: List[Articulo] = actualizar_precio(articulos, porcentaje_aumento) -precios_desactualizados: List[float] = [articulo.precio for articulo in articulos] -precios_actualizados: List[float] = [articulo.precio for articulo in articulos_actualizados] +productos_actualizados: List[Producto] = actualizar_precio(productos, porcentaje_aumento) +precios_desactualizados: List[float] = [producto.precio for producto in productos] +precios_actualizados: List[float] = [producto.precio for producto in productos_actualizados] -precios_desactualizados # => [10.25, 5.26, 350.16, 25.99, 18.76, 215.23] -precios_actualizados # => [11.28, 5.79, 385.18, 28.59, 20.64, 236.75] +print(precios_desactualizados) # => [10.25, 5.26, 350.16, 25.99, 18.76, 215.23] +print(precios_actualizados) # => [11.28, 5.79, 385.18, 28.59, 20.64, 236.75] #################################################### -# 5.5 Herencia +# 5.7 Herencia #################################################### @@ -274,70 +337,227 @@ def descripcion(self) -> str: #################################################### -# 5.5 Clases y Métodos abstractos +# 5.8 Constructor (__new__) #################################################### + +@dataclass +class Auto: + velocidad_maxima: float + precio: float + + def __new__(cls, velocidad_maxima: float, precio: float) -> Auto: + + if velocidad_maxima >= 300: + auto = super().__new__(AutoDeportivo) + elif precio >= 100_000: + auto = super().__new__(AutoLujoso) + else: + auto = super().__new__(cls) + + auto.velocidad_maxima = velocidad_maxima + auto.precio = precio + return auto + + +class AutoLujoso(Auto): + ... + + +class AutoDeportivo(Auto): + ... + + +auto_familiar = Auto(170, 3_000) +auto_formula1 = Auto(370, 5_000_000) +auto_famoso = Auto(250, 500_000) + +assert isinstance(auto_familiar, Auto) +assert isinstance(auto_formula1, Auto) +assert isinstance(auto_famoso, Auto) +assert isinstance(auto_formula1, AutoDeportivo) +assert isinstance(auto_famoso, AutoLujoso) + + +#################################################### +# 5.8 Clases y Métodos abstractos +#################################################### + + +# Referencia: https://docs.python.org/3/library/abc.html + from abc import ABC, abstractmethod +from typing import final # Python 3.8+ @dataclass -class Objeto(ABC): - id_: int +class Item(ABC): + _id: ClassVar[int] + id_: int = field(init=False) _nombre: str + def __post_init__(self): + self.id_ = self.__class__._get_next_id() + + @classmethod + @abstractmethod + def _get_next_id(cls) -> int: + ... + @abstractmethod def mostrar_id(self) -> str: - return str(self.id_) + ... @property @abstractmethod def nombre(self) -> str: - return self._nombre.capitalize() + ... @nombre.setter + @abstractmethod def nombre(self, value: str) -> None: - self._nombre = value + ... + + @final + def descripcion(self) -> str: + return f"ID: {self.id_} - Nombre: {self.nombre}" -class Ropa(Objeto): - pass +@dataclass +class Ropa(Item): + ... -class Material(Objeto): +@dataclass +class Material(Item): + _id: ClassVar[int] = 0 + id_: int = field(init=False) + _nombre: str + @classmethod + def _get_next_id(cls) -> int: + cls._id += 1 + return cls._id + + @property def nombre(self) -> str: return self._nombre + @nombre.setter + def nombre(self, value: str) -> None: + self._nombre = value + def mostrar_id(self) -> str: - return super().mostrar_id() + return str(self.id_).zfill(10) + +@dataclass +class MaterialLujoso(Material): + def descripcion(self) -> str: + return f'{super().descripcion()} - Material de Lujo' -# objeto = Objeto(10, "Objeto") # => Error -# objeto = Ropa(10, "Camisa") # => Error -objeto = Material(10, "Madera") -print(objeto) # => Material(id_=10, _nombre='Madera') -issubclass(type(objeto), Objeto) # => True -issubclass(type(objeto), Material) # => True -isinstance(objeto, Objeto) # => True -isinstance(objeto, Material) # => True +# item = Item("Item") # => Error +# item = Ropa("Camisa") # => Error +item_lujoso = MaterialLujoso("Formula 1") # => Sin Error - Warning en la declaración +print(item_lujoso.descripcion()) + +item = Material("Madera") +print(item) # => Material(id_=1, _nombre='Madera') + +assert issubclass(type(item), Item) +assert issubclass(type(item), Material) +assert isinstance(item, Item) +assert isinstance(item, Material) #################################################### -# 5.6 Sobrecarga de Métodos - Python 3.8+ +# 5.9 Interfaces (Protocols) #################################################### -from functools import singledispatchmethod # Biblioteca Estandar + +from typing import Protocol + + +class Identificable(Protocol): + @property + def nombre(self) -> str: + ... + + @property + def id_(self) -> int: + ... + + +def get_datos_resumen(objeto: Identificable): + return f"{objeto.id_} - {objeto.nombre}" + + +madera = Material("Madera") + +resumen = get_datos_resumen(madera) # No hay Warning de Tipos + # Incluso si Material no hereda de Identificable + # Incluso si id_ no es una property + # Incluso si + + +#################################################### +# 5.10 Sobrecarga de Métodos +#################################################### + + +from typing import overload, Sequence + +@overload +def duplicar(x: int) -> int: + ... + +@overload +def duplicar(x: Sequence[int]) -> list[int]: + ... + +def duplicar(x: int | Sequence[int]) -> int | list[int]: + if isinstance(x, Sequence): + return [i * 2 for i in x] + return x * 2 + +assert duplicar(2) == 4 # Sin Warning +assert duplicar([1, 2, 3]) == [2, 4, 6] # Sin Warning + + +#################################################### +# 5.11 Sobrecarga de Métodos - Caso Especial - Python 3.8+ +#################################################### + + +from typing import Union @dataclass class Empleado: sueldo: float + def calcular_sueldo(self, impuesto: Union[int, float]) -> float: + if isinstance(impuesto, int) or impuesto >= 1: + return self.sueldo - impuesto + + return self.sueldo * (1 - impuesto) + + +personal_limpieza_1 = Empleado(10_000) + + +from functools import singledispatchmethod # Biblioteca Estandar + +@dataclass +class EmpleadoAlternativo: + sueldo: float + @singledispatchmethod - def calcular_sueldo(self, impuesto): - raise NotImplementedError + def calcular_sueldo(self, impuesto: float) -> float: + raise NotImplementedError() + @calcular_sueldo.register def _(self, impuesto: float) -> float: if impuesto >= 1: - raise ValueError + return self.sueldo - impuesto return self.sueldo * (1 - impuesto) @calcular_sueldo.register @@ -345,46 +565,81 @@ def _(self, impuesto: int) -> float: return self.sueldo - impuesto -personal_limpieza = Empleado(10_000) -personal_limpieza.calcular_sueldo(1500) # => 8500 -personal_limpieza.calcular_sueldo(0.1) # => 9000.0 +personal_limpieza_2 = EmpleadoAlternativo(10_000) +assert personal_limpieza_2.calcular_sueldo(1500) == 8_500 +assert personal_limpieza_2.calcular_sueldo(0.1) == 9_000 + +assert personal_limpieza_1.calcular_sueldo(1500) == personal_limpieza_2.calcular_sueldo(1500) +assert personal_limpieza_1.calcular_sueldo(0.1) == personal_limpieza_2.calcular_sueldo(0.1) + + +#################################################### +# 5.12 Mixins (Herencia Múltiple) +#################################################### + + +import json +from typing import Any + + +class JsonSerializer: + + def to_json(self) -> str: + return json.dumps(vars(self)) + + def from_json(self, json_string: str) -> Any: + return json.loads(json_string) -from typing import Optional @dataclass -class EmpleadoAlternativo: - sueldo: float - ventas: Optional[float] = None - comision: Optional[float] = None +class EmpleadoBaseDeDatos(EmpleadoAlternativo, JsonSerializer): + tabla: str - def calcular_sueldo(self, impuesto: Optional[float]=None, - con_comision: bool=False) -> float: - if impuesto is None and con_comision is False: - return self.sueldo - - sueldo: float = self.sueldo - if con_comision and self.ventas is not None and self.comision is not None: - sueldo += self.ventas * self.comision - - if impuesto is not None: - if impuesto >= 1: - sueldo -= impuesto - else: - sueldo *= (1 - impuesto) - - return sueldo +personal_limpieza_2 = EmpleadoBaseDeDatos(10_000, "Empleados") + +assert personal_limpieza_2.to_json() == '{"sueldo": 10000, "tabla": "Empleados"}' + + +#################################################### +# 5.13 Descriptores +#################################################### + + +class Positivo: + + def __set_name__(self, _: Any, nombre: str) -> None: + self.nombre_atributo: str = f"_{nombre}" + + def __get__(self, objeto: Any, _: Any = None) -> Union[float, int]: + return getattr(objeto, self.nombre_atributo) # type: ignore + + def __set__(self, objeto: Any, valor: Union[float, int]) -> None: + if valor < 0: + raise ValueError(f"{self.nombre_atributo} debe ser positivo") + + setattr(objeto, self.nombre_atributo, valor) + + +class Celcius: + + def __get__(self, instancia: Any, _: Any = None) -> float: + return (instancia.farenheit - 32) * 5 / 9 + + def __set__(self, instancia: Any, valor: float) -> None: + instancia.farenheit = 32 + valor * 9 / 5 + + +@dataclass +class MaterialExperimento: + masa: float = Positivo() + temperatura: float = Celcius() + +concreto_armado = MaterialExperimento(masa=50, temperatura=100) +assert concreto_armado.masa == 50 +assert concreto_armado.temperatura == 100 +assert concreto_armado.farenheit == 212 # Warning pero no Error -manager = EmpleadoAlternativo(15_000) -manager.calcular_sueldo() # => 15000 -manager.calcular_sueldo(1500) # => 13500 -manager.calcular_sueldo(0.2) # => 12000 -vendedor = EmpleadoAlternativo(15_000, ventas=7000, comision=0.05) -vendedor.calcular_sueldo() # => 15000 -vendedor.calcular_sueldo(1500) # => 13500 -vendedor.calcular_sueldo(0.2) # => 12000 -vendedor.calcular_sueldo(1500, True) # => 13850 -vendedor.calcular_sueldo(con_comision=True) # => 15350 -vendedor.calcular_sueldo(impuesto=1500, con_comision=True) # => 13850 +#oxigeno = MaterialExperimento(masa=-21, -30) # Error -> ValueError: _masa debe ser positivo From 57a9c6006afdc00de87d2c78d05177c18d4eb01c Mon Sep 17 00:00:00 2001 From: ELC Date: Sat, 16 Apr 2022 17:41:55 -0300 Subject: [PATCH 3/5] Actualizar contenido Avanzado --- chapter7.py | 634 ++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 488 insertions(+), 146 deletions(-) diff --git a/chapter7.py b/chapter7.py index 81b7490..5e6209c 100644 --- a/chapter7.py +++ b/chapter7.py @@ -8,14 +8,14 @@ ## Índice #################################################### -# 7.1 Namedtuple -# 7.2 Otras colecciones -# 7.3 Generadores -# 7.4 Iteradores -# 7.5 Semicorrutinas (Generadores Avanzados) -# 7.6 Corrutinas (AsyncIO) -# 7.7 Decoradores -# 7.8 Context Manager +# 7.1 Tipos adicionales +# 7.2 Generadores +# 7.3 Iteradores +# 7.4 Semicorrutinas (Generadores Avanzados) +# 7.5 Corrutinas (AsyncIO) +# 7.6 Decoradores +# 7.7 Context Manager +# 7.8 Perlas de la Biblioteca Estándar - Pathlib # 7.9 Perlas de la Biblioteca Estándar - Itertools # 7.10 Perlas de la Biblioteca Estándar - OS # 7.11 Perlas de la Biblioteca Estándar - Serialización @@ -23,12 +23,17 @@ #################################################### -## 7.1 Namedtuple +## 7.1 Tipos Adicionales +#################################################### + + +#################################################### +## 7.1.1 namedtuple y NamedTuple #################################################### # Referencia: https://docs.python.org/3/library/collections.html#collections.namedtuple -from dataclasses import dataclass +from dataclasses import dataclass, astuple from collections import namedtuple @dataclass @@ -36,31 +41,123 @@ class Vector: x: float y: float -origen: Vector = Vector(0, 0) -origen.x # => 0 -origen.y # => 0 -print(origen) # => Vector(x=0, y=0) -# x, y = origen # => Error -origen.x = 1 -origen.y = 1 -print(origen) # => Vector(x=1, y=1) + def modulo(self) -> float: + return (self.x**2 + self.y**2)**0.5 + +origen = Vector(0, 0) +origen.x # => 0 +origen.y # => 0 +print(origen) # => Vector(x=0, y=0) +# x, y = origen # => Error +x, y = astuple(origen) # => Sin Error +# origen[0] # => Error No se pueden usar índices en clases +origen.x = 3 # => Atributos mutables por defecto +origen.y = 4 # => Atributos mutables por defecto +print(origen) # => Vector(x=3, y=4) + +assert origen.modulo() == 5 + + +# Usando frozen=True + +@dataclass(frozen=True) +class VectorInmutable: + x: float + y: float + + def modulo(self) -> float: + return (self.x**2 + self.y**2)**0.5 +origen = VectorInmutable(0, 0) +origen.x # => 0 +origen.y # => 0 +print(origen) # => Vector(x=0, y=0) +# x, y = origen # => Error +x, y = astuple(origen) # => Sin Error +# origen[0] # => Error No se pueden usar índices en clases +# origen.x = 3 # => Error atributos son inmutables +# origen.y = 4 # => Error atributos son inmutables + +assert origen.modulo() == 0 + + +# Usando collections.namedtuple + VectorAlternativo = namedtuple('VectorAlternativo', ['x', 'y']) -punto: VectorAlternativo = VectorAlternativo(1, 1) -punto.x # => 1 -punto.x # => 1 -print(punto) # => VectorAlternativo(x=1, y=1) -x, y = punto # => x=1, y=1 -# punto.x = 1 # => Error + +def modulo_sin_tipos(vector: VectorAlternativo) -> float: + return (vector.x**2 + vector.y**2)**0.5 # Warning por desconocer los tipos + + +punto = VectorAlternativo(3, 4) +punto.x # => 3 con Warning: No hay tipo especificado +punto.y # => 4 con Warning: No hay tipo especificado +print(punto) # => VectorAlternativo(x=3, y=4) +x, y = punto # => x=3, y=4 con Warning: No hay tipo especificado +# punto.x = 1 # => Error + +assert punto[0] == 3 +assert punto[1] == 4 +assert modulo_sin_tipos(punto) == 5 + + +# Usando NamedTuple como superclase + + +from typing import NamedTuple + +class VectorAlternativoTipado(NamedTuple): + x: float + y: float + + def modulo(self) -> float: + return (self.x**2 + self.y**2)**0.5 + + +def modulo_tipado_1(vector: VectorAlternativoTipado) -> float: + return (vector.x**2 + vector.y**2)**0.5 + + +punto_tipado_str = VectorAlternativoTipado("1", "1") # Warning String != Float + +punto_tipado_1 = VectorAlternativoTipado(3, 4) +punto_tipado_1.x # => 3 +punto_tipado_1.y # => 4 +print(punto_tipado_1) # => VectorAlternativo(x=3, y=4) +x, y = punto_tipado_1 # => x=3, y=4 +# punto_tipado_1.x = 1 # => Error + +assert punto_tipado_1[0] == 3 +assert punto_tipado_1[1] == 4 +assert modulo_tipado_1(punto_tipado_1) == 5 + + +# Usando NamedTuple como función + +VectorAlternativoTipado_2 = NamedTuple("VectorAlternativoTipado_2", [('x', float), ('y', float)]) + +def modulo_tipado_2(vector: VectorAlternativoTipado_2) -> float: + return (vector.x**2 + vector.y**2)**0.5 + + +punto_tipado_2 = VectorAlternativoTipado_2(3, 4) +punto_tipado_2.x # => 1 +punto_tipado_2.x # => 1 +print(punto_tipado_2) # => VectorAlternativo(x=1, y=1) +x, y = punto_tipado_2 # => x=1, y=1 +# punto_tipado_1.x = 1 # => Error + +assert punto_tipado_1[0] == 3 +assert punto_tipado_1[1] == 4 +assert modulo_tipado_2(punto_tipado_2) == 5 #################################################### -## 7.2 Otras colecciones +## 7.1.2 Counter #################################################### -## Counter # Referencia: https://docs.python.org/3/library/collections.html#collections.Counter @@ -88,48 +185,147 @@ class Vector: contador_de_palabras = Counter(texto.split()) contador_de_palabras.most_common(4) # => [('de', 12), ('que', 8), ('a', 6), ('en', 5)] -## Defaultdict + +#################################################### +## 7.1.3 Defaultdict +#################################################### + # Referencia: https://docs.python.org/3/library/collections.html#collections.defaultdict from collections import defaultdict -notas_alumnos = defaultdict(list) +notas_alumnos: defaultdict[str, List[int]] = defaultdict(list) notas_alumnos["Pedro"].append(8) # No lanza KeyError notas_alumnos["María"].append(9) notas_alumnos["Pedro"].append(3) notas_alumnos["María"].append(8) notas_alumnos["Pedro"].append(7) -notas_alumnos # => {'Pedro': [8, 3, 7], 'María': [9, 8]} +assert notas_alumnos == {'Pedro': [8, 3, 7], 'María': [9, 8]} + + +#################################################### +## 7.1.4 Enum +#################################################### -## Enum # Referencia: https://docs.python.org/3/library/enum.html -from enum import Enum +from enum import Enum, auto + class Permisos(Enum): - ADMIN = 1 - USER = 2 - EDITOR = 3 + ADMIN = auto() + USER = auto() + EDITOR = auto() + + @staticmethod + def tiene_acceso_panel_de_control_1(permiso: Permisos) -> bool: + return permiso is Permisos.ADMIN or permiso is Permisos.EDITOR + Permisos.ADMIN # => Permisos.ADMIN Permisos.ADMIN.value # => 1 Permisos['ADMIN'] # => Permisos.ADMIN Permisos['ADMIN'].value # => 1 +print(Permisos.__members__.keys()) # => ['ADMIN', 'USER', 'EDITOR'] +print(Permisos.__members__.values()) # => [, , ] + +Permisos.tiene_acceso_panel_de_control_1("REDACTOR") # Sin Error - Warning Tipo Incompatible + +assert Permisos.tiene_acceso_panel_de_control_1(Permisos.ADMIN) + + +# Alternativa usando typing.Literal + +from typing import Literal + +PermisosA = Literal["ADMIN", "EDITOR", "USER"] + +class PermisosAlternativo: + @staticmethod + def tiene_acceso_panel_de_control_2(permiso: PermisosA) -> bool: + return permiso in ["ADMIN", "EDITOR"] + + +PermisosAlternativo.tiene_acceso_panel_de_control_2("REDACTOR") # Sin Error - Warning Tipo Incompatible + +assert PermisosAlternativo.tiene_acceso_panel_de_control_2("ADMIN") + + +#################################################### +## 7.1.4 SimpleNameSpace +#################################################### + + +from dataclasses import dataclass + + +@dataclass +class Persona: + nombre: str + +empleado_1 = Persona("Juan") +empleado_1.nombre # => Juan +# empleado_1["nombre"] # => Error - No se pueden usar claves con dataclasses +empleado_1.edad = 24 # => Sin Error - Monkey Patching - NO RECOMENDADO + + +# Usando Diccionarios + +empleado_2 = {} +empleado_2["nombre"] = "Juan" +empleado_2["nombre"] # => Juan +#empleado_2.nombre # => Error - No se pueden acceder a valores usando . + + +# Usando SimpleNameSpace + +# Referencia: https://docs.python.org/3/library/types.html#types.SimpleNamespace + +from types import SimpleNamespace + +empleado_3 = SimpleNamespace() +empleado_3.nombre = "Juan" # => Sin Error - No es Mokey Patching - Uso Correcto +# empleado_3["nombre"] # => Error - No se pueden usar claves con SimpleNamespace + + +# Custom SimpleNamespace + +from dataclasses import field + +from typing import Any + +class Empleado(SimpleNamespace): + def __setitem__(self, clave: str, valor: Any) -> None: + setattr(self, clave, valor) + + def __getitem__(self, clave: str) -> Any: + return getattr(self, clave) + + +empleado_4 = Empleado() +empleado_4["nombre"] = "Juan" # => Sin Error - No es Mokey Patching - Uso Correcto +empleado_4.edad = 24 # => Sin Error - No es Mokey Patching - Uso Correcto + +assert empleado_4.nombre == "Juan" +assert empleado_4["nombre"] == "Juan" +assert empleado_4.edad == 24 +assert empleado_4["edad"] == 24 + #################################################### -## 7.3 Generadores +## 7.2 Generadores #################################################### # Referencia: https://docs.python.org/3/tutorial/classes.html#generators from typing import Generator, Iterator, List -def fibonacci_generador() -> Generator[int]: +def fibonacci_generador() -> Generator[int, None, None]: last: int = 1 current: int = 1 yield last @@ -139,20 +335,20 @@ def fibonacci_generador() -> Generator[int]: current, last = last + current, current yield current -generador: Generator[int] = fibonacci_generador() +generador = fibonacci_generador() fibonacci_10_primeros: List[int] = [] -for i in range(10): +for _ in range(10): next_fibonacci = next(generador) fibonacci_10_primeros.append(next_fibonacci) -fibonacci_10_primeros # => [1, 1, 2, 3, 5, 8, 13, 21, 34, 55] +assert fibonacci_10_primeros == [1, 1, 2, 3, 5, 8, 13, 21, 34, 55] # Pueden tomar parámetros comoo cualquier función def primos_menores(numero: int) -> Generator[int, None, None]: - visitados = [] - for number in range(1, numero): + visitados: List[int] = [] + for number in range(2, numero): no_es_primo = any(number % possible_divisor == 0 for possible_divisor in visitados) if not no_es_primo: @@ -161,21 +357,31 @@ def primos_menores(numero: int) -> Generator[int, None, None]: # Con bucle tradicional -generador: Generator[int] = primos_menores(25) -primos_menores_25 = [] + +generador = primos_menores(25) +primos_menores_25: List[int] = [] for primo in generador: primos_menores_25.append(primo) -primos_menores_25 # => [2, 3, 5, 7, 11, 13, 17, 19, 23] +assert primos_menores_25 == [2, 3, 5, 7, 11, 13, 17, 19, 23] + # Con comprensión -generador: Generator[int] = primos_menores(25) + +generador = primos_menores(25) primos_menores_25 = [primo for primo in generador] -primos_menores_25 # => [2, 3, 5, 7, 11, 13, 17, 19, 23] +assert primos_menores_25 == [2, 3, 5, 7, 11, 13, 17, 19, 23] + + +# Con list + +generador = primos_menores(25) +primos_menores_25 = list(generador) +assert primos_menores_25 == [2, 3, 5, 7, 11, 13, 17, 19, 23] #################################################### -## 7.4 Iteradores +## 7.3 Iteradores #################################################### # Referencia: https://docs.python.org/3/library/stdtypes.html#iterator-types @@ -208,21 +414,30 @@ def __next__(self): # Necesario para función next return self.numero_actual +# Con bucle tradicional + generador_con_clase: Iterator[int] = PrimosMenores(25) primos_menores_25: List[int] = [] -for i in range(9): - siguiente_fibonacci: int = next(generador_con_clase) - primos_menores_25.append(siguiente_fibonacci) -primos_menores_25 # => [2, 3, 5, 7, 11, 13, 17, 19, 23] +for primo in generador_con_clase: + primos_menores_25.append(primo) + +assert primos_menores_25 == [2, 3, 5, 7, 11, 13, 17, 19, 23] # Con comprensión + +generador_con_clase: Iterator[int] = PrimosMenores(25) +primos_menores_25 = [primo for primo in generador_con_clase] +assert primos_menores_25 == [2, 3, 5, 7, 11, 13, 17, 19, 23] + +# Con list + generador_con_clase: Iterator[int] = PrimosMenores(25) -primos_menores_25: List[int] = [primo for primo in generador_con_clase] -primos_menores_25 # => [2, 3, 5, 7, 11, 13, 17, 19, 23] +primos_menores_25 = list(generador_con_clase) +assert primos_menores_25 == [2, 3, 5, 7, 11, 13, 17, 19, 23] #################################################### -## 7.5 Semicorrutinas (Generadores Avanzados) +## 7.4 Semicorrutinas (Generadores con send) #################################################### from typing import Generator, Any @@ -241,7 +456,7 @@ def acumular() -> Generator[float, float, None]: semicorrutina.send(-1) resultado_acumulador: float = semicorrutina.send(20) # Finalizar -resultado_acumulador # => 29 +assert resultado_acumulador == 29 ## Caso de Uso @@ -256,16 +471,20 @@ def procesamiento_diferido() -> Generator[Any, Any, Any]: semicorrutina_diferida: Generator[Any, Any, Any] = procesamiento_diferido() next(semicorrutina_diferida) # Inicializar + resultado_diferido: float = semicorrutina_diferida.send(123) # => Procesando datos... - 123 + # Proceso diferido resultado_diferido = semicorrutina_diferida.send(654) # => Procesando datos... - 654 -resultado_diferido # => Listo + +assert resultado_diferido == "Listo" #################################################### -## 7.6 Corrutinas (AsyncIO) +## 7.5 Corrutinas (AsyncIO) #################################################### + # Referencia: https://docs.python.org/3/library/asyncio-task.html import asyncio @@ -301,7 +520,7 @@ async def main_serial() -> Tuple[int, int]: resultado_api = await respondiendo_api() return (resultado_db, resultado_api) -async def main_serial_alternativo() -> Tuple[int, int]: +async def main_serial_invertido() -> Tuple[int, int]: resultado_api = await respondiendo_api() resultado_db = await procesamiento_db() return (resultado_api, resultado_db) @@ -309,7 +528,7 @@ async def main_serial_alternativo() -> Tuple[int, int]: print(f"Empezado: {time.strftime('%X')}") resultados: Tuple[int, int] = asyncio.run(main_serial()) print(f"Finalizado: {time.strftime('%X')}") -resultados # => (0, 1) +assert resultados == (0, 1) # Empezado: 09:47:35 # DB: Enviando Consulta a la base de datos @@ -323,9 +542,9 @@ async def main_serial_alternativo() -> Tuple[int, int]: # Finalizado: 09:47:43 print(f"Empezado: {time.strftime('%X')}") -resultados: Tuple[int, int] = asyncio.run(main_serial_alternativo()) +resultados: Tuple[int, int] = asyncio.run(main_serial_invertido()) print(f"Finalizado: {time.strftime('%X')}") -resultados # => (1, 0) +assert resultados == (1, 0) # Empezado: 09:47:43 # API: Solicitando datos al usuario @@ -345,7 +564,7 @@ async def main_task() -> Tuple[int, int]: api_task = asyncio.create_task(respondiendo_api()) return (await db_task, await api_task) -async def main_task_alternativo() -> Tuple[int, int]: +async def main_task_invertido() -> Tuple[int, int]: api_task = asyncio.create_task(respondiendo_api()) db_task = asyncio.create_task(procesamiento_db()) return (await api_task, await db_task) @@ -353,7 +572,7 @@ async def main_task_alternativo() -> Tuple[int, int]: print(f"Empezado: {time.strftime('%X')}") resultados = asyncio.run(main_task()) print(f"Finalizado: {time.strftime('%X')}") -resultados # => (0, 1) +assert resultados == (0, 1) # Empezado: 09:47:52 # DB: Enviando Consulta a la base de datos @@ -367,9 +586,9 @@ async def main_task_alternativo() -> Tuple[int, int]: # Finalizado: 09:47:56 print(f"Empezado: {time.strftime('%X')}") -resultados = asyncio.run(main_task_alternativo()) +resultados = asyncio.run(main_task_invertido()) print(f"Finalizado: {time.strftime('%X')}") -resultados # => (1, 0) +assert resultados == (1, 0) # Empezado: 09:47:56 # API: Solicitando datos al usuario @@ -388,13 +607,13 @@ async def main_task_alternativo() -> Tuple[int, int]: async def main_gather() -> Tuple[int, int]: return await asyncio.gather(procesamiento_db(), respondiendo_api()) -async def main_gather_alternativo() -> Tuple[int, int]: +async def main_gather_invertido() -> Tuple[int, int]: return await asyncio.gather(respondiendo_api(), procesamiento_db()) print(f"Empezado: {time.strftime('%X')}") -resultados= asyncio.run(main_gather()) +resultados = asyncio.run(main_gather()) print(f"Finalizado: {time.strftime('%X')}") -resultados # => (0, 1) +assert resultados == [0, 1] # Empezado: 09:48:00 # DB: Enviando Consulta a la base de datos @@ -408,9 +627,9 @@ async def main_gather_alternativo() -> Tuple[int, int]: # Finalizado: 09:48:04 print(f"Empezado: {time.strftime('%X')}") -resultados = asyncio.run(main_gather_alternativo()) +resultados = asyncio.run(main_gather_invertido()) print(f"Finalizado: {time.strftime('%X')}") -resultados # => (1, 0) +assert resultados == [1, 0] # Empezado: 09:48:04 # API: Solicitando datos al usuario @@ -445,7 +664,7 @@ async def main_blocking_async(): print(f"Empezado: {time.strftime('%X')}") resultados_blocking: List[None] = main_blocking() print(f"Finalizado: {time.strftime('%X')}") -resultados_blocking # => [None, None, None, None, None, None, None, None, None, None] +assert resultados_blocking == [None] * 10 # Empezado: 10:28:34 # Finalizado: 10:28:44 @@ -454,21 +673,28 @@ async def main_blocking_async(): print(f"Empezado: {time.strftime('%X')}") resultados_blocking_async: Tuple[None] = asyncio.run(main_blocking_async()) print(f"Finalizado: {time.strftime('%X')}") -resultados_blocking_async # => [None, None, None, None, None, None, None, None, None, None] +assert resultados_blocking_async == [None] * 10 # Empezado: 10:28:44 # Finalizado: 10:28:46 #################################################### -## 7.7 Decoradores +## 7.6 Decoradores +#################################################### + + +#################################################### +## 7.6.1 Decoradores sin estado #################################################### + import time from typing import Tuple, Any -def medir_tiempo(function): +def medir_tiempo(function: Callable[..., Any]) ->Callable[..., Tuple[Any, float]]: - def helper(*args, **kargs) -> Tuple[Any, float]: + def helper(*args: Any, **kargs: Any) -> Tuple[Any, float]: + """Función Auxiliar""" inicio: float = time.perf_counter() resultado = function(*args, **kargs) @@ -482,66 +708,111 @@ def helper(*args, **kargs) -> Tuple[Any, float]: return helper -def funcion_lenta(): +def funcion_lenta() -> str: + """Función Original""" time.sleep(2) return "Hola mundo" # Invocación normal respuesta: str = funcion_lenta() -respuesta # => "Hola mundo" +assert respuesta == "Hola mundo" +import math # Invocación con wrapper sin decorador funcion_lenta_con_tiempo = medir_tiempo(funcion_lenta) -resultado: Tuple[str, float] = funcion_lenta_con_tiempo() -resultado # => ('Hola mundo', 2.0035091) +resultado, tiempo_ejecucion = funcion_lenta_con_tiempo() + +assert resultado == 'Hola mundo' +assert math.isclose(tiempo_ejecucion, 2, abs_tol=0.1) + +assert funcion_lenta.__name__ == "funcion_lenta" +assert funcion_lenta_con_tiempo.__name__ == "helper" # No Deseable +assert funcion_lenta_con_tiempo.__doc__ == "Función Auxiliar" # No Deseable + + +# Usando Wraps para "heredar" nombre y docstring + +from functools import wraps + +def medir_tiempo_alternativo(function: Callable[..., Any]) ->Callable[..., Tuple[Any, float]]: + + @wraps(function) + def helper(*args: Any, **kargs: Any) -> Tuple[Any, float]: + inicio: float = time.perf_counter() + + resultado = function(*args, **kargs) + + fin: float = time.perf_counter() + + transcurrido: float = fin - inicio + + return resultado, transcurrido + + return helper + + +funcion_lenta_con_tiempo = medir_tiempo_alternativo(funcion_lenta) +resultado, tiempo_ejecucion = funcion_lenta_con_tiempo() + +assert resultado == 'Hola mundo' +assert math.isclose(tiempo_ejecucion, 2, abs_tol=0.1) + +assert funcion_lenta.__name__ == "funcion_lenta" +assert funcion_lenta_con_tiempo.__name__ == "funcion_lenta" # Deseable +assert funcion_lenta_con_tiempo.__doc__ == "Función Original" # Deseable # Invocación con decorador -@medir_tiempo +@medir_tiempo_alternativo def funcion_lenta_medida(): time.sleep(2) return "Hola mundo" -resultado: Tuple[str, float] = funcion_lenta_con_tiempo() -resultado # => ('Hola mundo', 2.0083497) +resultado, tiempo_ejecucion = funcion_lenta_medida() + +assert resultado == 'Hola mundo' +assert math.isclose(tiempo_ejecucion, 2, abs_tol=0.1) # Decorador con estado (Stateful) -def contar_ejecuciones(func): - - def helper(*args, **kwargs): +def contar_ejecuciones(funcion: Callable[..., Any]) -> Callable[..., Any]: + @wraps(funcion) + def helper(*args: Any, **kwargs: Any): helper.ejecuciones += 1 - return func(*args, **kwargs) + return funcion(*args, **kwargs) - helper.ejecuciones = 0 + helper.ejecuciones = 0 # Warning - Monkey Patching - NO RECOMENDADO return helper @contar_ejecuciones -def funcion_contada(): +def funcion_contada() -> str: return "Hola mundo" for _ in range(10): funcion_contada() -funcion_contada.ejecuciones # => 10 +assert funcion_contada.ejecuciones == 10 # Warning Atributo desconocido + +#################################################### +## 7.6.1 Decoradores con estado (Stateful) +#################################################### -# Decorador con estado basado en clases (Stateful) from dataclasses import dataclass from typing import Callable @dataclass class Contador: - func: Callable + func: Callable[..., Any] ejecuciones: int = 0 - def __call__(self, *args, **kwargs): + def __call__(self, *args: Any, **kwargs: Any) -> Any: self.ejecuciones += 1 return self.func(*args, **kwargs) @@ -553,43 +824,49 @@ def funcion_contada_clase(): for _ in range(10): funcion_contada_clase() -funcion_contada_clase.ejecuciones # => 10 +assert funcion_contada_clase.ejecuciones == 10 # Sin Warning # Casos de uso - Cache y Memoization Manual -@contar_ejecuciones -def fibonacci(numero): +@Contador +def fibonacci(numero: int) -> int: if numero < 2: return numero return fibonacci(numero - 1) + fibonacci(numero - 2) resultado = fibonacci(20) -resultado # => 6765 -fibonacci.ejecuciones # => 21.891 +assert resultado == 6765 +assert fibonacci.ejecuciones == 21_891 -def memoization(func): +from typing import Dict - def helper(x): - if x not in helper.cache: - helper.cache[x] = func(x) - return helper.cache[x] - - helper.cache = {} + +def memoization(funcion: Callable[..., Any]) -> Callable[..., Any]: + cache: Dict[str, Any] = {} + + def helper(*args: Any, **kwargs: Any) -> Any: + nonlocal cache + key = str(tuple(sorted(args)) + tuple(sorted(kwargs.items()))) + if key not in cache: + cache[key] = funcion(*args, **kwargs) + return cache[key] return helper -@contar_ejecuciones + +@Contador @memoization -def fibonacci_memo(numero): +def fibonacci_memo(numero: int) -> int: if numero < 2: return numero return fibonacci_memo(numero - 1) + fibonacci_memo(numero - 2) + resultado = fibonacci_memo(20) -resultado # => 6765 -fibonacci_memo.ejecuciones # => 39 | 500 veces menos ejecuciones +assert resultado == 6765 +assert fibonacci_memo.ejecuciones == 39 # 500 veces menos ejecuciones # Casos de uso - Cache y Memoization LRU @@ -598,31 +875,29 @@ def fibonacci_memo(numero): # Referencia: https://docs.python.org/3/library/functools.html#functools.lru_cache -@contar_ejecuciones +@Contador @lru_cache(maxsize=None) # Equivalente a @cache en Python 3.9+ -def fibonacci_lru(numero): +def fibonacci_lru(numero: int) -> int: if numero < 2: return numero return fibonacci_lru(numero - 1) + fibonacci_lru(numero - 2) resultado = fibonacci_lru(20) -resultado # => 6765 -fibonacci_lru.ejecuciones # => 39 | 500 veces menos ejecuciones - -# Otros casos de uso: -# Dataclasses -# Métodos de Clase -# Propiedades -# Bibliotecas +assert resultado == 6765 +assert fibonacci_memo.ejecuciones == 39 # 500 veces menos ejecuciones #################################################### -## 7.8 Context Manager +## 7.7 Context Manager #################################################### # Referencia: https://docs.python.org/3/library/stdtypes.html#context-manager-types -# Usando Clases + +#################################################### +## 7.7.1 Context Manager con Clases +#################################################### + from dataclasses import dataclass from typing import IO, Optional, Any @@ -632,14 +907,14 @@ def fibonacci_lru(numero): class Temporizador: inicio: float = 0 fin: Optional[float] = None - transcurrido: Optional[float] = None + transcurrido: float = -1 excepcion: bool = False def __enter__(self) -> Temporizador: self.inicio = time.perf_counter() return self - def __exit__(self, tipo_excepcion: Optional[Any], _, __) -> bool: + def __exit__(self, tipo_excepcion: Optional[Any], _: Any, __: Any) -> bool: self.excepcion = tipo_excepcion is not None self.fin = time.perf_counter() self.transcurrido = round(self.fin - self.inicio, 3) @@ -649,10 +924,15 @@ def __exit__(self, tipo_excepcion: Optional[Any], _, __) -> bool: time.sleep(5) raise ValueError -tempo # => Temporizador(inicio=0.1141484, fin=5.1158644, transcurrido=5.002, excepcion=True) +print(tempo) # => Temporizador(inicio=0.1141484, fin=5.1158644, transcurrido=5.002, excepcion=True) +assert math.isclose(tempo.transcurrido, 5, abs_tol=0.1) +assert tempo.excepcion == True -# Usando contexlib + +#################################################### +## 7.7.2 Context Manager con Generadores y Contextlib +#################################################### # Referencia: https://docs.python.org/3/library/contextlib.html @@ -663,7 +943,7 @@ def __exit__(self, tipo_excepcion: Optional[Any], _, __) -> bool: def temporizador(): datos_internos = { "inicio": time.perf_counter(), - "fin": None, + "fin": -1, "transcurrido": None, "excepcion": False } @@ -675,11 +955,15 @@ def temporizador(): datos_internos['fin'] = time.perf_counter() datos_internos['transcurrido'] = round(datos_internos['fin'] - datos_internos['inicio'], 3) + with temporizador() as tempo: time.sleep(5) - raise ValueError + raise ValueError() + +print(tempo) # => Temporizador(inicio=0.1141484, fin=5.1158644, transcurrido=5.002, excepcion=True) -tempo # => {'inicio': 5.1166176, 'fin': 10.1197138, 'transcurrido': 5.003, 'excepcion': True} +assert math.isclose(tempo["transcurrido"], 5, abs_tol=0.1) +assert tempo["excepcion"] == True ## Caso de uso - Archivos Temporales @@ -761,6 +1045,56 @@ def temporizador(): conexion.execute("select * from Persona") +#################################################### +## 7.8 Perlas de la Biblioteca Estándar - Pathlib +#################################################### + +from pathlib import Path + +# Atributos + +archivo_ejemplo = Path("/data/ejemplo/main.py") +print(archivo_ejemplo.parent) # => "\data\ejemplo" en Windows, /data/ejemplo en Unix +assert archivo_ejemplo.name == "main.py" +assert archivo_ejemplo.stem == "main" +assert archivo_ejemplo.suffix == ".py" + +# Concatenación + +carpeta = Path("/data/ejemplo") +archivo_nuevo = carpeta / "test.py" + +# Caso espcial: Archivo actual + +archivo_actual = Path(__file__).resolve() + +# Manipulación de Archivos con open y close automáticos + +carpeta_actual = archivo_actual.parent +carpeta_nueva = carpeta_actual / "chapter7" + +carpeta_nueva.mkdir(exist_ok=True, parents=True) # Crea arbol de directorios + +archivo_vacio = carpeta_nueva / "test_pathlib.txt" # Definir archivo nuevo +archivo_vacio.touch(exist_ok=True) # Crea un archivo vacio + +archivo_texto = carpeta_nueva / "test_pathlib.txt" # Definir archivo nuevo +archivo_texto.write_text("Hola Mundo") # Escribir Texto +contenido_text = archivo_texto.read_text() # Leer contenido + +archivo_bytes = carpeta_nueva / "test_bytes.txt" # Definir archivo nuevo +archivo_bytes.write_bytes(b"Hola Mundo") # Escribir Texto +contenido_bytes = archivo_bytes.read_bytes() # Leer contenido + +archivo_vacio.unlink(missing_ok=True) # Borrar archivo +archivo_texto.unlink(missing_ok=True) # Borrar archivo +archivo_bytes.unlink(missing_ok=True) # Borrar archivo +carpeta_nueva.rmdir() # Borrar carpeta + +assert contenido_text == "Hola Mundo" +assert contenido_bytes == b"Hola Mundo" + + #################################################### ## 7.9 Perlas de la Biblioteca Estándar - Itertools #################################################### @@ -785,20 +1119,20 @@ def temporizador(): from typing import Iterable, Any -def pairwise(iterable: Iterable) -> Iterator[Tuple[Any, Any]]: +def pairwise(iterable: Iterable[Any]) -> Iterator[Tuple[Any, Any]]: "s -> (s0, s1), (s1, s2), (s2, s3), ..." a, b = itertools.tee(iterable) next(b, None) return zip(a, b) -def powerset(iterable: Iterable) -> Iterator[Tuple[Any, ...]]: +def powerset(iterable: Iterable[Any]) -> Iterator[Tuple[Any, ...]]: "powerset([1,2,3]) --> () (1,) (2,) (3,) (1,2) (1,3) (2,3) (1,2,3)" s = list(iterable) return itertools.chain.from_iterable(itertools.combinations(s, r) for r in range(len(s)+1)) -def roundrobin(*iterables: List[Iterable]) -> Generator[Any]: +def roundrobin(*iterables: List[Iterable[Any]]) -> Generator[Any, None, None]: "roundrobin('ABC', 'D', 'EF') --> A D E B F C" # Recipe credited to George Sakkis num_active = len(iterables) @@ -853,13 +1187,18 @@ class Alumno(): 'e': Counter(a=10, b=4, c=2) } -with open('datos.pickle', 'wb') as pickle_file: +archivo = Path('datos.pickle') + +with open(archivo, 'wb') as pickle_file: pickle.dump(datos, pickle_file) -with open('datos.pickle', 'rb') as f: +with open(archivo, 'rb') as f: datos_cargados = pickle.load(f) -datos == datos_cargados # => True +archivo.unlink() + +assert datos == datos_cargados + # Usando JSON @@ -877,13 +1216,17 @@ class Alumno(): 'c': [None, True, False], } -with open('datos.json', 'w') as json_file: +archivo = Path('datos.json') + +with open(archivo, 'w') as json_file: json.dump(datos, json_file) -with open('datos.json', 'r') as json_file: +with open(archivo, 'r') as json_file: datos_cargados = json.load(json_file) -datos == datos_cargados # => True +archivo.unlink() + +assert datos == datos_cargados #################################################### @@ -899,7 +1242,7 @@ class Alumno(): from email import encoders -def enviar_email(email, asunto, contenido="", archivo=None): +def enviar_email(email: str, asunto: str, contenido: str = "", archivo: Optional[str] = None): usuario = os.environ["email_username"] clave = os.environ["email_password"] @@ -911,19 +1254,18 @@ def enviar_email(email, asunto, contenido="", archivo=None): mime.attach(MIMEText(contenido, 'plain')) - if archivo is not None: - with open(archivo, "rb") as attachment: - base = MIMEBase('application', 'octet-stream') - base.set_payload(attachment.read()) - encoders.encode_base64(base) - base.add_header('Content-Disposition', f"attachment; filename= {archivo}") - mime.attach(base) - - context = ssl.create_default_context() + if archivo is not None: + base = MIMEBase('application', 'octet-stream') + archivo_bytes = Path(archivo).read_bytes() + base.set_payload(archivo_bytes) + encoders.encode_base64(base) + base.add_header('Content-Disposition', f"attachment; filename={archivo}") + mime.attach(base) servidor_url = os.environ["email_server"] puerto = int(os.environ["email_port"]) + context = ssl.create_default_context() with smtplib.SMTP_SSL(servidor_url, puerto, context=context) as servidor: servidor.ehlo() servidor.login(usuario, clave) From f5a19809cd104a8a5c6da3ea13266bdc773c2209 Mon Sep 17 00:00:00 2001 From: ELC Date: Sat, 16 Apr 2022 17:46:19 -0300 Subject: [PATCH 4/5] =?UTF-8?q?A=C3=B1adir=20asserts=20al=20cap=C3=ADtulo?= =?UTF-8?q?=20de=20funciones?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter4.py | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/chapter4.py b/chapter4.py index be44610..bb31a69 100644 --- a/chapter4.py +++ b/chapter4.py @@ -19,10 +19,10 @@ def sin_return(x: float, y: float) -> float: # Recomendaciones con Type-Hints x / y -dividir(10, 8) # => 1.25 Orden idéntico a la definición -dividir(8, 10) # => 0.8 El Orden es importante -dividir(x=10, y=8) # => 1.25 Usando parametros explícitos -dividir(y=8, x=10) # => 1.25 Orden irrelevante +assert dividir(10, 8) == 1.25 # Orden idéntico a la definición +assert dividir(8, 10) == 0.8 # El Orden es importante +assert dividir(x=10, y=8) == 1.25 # Usando parametros explícitos +assert dividir(y=8, x=10) == 1.25 # Orden irrelevante def es_mayor_de_edad(edad: int, limite: int = 18) -> bool: # Valor por defecto @@ -43,9 +43,9 @@ def es_mayor_de_edad(edad: int, limite: int = 18) -> bool: # Return expression return edad >= limite -es_mayor_de_edad(10) # => False -es_mayor_de_edad(18) # => True -es_mayor_de_edad(24) # => True +assert not es_mayor_de_edad(10) +assert es_mayor_de_edad(18) +assert es_mayor_de_edad(24) from typing import List, Tuple # Biblioteca Estándar @@ -61,10 +61,10 @@ def hay_oferta(precios: List[float]) -> Tuple[bool, float]: return False, precio_mas_bajo -hay_oferta(precios) # => (True, 0.09) | Devuelve Tupla +assert hay_oferta(precios) == (True, 0.09) # => Devuelve Tupla existe_oferta, monto = hay_oferta(precios) # => Desempaquetado -existe_oferta # => True -monto # => 0.09 +assert existe_oferta +assert monto == 0.09 #################################################### @@ -78,24 +78,21 @@ def suma(*args: float): # Parametros posicionales arbitrarios return resultado -suma(1, 2, 3) # => 6 +assert suma(1, 2, 3) == 6 from typing import Dict # Biblioteca Estándar def concatenate(**kwargs: str): # Parametros de palabra clave arbitrarios - result = "" - for arg in kwargs.values(): - result += arg + " " - return result + return " ".join(kwargs.values()) concatenate(a="Hola", b="Mundo") # => 'Hola Mundo ' numeros: List[float] = [1, 2, 3, 4] palabras: Dict[str, str] = {"a": "Hola", "b": "Mundo"} -suma(*numeros) # => 10 -concatenate(**palabras) # => 'Hola Mundo ' +assert suma(*numeros) == 10 +assert concatenate(**palabras) == 'Hola Mundo' #################################################### @@ -119,7 +116,7 @@ def cuadrado(x: float) -> float: lista: List[float] = [1, 2, 3, 4, 5, 6] -aplicar_funcion(lista, cuadrado) # => [1, 4, 9, 16, 25, 36] +assert aplicar_funcion(lista, cuadrado) == [1, 4, 9, 16, 25, 36] # Funciones dentro de funciones (Closures) @@ -134,7 +131,7 @@ def auxiliar(x: float) -> float: lista: List[float] = [1, 2, 3, 4, 5, 6] elevar_cuadrado: Callable[[float], float] = elevar(2) -aplicar_funcion(lista, elevar_cuadrado) # => [1, 4, 9, 16, 25, 36] +assert aplicar_funcion(lista, elevar_cuadrado) == [1, 4, 9, 16, 25, 36] # Evaluación Parcial @@ -147,17 +144,17 @@ def elevar_xy(x: float, y: float) -> float: lista: List[float] = [1, 2, 3, 4, 5, 6] elevar_cuadrado_parcial: Callable[[float], float] = partial(elevar_xy, y=2) -aplicar_funcion(lista, elevar_cuadrado_parcial) # => [1, 4, 9, 16, 25, 36] +assert aplicar_funcion(lista, elevar_cuadrado_parcial) == [1, 4, 9, 16, 25, 36] # Funciones anónimas (Lambdas) lista: List[float] = [1, 2, 3, 4, 5, 6] -aplicar_funcion(lista, lambda x: x**2) # => [1, 4, 9, 16, 25, 36] +assert aplicar_funcion(lista, lambda x: x**2) == [1, 4, 9, 16, 25, 36] #################################################### -# 4.3 Funciones sobre Funciones +# 4.3 Funciones de orden superior comunes (map, filter reduce) #################################################### from typing import Iterator # Biblioteca estándar @@ -168,6 +165,7 @@ def elevar_xy(x: float, y: float) -> float: cuadrados_pares: Iterator[float] = filter(lambda x: x > 5, cuadrados) # => [9, 16, 25, 36] suma_pares: float = reduce(lambda x, y: x + y, cuadrados_pares) # => 86 +assert suma_pares == 86 #################################################### # 4.4 Comprensiones @@ -178,10 +176,12 @@ def elevar_xy(x: float, y: float) -> float: cuadrados_pares_: List[float] = [x for x in cuadrados_ if x > 5] # => [9, 16, 25, 36] suma_pares: float = sum(cuadrados_pares_) # => 86 +assert suma_pares == 86 lista: List[float] = [1, 2, 3, 4, 5, 6] suma_pares: float = sum(elevar_cuadrado(x) for x in lista if elevar_cuadrado(x) > 5) -suma_pares # => 86< + +assert suma_pares == 86 # Código equivalente a la comprensión con bucle FOR @@ -193,4 +193,4 @@ def elevar_xy(x: float, y: float) -> float: if auxiliar > 5: resultado += auxiliar -resultado # => 86 +assert resultado == 86 From 05930c72780a28a0f1424e87a1230b0381c694dd Mon Sep 17 00:00:00 2001 From: ELC Date: Sat, 16 Apr 2022 17:46:37 -0300 Subject: [PATCH 5/5] =?UTF-8?q?A=C3=B1adir=20m=C3=A9todos=20est=C3=A1ticos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter5.py | 52 ++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/chapter5.py b/chapter5.py index d7acd8e..ab5beb3 100644 --- a/chapter5.py +++ b/chapter5.py @@ -61,7 +61,35 @@ def _get_next_id(cls): #################################################### -# 5.3 Dataclasses +# 5.3 Métodos estáticos +#################################################### + + +class Temperatura: + + def __init__(self, region: str, temperatura: float) -> None: + self.region = region + self.temperatura = temperatura + + @staticmethod + def celcius_a_farenheit(temperatura: float) -> float: # Sin self + return 32 + temperatura * 9 / 5 + + @staticmethod + def farenheit_a_celcius(temperatura: float) -> float: # Sin self + return (temperatura - 32) * 5 / 9 + + +temperatura_hoy = Temperatura("Mesopotamia", 35) + +assert Temperatura.celcius_a_farenheit(35) == 95 # Invocación desde clase +assert Temperatura.farenheit_a_celcius(95) == 35 # Invocación desde clase +assert temperatura_hoy.celcius_a_farenheit(35) == 95 # Invocación desde instancia +assert temperatura_hoy.farenheit_a_celcius(95) == 35 # Invocación desde instancia + + +#################################################### +# 5.4 Dataclasses #################################################### @@ -127,7 +155,7 @@ def _get_next_dni(cls) -> int: #################################################### -# 5.4 Sobrecarga de Operadores +# 5.5 Sobrecarga de Operadores #################################################### @@ -213,7 +241,7 @@ def __add__(self, other: ShoppingCart) -> ShoppingCart: #################################################### -# 5.5 Instancias como Functiones (__call__) +# 5.6 Instancias como Functiones (__call__) #################################################### @@ -256,7 +284,7 @@ def __call__(self, valor: Union[int, float]) -> None: #################################################### -# 5.6 Propiedades y Copia Profunda +# 5.7 Propiedades y Copia Profunda #################################################### @@ -310,7 +338,7 @@ def actualizar_precio(productos: List[Producto], porcentaje_aumento: float) -> L #################################################### -# 5.7 Herencia +# 5.8 Herencia #################################################### @@ -337,7 +365,7 @@ def descripcion(self) -> str: #################################################### -# 5.8 Constructor (__new__) +# 5.9 Constructor (__new__) #################################################### @@ -380,7 +408,7 @@ class AutoDeportivo(Auto): #################################################### -# 5.8 Clases y Métodos abstractos +# 5.10 Clases y Métodos abstractos #################################################### @@ -469,7 +497,7 @@ def descripcion(self) -> str: #################################################### -# 5.9 Interfaces (Protocols) +# 5.11 Interfaces (Protocols) #################################################### @@ -499,7 +527,7 @@ def get_datos_resumen(objeto: Identificable): #################################################### -# 5.10 Sobrecarga de Métodos +# 5.12 Sobrecarga de Métodos #################################################### @@ -523,7 +551,7 @@ def duplicar(x: int | Sequence[int]) -> int | list[int]: #################################################### -# 5.11 Sobrecarga de Métodos - Caso Especial - Python 3.8+ +# 5.13 Sobrecarga de Métodos - Caso Especial - Python 3.8+ #################################################### @@ -574,7 +602,7 @@ def _(self, impuesto: int) -> float: #################################################### -# 5.12 Mixins (Herencia Múltiple) +# 5.14 Mixins (Herencia Múltiple) #################################################### @@ -602,7 +630,7 @@ class EmpleadoBaseDeDatos(EmpleadoAlternativo, JsonSerializer): #################################################### -# 5.13 Descriptores +# 5.15 Descriptores ####################################################