Desafio Python

Mini calculadora com tipos dinâmicos

Enunciado

Implemente uma função calcular(a, op, b) que receba dois valores e um operador ("+", "-", "*", "/"). Se a ou b for None, retorne None. Se algum dos dois não for int ou float (ou for bool), levante TypeError com a mensagem "operandos devem ser números". Se op não for uma das quatro operações, levante ValueError com a mensagem "operador inválido". Em divisão por zero, deixe o ZeroDivisionError propagar. Teste com uma lista de chamadas, capturando exceções e imprimindo o resultado ou a mensagem de erro.

Requisitos

  • Retornar None quando a ou b for None.
  • Rejeitar bool como operando, mesmo sendo subclasse de int.
  • Levantar TypeError e ValueError com as mensagens exatas.
  • Capturar as exceções no código de teste e imprimir Erro: <mensagem>.

Código inicial

def calcular(a, op, b):
    # complete aqui
    pass

casos = [
    (2, "+", 3),
    (None, "+", 1),
    ("2", "+", 3),
    (10, "/", 0),
    (2, "%", 3),
]
for a, op, b in casos:
    try:
        print(calcular(a, op, b))
    except Exception as e:
        print(f"Erro: {e}")

Saída esperada

5
None
Erro: operandos devem ser números
Erro: division by zero
Erro: operador inválido
Ver dica

Para excluir bool, cheque isinstance(x, bool) antes de aceitar int e float.

Mostrar solução
def calcular(a, op, b):
    # None indica ausência de valor: devolvemos None sem erro.
    if a is None or b is None:
        return None
    # bool é subclasse de int; rejeitamos explicitamente.
    if isinstance(a, bool) or isinstance(b, bool):
        raise TypeError("operandos devem ser números")
    if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):
        raise TypeError("operandos devem ser números")
    # Validamos o operador antes de calcular.
    if op not in ("+", "-", "*", "/"):
        raise ValueError("operador inválido")
    if op == "+":
        return a + b
    if op == "-":
        return a - b
    if op == "*":
        return a * b
    return a / b  # ZeroDivisionError propaga naturalmente

casos = [
    (2, "+", 3),
    (None, "+", 1),
    ("2", "+", 3),
    (10, "/", 0),
    (2, "%", 3),
]
for a, op, b in casos:
    try:
        print(calcular(a, op, b))
    except Exception as e:
        print(f"Erro: {e}")

Passo a passo

  1. A primeira checagem, a is None or b is None, usa is e devolve None imediatamente, sem levantar erro.
  2. Em seguida rejeitamos bool explicitamente, pois True e False são subclasses de int e não fazem sentido como operandos.
  3. A checagem isinstance(a, (int, float)) garante que só números entram no cálculo; caso contrário, TypeError com a mensagem pedida.
  4. Validamos op contra a tupla de operadores válidos; se não estiver, ValueError.
  5. Os if encadeados aplicam a operação correta; a divisão fica por último e deixa ZeroDivisionError propagar.
  6. No bloco de teste, cada chamada é envolvida em try/except Exception, imprimindo o resultado ou Erro: <mensagem>.

Por que funciona

A função combina tipagem dinâmica com verificações explícitas: em vez de confiar no tipo inferido, inspecionamos os valores com isinstance e tratamos None e bool como casos especiais. Isso mostra como Python permite escrever código flexível, mas exige checagens quando queremos garantir contratos. As exceções TypeError e ValueError comunicam claramente o que deu errado, e o try/except no teste evita que o programa pare na primeira falha.

Erros comuns

  • Usar if not a or not b para checar None: isso também rejeita 0 e 0.0. Prefira a is None or b is None.
  • Esquecer de rejeitar bool: calcular(True, "+", 1) retornaria 2 em vez de erro.
  • Checar o operador depois de calcular: se op for inválido, poderíamos lançar outro erro antes. Valide primeiro.
  • Capturar apenas TypeError no teste: ZeroDivisionError e ValueError escapariam. Use except Exception.

Outra forma de resolver

Usando o módulo operator:

import operator

OPS = {"+": operator.add, "-": operator.sub, "*": operator.mul, "/": operator.truediv}

def calcular(a, op, b):
    if a is None or b is None:
        return None
    if isinstance(a, bool) or isinstance(b, bool) or not isinstance(a, (int, float)) or not isinstance(b, (int, float)):
        raise TypeError("operandos devem ser números")
    if op not in OPS:
        raise ValueError("operador inválido")
    return OPS[op](a, b)

É preferível quando há muitas operações, pois evita a cadeia de if.

Saída esperada

5
None
Erro: operandos devem ser números
Erro: division by zero
Erro: operador inválido