Do tradutorBrandon Rhodes é uma pessoa muito modesta que se apresenta no Twitter como "um programador Python que paga um empréstimo à comunidade na forma de relatórios ou ensaios". O número desses "relatórios e ensaios" é impressionante, assim como o número de projetos gratuitos dos quais Brandon foi ou contribui. E Brandon publicou dois livros e está escrevendo um terceiro.
Muitas vezes encontro nos comentários sobre Habré um equívoco ou rejeição fundamental de linguagens dinâmicas, tipagem dinâmica, programação generalizada e outros paradigmas. Estou publicando esta tradução (transcrição) autorizada (abreviada) de um dos relatórios de Brandon, na esperança de que ajude os programadores existentes nos paradigmas das linguagens estáticas a entender melhor as linguagens dinâmicas, em particular o Python.
Como é habitual comigo, peço que me informe no PM sobre meus erros e erros de digitação.
O que a frase "caso marginal" significa no título do meu relatório? O caso limite surge quando você itera uma sequência de opções até atingir o valor extremo. Por exemplo, um polígono de n lados. Se n = 3, então este é um triângulo, n = 4 é um quadrângulo, n = 5 é um pentágono etc. À medida que n se aproxima do infinito, os lados se tornam menores e maiores, e o contorno do polígono se torna como um círculo. Assim, o círculo é o caso limitante para polígonos regulares. É o que acontece quando uma certa idéia é levada ao limite.
Eu quero falar sobre Python como um caso extremo de C ++. Se você pegar todas as boas idéias do C ++ e limpá-las até a conclusão lógica, tenho certeza de que você terminará no Python tão naturalmente quanto uma série de polígonos chega a um círculo.
Ativos não essenciais
Fiquei interessado em Python nos anos 90: foi um período na minha vida em que me livrei de "ativos não essenciais", como eu o chamo. Muitas coisas começaram a me aborrecer. Interrupções, por exemplo. Lembre-se, uma vez em muitas placas de computador, houve contatos com jumpers? E você define esses jumpers nos manuais para que a placa de vídeo receba uma interrupção de prioridade mais alta, para que seu jogo corra mais rápido? Então, eu estava cansado de alocar e liberar memória usando malloc()
e free()
ao mesmo tempo em que parei de ajustar o desempenho do meu computador com jumpers. Era 1997 mais ou menos.
Quero dizer, quando estudamos um processo, geralmente nos esforçamos para obter controle completo sobre ele, para ter em mãos todas as alavancas e botões possíveis. Então algumas pessoas ainda estão fascinadas por essa possibilidade de controle. Mas meu personagem é que, assim que me acostumo à gerência e entendo o que é isso, começo imediatamente a procurar a oportunidade de abrir mão de alguns dos meus poderes, transferir alavancas e botões para alguma máquina, para que isso atribua interrupções para mim.
Portanto, no final dos anos 90, eu estava procurando uma linguagem de programação que me permitisse focar na área de assunto e na modelagem de tarefas, em vez de me preocupar com a área da memória do meu computador armazenada. Como podemos simplificar o C ++ sem repetir os pecados das famosas linguagens de script?
Por exemplo, eu não poderia usar Perl, e você sabe por quê? Este cifrão! Ele imediatamente deixou claro que o criador do Perl não entendia como as linguagens de programação funcionavam. Você usa o dólar no Bash para separar nomes de variáveis do restante da cadeia, porque um programa Bash consiste em comandos literalmente percebidos e seus parâmetros. Mas depois de conhecer essas linguagens de programação, nas quais as seqüências de caracteres são colocadas entre pares de caracteres pequenos chamados aspas e não em todo o texto do programa, você começa a perceber $
como lixo visual. O cifrão é inútil, é feio, deve desaparecer! Se você deseja criar uma linguagem para programação séria, não use caracteres especiais para indicar variáveis.
Sintaxe
E a sintaxe? Tome C como base! Funciona muito bem. Permita que a atribuição seja indicada por um sinal de igual. Essa designação não é aceita em todos os idiomas, mas, de uma maneira ou de outra, muitos estão acostumados. Mas não vamos fazer da atribuição uma expressão. Os usuários de nossa linguagem serão não apenas programadores profissionais, mas também crianças em idade escolar, cientistas ou cientistas de dados (se você não souber qual dessas categorias de usuários escreve o pior código, sugerirei que não são crianças em idade escolar). Não daremos aos usuários a oportunidade de alterar o estado das variáveis em locais inesperados e tornaremos a atribuição um operador.
O que então deve ser usado para denotar igualdade se o sinal de igual já tiver sido usado para atribuição? Claro, dupla atribuição, como é feito em C! Muitos já estão acostumados. Também emprestaremos de C a notação para todas as operações aritméticas e bit a bit, porque essas notações funcionam e muitas estão felizes com elas.
Claro, podemos melhorar alguma coisa. O que você pensa quando vê o sinal de porcentagem no texto do programa? Sobre interpolação de strings, é claro! Embora o %
seja principalmente um operador de captura de módulo, ele era simplesmente indefinido para seqüências de caracteres. E se sim, por que não reutilizá-lo?
Literais numéricos e de cadeias que controlam seqüências com barras invertidas - tudo isso será semelhante a C.
Controle de fluxo de execução? O mesmo if
, else
, while
, break
e continue
. Obviamente, adicionaremos um pouco de diversão ao cooptar os bons e velhos for
iterar sobre estruturas de dados e intervalos de valores. Isso será proposto posteriormente no C ++ 11, mas no Python, o operador for
encapsulou todas as operações para calcular tamanhos, atravessar links, incrementar o contador etc., ou seja, fazer tudo o que era necessário para fornecer ao usuário um elemento da estrutura de dados. Que tipo de estruturas? Não importa, basta passá-lo for
, ele vai descobrir.
Também tomaremos emprestadas exceções do C ++, mas as tornaremos tão baratas em termos de consumo de recursos que elas podem ser usadas não apenas para lidar com erros, mas também para controlar o fluxo de execução. Tornaremos a indexação mais interessante adicionando o fatiamento - a capacidade de indexar não apenas elementos individuais de estruturas de dados sequenciais, mas também seus intervalos.
Ah sim! Corrigiremos a falha de design original em C - adicione uma vírgula pendente!
Essa história começou com Pascal, uma linguagem terrível na qual um ponto e vírgula é usado como delimitador de expressão. Isso significa que o usuário deve colocar um ponto e vírgula no final de cada expressão no bloco, exceto a última . Portanto, toda vez que você altera a ordem das expressões em um programa no Pascal, corre o risco de receber um erro de sintaxe se não remover o ponto-e-vírgula da última linha e adicioná-lo ao final da linha que costumava ser a última.
If (n = 0) then begin writeln('N is now zero'); func := 1 end
Kernigan e Ritchie fizeram a coisa certa quando definiram o ponto-e-vírgula em C como o terminador da expressão, e não o delimitador, criando essa maravilhosa simetria quando cada linha do programa, incluindo a última, termina a mesma e pode ser trocada livremente. Infelizmente, no futuro, um senso de harmonia mudou para eles, e eles fizeram da vírgula um separador em inicializadores estáticos. Isso parece bom quando a expressão se encaixa em uma linha:
int a[] = {4, 5, 6};
mas quando seu inicializador fica mais longo e você o organiza verticalmente, você obtém a mesma assimetria desconfortável que em Pascal:
int a[] = { 4, 5, 6 };
No estágio inicial de seu desenvolvimento, o Python tornou a vírgula suspensa nas estruturas de dados completamente opcional, independentemente de como os elementos dessa estrutura são organizados: horizontal ou verticalmente. A propósito, isso é muito conveniente para a geração automática de código: você não precisa tratar o último elemento como um caso especial.
Posteriormente, os padrões C99 e C ++ 11 também corrigiram o mal-entendido inicial, permitindo adicionar uma vírgula após o último literal no inicializador.
Namespaces
Também precisamos implementar em nossa linguagem de programação algo como namespaces ou namespaces. Esta é uma parte crítica da linguagem que deve nos salvar de erros como conflitos de nome. Seremos mais fáceis que o C ++: em vez de dar ao usuário a capacidade de nomear arbitrariamente o espaço para nome, criaremos um espaço para nome por módulo (arquivo) e os designaremos com nomes de arquivo. Por exemplo, se você criar o módulo foo.py
, ele receberá o espaço de nomes foo
.
Para trabalhar com um modelo simplificado de namespaces, um usuário precisa de apenas um operador.
Crie o diretório my_package
, coloque o arquivo my_module.py
e declare a classe no arquivo:
class C(object): READ = 1 WRITE = 2
o acesso aos atributos da classe será o seguinte:
import my_package.my_module my_package.my_module.C.READ
Não se preocupe, não forçaremos o usuário a imprimir o nome completo toda vez. Daremos a ele a oportunidade de usar várias versões da declaração de import
para variar o grau de "proximidade" do espaço para nome:
import my_package.my_module my_package.my_module.C.READ from my_package import my_module my_module.C.READ from my_package.my_module import C C.READ
Assim, os mesmos nomes dados em pacotes diferentes nunca entrarão em conflito:
import json j = json.load(file) import pickle p = pickle.load(file)
O fato de cada módulo ter seu próprio espaço para nome também significa que não precisamos de um modificador static
. No entanto, lembramos de uma função executada static
- encapsulando variáveis internas. Para mostrar aos colegas que um determinado nome (variável, classe ou módulo) não é público, iniciamos com um sublinhado, por exemplo, _ignore_this
. Também pode ser um sinal para o IDE não usar esse nome no preenchimento automático.
Sobrecarga de função
Não implementaremos sobrecarga de funções em nosso idioma. O mecanismo de sobrecarga é muito complicado. Em vez disso, usaremos argumentos opcionais com valores padrão que podem ser omitidos da chamada, bem como argumentos nomeados para “pular” sobre os argumentos opcionais com padrões válidos e definir apenas os valores que diferem dos padrões. É importante ressaltar que a falta de sobrecarga nos poupará da necessidade de determinar qual função do conjunto de funções sobrecarregadas acabou de ser chamada, como o gerenciador de chamadas funcionava: a função é sempre uma neste módulo, é fácil encontrar pelo nome.
APIs do sistema
Daremos ao usuário acesso total a muitas APIs do sistema, incluindo soquetes. Não entendo por que os autores de linguagens de script sempre oferecem suas próprias maneiras engenhosas de abrir um soquete. No entanto, eles nunca realizam a API do Unix Socket completa. Eles implementam 5-6 funções que entendem e jogam fora todo o resto. O Python, diferentemente deles, possui módulos padrão para interagir com o sistema operacional que implementam cada chamada de sistema padrão. Isso significa que você pode abrir o livro de Stevens agora e começar a escrever o código. E todos os seus soquetes, processos e garfos funcionarão exatamente como ele diz. Sim, é possível que Guido ou os primeiros colaboradores do Python tenham feito exatamente isso, porque estavam com preguiça de escrever sua implementação das bibliotecas do sistema, com preguiça de explicar aos usuários novamente como os soquetes funcionam. Mas, como resultado, eles obtiveram um efeito maravilhoso: você pode transferir todo o seu conhecimento UNIX adquirido em C e C ++ para o ambiente Python.
Portanto, decidimos quais recursos “emprestaremos” do C ++ para criar nossa linguagem de script simples. Agora precisamos decidir o que queremos consertar.
Comportamento indefinido
Comportamento desconhecido, comportamento indefinido, comportamento definido pela implementação ... Todas essas são idéias ruins para a linguagem que será usada por crianças em idade escolar, cientistas e cientistas de dados. E o ganho de desempenho para o qual essas coisas são permitidas geralmente é insignificante em comparação com os inconvenientes. Em vez disso, anunciaremos que qualquer programa sintaticamente correto produz o mesmo resultado em qualquer plataforma. Vamos descrever o padrão da linguagem com frases como “Python avalia todas as expressões da esquerda para a direita” em vez de tentar reorganizar os cálculos, dependendo do processador, sistema operacional ou fase da lua. Se o usuário tem certeza de que a ordem dos cálculos é importante, ele tem o direito de reescrever o código corretamente: no final, o usuário é o principal.
Prioridades de operação
Você deve ter encontrado erros semelhantes: expressão
oflags & 0x80 == nflags & 0x80
sempre retorna 0, porque as comparações em C têm precedência sobre operações bit a bit. Em outras palavras, essa expressão avalia como
oflags & (0x80 == nflags) & 0x80
Oh, esse C!
Eliminaremos a causa potencial de tais erros em nossa linguagem de script simples, colocando a prioridade das operações de comparação atrás da aritmética e da manipulação de bits, para que a expressão do nosso exemplo seja calculada de maneira mais intuitiva:
(oflags & 0x80) == (nflags & 0x80)
Outras melhorias
A legibilidade do código é importante para nós. Se as operações aritméticas da linguagem C são familiares ao usuário, mesmo pela aritmética da escola, a confusão entre operações lógicas e bit a bit é uma fonte clara de erros. Substituiremos o duplo e comercial pela palavra ee a dupla linha vertical pela palavra or
, para que nossa linguagem se pareça mais com a fala humana do que com o piquete de caracteres de “computador”.
Vamos deixar a possibilidade de computação abreviada para nossos operadores lógicos ( https://en.wikipedia.org/wiki/Short-circuit_evaluation ), mas também damos a eles a capacidade de retornar o valor final de qualquer tipo, não apenas booleano. Então expressões como
s = error.message or 'Error'
Neste exemplo, a variável será configurada como error.message
se não for vazia, caso contrário, a string 'Error'.
Estendemos a ideia de C de que 0 é equivalente a false para objetos que não sejam números inteiros. Por exemplo, em linhas e contêineres vazios.
Destruiremos o excesso de número inteiro. Nossa linguagem será consistente na implementação e fácil de usar; portanto, nossos usuários não precisarão lembrar de um valor especial suspeito, próximo a dois bilhões, após o qual o todo, aumentado em um, muda repentinamente de sinal. Implementamos números inteiros que se comportam como números inteiros até esgotar toda a memória disponível.
Digitação estrita vs fraca
Outra questão importante no design da linguagem de script: o rigor da digitação. Muitos na platéia estão familiarizados com JavaScript? O que acontece se o número 3 for subtraído da sequência '4'?
js> '4' - 3 1
Ótimo! E se você adicionar o número 3 à sequência '4'?
js> '4' + 3 "43"
Isso é chamado de digitação frouxa (ou fraca). Isso é algo como um complexo de inferioridade quando uma linguagem de programação pensa que um programador a condenará se não puder retornar o resultado de qualquer expressão, mesmo obviamente sem sentido, por tipos de conversão repetidos. O problema é que a conversão de tipo, que uma linguagem de tipo fraco produz automaticamente, muito raramente leva a um resultado significativo. Vamos tentar conversões um pouco mais complexas:
js> [] + [] "" js> [] + {} "[object Object]"
Esperamos que a operação de adição seja comutativa, mas o que acontece se mudarmos os termos no último caso?
js> {} + [] 0
JavaScript não está sozinho em seus problemas. Perl em uma situação semelhante também tenta retornar pelo menos algo:
perl> "3" + 1 4
E o awk fará algo assim:
$ echo | awk '{print "3" + 1}' 4
Os criadores de linguagens de script tradicionalmente acreditam que a digitação solta é conveniente . Eles estavam enganados: a digitação solta é terrível ! Viola o princípio da localidade. Se houver um erro no código, a linguagem de programação deverá informar o usuário sobre isso, causando uma exceção o mais próximo possível do local problemático no código. Mas em todas essas linguagens, que lançam tipos infinitamente, até que algo seja resolvido, o controle geralmente chega ao fim e obtemos o resultado, julgando pelo qual, em nosso programa, algo está errado em algum lugar. E temos que depurar todo o nosso programa, uma linha após a outra, para encontrar esse erro.
A digitação solta também diminui a legibilidade do código, porque, mesmo que usemos corretamente a conversão implícita de tipos em um programa, isso acontece inesperadamente para outro programador.
No Python, como no C ++, essas expressões retornarão um erro.
>>> '4' - 3 TypeError >>> '4' + 3 TypeError
Como a conversão de tipo, se realmente necessário, é fácil de escrever explicitamente:
>>> int('4') + 3 7 >>> '4' + str(3) '43'
Esse código é fácil de ler e manter, deixa claro o que exatamente acontece no programa, o que leva a esse resultado. Isso ocorre porque os programadores Python acreditam que explícito é melhor que implícito, e o erro não deve passar despercebido.
Python é uma linguagem fortemente tipada e a única conversão implícita de tipo ocorre durante operações aritméticas em números inteiros, cujo resultado deve ser expresso como um número fracionário. Talvez isso também não deva ser permitido no programa, mas nesse caso muitos usuários precisariam explicar imediatamente a diferença entre números inteiros e números de ponto flutuante, o que complicaria seus primeiros passos no Python.
Continuação: “ Python como o caso final de C ++. Parte 2/2 . "