Tecnicas Avançadas

Programação Assíncrona

A programação assíncrona permite que o programa não fique parado esperando essas operações terminarem.

Uma corrotina (coroutine) é um objeto que representa uma tarefa assíncrona que pode ser pausada e retomada.

  • async def: Declara que a função será assíncrona.

  • await: Diz ao Python para esperar essa operação terminar, sem bloquear o resto do programa.

  • asyncio.gather(tarefa1(),tarefa2()): Executa várias tarefas ao mesmo tempo. sem esse método teríamos que esperar uma função terminar antes de chamar a outra, o que anula a vantagem da programação assíncrona. O tempo total de execução é igual ao tempo da tarefa mais longa

  • asyncio.run(funcao_assincrona()) responsavel por executa-la. asyncdefine a função como assíncrona, mas não a executa, o run que é responsavel por isso. se eu chamar sem o run, retorna um objeto corrotina, mas não inicia a execução da função.

O GIL (Global Interpreter Lock) impede que múltiplas threads usem o processador ao mesmo tempo. Porém, o GIL não afeta processos diferentes! Então, podemos usar multiprocessing para rodar tarefas em paralelo, usando vários núcleos da CPU. Lembrando que o paralelismo consume muitos recursos computacionais, e pode se tornar inviavel.

 

Conceito Como funciona Exemplo
Concorrência (asyncio, threads) Alterna entre tarefas rapidamente A cpu trabalha em varios processos ao mesmo tempo
Paralelismo (multiprocessing) Executa tarefas ao mesmo tempo, usando vários núcleos do processador Cada cpu trabalha em um processo separado

 

Nesse exemplo, se usassemos funções sincronas, demoraria 11 segundos para que todos os pedidos ficassem prontos. mas demorou apenas 5, pois em 3 o hambuguer e o arroz ficaram prontos, e em mais 2 (5 no total) a pizza.

AsyncIO.py
import asyncio

async def fazer_hamburguer():
    print("🍔 Começando a preparar o hambúrguer...")
    await asyncio.sleep(3)  # Simula o tempo de preparo
    print("🍔 Hambúrguer pronto!")

async def fazer_arroz():
    print("🍚 Começando a preparar o Arroz...")
    await asyncio.sleep(3)  # Simula o tempo de preparo
    print("🍚 Arroz pronto!")

async def fazer_pizza():
    print("🍕 Começando a preparar a pizza...")
    await asyncio.sleep(5)  # Simula o tempo de preparo
    print("🍕 Pizza pronta!")

# Essa Função coordena as tarefas
async def main():
    await asyncio.gather(fazer_hamburguer(), fazer_pizza(), fazer_arroz())

print("🛎️ Pedido recebido!")
asyncio.run(main())  
print("✅ Pedido finalizado!")

Saída:
🛎️ Pedido recebido!
🍔 Começando a preparar o hambúrguer...
🍕 Começando a preparar a pizza...
🍚 Começando a preparar o Arroz...
🍔 Hambúrguer pronto!
🍚 Arroz pronto!
🍕 Pizza pronta!
✅ Pedido finalizado!

multiprocessing.py
import multiprocessing
import time

def fazer_hamburguer():
    print("🍔 Começando a preparar o hambúrguer...")
    time.sleep(3)  
    print("🍔 Hambúrguer pronto!")

def fazer_arroz():
    print("🍚 Começando a preparar o arroz...")
    time.sleep(3)  
    print("🍚 Arroz pronto!")

def fazer_pizza():
    print("🍕 Começando a preparar a pizza...")
    time.sleep(5)  
    print("🍕 Pizza pronta!")

if __name__ == "__main__":
    print("🛎️ Pedido recebido!")

    # Criando processos separados
    p1 = multiprocessing.Process(target=fazer_hamburguer)
    p2 = multiprocessing.Process(target=fazer_arroz)
    p3 = multiprocessing.Process(target=fazer_pizza)

    # Iniciando os processos (rodando em paralelo)
    p1.start()
    p2.start()
    p3.start()

    # Espera todos terminarem
    p1.join()
    p2.join()
    p3.join()

    print("✅ Pedido finalizado!")

Programação funcional

Programação funcional é um estilo de programação baseado em funções matemáticas e na imutabilidade dos dados.

Isso significa que em vez de alterar variáveis e estados, criamos funções que recebem entradas (inputs) e retornam saídas (outputs) sem efeitos colaterais.

PRINCIPAIS CARACTERISTICAS
Funções como "cidadãos de primeira classe": Isso significa que podemos tratar funções como variáveis: passá-las como argumento, retorná-las e armazená-las em listas ou dicionários.
Imutabilidade (evitar modificar variáveis): Na programação funcional, tentamos não modificar variáveis já criadas, mas sim criar novos valores.
Ausência de efeitos colaterais: Um efeito colateral acontece quando uma função modifica algo fora dela (exemplo: modificar uma variável global, alterar um arquivo no disco, mudar um banco de dados etc.).
Uso de funções puras: Uma função pura é aquela que sempre retorna o mesmo resultado para a mesma entrada e não tem efeitos colaterais.
Uso de funções de alta ordem: São funções que recebem outras funções como argumento ou retornam funções.

Embora seja útil, programação funcional não é ideal para tudo.

Se você precisa modificar estados (bancos de dados, arquivos, variáveis globais, etc.)
Se o código fica mais complicado sem necessidade
Se o desempenho for crítico (Python não otimiza tanto funções puras quanto linguagens como Haskell)

programacao_funcional.py
# --- Exemplo 1 --- Cidadãos de primeira classe --- #


# Aqui, a variável mensagem guarda a referência da função saudacao() e pode ser chamada depois.
def saudacao():
    return "Olá!"
# Atribuímos a função a uma variável
mensagem = saudacao  
print(mensagem())  # Saída: Olá!


# --- Exemplo 2 --- Imutabilidade (Evitar Mudar Variáveis) --- #


# Aqui a lista numeros foi modificada
numeros = [1, 2, 3]
for i in range(len(numeros)):
    numeros[i] *= 2
print(numeros)  # [2, 4, 6]

# A lista original não foi alterada, apenas criamos uma nova!
numeros = [1, 2, 3]
novos_numeros = list(map(lambda x: x * 2, numeros))
print(novos_numeros)  # [2, 4, 6]


# --- Exemplo 3 --- Ausência de Efeitos Colaterais --- #


# Aqui, incrementar() modificou contador, o que pode causar bugs difíceis de rastrear.
contador = 0
def incrementar():
    global contador
    contador += 1
incrementar()
print(contador)  # 1 (modificou uma variável externa)

# Nenhuma variável externa foi alterada! A função apenas recebe um valor e retorna outro.
def incrementar(contador):
    return contador + 1
novo_contador = incrementar(0)
print(novo_contador)  # 1


# --- Exemplo 4 --- Funções Pura --- #


# Aqui o retorno depende de algo externo (o valor de total muda toda vez que chamamos a função).
total = 0
def somar_e_acumular(a):
    global total  # Modifica uma variável externa (efeito colateral)
    total += a
    return total

# Aqui sempre gera o mesmo resultado
def soma(a, b):
    return a + b
print(soma(3, 4))  # 7
print(soma(3, 4))  # 7 (sempre o mesmo resultado)


# --- Exemplo 5 --- Funções de Alta Ordem --- #


# Aqui, aplicar_operacao() recebe qualquer função e um número, tornando o código mais flexível.
def aplicar_operacao(funcao, valor):
    return funcao(valor)
def quadrado(x):
    return x * x
print(aplicar_operacao(quadrado, 5))  # 25

Codificação de Caracteres (Unicode e Charsets)

Unicode é um padrão universal de representação de todos os caracteres, onde cada caractere recebe um número único chamado code pointA → U+0041, ç → U+00E7,🙂 → U+1F642 e etc. 

Esses code points precisam ser transformados em bytes para serem guardados ou enviados, usando as codificações.
Os tipos de codificação de caracteres (character encodings ou charsets), definem como os caracteres (letras, números, símbolos) são representados internamente em bytes no computador ou transmitidos entre sistemas.
Nome Caracteres suportados Tamanho por caractere Observações
ASCII 128 caracteres (A–Z, a–z, 0–9, símbolos básicos) 1 byte (apenas 7 bits usados) Simples, muito antigo, sem acentos.
Latin1 (ISO-8859-1) 256 caracteres 1 byte Inclui caracteres acentuados usados em idiomas da Europa Ocidental (ex: á, ç).
UTF-8 Todos os caracteres Unicode (milhares de idiomas e símbolos) Variável (1 a 4 bytes) Compatível com ASCII e recomendado para web.


Tutórial: Normalização Unicode

encode_decode_unicode.py
# ---- Exemplo do caracter A em unicode usando varias formas ---- #

print("A")  # Caractere literal A
print(chr(0x41))  # Usando a função chr com hexadecimal
print(chr(65))  # Usando a função chr com decimal
print("\N{LATIN CAPITAL LETTER A}")  # Usando o nome do caractere
print("\u0041")  # Usando um hexadecimal 16-bit
print("\U00000041")  # Usando um hexadecimal 32-bit
print(ord("A"))  # Obtém o valor decimal que representa A
print(hex(ord("A")))  # Obtém o valor hexadecimal que representa A


# ---- ENCODING DECODING ---- #
# Converter string utf-8 para byte utf-8
nome_em_byte = "Diogo Mamédio".encode("utf-8")  # noqa: UP012
print(nome_em_byte)  # Saída: b'Diogo Mam\xc3\xa9dio'
# Converter byte utf-8 para String utf-8
print(nome_em_byte.decode("utf-8"))
print("b'Diogo Mam\xc3\xa9dio'")  # Saída: Diogo Mamédio

Quando você faz print() em um objeto bytes, o Python mostra a representação textual dele, não os caracteres diretamente.