Zig para Programadores Python: Guia de Transição

Se você é um programador Python e está curioso sobre programação de sistemas, a zig lang é uma excelente porta de entrada. A linguagem Zig oferece performance próxima ao C, controle total sobre a memória e uma sintaxe surpreendentemente legível para quem vem de linguagens de alto nível. Neste guia, vamos explorar as principais diferenças entre Python e Zig, com exemplos lado a lado para facilitar sua transição.

Por Que um Programador Python Deveria Aprender Zig

Python é fantástico para prototipagem rápida, ciência de dados, automação e desenvolvimento web. No entanto, existem situações em que Python simplesmente não entrega a performance necessária. É aí que Zig entra.

Aqui estão os principais motivos para considerar Zig:

  • Performance real: Zig compila para código nativo otimizado, atingindo velocidades comparáveis ao C. Operações que levam segundos em Python podem levar microssegundos em Zig.
  • Entendimento profundo: Aprender Zig vai transformar a forma como você pensa sobre memória, alocação e eficiência, tornando você um programador Python melhor também.
  • Extensões nativas: Você pode escrever módulos nativos em Zig para acelerar partes críticas do seu código Python.
  • Sem garbage collector: Zig dá controle total sobre a memória, eliminando pausas imprevisíveis do GC.
  • Cross-compilation embutida: Compile para qualquer plataforma diretamente, sem configuração complexa.

Diferenças Fundamentais: Python vs Zig

Antes de mergulhar nos exemplos, é importante entender as diferenças filosóficas entre as duas linguagens.

AspectoPythonZig
TipagemDinâmicaEstática
ExecuçãoInterpretado (CPython)Compilado (LLVM)
MemóriaGarbage CollectorGerenciamento manual
Null/NoneNone é um valornull é tipo opcional explícito
ErrosExceções (try/except)Error unions
ParadigmaMultiparadigma (OOP forte)Procedural com generics
VelocidadeLenta (relativo)Muito rápida

Variáveis e Tipos

Em Python, variáveis não têm tipo declarado. Em Zig, cada variável precisa de um tipo explícito ou inferido.

Python:

nome = "Maria"
idade = 28
pi = 3.14159
ativo = True

Zig:

const nome = "Maria";
var idade: u32 = 28;
const pi: f64 = 3.14159;
const ativo: bool = true;

Observe que Zig distingue entre const (imutável) e var (mutável). Isso é parecido com boas práticas em Python onde evitamos reatribuir variáveis, mas em Zig o compilador realmente impõe a imutabilidade.

Os tipos numéricos em Zig são explícitos sobre tamanho e sinalização: u32 é um inteiro sem sinal de 32 bits, i64 é com sinal de 64 bits, f64 é ponto flutuante de 64 bits.

Funções

Python:

def somar(a, b):
    return a + b

resultado = somar(3, 5)
print(resultado)

Zig:

const std = @import("std");

fn somar(a: i32, b: i32) i32 {
    return a + b;
}

pub fn main() void {
    const resultado = somar(3, 5);
    std.debug.print("Resultado: {}\n", .{resultado});
}

Diferenças notáveis:

  • Em Zig, os tipos dos parâmetros e do retorno são obrigatórios.
  • Não existe print() global; usamos std.debug.print.
  • Todo programa Zig precisa de uma função main.
  • A sintaxe .{resultado} é uma tupla anônima usada para formatação.

Loops e Iteração

Python é famoso por seus loops elegantes com for...in. Zig tem uma abordagem diferente, mas igualmente poderosa.

Python - iterando sobre uma lista:

frutas = ["maçã", "banana", "laranja"]
for fruta in frutas:
    print(fruta)

for i in range(10):
    print(i)

Zig - iterando sobre um array:

const std = @import("std");

pub fn main() void {
    const frutas = [_][]const u8{ "maçã", "banana", "laranja" };
    for (frutas) |fruta| {
        std.debug.print("{s}\n", .{fruta});
    }

    // Equivalente ao range(10)
    var i: u32 = 0;
    while (i < 10) : (i += 1) {
        std.debug.print("{}\n", .{i});
    }
}

Em Zig, o for itera sobre slices e arrays com uma sintaxe de captura usando |variavel|. Para loops numéricos, usamos while com uma cláusula de continuação.

Strings e Texto

O tratamento de strings é uma das maiores diferenças entre Python e Zig.

Python:

texto = "Olá, mundo!"
print(len(texto))
print(texto.upper())
print(texto[0:3])
partes = texto.split(", ")

Zig:

const std = @import("std");

pub fn main() void {
    const texto = "Olá, mundo!";

    // Comprimento em bytes (não caracteres Unicode)
    std.debug.print("Tamanho: {}\n", .{texto.len});

    // Zig não tem .upper() embutido
    // Strings são slices de bytes: []const u8

    // Fatiamento (slice)
    const fatia = texto[0..5];
    std.debug.print("Fatia: {s}\n", .{fatia});
}

Em Python, strings são objetos ricos com dezenas de métodos. Em Zig, strings são simplesmente slices de bytes ([]const u8). Isso significa que operações como upper(), split() e replace() precisam ser feitas manualmente ou usando funções da biblioteca padrão como std.mem. Para entender melhor como Zig lida com texto e coleções, confira o tutorial sobre strings e arrays em Zig.

Essa abordagem pode parecer primitiva inicialmente, mas garante zero alocações ocultas e controle total sobre a memória.

Tratamento de Erros: Exceções vs Error Unions

Uma das diferenças mais elegantes entre Python e Zig está no tratamento de erros.

Python:

def dividir(a, b):
    if b == 0:
        raise ValueError("Divisão por zero!")
    return a / b

try:
    resultado = dividir(10, 0)
except ValueError as e:
    print(f"Erro: {e}")

Zig:

const std = @import("std");

const MathError = error{
    DivisaoPorZero,
};

fn dividir(a: f64, b: f64) MathError!f64 {
    if (b == 0.0) return MathError.DivisaoPorZero;
    return a / b;
}

pub fn main() void {
    const resultado = dividir(10.0, 0.0) catch |err| {
        std.debug.print("Erro: {}\n", .{err});
        return;
    };
    std.debug.print("Resultado: {d}\n", .{resultado});
}

Em Zig, erros não são exceções que “voam” pela pilha de chamadas. Eles são valores de retorno explícitos usando error unions (MathError!f64). Isso significa que o tipo de retorno MathError!f64 pode ser um f64 válido OU um erro do tipo MathError.

A vantagem? Você nunca é surpreendido por uma exceção não tratada. O compilador exige que todo erro seja explicitamente tratado com catch ou propagado com try.

Listas vs Arrays e Slices

Python - listas dinâmicas:

numeros = [1, 2, 3, 4, 5]
numeros.append(6)
numeros.pop()
filtrados = [x for x in numeros if x > 2]

Zig - arrays e ArrayList:

const std = @import("std");

pub fn main() !void {
    // Array de tamanho fixo (como uma tupla fixa em Python)
    const fixo = [_]i32{ 1, 2, 3, 4, 5 };
    _ = fixo;

    // ArrayList (equivalente a list do Python)
    var lista = std.ArrayList(i32).init(std.heap.page_allocator);
    defer lista.deinit(); // Libera memória automaticamente ao sair do escopo

    try lista.append(1);
    try lista.append(2);
    try lista.append(3);

    // Iteração
    for (lista.items) |item| {
        std.debug.print("{} ", .{item});
    }
    std.debug.print("\n", .{});
}

A palavra-chave defer é extremamente útil: ela garante que lista.deinit() será chamado quando o bloco atual terminar, similar a um context manager (with) em Python.

Classes e Objetos vs Structs

Python é orientado a objetos. Zig não tem classes, herança ou polimorfismo de subtipos. Em vez disso, usa structs com métodos.

Python:

class Retangulo:
    def __init__(self, largura, altura):
        self.largura = largura
        self.altura = altura

    def area(self):
        return self.largura * self.altura

    def __str__(self):
        return f"Retângulo({self.largura}x{self.altura})"

r = Retangulo(5, 3)
print(r.area())
print(r)

Zig:

const std = @import("std");

const Retangulo = struct {
    largura: f64,
    altura: f64,

    pub fn area(self: Retangulo) f64 {
        return self.largura * self.altura;
    }

    pub fn formato(self: Retangulo, buf: []u8) []u8 {
        return std.fmt.bufPrint(buf, "Retângulo({d}x{d})", .{
            self.largura,
            self.altura,
        }) catch "erro";
    }
};

pub fn main() void {
    const r = Retangulo{
        .largura = 5.0,
        .altura = 3.0,
    };
    std.debug.print("Área: {d}\n", .{r.area()});
}

Em Zig, structs são tipos de valor (não referência como objetos Python). Não existe self implícito; o parâmetro self é explícito na assinatura do método. Campos são inicializados usando a sintaxe .campo = valor.

Dicionários vs HashMap

Python:

capitais = {
    "Brasil": "Brasília",
    "Argentina": "Buenos Aires",
    "Chile": "Santiago"
}

print(capitais["Brasil"])
capitais["Peru"] = "Lima"

Zig:

const std = @import("std");

pub fn main() !void {
    var mapa = std.StringHashMap([]const u8).init(std.heap.page_allocator);
    defer mapa.deinit();

    try mapa.put("Brasil", "Brasília");
    try mapa.put("Argentina", "Buenos Aires");
    try mapa.put("Chile", "Santiago");

    if (mapa.get("Brasil")) |capital| {
        std.debug.print("Capital: {s}\n", .{capital});
    }

    try mapa.put("Peru", "Lima");
}

Note como em Zig precisamos alocar memória explicitamente e usar defer para liberar. Cada operação put pode falhar (retornando um erro), então usamos try.

Valores Opcionais: None vs Optional

Python:

def buscar_usuario(id):
    if id == 42:
        return {"nome": "Ana"}
    return None

usuario = buscar_usuario(1)
if usuario is not None:
    print(usuario["nome"])

Zig:

const std = @import("std");

const Usuario = struct {
    nome: []const u8,
};

fn buscarUsuario(id: u32) ?Usuario {
    if (id == 42) {
        return Usuario{ .nome = "Ana" };
    }
    return null;
}

pub fn main() void {
    if (buscarUsuario(42)) |usuario| {
        std.debug.print("Nome: {s}\n", .{usuario.nome});
    } else {
        std.debug.print("Usuário não encontrado\n", .{});
    }
}

O tipo ?Usuario é um optional: pode conter um Usuario ou null. A sintaxe if (valor) |captura| faz o “unwrap” seguro do optional, similar ao pattern matching.

Quando Usar Zig Junto com Python

Você não precisa abandonar Python para usar Zig. Na verdade, as duas linguagens se complementam muito bem:

  • Módulos nativos: Escreva funções críticas em Zig e chame-as do Python usando ctypes ou criando extensões C.
  • Ferramentas de CLI: Use Zig para criar ferramentas de linha de comando ultra-rápidas que seu código Python invoca.
  • Processamento de dados: Para pipelines de dados onde performance é crítica, Zig pode processar arquivos binários muito mais rápido.
  • WebAssembly: Compile Zig para WASM e use no backend ou frontend do seu projeto Python.

Exemplo simples de integração via biblioteca compartilhada:

// lib.zig - compila para .so/.dll
export fn fibonacci(n: u32) u32 {
    if (n <= 1) return n;
    var a: u32 = 0;
    var b: u32 = 1;
    var i: u32 = 2;
    while (i <= n) : (i += 1) {
        const temp = a + b;
        a = b;
        b = temp;
    }
    return b;
}
# main.py
import ctypes

lib = ctypes.CDLL("./libfib.so")
lib.fibonacci.restype = ctypes.c_uint32
lib.fibonacci.argtypes = [ctypes.c_uint32]

resultado = lib.fibonacci(40)
print(f"Fibonacci(40) = {resultado}")

Para compilar a biblioteca compartilhada em Zig, usamos o sistema de build do Zig:

// build.zig
const std = @import("std");

pub fn build(b: *std.Build) void {
    const lib = b.addSharedLibrary(.{
        .name = "fib",
        .root_source_file = b.path("lib.zig"),
        .target = b.standardTargetOptions(.{}),
        .optimize = b.standardOptimizeOption(.{}),
    });
    b.installArtifact(lib);
}

Tabela de Referência Rápida

PythonZigNotas
liststd.ArrayListArrayList precisa de allocator
dictstd.HashMapHashMap precisa de allocator
NonenullTipo optional ?T
try/exceptcatch/tryError unions são tipos
classstructSem herança
with (context manager)deferExecuta ao sair do escopo
range()while com contadorOu for com slice
lambdaNão existeUse funções normais
*args, **kwargsComptime genericsAbordagem diferente
import modulo@import("modulo")Sistema de módulos baseado em arquivos

Dicas Práticas para a Transição

  1. Comece pelo compilador: O compilador Zig tem mensagens de erro excelentes. Leia cada mensagem com atenção.
  2. Pense em memória: Em Python, você nunca pensa em alocação. Em Zig, toda alocação é explícita. Use defer para não esquecer de liberar.
  3. Abraçe a tipagem: Tipos explícitos podem parecer verbosos, mas pegam bugs em tempo de compilação que em Python só aparecem em produção.
  4. Use comptime: O recurso de computação em tempo de compilação de Zig substitui muitas metaprogramações que você faria com decorators ou metaclasses em Python.
  5. Pratique com projetos pequenos: Comece reescrevendo scripts Python simples em Zig para ganhar familiaridade.

Conclusão

A transição de Python para Zig não é sobre trocar uma linguagem pela outra, mas sobre expandir seu repertório. Python continua excelente para prototipagem e produtividade, enquanto Zig oferece controle e performance quando você precisa chegar mais perto do hardware. Dominar ambas as linguagens fará de você um desenvolvedor mais completo e versátil.

Leia Também

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.