- Aprender
- Computação e TI
- Ciência da computação
- Introdução à ciência da computação
- Paradigmas de programação
Linguagens e paradigmas de programação
BásicoAula 5 de 7 do curso Curso de Introdução à ciência da computação
O que você vai aprender
- Acompanhe um programa desde o código escrito por uma pessoa até as instruções binárias executadas por um processador e compare as formas de fazer essa tradução: antecipadamente, durante a execução do programa ou combinando as duas.
- Comparar código de máquina, linguagem de montagem e linguagens de alto nívelDescreva os três níveis em que programas podem ser escritos, mostre como uma linha de código de alto nível corresponde a várias instruções de máquina e explique por que a maior parte do software é escrita no nível mais alto.
- Explicar o que um compilador fazDescreva como um compilador traduz um programa inteiro antecipadamente, nomeie suas principais etapas e explique o que acontece com os erros e com o executável concluído.
- Explicar interpretadores, bytecode e compilação em tempo de execuçãoDescreva como um interpretador executa um programa, como linguagens como Java e Python combinam compilação e interpretação e como avaliar os compromissos entre essas abordagens.
- Comparar código de máquina, linguagem de montagem e linguagens de alto nível
- Separe as regras de como o código deve ser escrito das regras sobre o que ele faz, reconheça as estruturas básicas compartilhadas pela maioria das linguagens de alto nível e acompanhe programas curtos para prever seus resultados.
- Distinguir sintaxe de semânticaExplique a diferença entre a forma de um programa e seu significado, diferencie erros de sintaxe, em tempo de execução e de lógica e identifique quando linguagens atribuem significados diferentes a códigos de mesma aparência.
- Rastrear a execução das estruturas básicas comuns aos programas de alto nívelReconheça as estruturas encontradas em quase todas as linguagens de alto nível (variáveis, expressões, fluxo de controle e funções com seus escopos) e acompanhe um programa curto passo a passo para prever sua saída.
- Distinguir sintaxe de semântica
- Descreva como as linguagens classificam e verificam valores por tipo, onde um programa em execução mantém seus dados e como a memória é reservada e liberada, por quem programa ou automaticamente, com os compromissos de segurança e velocidade de cada escolha.
- Comparar como as linguagens usam e verificam tipos de dadosExplique o que é um tipo de dado, compare a tipagem estática e dinâmica e as regras de conversão estritas e flexíveis, e preveja como linguagens comuns tratam a mistura de tipos e os limites numéricos.
- Descrever onde um programa em execução mantém seus dadosExplique variáveis como posições de memória nomeadas, as funções da pilha de chamadas e da área de alocação dinâmica e a diferença entre copiar um valor e compartilhar uma referência.
- Comparar o gerenciamento manual e automático de memóriaExplique como a memória é liberada em linguagens nas quais a pessoa que programa a gerencia e em linguagens com coleta de lixo ou regras de propriedade de valores, e relacione erros de memória a falhas de programas e vulnerabilidades de segurança.
- Comparar como as linguagens usam e verificam tipos de dados
- Descreva programas como sequências de comandos que alteram o estado armazenado e depois como conjuntos de objetos que reúnem dados e comportamento, e use classes, encapsulamento, herança e polimorfismo para organizar o código.
- Descrever os estilos imperativo e proceduralExplique a programação imperativa como comandos passo a passo que alteram o estado de um programa, mostre como procedimentos e fluxo de controle estruturado organizam esses comandos e reconheça os riscos de um estado compartilhado que muda.
- Modelar dados e comportamentos com classes e objetosExplique como a programação orientada a objetos agrupa dados e as operações sobre eles em objetos, leia e projete uma classe simples e use o encapsulamento para proteger os dados de um objeto.
- Reutilizar e estender classes com herança, polimorfismo e composiçãoUse herança para criar classes especializadas, explique como o polimorfismo permite que uma mesma chamada se comporte de maneira diferente para objetos diferentes e decida quando combinar objetos em vez de usar herança.
- Descrever os estilos imperativo e procedural
- Crie programas com funções que retornam novos valores em vez de alterar os antigos e escreva programas que declaram o resultado desejado enquanto um mecanismo da linguagem decide como calculá-lo.
- Criar programas com funções puras e dados imutáveisExplique funções puras, imutabilidade, funções de primeira classe e de ordem superior e recursão, e reescreva um laço como uma sequência de operações
map,filterereduce. - Descrever o resultado com linguagens declarativasExplique a programação declarativa como a descrição do resultado desejado, em vez de como obtê-lo, e leia exemplos em uma linguagem de consulta a bancos de dados, uma linguagem lógica e outras notações declarativas.
- Criar programas com funções puras e dados imutáveis
- Resolva o mesmo problema em estilos diferentes, reconheça que a maioria das linguagens modernas combina paradigmas e avalie os fatores técnicos e práticos que determinam qual linguagem é adequada a um projeto.
- Resolver um problema usando vários paradigmasEscreva a mesma pequena tarefa nos estilos imperativo, orientado a objetos, funcional e declarativo, compare o que cada versão facilita e reconheça linguagens multiparadigma.
- Escolher uma linguagem de programação para uma tarefaAvalie os fatores que determinam qual linguagem é adequada a um projeto (plataforma, domínio, desempenho, segurança, ecossistema e pessoas) e justifique uma escolha para um cenário concreto.
- Resolver um problema usando vários paradigmas
Guia de estudo de Paradigmas de programação
1Do código-fonte às instruções em execução
Um processador executa pequenas instruções binárias, enquanto as pessoas geralmente escrevem programas em linguagens construídas em torno do problema que querem resolver. Tradutores conectam esses dois níveis. Acompanhe o mesmo cálculo por essas camadas e depois compare a construção de um programa antes de sua execução com sua interpretação ou com a compilação de partes durante a execução. O caminho afeta a portabilidade, o tempo de inicialização, a velocidade e o momento em que um erro pode aparecer.
1.1 Três níveis de código de programa
Uma linha como x = a + b indica um cálculo sem mencionar um registrador do processador. A abstração torna isso possível: uma linguagem oculta detalhes de níveis inferiores para que quem programa possa se concentrar no trabalho que o programa deve fazer.
O que o processador executa
O código de máquina consiste em instruções codificadas como números binários. A arquitetura do conjunto de instruções (ISA) de um processador define suas instruções e suas codificações. x86-64 é comum em PCs e servidores; ARM é comum em celulares, tablets e muitos laptops. Código construído para uma ISA precisa de tradução ou emulação para funcionar em outra.
O sistema hexadecimal torna mais curta a exibição dos bytes de instruções. Em x86-64, B8 01 00 00 00 coloca 1 no registrador EAX. Os bytes 01 D8 somam o valor em EBX ao EAX. Registradores são pequenas posições de armazenamento dentro do processador. Cada instrução realiza uma pequena ação, como mover dados, somar, comparar ou saltar. O ciclo de busca, decodificação e execução executa essas instruções.
Nomes para as mesmas pequenas ações
A linguagem de montagem substitui os bytes das instruções por nomes legíveis chamados mnemônicos. Aquelas duas instruções x86-64 podem ser escritas:
mov eax, 1
add eax, ebx
Um montador converte a linguagem de montagem em código de máquina. Uma linha de instrução comum normalmente produz uma instrução de máquina. Os nomes dos registradores e as instruções ainda pertencem a uma ISA específica, então usar palavras em vez de bytes não torna a linguagem de montagem portável.
Descreva um problema em menos linhas
Uma linguagem de alto nível oferece variáveis, expressões, condições, laços e funções. Python, C, Java e JavaScript permitem descrever operações sem selecionar registradores. Uma instrução normalmente exige várias instruções de máquina. Uma tradução simplificada de x = a + b faria o seguinte:
- Carregar o valor de
aem um registrador. - Carregar o valor de
bem outro registrador. - Somar os valores dos registradores.
- Armazenar o resultado para
x.
Um compilador real pode combinar ou reorganizar esses passos mantendo o comportamento exigido.
| Nível | O que a pessoa escreve | O que cuida dos detalhes do hardware? |
|---|---|---|
| Código de máquina | Instruções codificadas do processador | A pessoa que programa ou o gerador de código |
| Linguagem de montagem | Mnemônicos e registradores | A pessoa que programa, com um montador codificando as instruções |
| Código de alto nível | Variáveis e operações sobre os dados do problema | Um compilador, um interpretador ou uma combinação dos dois |
O código-fonte de alto nível é mais fácil de ler, alterar e verificar. Um código-fonte C portável pode ser compilado uma vez para um sistema x86-64 e novamente para um sistema ARM. Cada construção ainda precisa do suporte e das bibliotecas corretos do sistema operacional. A portabilidade do código-fonte não torna um executável nativo universal.
Por que os níveis mais baixos ainda importam
A linguagem de montagem continua útil em código de inicialização, algumas rotinas de sistemas operacionais e drivers, pequenos sistemas embarcados e análises de segurança de programas cujo código-fonte não está disponível. Para código de aplicativos comuns, compiladores modernos geralmente produzem código de máquina com desempenho comparável ao da linguagem de montagem escrita à mão.
A mudança para níveis mais altos ocorreu gradualmente. A linguagem de montagem se difundiu no fim da década de 1940 e no início da década de 1950. O sistema A-0 de Grace Hopper, em 1952, ajudou a avançar a tradução automática. A IBM lançou o FORTRAN em 1957; o COBOL veio em 1959, e o Lisp surgiu por volta de 1958–1960. “Alto nível” indica a distância dos detalhes do hardware, não a dificuldade de aprender a linguagem.
Principais conclusões
- Um processador executa instruções de máquina definidas por sua ISA.
- Um montador codifica instruções de montagem; a linguagem de montagem continua vinculada a uma ISA.
- Instruções de alto nível ocultam passos do hardware e geralmente se tornam várias instruções.
- Código-fonte portável pode ser traduzido para diferentes alvos; o código de máquina nativo é específico do alvo.
- Código de baixo nível continua útil quando o acesso ao hardware ou sua análise o exige.
1.2 Como um compilador constrói um programa
Um compilador traduz código-fonte em código-alvo. O alvo pode ser código de máquina nativo ou uma forma intermediária, como bytecode. A compilação antecipada (AOT) acontece antes da execução do programa; a compilação em tempo de execução acontece durante a execução.
Do texto às instruções
Em uma construção antecipada, várias etapas transformam um arquivo-fonte em instruções:
- A análise léxica divide o texto em unidades léxicas: partes com significado, como nomes, operadores e números.
- A análise sintática verifica a gramática e constrói uma estrutura que mostra como as partes se encaixam.
- A análise semântica verifica regras de significado, incluindo declarações e combinações de tipos permitidas.
- A otimização encontra uma maneira mais eficiente de realizar o trabalho exigido.
- A geração de código produz as instruções-alvo.
Considere a instrução C total = price * 3;. Suas seis unidades léxicas são total, =, price, *, 3 e ;. Os nomes são identificadores, 3 é um literal inteiro e o ponto e vírgula encerra a instrução. A análise sintática registra que a multiplicação faz parte da atribuição: calcule price * 3 e depois armazene seu valor em total.
Essas etapas têm funções diferentes. Uma sequência de unidades léxicas corretamente escritas ainda pode violar a gramática. Uma instrução gramaticalmente correta ainda pode usar um tipo de maneira incorreta. Encontrar as unidades léxicas não basta para estabelecer que um programa é válido.
Faça o mesmo trabalho com menos recursos
Suponha que o código-fonte contenha seconds_per_day = 60 * 60 * 24. Os fatores são constantes, então o compilador pode multiplicar 60 por 60 para obter 3600 e depois multiplicar por 24 para obter 86400 durante a construção. Ele pode colocar o valor final na saída em vez de repetir a multiplicação em tempo de execução. Isso é dobramento de constantes.
Uma otimização deve preservar o comportamento exigido pela linguagem. Ela não pode substituir um cálculo por outro mais rápido que produza resultados diferentes dos exigidos. O objetivo é reduzir trabalho, tempo ou memória, mantendo o significado do programa.
Reúna as partes
Programas grandes geralmente são construídos a partir de muitos arquivos. Um compilador pode produzir um arquivo objeto para cada arquivo-fonte. Um arquivo objeto contém código traduzido que ainda pode precisar de conexões com outros códigos. Um ligador reúne as partes e resolve chamadas a rotinas de bibliotecas, como funções de impressão ou matemáticas.
| Método de ligação | Origem do código da biblioteca em tempo de execução |
|---|---|
| Ligação estática | Código incluído no executável durante a construção |
| Ligação dinâmica | Uma biblioteca compartilhada compatível carregada na inicialização ou durante a execução |
O executável resultante é construído para uma plataforma-alvo, incluindo uma ISA e convenções de sistema operacional. O usuário não precisa do compilador antecipado para executá-lo. Bibliotecas compartilhadas ou componentes de execução ainda podem ser necessários.
Entenda o que uma construção bem-sucedida demonstra
Em Java, int count = "five"; falha em uma verificação de tipo antes da execução. Texto não pode inicializar essa variável inteira. O programa deve passar pelas verificações exigidas em tempo de compilação antes que uma versão executável esteja disponível.
Outras falhas sobrevivem à compilação. A divisão por um zero informado pelo usuário depende dos dados em tempo de execução. Um programa que deveria subtrair, mas soma, pode ser executado e retornar o valor errado. A compilação verifica as regras da linguagem; os testes também devem verificar se o resultado corresponde à tarefa pretendida.
Principais conclusões
- A compilação traduz código-fonte em código-alvo, que não precisa ser código de máquina.
- As etapas do compilador verificam unidades léxicas, estrutura e significado antes de produzir instruções-alvo.
- A otimização preserva o comportamento exigido; o dobramento de constantes avalia constantes durante a construção.
- Um ligador conecta arquivos objeto e código de bibliotecas.
- Uma construção bem-sucedida ainda pode conter falhas em tempo de execução ou erros de lógica.
1.3 Interpretação, bytecode e compilação JIT
Um programa não precisa se tornar um executável nativo antes de o usuário iniciá-lo. Um interpretador executa o programa por meio de outro programa em execução. Muitas implementações combinam interpretação com compilação, então a pergunta útil é como esse sistema específico executa o código.
Execute por meio de um interpretador
Um interpretador precisa estar disponível sempre que executar o programa. Um REPL interativo repete quatro ações: ler uma entrada, avaliá-la, imprimir um resultado e repetir o ciclo para a próxima entrada. Ele oferece retorno rápido ao experimentar pequenas expressões.
Interpretar não significa necessariamente ler o código-fonte bruto uma linha de cada vez. O CPython, a implementação padrão do Python, primeiro compila um arquivo em bytecode. Uma máquina virtual de software executa então essas instruções internas. O bytecode pertence àquele ambiente de execução; ele não é o conjunto de instruções nativo do processador.
Por que alguns erros aparecem antes
Compare dois arquivos Python. Este tem sintaxe válida:
print("start")
x = 10
print(x + "5")
Ele imprime “start”, associa x a 10 e então gera TypeError. Somar esse inteiro a uma cadeia de caracteres falha apenas quando a expressão é executada. Uma operação semelhante em um ramo não percorrido pode passar despercebida até que o ramo seja executado.
Agora remova o parêntese de fechamento da última linha. O CPython não consegue compilar o arquivo porque sua sintaxe está incompleta. Ele informa um erro de sintaxe antes de qualquer instrução do arquivo ser executada, então “start” não é impresso. A verificação de sintaxe do arquivo inteiro e a verificação em tempo de execução de uma operação executada acontecem em momentos diferentes.
Compartilhe bytecode, especialize durante a execução
Java mostra um caminho híbrido comum. javac Hello.java produz Hello.class, que contém bytecode. java Hello inicia uma máquina virtual Java (JVM) compatível. Diferentes implementações de JVM permitem executar esse bytecode em diferentes famílias de processadores. O bytecode é portável entre ambientes de execução compatíveis; cada ambiente precisa funcionar em sua própria plataforma hospedeira. C# também tem como alvo uma linguagem intermediária usada pelo .NET.
Um compilador em tempo de execução (JIT) gera código de máquina enquanto o programa é executado. O ambiente de execução HotSpot de Java costuma começar com interpretação e compila métodos usados com frequência. Mecanismos JavaScript de navegadores também combinam técnicas de execução, incluindo compilação JIT. Compilar as partes mais usadas pode melhorar a velocidade sustentada, mas consome tempo e memória durante a execução.
| Caminho | Vantagem útil | Custo a considerar |
|---|---|---|
| Construção nativa antecipada | O trabalho é traduzido antes da inicialização; o usuário não precisa de um compilador | Construções compatíveis separadas para as plataformas-alvo |
| Interpretação | Retorno rápido no ciclo de edição e execução com um interpretador instalado | Trabalho repetido em tempo de execução pode tornar mais lento o código com muitos cálculos |
| Bytecode com máquina virtual e JIT | Código intermediário portável com otimização em tempo de execução | Instalação do ambiente de execução, aquecimento e uso adicional de memória |
Um laço que soma um bilhão de valores geralmente é executado muito mais rápido em C compilado do que no CPython: o CPython realiza trabalho de interpretação e verificações de tipo em torno de muitas operações. Para um script que renomeia 20 arquivos, o esforço de desenvolvimento pode importar muito mais que essa diferença. Meça a carga de trabalho antes de usar a classificação de uma linguagem para prever sua velocidade.
Compilado e interpretado descrevem escolhas de implementação. O CPython usa interpretação de bytecode; o PyPy usa técnicas JIT. Uma linguagem pode ter mais de uma implementação, e uma implementação pode usar vários métodos de execução.
Principais conclusões
- Um interpretador executa um programa e precisa estar presente para executá-lo.
- O CPython compila um arquivo em bytecode antes de executá-lo.
- Um erro de sintaxe pode impedir que um arquivo comece a ser executado; um erro em tempo de execução aparece quando sua operação é executada.
- Um compilador JIT cria código nativo durante a execução, muitas vezes para partes usadas com frequência.
- Os métodos de execução pertencem às implementações, e o desempenho depende da carga de trabalho.
2Leia o que programas curtos fazem
Ler código envolve duas perguntas: se sua forma segue as regras da linguagem e o que suas operações significam. Uma expressão de mesma aparência pode se comportar de maneira diferente em duas linguagens. Aprenda a classificar erros e depois rastreie atribuições, decisões, laços e chamadas de funções na ordem de execução. Uma pequena tabela de valores que mudam torna visível a diferença entre o que a pessoa pretendia e o que o programa realmente faz.
2.1 Sintaxe, significado e três tipos de erro
A sintaxe estabelece as regras para um texto de programa válido. A semântica dá significado a esse texto. Um programa pode seguir todas as regras gramaticais e ainda calcular o resultado errado, então verificar sua aparência é apenas a primeira parte de sua leitura.
Reconheça as partes e a gramática
As unidades léxicas incluem palavras-chave como if, identificadores como total, literais como 3 ou "hello", operadores como + e == e sinais de pontuação. Uma gramática descreve como essas partes podem formar expressões e instruções.
Python exige dois-pontos após if x > 3: e usa a indentação para marcar as instruções controladas por essa condição. C, Java e JavaScript usam chaves para blocos; C e Java também exigem ponto e vírgula após muitas instruções. Nessas linguagens, a indentação ajuda a pessoa que lê, mas não seleciona o bloco.
Sintaxes diferentes podem expressar uma ação semelhante. Estas instruções imprimem uma saudação:
print("hello")
printf("hello\n");
System.out.println("hello");
A sequência de escape \n em C acrescenta uma quebra de linha. Conhecer a ideia de impressão ajuda a aprender a forma de expressá-la em cada linguagem.
Os mesmos símbolos podem significar coisas diferentes
| Expressão e linguagem | Resultado | Regra |
|---|---|---|
7 / 2 em Python ou JavaScript | 3.5 | A divisão produz um resultado fracionário |
7 / 2 com operandos inteiros em C ou Java | 3 | A divisão inteira trunca em direção a zero |
-7 // 2 em Python | -4 | A divisão por piso arredonda para baixo |
-7 / 2 com inteiros em C | -3 | O truncamento ocorre em direção a zero |
"3" * 4 em Python | "3333" | Uma cadeia de caracteres é repetida |
"3" * 4 em JavaScript | 12 | O texto é convertido em número para a multiplicação |
A aparência familiar de um símbolo não determina seu significado em todas as linguagens.
A ordem de avaliação também importa. A precedência faz a multiplicação e a divisão acontecerem antes da adição. Com a = 4 e b = 6, average = a + b / 2 divide 6 por 2 primeiro e depois soma 4, produzindo 7,0. Os parênteses em average = (a + b) / 2 fazem a soma acontecer primeiro e resultam em 5,0.
Diferencie os erros
Um erro de sintaxe desrespeita a gramática, como a falta de dois-pontos em Python. Um erro em tempo de execução ocorre durante uma operação executada, como uma divisão por zero em Python. Um erro de lógica permite a execução, mas fornece uma resposta não pretendida, como no cálculo incorreto da média.
Algumas expressões válidas pulam uma operação deliberadamente. Em Python, x != 0 and 10 / x > 2 usa avaliação de curto-circuito. Quando x é zero, a primeira condição é falsa, então o lado direito não é avaliado e nenhuma divisão ocorre. Inverter essas condições eliminaria essa proteção. Python escreve and, or e not; C, Java e JavaScript usam &&, || e !.
Atribuição e comparação são outra distinção importante. = armazena um valor; == testa a igualdade. C permite if (x = 5): ele atribui 5 e trata esse valor diferente de zero como verdadeiro. Python rejeita if x = 5: como sintaxe inválida. O mesmo erro pode, portanto, ser detectado em etapas diferentes em linguagens diferentes.
Principais conclusões
- A sintaxe define a forma válida; a semântica define o significado.
- Operadores podem ter significados diferentes em linguagens diferentes.
- A precedência e os parênteses determinam quais operações são agrupadas.
- Erros de sintaxe, em tempo de execução e de lógica exigem tipos diferentes de verificação.
- A avaliação de curto-circuito pode pular uma operação insegura.
- A atribuição armazena um valor; a comparação de igualdade testa valores.
2.2 Rastreie valores no fluxo de controle e nas chamadas
Um programa curto é mais fácil de entender quando você acompanha uma instrução executada de cada vez. Mantenha os valores atuais visíveis em vez de saltar da primeira linha para um resultado presumido.
Expressões e atribuições
Uma expressão produz um valor: price * 3, x > 10 e len(name) são exemplos. Uma instrução realiza uma ação, como atribuir um valor ou controlar um bloco. Em total = price * 3, a expressão à direita é avaliada antes de seu resultado ser atribuído a total.
Uma variável é um nome que se refere a um valor. A atribuição dá ao nome seu valor atual; ela não é uma afirmação matemática de que os dois lados sempre foram iguais. Se count contém 4, count = count + 1 primeiro lê 4, soma 1 e associa count a 5.
Acompanhe a próxima instrução executada
Três estruturas organizam o fluxo de controle:
- A sequência executa instruções em ordem.
- A seleção usa uma condição para escolher um caminho, como em
ifeelse. - A iteração repete um bloco com um laço
forouwhile.
O teorema do programa estruturado, associado a Böhm e Jacopini em 1966, mostra que essas estruturas podem expressar qualquer algoritmo. Um laço for geralmente percorre os elementos de uma coleção. Um laço while se repete enquanto sua condição permanece verdadeira.
Considere este laço em Python:
total = 0
for n in [3, 8, 5]:
total = total + n
A atribuição indentada é executada uma vez para cada elemento. Uma tabela de rastreamento registra o estado após cada atualização:
| Momento | n atual | total |
|---|---|---|
| Antes do laço | Ainda sem elemento | 0 |
| Primeira atualização | 3 | 3 |
| Segunda atualização | 8 | 11 |
| Terceira atualização | 5 | 16 |
Cada novo total usa o anterior. Reiniciar total dentro do laço produziria um cálculo diferente, pois as somas anteriores seriam perdidas.
Entre em uma função e saia dela
Uma função agrupa uma tarefa sob um nome. Um parâmetro é um nome na definição; um argumento é um valor fornecido por uma chamada:
def area(width, height):
return width * height
result = area(3, 4)
A chamada associa width a 3 e height a 4. A multiplicação produz 12, e return o envia a quem chamou a função. A execução continua na atribuição, então result recebe 12. Os nomes descrevem uma tarefa que pode ser reutilizada com outros argumentos.
Um nome tem um escopo
O escopo determina onde um nome pode ser usado. Nomes criados dentro de uma função normalmente são locais àquela chamada. Um nome no nível superior é global em seu módulo. Chamadas separadas têm associações locais separadas, mesmo quando usam nomes idênticos.
count = 7
def reset():
count = 0
reset()
print(count)
Isso imprime 7. A atribuição dentro de reset cria um count local; ela não substitui a associação global. Uma função pode, em vez disso, retornar um resultado e deixar que quem a chamou atribua esse resultado.
Escopo e tempo de vida respondem a perguntas diferentes. Um nome local não fica disponível para código externo apenas porque o objeto ao qual se referia ainda existe. Objetos retornados podem permanecer ativos, e uma função aninhada retornada pode manter acesso a variáveis da chamada que a contém; esse ambiente preservado faz parte de um fechamento.
Principais conclusões
- Avalie a expressão à direita antes de atualizar o destino de uma atribuição.
- Rastreie sequência, seleção e iteração na ordem em que são realmente executadas.
- Parâmetros são nomes na definição; argumentos são valores fornecidos.
- Um valor de retorno passa um resultado de volta a quem fez a chamada.
- Nomes locais e globais podem ter associações separadas; escopo difere de tempo de vida.
3Tipos, referências e memória
Valores precisam de um tipo, de um lugar na memória e de um tempo de vida. Essas escolhas explicam por que um texto que parece numérico pode rejeitar operações aritméticas, por que dois nomes podem mostrar a mesma lista alterada e por que dados não usados às vezes continuam alocados. Compare as verificações feitas pelas linguagens, acompanhe uma chamada de função na memória e examine três formas de liberar armazenamento. Cada abordagem altera o equilíbrio entre controle, conveniência e segurança.
3.1 Tipos determinam quais operações fazem sentido
Um tipo de dado especifica os valores possíveis e as operações permitidas sobre eles. Um inteiro permite operações aritméticas com números inteiros; um valor booleano representa verdadeiro ou falso. Cadeias de caracteres, listas, registros e objetos combinam ou organizam outros valores. Os tipos também orientam a representação dos valores na memória, embora o tamanho de um valor possa depender do próprio valor e da implementação.
Quando os tipos são verificados?
| Abordagem | Quando ocorrem as verificações | Exemplo |
|---|---|---|
| Tipagem estática | Antes da execução | Java rejeita int count = "five"; |
| Tipagem dinâmica | Durante a execução | Python verifica se uma operação executada é adequada a seus operandos |
Em Python, count = 5 seguido de count = "five" é válido. O nome agora se refere a uma cadeia de caracteres. Avaliar count + 1 em seguida gera TypeError. A tipagem dinâmica dá tipos definidos aos valores; ela não elimina os tipos.
A tipagem estática não exige uma indicação de tipo visível em cada variável. A inferência de tipos permite que um compilador deduza um tipo a partir do código ao redor. Kotlin infere um inteiro para val n = 5, e Rust pode inferir um inteiro para let n = 5;. As anotações de tipo opcionais do Python são diferentes: ferramentas separadas, como o mypy, podem verificá-las, mas a execução comum do Python não as impõe.
Quando os valores são convertidos?
O momento da verificação e o rigor da conversão são questões separadas. Python rejeita "3" + 4; a conversão explícita em int("3") + 4 resulta em 7. JavaScript aplica regras de conversão automática diferentes para operadores diferentes:
"3" + 4resulta na cadeia de caracteres"34"."3" * 4resulta no número12."3" - 4resulta no número-1.
A conversão automática é chamada de coerção de tipos. Os rótulos tipagem “forte” e “fraca” são formas informais de discutir o rigor da conversão, não outros nomes para tipagem estática e dinâmica. Python é dinâmica e estrita quanto à adição de tipos mistos acima; C é estática e ainda permite muitas conversões numéricas implícitas.
O input() do Python retorna uma cadeia de caracteres. Se uma pessoa digita “30”, o resultado ainda tem o tipo str. age + 1 falha, enquanto int(age) + 1 resulta em 31 quando age contém esse texto. O que aparece na tela não basta para determinar o tipo.
Números têm limites de representação
Um inteiro com sinal de 32 bits representa valores de −2.147.483.648 até 2.147.483.647. O int de Java retorna ciclicamente do máximo ao mínimo quando 1 é somado. O estouro de inteiro com sinal em C é um comportamento indefinido, então não se deve presumir o mesmo retorno cíclico em C. Inteiros em Python crescem conforme a memória permite: 2 ** 100 é exatamente 1.267.650.600.228.229.401.496.703.205.376.
Números de ponto flutuante representam a maioria das frações decimais de forma aproximada em binário. Em Python e JavaScript, 0.1 + 0.2 produz 0.30000000000000004. Comparar esse resultado diretamente com 0.3 resulta em falso.
Quando um cálculo permite um pequeno erro, compare os valores usando uma tolerância escolhida para a tarefa, por exemplo abs(a - b) < 1e-9. Uma tolerância absoluta tão pequena é adequada apenas a algumas escalas. Para cálculos contábeis decimais exatos, use quantidades inteiras da menor unidade ou um tipo decimal adequado. Alterar a comparação não pode tornar exata uma representação aproximada.
Principais conclusões
- Um tipo define valores e operações permitidas.
- Tipagem estática e dinâmica descrevem o momento da verificação, enquanto coerção diz respeito às regras de conversão.
- A inferência de tipos apoia a tipagem estática; anotações de tipo em Python precisam de ferramentas de verificação separadas.
- Inteiros de largura fixa podem sofrer estouro, com comportamento específico de cada linguagem.
- O arredondamento em ponto flutuante afeta testes de igualdade e a escolha da representação numérica.
3.2 Acompanhe nomes, objetos e chamadas de funções na memória
A memória principal armazena os dados de um programa em execução. Cada byte tem um endereço. As linguagens oferecem nomes para que quem programa possa trabalhar com valores sem escrever endereços brutos para cada operação.
Chamadas usam registros de ativação; objetos têm tempos de vida
Em C, um int geralmente ocupa 4 bytes, e &x obtém o endereço de x. Linguagens de nível mais alto frequentemente ocultam endereços por trás de referências. A distinção importante é entre uma associação que nomeia um objeto e o próprio objeto.
A pilha de chamadas acompanha as chamadas de funções ativas. Cada chamada tem um registro de ativação com informações como parâmetros, associações locais e o ponto em que a execução deve continuar após o retorno. Se main chama area(3, 4), um registro de area é adicionado acima do registro de quem chamou a função. A função calcula 12, retorna e seu registro é removido. Em chamadas aninhadas comuns, a chamada mais recente retorna primeiro.
A recursão acrescenta uma chamada para cada nível ainda não concluído. Se as chamadas continuam se acumulando, o espaço disponível na pilha pode se esgotar. Python normalmente gera RecursionError após aproximadamente 1.000 chamadas aninhadas, com um limite que depende de suas configurações. Uma condição de parada precisa ser alcançada para que um cálculo recursivo finito termine.
A área de alocação dinâmica contém objetos alocados dinamicamente, como uma lista que cresce ou um objeto que dura mais que a função que o criou. A alocação reserva armazenamento. Uma lista retornada pode permanecer nessa área depois que a chamada que a criou terminou.
Copiar uma referência compartilha um objeto
Uma referência identifica um objeto. Um ponteiro em C ou C++ representa explicitamente um endereço usado para acessar dados. Copiar uma referência copia o acesso ao objeto, não todos os dados do objeto.
a = [1, 2]
b = a
b.append(3)
Após a primeira linha, há uma lista. A segunda linha associa outro nome a ela. A última linha altera essa lista, então tanto a quanto b mostram [1, 2, 3]. Esse compartilhamento é o compartilhamento por referências. Usar b = a.copy() criaria uma lista externa separada; para essa lista de inteiros, acrescentar um elemento à nova lista deixaria a inalterada.
Compare atribuição com mutação:
x = 5
y = x
y = y + 1
O inteiro 5 é imutável. A adição produz 6, e apenas y é associado a outro valor. x ainda se refere a 5. Números, cadeias de caracteres e tuplas em Python são imutáveis; listas e dicionários são mutáveis. O conteúdo fixo de uma tupla pode incluir uma referência a uma lista mutável, então a imutabilidade do contêiner externo não torna imutável todo objeto aninhado.
Aplique as mesmas regras aos argumentos
Quando Python passa uma lista a uma função, o parâmetro se refere ao mesmo objeto. Chamar items.append(4) dentro da função altera essa lista. Atribuir items = [] associa apenas o nome local do parâmetro a uma nova lista. O nome de quem chamou a função continua associado ao objeto original.
C passa os valores dos argumentos por valor. Para permitir que uma função atualize a variável de quem a chamou por meio de seu endereço, passe um ponteiro para ela. Em cada linguagem, faça duas perguntas separadas durante o rastreamento: “A qual objeto este nome se refere?” e “Esta operação altera esse objeto ou associa um nome a outra coisa?”.
Principais conclusões
- A pilha de chamadas acompanha chamadas não concluídas; a área de alocação dinâmica contém objetos alocados dinamicamente.
- Um objeto pode durar mais que a função que o criou.
- Copiar uma referência compartilha um objeto; não duplica seus dados.
- A mutação altera um objeto, enquanto uma nova associação altera aquilo a que um nome se refere.
- A passagem de argumentos segue a mesma distinção entre objetos compartilhados e associações locais.
3.3 Libere memória sem perder dados ainda necessários
A memória alocada precisa, em algum momento, voltar a ficar disponível para uso. O desafio é liberar os dados após seu último uso e manter todos os objetos de que o programa ainda precisa. As linguagens atribuem essa responsabilidade a quem programa, a um sistema de execução ou a regras de propriedade verificadas pelo compilador.
Gerenciamento manual e suas falhas
Em C, malloc(n) solicita n bytes e retorna um ponteiro para o bloco se a operação for bem-sucedida. free(p) libera um bloco obtido por alocação. Quem programa deve usá-lo no momento certo e exatamente uma vez. C++ também permite o gerenciamento manual, embora suas ferramentas padrão possam automatizar a liberação por meio dos tempos de vida dos objetos.
Vários erros têm causas diferentes:
| Falha | O que deu errado |
|---|---|
| Vazamento de memória | O armazenamento alocado que não é mais necessário não é liberado |
| Uso após liberação | Um ponteiro é usado depois que o armazenamento para o qual aponta foi liberado |
| Liberação dupla | O mesmo bloco alocado é liberado duas vezes |
| Estouro de buffer | Uma escrita ultrapassa os limites de um bloco de memória |
Um ponteiro que continua se referindo a armazenamento liberado é pendente. O armazenamento pode ser reutilizado para outra coisa, então acessá-lo pode corromper dados ou causar uma falha no programa.
Para um array C int scores[10], as posições válidas vão de 0 a 9. Escrever em scores[10] ultrapassa o array, e C não oferece verificação automática de limites. Python gera IndexError quando o índice 10 é usado em uma lista de dez elementos, impedindo que esse acesso sobrescreva a memória adjacente.
Rastreie o alcance ou conte referências
Um coletor de lixo por rastreamento começa por raízes ativas, como referências ativas, segue ligações entre objetos e pode recuperar objetos inalcançáveis. Java, C#, Go e JavaScript usam abordagens por rastreamento. A coleta exige trabalho e memória em tempo de execução; dependendo do coletor, ela pode introduzir pausas.
A contagem de referências acompanha as referências a um objeto. Para uma lista Python recém-criada, a sequência simplificada é: a = [1, 2] fornece uma referência; b = a fornece uma segunda; del a remove uma; b = None remove a outra. O CPython normalmente recupera essa lista comum quando sua contagem chega a zero.
Um grupo de objetos pode manter as contagens uns dos outros acima de zero mesmo quando nenhuma parte ativa do programa consegue alcançar o grupo. Isso é um ciclo de referências. O CPython complementa a contagem de referências com a coleta de ciclos. Tanto a contagem de referências quanto o rastreamento são técnicas automáticas de gerenciamento de memória.
Verifique a propriedade antes da execução
Rust dá a cada valor um proprietário e verifica regras para o empréstimo de referências. Quando a propriedade termina, o código de limpeza gerado libera o valor. Rust seguro rejeita usos de referências que poderiam durar mais que o alvo válido, sem exigir um coletor de lixo por rastreamento. Swift usa contagem automática de referências inserida pelo compilador.
A segurança de memória importa além das falhas de programas. A Microsoft informou em 2019 que aproximadamente 70% das vulnerabilidades às quais atribuía CVEs a cada ano envolviam segurança de memória; o Chromium informou uma proporção semelhante de erros graves em 2020. Em dezembro de 2023, CISA, NSA, FBI e parceiros internacionais recomendaram que fabricantes de software planejassem transições para linguagens seguras quanto à memória.
A recuperação automática ainda mantém objetos alcançáveis. Uma lista global que cresce continuamente pode manter todos os registros antigos ativos mesmo depois que a aplicação terminou de usá-los. Um coletor não consegue decidir que esses registros alcançáveis se tornaram desnecessários para a tarefa; o programa precisa deixar de retê-los.
Principais conclusões
- A alocação manual exige a liberação no momento correto.
- Vazamentos, uso após liberação, liberação dupla e estouro de buffer têm causas distintas.
- O rastreamento encontra objetos inalcançáveis; a contagem de referências reage à perda de referências.
- O CPython acrescenta a coleta de ciclos à contagem de referências.
- A propriedade de valores pode organizar a liberação sem um coletor por rastreamento.
- A coleta automática não pode recuperar objetos que o programa ainda mantém.
4Organize estados que mudam e objetos que cooperam
Uma forma de descrever um programa é como comandos que atualizam valores. Outra agrupa esses valores com as operações que os controlam. Essas visões muitas vezes trabalham juntas: um método de objeto pode conter um laço comum. Primeiro, rastreie as mudanças de estado; depois, crie um pequeno modelo de conta para ver como classes protegem regras e como herança, polimorfismo e composição permitem que objetos relacionados compartilhem uma interface sem assumir responsabilidades inadequadas.
4.1 Comandos, procedimentos e estado compartilhado
Um paradigma de programação é uma forma de organizar código e pensar sobre computação. Na programação imperativa, um programa fornece comandos que alteram seu estado: os valores atualmente mantidos por variáveis e objetos.
Torne visíveis as mudanças de estado
Um total acumulado é um cálculo imperativo simples:
total = 0
for n in [3, 8, 5]:
total = total + n
O estado inicial é total = 0. Somar 3 o altera para 3. Somar 8 o altera para 11. Somar 5 o altera para 16. Cada atribuição usa o estado deixado pelo passo anterior, então a ordem faz parte do método.
Esse estilo corresponde às alterações passo a passo que o processador faz em registradores e memória. Ele também se adapta a algoritmos que atualizam uma coleção deliberadamente no próprio local, como uma rotina de ordenação que troca elementos.
Dê nomes às tarefas
A programação procedural organiza código imperativo em funções ou procedimentos. Um programa de relatórios pode chamar read_scores(), average(scores) e print_report(avg). Os nomes permitem que a pessoa veja a tarefa geral sem acompanhar todas as linhas internas de uma vez. Cada procedimento pode ser entendido e testado separadamente.
C, Pascal, Fortran e COBOL são linguagens procedurais clássicas. Python também oferece suporte a esse estilo: um script pode usar funções nomeadas sem definir nenhuma classe.
A programação estruturada organiza o fluxo de controle com sequência, seleção, laços e chamadas de procedimentos. Ela evita saltos arbitrários que tornam difícil prever a próxima instrução executada. A carta de Edsger Dijkstra de 1968, “Go To Statement Considered Harmful”, ajudou a popularizar essa abordagem. Saídas controladas como break, continue e return continuam úteis: elas permitem sair de uma iteração de laço, de um laço ou de uma função definidos.
Efeitos podem ir além de um valor de retorno
Um efeito colateral altera algo além do resultado retornado por uma função. Imprimir, salvar um arquivo, enviar uma mensagem pela rede e alterar dados compartilhados são efeitos colaterais. Programas precisam dessas ações para interagir com o mundo.
Problemas surgem quando códigos não relacionados alteram silenciosamente o mesmo estado. Suponha que apply_discount() reduza uma variável global price em 10%, e que add_shipping() acrescente 5. Partindo de 100, aplicar o desconto primeiro resulta em 90, e depois acrescentar o frete resulta em 95. Acrescentar o frete primeiro resulta em 105, e depois aplicar o desconto resulta em 94,5. Ambos os procedimentos realizam seus próprios passos corretamente, mas o resultado combinado depende de sua ordem.
Passar valores e retornar novos resultados pode tornar essa ordem visível:
price = add_shipping(apply_discount(100))
A chamada interna é executada primeiro: desconto, depois frete. Um aninhamento diferente expressa uma regra diferente. Um fluxo de dados claro ajuda a revelar se o próprio frete deve receber o desconto.
| Organização | Pergunta principal durante a leitura |
|---|---|
| Comandos imperativos | Qual valor muda em seguida? |
| Procedimentos nomeados | Qual tarefa esta chamada realiza? |
| Atualizações globais compartilhadas | Qual ação anterior alterou este valor? |
Código imperativo ainda é comum dentro de métodos de objetos e tratadores de eventos modernos. Escolher um projeto mais amplo orientado a objetos ou funcional não impede que uma pequena parte use comandos diretos quando esses passos tornam o algoritmo claro.
Principais conclusões
- Código imperativo descreve passos que alteram o estado armazenado.
- Procedimentos dão nomes e limites a tarefas coerentes.
- O fluxo de controle estruturado torna os caminhos de execução mais fáceis de acompanhar.
- Efeitos colaterais incluem alterações em dados compartilhados e interações com o mundo externo.
- Atualizações compartilhadas podem fazer os resultados dependerem da ordem das chamadas.
4.2 Classes mantêm dados junto de suas regras
A programação orientada a objetos (OOP) modela um programa como objetos que cooperam entre si. Cada objeto combina dados com operações sobre esses dados. Uma conta pode conter seu titular e saldo, oferecendo operações de depósito e saque.
Separe o modelo de suas instâncias
Uma classe define os atributos e métodos oferecidos por seus objetos. Um objeto, ou instância, é um elemento específico dessa classe. BankAccount é o modelo; a conta de Ana com 150 e a conta de Ben com 40 são instâncias distintas. Seus saldos podem mudar independentemente.
Um atributo armazena dados associados a um objeto. Um método é uma operação chamada por meio de um objeto, como acct.deposit(50). self em um método de instância Python se refere ao objeto que recebe a chamada.
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self._balance = balance
def deposit(self, amount):
if amount <= 0:
raise ValueError("amount must be positive")
self._balance = self._balance + amount
Chamar BankAccount("Ana", 100) constrói uma nova instância. Seu método __init__ inicializa os dados da instância. Um construtor é o processo de preparação de um novo objeto; Python separa a criação dessa etapa de inicialização. Chamar acct.deposit(50) nessa conta altera seu saldo de 100 para 150.
Coloque a regra no ponto de atualização
O encapsulamento reúne dados e as operações que mantêm suas regras. Suponha que uma conta nunca deva ter saldo negativo. O método de saque verifica a alteração proposta antes de realizá-la:
def withdraw(self, amount):
if amount <= 0:
raise ValueError("amount must be positive")
if amount > self._balance:
raise ValueError("insufficient funds")
self._balance = self._balance - amount
Esse método pertence a BankAccount. Partindo de um saldo de 150, withdraw(200) gera um erro e mantém 150. withdraw(30) é bem-sucedido e deixa 120. A chamada que falha não subtrai primeiro para depois descobrir o problema; a verificação acontece antes da atualização.
Outros códigos devem usar esses métodos em vez de atribuir um saldo sem verificação. Java e C# podem restringir o acesso direto com private. O sublinhado inicial de Python em _balance indica um detalhe interno por convenção; ele não impõe privacidade.
A ocultação de informação vai além: quem chama depende das operações oferecidas por uma classe sem precisar conhecer sua representação interna. O saldo poderia ser armazenado de outra forma posteriormente, enquanto as operações públicas de depósito e saque manteriam seu significado.
Deixe o problema sugerir responsabilidades
Um sistema de biblioteca tem livros, membros e empréstimos. Esses substantivos sugerem possíveis classes ou atributos; ações como emprestar e renovar sugerem métodos. Escolha uma responsabilidade clara para cada classe. Uma classe de conta que também envia todos os e-mails e formata todos os relatórios se torna mais difícil de alterar porque trabalhos não relacionados estão misturados.
Classes e objetos surgiram em Simula 67 em 1967; Smalltalk ajudou a difundir a abordagem na década de 1970. Java, C#, C++ e Python oferecem suporte à programação orientada a objetos, mas usar uma dessas linguagens não obriga que todo programa use classes. A pergunta útil de projeto é quais dados e regras pertencem ao mesmo lugar.
Principais conclusões
- Uma classe define atributos e métodos; cada instância tem seus próprios dados de instância.
- Métodos atuam sobre o objeto que recebe a chamada.
- O encapsulamento reúne validação e mudanças de estado.
- Sublinhados em Python indicam atributos internos por convenção; algumas linguagens impõem acesso privado.
- A ocultação de informação permite que quem chama dependa das operações públicas, em vez dos detalhes internos.
- Uma classe deve ter uma responsabilidade coerente.
4.3 Especialize comportamentos e combine objetos
Objetos muitas vezes precisam compartilhar operações e, ao mesmo tempo, diferir em detalhes. Herança, polimorfismo e composição resolvem partes diferentes desse problema de projeto.
Especialize um tipo existente de objeto
A herança define uma subclasse a partir de uma superclasse. Em Python, class SavingsAccount(BankAccount): dá à classe especializada acesso ao comportamento herdado da conta. Ela pode acrescentar add_interest(rate): um saldo de 1.000 com acréscimo de juros de 2% se torna 1.020.
A relação pretendida é de tipo. Uma conta de poupança ainda é uma conta bancária e deve respeitar as regras esperadas de contas bancárias. Reutilizar algumas linhas não é razão suficiente para afirmar que um tipo de objeto é outro.
Uma subclasse pode sobrescrever um método com sua própria implementação. Suponha que uma conta corrente cobre uma tarifa de 1 por cada saque bem-sucedido. Com saldo de 100, sacar 30 custa 31 e deixa 69. Para preservar a regra da conta, o método deve verificar o saque mais a tarifa antes de alterar o saldo. Se esse custo combinado exceder o saldo, ele recusa toda a operação.
Faça uma chamada funcionar com objetos diferentes
O polimorfismo permite que a mesma operação funcione com tipos de objetos diferentes. Um círculo e um retângulo podem oferecer area():
for shape in shapes:
print(shape.area())
O laço não precisa de um if para cada classe de forma. Cada objeto fornece sua própria implementação. Selecionar essa implementação durante a execução é o despacho dinâmico.
Para Circle(2), a área é de cerca de 12,57 unidades quadradas. Para Rectangle(3, 4), é de 12 unidades quadradas. A área combinada é de cerca de 24,57 unidades quadradas. Uma classe Triangle acrescentada depois pode integrar a coleção ao oferecer uma operação area() compatível; o próprio laço não precisa de outro ramo.
Uma interface declara as operações que um tipo deve oferecer. interface Shape { double area(); } em Java exige que as classes que a implementam forneçam o método de área. Uma classe abstrata pode fornecer parte da implementação compartilhada e deixar outros métodos para as subclasses. Python costuma usar tipagem por comportamento: um objeto pode funcionar no laço se oferecer a operação exigida, mesmo sem uma superclasse compartilhada.
Mantenha uma parte em vez de se tornar ela
A composição armazena outros objetos como partes. Um carro tem um motor e rodas. Ele pode chamar o método start() de seu motor sem herdar de Engine.
| Relação | Projeto adequado | Motivo |
|---|---|---|
| Conta de poupança é uma conta | Herança | Ela preserva o comportamento esperado de uma conta |
| Carro tem um motor | Composição | O motor é um componente, não a categoria do carro |
| Formas diferentes oferecem área | Operação ou interface compartilhada | Quem chama pode solicitar a operação de modo uniforme |
Cadeias profundas de herança podem espalhar os efeitos de uma alteração em uma superclasse por muitas classes. Uma subclasse que precisa desativar um comportamento herdado pode indicar uma relação de tipo inadequada. A composição muitas vezes mantém essas dependências menores e mais claras.
Os quatro pilares da programação orientada a objetos comumente citados se encaixam aqui: o encapsulamento protege dados por meio de operações, a abstração oculta detalhes de implementação, a herança especializa um tipo e o polimorfismo permite que quem chama use uma operação compartilhada em diferentes implementações.
Principais conclusões
- A herança modela uma relação de tipo e deve preservar o comportamento esperado.
- A sobrescrita substitui a implementação de um método em uma classe especializada.
- O polimorfismo permite que uma operação invoque implementações diferentes.
- Uma interface declara as operações exigidas; a tipagem por comportamento depende das operações oferecidas por um objeto.
- A composição modela objetos que têm outros objetos como partes.
5Calcule novos valores e descreva resultados desejados
Um programa pode se concentrar em transformações de valores ou no resultado que um mecanismo deve produzir. Funções puras tornam as dependências explícitas, enquanto dados imutáveis evitam alterações por meio de referências compartilhadas. Consultas declarativas permitem que um mecanismo especializado escolha os passos de execução. Acompanhe uma sequência de transformações de dados e uma consulta a um banco de dados para ver o que essas abordagens facilitam e onde entrada, saída e instruções concretas de máquina ainda entram em cena.
5.1 Funções puras e sequências de transformações de dados
A programação funcional constrói resultados combinando funções. Seu hábito central é produzir novos valores a partir de entradas e limitar a mutação compartilhada. Lisp introduziu ideias funcionais amplamente usadas por volta de 1958; Haskell, Erlang, Clojure, F# e OCaml desenvolvem essas ideias de maneiras diferentes.
Saiba o que torna uma função pura
Uma função pura fornece o mesmo resultado para os mesmos argumentos e não tem efeitos colaterais. def with_tax(price): return price * 1.2 ilustra a ideia: with_tax(50) resulta em 60,0. Uma função que também acrescenta algo a uma lista global ou imprime uma mensagem tem um efeito além de seu valor de retorno. Um resultado que depende da hora atual também depende de mais do que os argumentos.
Uma chamada pura pode ser substituída por seu valor de retorno sem alterar o comportamento do programa. Essa propriedade é a transparência referencial. Ela torna simples os testes isolados e permite que chamadas independentes sejam executadas em paralelo sem disputar a atualização de um estado compartilhado.
A imutabilidade mantém um valor existente inalterado. [n + 1 for n in nums] cria uma nova lista, deixando nums intacta. Um programa ainda pode representar informações que mudam produzindo novas versões; ele não precisa sobrescrever a versão antiga.
Passe comportamento como dados
Funções são de primeira classe quando podem ser atribuídas a nomes, passadas como argumentos e retornadas como valores. Uma função de ordem superior recebe uma função ou retorna uma. Uma expressão lambda é uma expressão de função sem nome, como lambda n: n * n.
Três operações comuns de ordem superior formam uma sequência:
filtermantém elementos que satisfazem um teste.maptransforma cada elemento restante.reducecombina elementos em um resultado usando um acumulador.
Para somar os quadrados dos números pares de 1 até 6, a filtragem deixa 2, 4 e 6; elevar ao quadrado produz 4, 16 e 36; somar produz 56.
sum(map(lambda n: n * n,
filter(lambda n: n % 2 == 0, range(1, 7))))
Aqui % 2 == 0 testa se um número é par, e range(1, 7) fornece os números de 1 até 6. Haskell expressa o mesmo cálculo como sum (map (^2) (filter even [1..6])).
Com reduce importado de functools do Python, reduce(lambda acc, n: acc + n, [3, 8, 5], 0) combina 0 com 3 para obter 3, depois com 8 para obter 11 e depois com 5 para obter 16. A sequência descreve transformações sem um laço externo que atualize um total compartilhado.
Preserve um ambiente ou chame um problema menor
def multiplier(k): return lambda x: k * x retorna uma função que mantém acesso a k. Assim, multiplier(3)(10) retorna 30 mesmo depois que a chamada externa terminou. A função retornada e o ambiente preservado formam um fechamento.
Na recursão, uma função chama a si mesma para um problema menor. O fatorial retorna 1 em n == 0; nos demais casos, retorna n * factorial(n - 1). Para 5, os produtos resultam em 120. Um cálculo finito precisa de um caso base alcançável. As chamadas aninhadas não resolvidas do Python acabam gerando RecursionError; linguagens com suporte adequado a chamadas de cauda podem reutilizar espaço na pilha para chamadas na posição final.
Usar funções por si só não torna o código funcional: funções procedurais podem alterar o estado. Programas funcionais reais ainda leem entradas e escrevem saídas, geralmente mantendo efeitos próximos às extremidades do programa e os cálculos centrais puros.
Principais conclusões
- Funções puras dependem apenas de seus argumentos e não têm efeitos colaterais.
- Atualizações imutáveis criam novos valores.
- Funções de primeira classe podem ser passadas e retornadas; funções de ordem superior recebem ou retornam funções.
- Filter seleciona, map transforma e reduce combina.
- Um fechamento preserva seu ambiente de criação.
- Um cálculo recursivo finito precisa de um caso base que ele realmente alcance.
5.2 Declare um resultado e deixe um mecanismo encontrá-lo
A programação declarativa descreve o resultado desejado e deixa os passos de execução a cargo de um mecanismo da linguagem. O código funcional geralmente é agrupado nessa ideia ampla, junto com linguagens de consulta e programação lógica.
Leia uma consulta como uma solicitação
Um banco de dados relacional armazena dados em tabelas. Suponha que students tenha as linhas de nome e nota Ana 92, Ben 75, Chen 80 e Dara 64:
SELECT name
FROM students
WHERE grade >= 80
ORDER BY name;
FROM indica a tabela, WHERE fornece a condição, SELECT escolhe o campo retornado e ORDER BY solicita ordem alfabética. Ana e Chen satisfazem a condição, incluindo a nota de Chen, exatamente 80. O resultado são seus nomes nessa ordem.
Um programa imperativo criaria uma lista de resultados, percorreria as linhas, testaria cada nota, acrescentaria os nomes que atendem à condição e ordenaria a lista. SQL fornece ao banco de dados o resultado exigido sem escrever esse laço.
Um otimizador de consultas escolhe um plano de execução. Ele pode varrer a tabela ou usar um índice disponível, uma estrutura de busca que pode acelerar pesquisas adequadas. Muitos índices usam estruturas ordenadas; outros usam organizações diferentes. Acrescentar um índice pode alterar o plano sem alterar a consulta. Mais dados ainda significam trabalho: declarar o resultado não garante que toda consulta exija poucos recursos.
Pergunte o que decorre de fatos e regras
A programação lógica descreve fatos e relações e depois procura valores que satisfaçam uma pergunta. Em Prolog:
parent(tom, bob).
parent(bob, ann).
grandparent(X, Z) :- parent(X, Y), parent(Y, Z).
A regra significa que X é avô ou avó de Z quando há um Y para o qual ambas as relações de pai ou mãe são verdadeiras. A consulta ?- grandparent(tom, Who). encontra Who = ann: Tom é pai de Bob e Bob é pai de Ann. Nomes iniciados por maiúsculas representam variáveis; os nomes em minúsculas identificam pessoas específicas.
Reconheça ferramentas declarativas ao seu redor
Uma fórmula de planilha =SUM(B2:B5) declara uma dependência. A planilha a recalcula quando células relevantes mudam. A expressão regular [0-9]{3}-[0-9]{4} descreve três algarismos, um hífen e quatro algarismos, como em “555-0199”. Um mecanismo de correspondência de texto realiza a busca.
CSS declara a aparência com uma regra como h1 { color: navy; }; HTML marca a estrutura da página. São notações declarativas, normalmente classificadas como linguagens de estilo e de marcação. Sua função difere da de uma linguagem de propósito geral que pode expressar uma ampla variedade de computações.
| Tipo de linguagem | Foco da tarefa | Exemplos |
|---|---|---|
| Linguagem específica de domínio | Um tipo específico de trabalho | SQL, expressões regulares, CSS |
| Linguagem de propósito geral | Muitos tipos de programas | Python, Java, Haskell |
O código declarativo pode permanecer próximo da descrição de um problema e permitir que um mecanismo melhore seu método. Ele também dá a quem programa menos controle direto sobre passos individuais, o que pode tornar mais difícil diagnosticar um plano lento. Instruções concretas ainda são executadas por trás: declarativo descreve a forma como a pessoa expressa a tarefa, não um processador que funciona sem passos.
Principais conclusões
- Código declarativo declara um resultado desejado; um mecanismo escolhe os passos de execução.
- SQL seleciona linhas e campos por meio de condições e da ordenação solicitada.
- Um otimizador pode alterar um plano de consulta sem alterar a consulta.
- A programação lógica deriva respostas de fatos e regras.
- Linguagens específicas de domínio se destinam a tarefas específicas; linguagens de propósito geral oferecem suporte a muitos tipos de programas.
6Escolha um estilo e uma linguagem para a tarefa
Estilos de programação diferentes podem produzir a mesma resposta e organizar o trabalho de maneiras diferentes. Compará-los em uma tarefa torna seus compromissos mais fáceis de perceber. A escolha de uma linguagem acrescenta restrições práticas: a plataforma-alvo, as bibliotecas disponíveis, os limites de memória e velocidade, as necessidades de segurança e as pessoas que mantêm o código. Um projeto com várias partes pode usar uma linguagem diferente e uma combinação diferente de estilos para cada parte.
6.1 Uma tarefa em vários estilos de programação
Um paradigma altera a forma como uma solução é expressa, não a resposta numérica que uma solução correta deve produzir. Considere pedidos com valores [120, 45, 300, 80]: encontre o total dos valores estritamente maiores que 100.
Acompanhe as mudanças de estado
A versão imperativa expõe o laço e o acumulador:
total = 0
for amount in amounts:
if amount > 100:
total = total + amount
O primeiro valor altera total para 120. O segundo o deixa inalterado. O terceiro o altera para 420, e o quarto o mantém nesse valor. O programa torna visível a ordem dos testes e das atualizações.
Transforme uma coleção
Uma expressão Python compacta no estilo funcional é sum(a for a in amounts if a > 100). Ela seleciona 120 e 300 e os combina em 420. Outra versão é sum(filter(lambda a: a > 100, amounts)). Aqui o predicado descreve quais valores pertencem ao conjunto selecionado, e sum os combina.
Uma expressão geradora ou uma chamada filter usa recursos multiparadigma do Python; sua presença não torna puras todas as partes da aplicação ao redor.
Coloque regras junto dos objetos
Em um projeto orientado a objetos, um Order armazena um amount e oferece is_large(), que retorna self.amount > 100. Um OrderBook contém objetos de pedidos e oferece large_total(). Esse método pode percorrer os pedidos em um laço, perguntar a cada um se atende à condição e somar os valores correspondentes.
O total continua sendo 420. A diferença está em onde ficam as responsabilidades: a regra para um pedido grande pertence a Order, e o trabalho no nível da coleção pertence a OrderBook. Outras funcionalidades podem reutilizar a regra sem copiar seu teste de limite.
Consulte o banco de dados
Se os dados já estiverem em um banco de dados relacional, SQL pode solicitar o total:
SELECT SUM(amount)
FROM orders
WHERE amount > 100;
O banco de dados escolhe o plano de busca e agregação. A consulta não instrui quem a chama a percorrer todas as linhas ou manter um acumulador local.
| Estilo | O que o exemplo enfatiza |
|---|---|
| Imperativo | Testes e atualizações ordenados de um total |
| Orientado a objetos | Pedidos e objetos de coleção com responsabilidades definidas |
| Funcional | Seleção e combinação de valores |
| SQL declarativo | O resultado exigido sobre os dados armazenados |
Combine estilos onde eles ajudam
Python oferece suporte a técnicas procedurais, orientadas a objetos e funcionais. JavaScript fornece objetos, funções de primeira classe e fechamentos. C++, C#, Kotlin, Scala, Swift e Rust também combinam estilos. Java é fortemente associada à programação orientada a objetos, mas oferece suporte a expressões lambda desde Java 8, em 2014; Haskell é puramente funcional.
Uma aplicação web pode usar classes para usuários e pedidos, transformações funcionais para dados de relatórios, atualizações imperativas dentro de métodos e SQL para consultas ao banco de dados. Essas escolhas funcionam em camadas diferentes.
A programação orientada a eventos acrescenta outra ideia de organização: registrar um tratador que é executado quando um evento chega. Em JavaScript, button.addEventListener("click", showMenu) passa showMenu como um valor de função. Um clique posterior o invoca. O próprio tratador pode usar objetos, transformações puras ou passos imperativos. Sua função orientada a eventos indica quando ele é executado, não o único estilo que seu corpo pode usar.
Principais conclusões
- Soluções corretas em paradigmas diferentes podem calcular o mesmo resultado.
- Um estilo altera onde aparecem passos, estado, regras e responsabilidades.
- Linguagens modernas geralmente oferecem suporte a vários paradigmas.
- Um tratador de eventos é executado em resposta a um evento e pode usar outros estilos internamente.
6.2 Escolha considerando restrições e compromissos
Nenhuma linguagem de programação é a mais adequada para todo projeto. Linguagens de propósito geral geralmente são Turing completas: com tempo e memória ilimitados, podem expressar a mesma classe de computações. As escolhas reais dependem de a solução ser adequada à plataforma, aos recursos, às ferramentas e às pessoas envolvidas.
Comece pelo que precisa funcionar
A plataforma costuma ser uma restrição rígida. Navegadores executam JavaScript e podem executar módulos compilados por meio do WebAssembly. TypeScript acrescenta verificação estática e compila para JavaScript. Swift é uma escolha padrão para aplicativos das plataformas Apple; Kotlin é recomendada para Android. SQL é a linguagem usual para consultas relacionais.
Em seguida, considere o domínio e o ecossistema. Python tem bibliotecas de dados e aprendizado de máquina amplamente usadas, como NumPy e pandas. R se concentra em estatística. C e C++ são comuns em sistemas operacionais, motores de jogos e código embarcado. Java e C# são comuns em grandes sistemas empresariais; Bash é útil para automatizar tarefas do sistema operacional.
Essas são associações práticas, não territórios exclusivos. Uma biblioteca adequada pode economizar muito mais trabalho de desenvolvimento do que uma pequena diferença na sintaxe da linguagem.
Examine as necessidades de recursos e segurança
Um microcontrolador com apenas alguns kilobytes de RAM pode precisar de controle detalhado sobre a memória e a sobrecarga de execução. C, C++ ou Rust podem ser adequadas quando o alvo oferece suporte a elas. Componentes exigentes de jogos ou bancos de dados também costumam favorecer essas linguagens, mas motores e limites de recursos variam: Unity usa C#, e alguns bancos de dados usam Java ou Go.
Antes de escolher pela velocidade, encontre o gargalo: a parte que limita o desempenho. Um programa que espera principalmente por uma resposta da rede não ficará muito mais rápido apenas porque um pequeno laço local é compilado com mais eficiência. Meça uma carga de trabalho representativa na plataforma-alvo.
A segurança de memória reduz erros como acesso inválido à memória. Rust seguro verifica regras de propriedade em tempo de compilação; linguagens gerenciadas como Java, C#, Go e Python oferecem gerenciamento de memória em tempo de execução. A tipagem estática detecta muitas operações incompatíveis antes da execução, o que pode ajudar em uma base de código grande. Nem a segurança de memória nem a tipagem estática provam que a lógica do programa está correta.
Inclua o custo de manter o resultado
O conhecimento da equipe, os desenvolvedores disponíveis, depuradores, ferramentas de teste, bibliotecas, licenças e código existente afetam a decisão. Uma linguagem conhecida e com suporte pode reduzir os custos de entrega e manutenção. Reescrever um sistema que funciona cria trabalho de migração e riscos, mesmo que a nova linguagem tenha recursos atraentes.
Em uma rede meteorológica de uma cidade, componentes diferentes têm necessidades diferentes:
| Componente | Escolha razoável de linguagem | Motivo |
|---|---|---|
| Firmware de pequenos sensores | C ou Rust | Controle de memória e suporte aos alvos de hardware |
| Leituras armazenadas | SQL | Filtragem e agregação no banco de dados |
| Limpeza e análise | Python | Bibliotecas de processamento de dados |
| Exibição interativa no navegador | JavaScript ou TypeScript | Suporte do navegador e ferramentas de interface |
Uma justificativa útil relaciona cada escolha a um requisito: “A equipe já mantém o serviço Java, e suas bibliotecas oferecem suporte à nova funcionalidade”. Uma posição em um ranking de popularidade, sozinha, não fornece esse raciocínio. Pesquisas podem indicar uma comunidade grande ou um grupo amplo de profissionais disponíveis para contratação; elas não estabelecem a melhor linguagem para uma carga de trabalho específica.
Principais conclusões
- Verifique as restrições da plataforma antes das preferências.
- Bibliotecas e ferramentas conectam uma linguagem a seu domínio de problema.
- Meça o gargalo antes de escolher principalmente pela velocidade.
- A segurança de memória e a verificação estática reduzem algumas falhas, mas a lógica ainda precisa de testes.
- As habilidades da equipe, o código existente e os custos de manutenção fazem parte da decisão.
- Componentes diferentes podem justificar linguagens diferentes.
Exercícios resolvidos de Paradigmas de programação
1Acompanhe um programa do código-fonte à execução
13 passos
- 1IntroduçãoUm cálculo atravessa várias camadas
Você escreve um cálculo, mas um processador precisa de pequenas instruções codificadas chamadas código de máquina. Um tradutor fornece os detalhes que faltam.
Acompanhe
x = a + bdo código-fonte para os níveis inferiores e depois acompanhe o programa durante a construção e a execução. Mantenha duas perguntas separadas: em que forma está o código e qual software o executa? Um compilador, um interpretador e um compilador JIT respondem à segunda de maneira diferente. Essa distinção explicará tanto a portabilidade quanto o momento em que os erros ocorrem. - 2ExemploUm cálculo se torna várias ações
Para
x = a + b, imagine quatro ações de nível mais baixo:- Carregar o valor de
aem um registrador. - Carregar o valor de
bem outro registrador. - Somar os valores dos dois registradores.
- Armazenar o resultado para
x.
A instrução de alto nível oculta a seleção de registradores e detalhes de armazenamento. As instruções geradas podem ser diferentes porque um compilador pode otimizar o trabalho. O cálculo exigido permanece o mesmo.
- Carregar o valor de
- 3ExplicaçãoInstruções legíveis ainda são destinadas a um processador
A linguagem de montagem usa nomes de instruções como
moveadd. Um montador os codifica como instruções de máquina.Para x86-64,
mov eax, 1coloca 1 em EAX;add eax, ebxsoma EBX a ele. Esses registradores e codificações pertencem àquela arquitetura do conjunto de instruções. Substituir bytes por nomes legíveis ajuda uma pessoa, mas não transforma as instruções em uma descrição portável de alto nível. - 4DesafioTransfira o programa para outro processador
Uma equipe tem código-fonte C portável e um executável nativo x86-64. Ela precisa de um executável nativo ARM. Qual artefato pode usar como ponto de partida e que trabalho é necessário?
---reveal---
Use o código-fonte e compile-o para o alvo ARM, com o suporte necessário da plataforma. O executável x86-64 contém instruções para uma ISA diferente. A portabilidade do código-fonte permite outra tradução; ela não transforma aquelas instruções de máquina existentes em código nativo ARM.
- 5ExemploIdentifique as unidades léxicas antes da estrutura
Considere
total = price * 3;em C.A análise léxica encontra seis unidades:
total,=,price,*,3e;. A análise sintática então agrupaprice * 3como a expressão cujo resultado é atribuído atotal.São tarefas distintas. A análise léxica identifica as partes; a análise sintática estabelece como elas se encaixam. A análise semântica pode então verificar questões como se os nomes foram declarados e se os tipos dos operandos são compatíveis.
- 6ExemploTransfira cálculos constantes para a construção
A expressão no código-fonte
60 * 60 * 24usa apenas constantes. O dobramento de constantes pode avaliá-la antes da execução: primeiro 3.600, depois 86.400.A geração de código pode usar esse resultado em vez de instruções que repetem as multiplicações. Isso é uma otimização porque altera como o trabalho é realizado, preservando o resultado exigido. Substituir um valor por uma aproximação apenas conveniente não seria justificado por essa regra.
- 7ExplicaçãoConecte arquivos traduzidos a rotinas de biblioteca
A compilação pode produzir arquivos objeto separados. Um ligador conecta esses arquivos e resolve referências a operações de bibliotecas.
A ligação estática inclui código de biblioteca no executável construído. A ligação dinâmica depende de uma biblioteca compartilhada compatível carregada posteriormente. Nenhuma das abordagens exige que a máquina do usuário tenha o compilador antecipado, mas um executável ligado dinamicamente ainda precisa de suas dependências de execução.
- 8DesafioUma construção bem-sucedida dá a resposta errada
Um programa C é construído com sucesso. Seu relatório deveria subtrair uma quantidade usada de uma quantidade inicial, mas o código as soma. O compilador deixou de verificar uma regra da linguagem?
---reveal---
Não. A adição pode ter sintaxe válida e tipos compatíveis, mas expressar a tarefa errada. Isso é um erro de lógica. O compilador verifica as regras da linguagem e gera código; é necessário um teste com resultado esperado conhecido para revelar a diferença entre o cálculo e sua finalidade pretendida.
- 9ExplicaçãoUm ambiente de execução pode executar instruções intermediárias
O CPython compila um arquivo Python válido em bytecode e depois executa esse bytecode em sua máquina virtual. O processador executa as instruções de máquina do ambiente de execução enquanto o ambiente realiza as operações Python.
Java também usa bytecode:
javacproduz arquivos de classe para uma JVM compatível. Bytecode pertence a um modelo de instruções de um ambiente de execução, então não deve ser confundido com um conjunto universal de instruções de processador. - 10ExemploCompare dois momentos em que a execução pode parar
Neste arquivo Python válido, a primeira linha é executada:
print("start") x = 10 print(x + "5")Ele imprime “start” antes que a última linha gere
TypeError. Agora remova o último parêntese de fechamento: o CPython não consegue compilar o arquivo, então nenhuma instrução desse arquivo é executada. Uma falha de sintaxe durante a preparação e uma falha de tipo durante uma operação executada param o programa em etapas diferentes. - 11ExplicaçãoCompile código muito usado durante a execução
Um compilador JIT produz código de máquina enquanto o programa é executado. Um ambiente de execução pode primeiro interpretar, observar métodos usados com frequência e compilá-los.
Isso desloca os custos: a compilação usa tempo e memória durante a execução, enquanto a execução repetida pode ficar mais rápida depois. Um serviço de longa duração pode se beneficiar mais desse aquecimento do que um pequeno script executado uma única vez. O resultado depende da implementação e da carga de trabalho.
- 12DesafioSepare a portabilidade do bytecode da velocidade nativa
Um arquivo de classe Java é executado por uma JVM compatível em duas ISAs diferentes. Durante cada execução, o ambiente compila um método usado com frequência. Por que um arquivo de classe pode ser compartilhado enquanto o código de máquina gerado é diferente?
---reveal---
O arquivo de classe contém bytecode da JVM, que ambos os ambientes de execução compatíveis compreendem. Cada JIT tem como alvo seu próprio processador hospedeiro. A portabilidade vem do formato intermediário compartilhado e dos ambientes de execução compatíveis; as instruções de máquina produzidas durante a execução continuam específicas da plataforma.
- 13RecapitulaçãoMantenha separados os níveis e os caminhos de execução
O código de máquina tem uma ISA como alvo; a linguagem de montagem dá nomes legíveis às suas instruções; o código-fonte de alto nível oculta mais detalhes do hardware.
Um compilador traduz código, um interpretador o executa e um JIT compila durante a execução. Um ambiente de execução pode combinar esses métodos. Para explicar um sistema real, identifique suas formas de código-fonte, intermediária e nativa; depois, diga qual etapa verifica a sintaxe e qual executa a operação que pode falhar.
2Rastreie o significado em um programa curto
10 passos
Salve esta aula para seguir cada Lição Interativa passo a passo.
- 1IntroduçãoForma e significado respondem a perguntas diferentes
- 2ExemploCódigo válido pode agrupar o cálculo errado
- 3ExplicaçãoA primeira condição pode proteger a segunda
- 4DesafioPreveja se a divisão é executada
- 5ExplicaçãoUma atribuição lê antes de atualizar
- 6ExemploRastreie cada atualização do acumulador
- 7ExemploAcompanhe uma chamada e seu valor de retorno
- 8DesafioUma atualização local pode deixar quem chamou a função inalterado
- 9DesafioAcompanhe gramática e estado juntos
- 10RecapitulaçãoLeia o caminho executado
3Acompanhe tipos e objetos compartilhados
14 passos
Salve esta aula para seguir cada Lição Interativa passo a passo.
- 1IntroduçãoAcompanhe valores, objetos e armazenamento
- 2ExplicaçãoValores tipados podem passar de um nome a outro
- 3Ideia-chaveInferência e conversão respondem a perguntas diferentes
- 4DesafioLeia os algarismos como o valor realmente retornado
- 5ExplicaçãoA representação afeta os resultados numéricos
- 6ExplicaçãoUm registro de chamada pode terminar antes de um objeto
- 7ExemploDois nomes apontam para uma lista
- 8DesafioUm parâmetro pode alterar um objeto e depois apontar para outro
- 9ExplicaçãoIdentifique exatamente como o armazenamento é usado incorretamente
- 10ExemploAcompanhe as referências até a última desaparecer
- 11Ideia-chaveUm ciclo pode não ter ponto de acesso ativo
- 12ExplicaçãoA propriedade verifica o acesso antes da execução
- 13DesafioDescubra por que a coleta não pode recuperar uma lista
- 14RecapitulaçãoAcompanhe três decisões diferentes
4Proteja regras e escolha relações entre objetos
12 passos
Salve esta aula para seguir cada Lição Interativa passo a passo.
- 1IntroduçãoPassos precisam de limites confiáveis
- 2ExemploA ordem das operações altera o estado compartilhado
- 3ExplicaçãoTorne útil o limite de um procedimento
- 4DesafioPreveja uma atualização em outra ordem
- 5ExplicaçãoDê a cada conta seu próprio estado
- 6ExemploValide antes de alterar um saldo
- 7DesafioLocalize uma regra contornada
- 8ExemploEspecialize sem quebrar o contrato
- 9ExplicaçãoDeixe o objeto escolher seu método de área
- 10ConexãoMantenha o componente que faz o trabalho
- 11DesafioEscolha duas relações em um novo projeto
- 12RecapitulaçãoAssocie limites a responsabilidades
5Transforme uma coleção e consulte um resultado
13 passos
Salve esta aula para seguir cada Lição Interativa passo a passo.
- 1IntroduçãoDeclare uma transformação ou uma solicitação
- 2ExplicaçãoA pureza descreve mais que o número retornado
- 3ExemploSelecione primeiro os valores pares
- 4ExemploTransforme e combine os valores selecionados
- 5DesafioAplique a sequência a um novo caso
- 6ExplicaçãoRetorne um comportamento que lembra seu fator
- 7ExemploReduza o problema até que um caso direto se aplique
- 8ExemploLeia SQL como uma solicitação sobre linhas
- 9Ideia-chaveMantenha o resultado enquanto altera o plano
- 10ExemploConecte dois fatos por meio de uma regra
- 11DesafioLeia uma nova consulta incluindo seu valor-limite
- 12DesafioCombine uma consulta com uma transformação pura
- 13RecapitulaçãoMantenha visível a relação entre entrada e resultado
6Compare estilos e justifique a escolha de uma linguagem
10 passos
Salve esta aula para seguir cada Lição Interativa passo a passo.
- 1IntroduçãoMantenha a tarefa fixa
- 2ExemploMantenha a tarefa fixa enquanto muda o estilo
- 3ExemploColoque a mesma regra em objetos ou em uma consulta
- 4DesafioDecida se um projeto tem um único estilo exclusivo
- 5ExplicaçãoElimine primeiro os alvos sem suporte
- 6ExemploEscolha para um sistema meteorológico completo
- 7DicaMeça o que consome o tempo
- 8DesafioDefenda uma escolha diante de restrições concorrentes
- 9DesafioConecte a escolha da linguagem à organização do programa
- 10RecapitulaçãoDê um motivo ligado ao requisito
Questões de Paradigmas de programação
Conjunto 1: Leia código e identifique o caminho de execução10 Questões de Prática
Questão 1 de 10
mov e jmp. Em qual nível de linguagem o programa está escrito agora?Mostrar respostaOcultar resposta
Questão 2 de 10
Mostrar respostaOcultar resposta
Questão 3 de 10
Mostrar respostaOcultar resposta
Questão 4 de 10
Mostrar respostaOcultar resposta
Questão 5 de 10
Qual valor final este código Python armazena em total?
total = 1
for n in [4, 7, 2]:
total = total + n
Mostrar respostaOcultar resposta
Questão 6 de 10
label = "18" é seguido de label = 18. O nome pode mudar de tipo, mas "18" + 2 gera um erro. Como esses dois fatos são descritos?Mostrar respostaOcultar resposta
Questão 7 de 10
O que first contém após a execução deste código Python?
first = [4, 8]
second = first
second.append(6)
Mostrar respostaOcultar resposta
second altera a lista compartilhada, então first também mostra o 6 acrescentado. Para manter [4, 8], seria necessária uma lista separada, como uma criada por cópia antes do acréscimo.Questão 8 de 10
read_data(), calculate_total() e save_report(), cada uma contendo comandos ordenados. Que organização isso exemplifica?Mostrar respostaOcultar resposta
Questão 9 de 10
n == 0; caso contrário, chama a si mesma com n + 1. Começando no valor positivo 2, por que o caso base escrito não faz o cálculo terminar?Mostrar respostaOcultar resposta
Questão 10 de 10
Mostrar respostaOcultar resposta
Conjunto 2: Aplique regras de tipos, objetos e consultas10 Questões de Prática
Salve esta aula para ver todas as respostas e praticar cada conjunto.
Questão 1 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 2 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 3 de 10
a = 8 e b = 12 em Python, quais valores a + b / 2 e (a + b) / 2 produzem, respectivamente?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 4 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 5 de 10
balance e um método deposit. Duas contas são criadas com saldos iniciais diferentes. Onde ficam esses saldos distintos?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 6 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 7 de 10
[1, 4, 7, 10], uma sequência de operações mantém valores maiores que 3, subtrai 1 de cada valor mantido e depois os soma. Qual é o resultado?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 8 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 9 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 10 de 10
Quais são os valores de x e y após este código Python?
x = [12]
y = x
x = x + [3]
y.append(6)
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Conjunto 3: Rastreie chamadas, efeitos e contratos de objetos10 Questões de Prática
Salve esta aula para ver todas as respostas e praticar cada conjunto.
Questão 1 de 10
minutes = 60 * 12 pelo valor pré-calculado 720. Qual otimização é usada e o que ela deve preservar?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 2 de 10
O que este programa Python imprime?
score = 11
def change():
score = 4
return score + 2
print(change())
print(score)
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 3 de 10
Qual é o valor de items após a chamada?
items = [3]
def revise(values):
values.append(8)
values = [1]
values.append(5)
revise(items)
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 4 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 5 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 6 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 7 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 8 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 9 de 10
showDetails para um evento de clique. Por que esse mesmo tratador ainda pode usar métodos de objetos e atribuições imperativas?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 10 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Conjunto 4: Raciocine entre limites de execução e de projeto10 Questões de Prática
Salve esta aula para ver todas as respostas e praticar cada conjunto.
Questão 1 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 2 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 3 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 4 de 10
n = 0 em Python, compare n != 0 and 36 / n > 4 com 36 / n > 4 and n != 0. O que acontece?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 5 de 10
Qual valor final é armazenado em total?
def bump(x):
return x + 2
total = 0
for n in [2, 5, 8]:
if n > 3:
total = total + bump(n)
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 6 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 7 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 8 de 10
item.render(). Uma nova classe sem relação com as outras fornece um método render() compatível. O código Python que faz a chamada não exige uma superclasse. Por que o objeto pode participar do laço sem um ramo específico para seu tipo?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 9 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 10 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Conjunto 5: Diagnostique limites e combine as ideias10 Questões de Prática
Salve esta aula para ver todas as respostas e praticar cada conjunto.
Questão 1 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 2 de 10
O que este programa Python imprime?
value = 6
def step(value):
value = value + 3
return value
for n in [1, 2]:
result = step(value)
print(value, result)
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 3 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 4 de 10
double() o multiplica por 2; add_three() soma 3. Partindo do mesmo valor, qual é sempre a diferença entre dobrar e depois somar, e somar e depois dobrar?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 5 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 6 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 7 de 10
readings tem as linhas (level, active): (50, true), (65, true), (20, false), (85, true) e (90, false). Uma consulta solicita SUM(level) onde active = true AND level > 50. Após a adição de um índice adequado, qual total é exigido e o plano de execução pode mudar?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 8 de 10
parent(lea, noah). e parent(noah, sora)., além da regra grandparent(X, Z) :- parent(X, Y), parent(Y, Z).. O que grandparent(lea, Who) encontra?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 9 de 10
[50, 65, 20, 85]. Quais devem ser seus resultados corretos?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 10 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Travou em uma questão sua? Ajuda com Tarefas
Termos-Chave
A
Abstração
Alocação de memória
Análise léxica
price * 3 é separado em um nome, um operador de multiplicação e um número.Análise sintática
total = price * 3; faz parte da atribuição.Arquitetura do conjunto de instruções (ISA)
Atribuição
count = count + 1 lê a contagem antiga antes de substituí-la pelo valor incrementado.Atributo
Avaliação de curto-circuito
and.Á
Área de alocação dinâmica
B
Bytecode
C
Caso base
Classe
Loan de uma biblioteca pode definir o membro, o livro, a data de devolução e a operação de renovação de cada empréstimo.Código de máquina
Coerção de tipos
Coleta de lixo
Compartilhamento por referências
b = a, o compartilhamento por referências explica por que acrescentar um elemento por meio de b também pode alterar o que é visto por meio de a.Compilação antecipada (AOT)
Compilação em tempo de execução (JIT)
Compilador
Completude de Turing
Composição
Contagem de referências
D
Despacho dinâmico
shape.area() para a implementação de retângulo quando o objeto que recebe a chamada é um retângulo.Dobramento de constantes
60 * 60 * 24 por 86.400 antes de o programa iniciar.E
Ecossistema
Efeito colateral
Encapsulamento
Erro de lógica
a + b / 2 para calcular a média de dois valores é um erro de lógica porque a divisão acontece antes da adição.Erro em tempo de execução
Escopo
Estouro
Estouro de buffer
Expressão
width * height fornece o valor retornado por uma função de área de retângulo.Expressão lambda
lambda n: n * n fornece uma operação de elevação ao quadrado a uma sequência de transformações.Expressão regular
[0-9]{3}-[0-9]{4} corresponde a um grupo de três algarismos seguido de um hífen e quatro algarismos.F
Fechamento
Função de ordem superior
filter recebe um predicado que decide quais valores de entrada manter.Função de primeira classe
map como a operação a aplicar a cada elemento de uma coleção.Função de retorno de chamada
showMenu como função de retorno de chamada permite que um sistema de botões chame essa função quando o usuário clica.Função pura
G
Gargalo
H
Herança
I
Implementação
Imutabilidade
Inferência de tipos
Instrução
total = price * 3 atribui o resultado de uma multiplicação a uma variável.Interface
area() e deixar que cada forma calcule sua área de maneira diferente.Interpretador
L
Laço de leitura, avaliação e impressão (REPL)
Liberação dupla
free novamente sobre uma alocação já liberada pode criar uma liberação dupla e comportamento imprevisível.Ligador
Linguagem de alto nível
Linguagem de montagem
Linguagem específica de domínio
Linguagem multiparadigma
M
Máquina virtual
Método
Montador
add eax, ebx nos bytes correspondentes da instrução x86-64.Mutação
N
Número de ponto flutuante
0.1 + 0.2 e o valor armazenado de 0.3.O
Objeto
Ocultação de informação
Otimizador de consultas
P
Paradigma de programação
Parâmetro
width recebe 3 quando quem chama avalia area(3, 4).Pilha de chamadas
area acrescenta uma entrada à pilha de chamadas e a remove quando seu resultado retorna a quem chamou a função.Plataforma
Polimorfismo
area() em círculos e retângulos sem um ramo separado para cada um.Ponteiro pendente
Programação declarativa
Programação estruturada
Programação funcional
Programação imperativa
Programação lógica
Programação orientada a eventos
Programação procedural
Propriedade de valores
R
Recursão
Referência
Registro de ativação
S
Segurança de memória
Semântica
Sintaxe
if e indentação para o bloco controlado.Sobrescrita de métodos
SQL
T
Tabela de rastreamento
Tipagem dinâmica
Tipagem estática
Tipagem por comportamento
area() compatível.Tipo de dado
Tratador de eventos
U
Unidade léxica
Uso após liberação
V
Vazamento de memória
Sobre esta aula
Crie seus próprios Materiais de Estudo
Transforme suas anotações, slides e documentos em Guias de Estudo, Questões de Prática, Flashcards e mais 6 ferramentas.