FAQ sobre design e histórico

Por que o Python usa indentação para agrupamento de instruções?

Guido van Rossum acredita que usar indentação para agrupamento é extremamente elegante e contribui muito para a clareza de programa Python mediano. Muitas pessoas aprendem a amar esta ferramenta depois de um tempo.

Uma vez que não há colchetes de início / fim, não pode haver um desacordo entre o agrupamento percebido pelo analisador sintático e pelo leitor humano. Ocasionalmente, programadores C irão encontrar um fragmento de código como este:

if (x <= y)
        x++;
        y--;
z++;

Somente a instrução x++ é executada se a condição for verdadeira, mas a indentação leva muitos a acreditarem no contrário. Com frequência, até programadores C experientes a observam fixamente por um longo tempo, perguntando-se por que y está sendo decrementada até mesmo para x > y.

Como não há chaves de início / fim, o Python é muito menos propenso a conflitos no estilo de codificação. Em C, existem muitas maneiras diferentes de colocar as chaves. Depois de se tornar habitual a leitura e escrita de código usando um estilo específico, é normal sentir-se um pouco receoso ao ler (ou precisar escrever) em um estilo diferente.

Muitos estilos de codificação colocam chaves de início / fim em uma linha sozinhos. Isto torna os programas consideravelmente mais longos e desperdiça espaço valioso na tela, dificultando a obtenção de uma boa visão geral de um programa. Idealmente, uma função deve caber em uma tela (digamos, 20 a 30 linhas). 20 linhas de Python podem fazer muito mais trabalho do que 20 linhas de C. Isso não se deve apenas à falta de colchetes de início/fim – a falta de declarações e os tipos de dados de alto nível também são responsáveis – mas a sintaxe baseada em indentação certamente ajuda.

Por que eu estou recebendo resultados estranhos com simples operações aritméticas?

Veja a próxima questão.

Por que o cálculo de pontos flutuantes são tão imprecisos?

Usuários são frequentemente surpreendidos por resultados como este:

>>> 1.2 - 1.0
0.19999999999999996

e pensam que isto é um bug do Python. Não é não. Isto tem pouco a ver com o Python, e muito mais a ver com como a estrutura da plataforma lida com números em ponto flutuante.

O tipo float no CPython usa um double do C para armazenamento. O valor de um objeto float é armazenado em ponto flutuante binário com uma precisão fixa (normalmente 53 bits) e Python usa operações C, que por sua vez dependem da implementação de hardware no processador, para realizar operações de ponto flutuante. Isso significa que, no que diz respeito às operações de ponto flutuante, Python se comporta como muitas linguagens populares, incluindo C e Java.

Muitos números podem ser escritos facilmente em notação decimal, mas não podem ser expressados exatamente em ponto flutuante binário. Por exemplo, após:

>>> x = 1.2

o valor armazenado para x é uma (ótima) aproximação para o valor decimal 1.2, mas não é exatamente igual. Em uma máquina típica, o valor real armazenado é:

1.0011001100110011001100110011001100110011001100110011 (binário)

que é exatamente:

1.1999999999999999555910790149937383830547332763671875 (decimal)

A precisão típica de 53 bits fornece floats do Python com precisão de 15 a 16 dígitos decimais.

Para uma explicação mais completa, consulte o capítulo de aritmética de ponto flutuante no tutorial Python.

Por que strings do Python são imutáveis?

Existem várias vantagens.

Uma delas é o desempenho: saber que uma string é imutável significa que podemos alocar espaço para ela no momento da criação, e os requisitos de armazenamento são fixos e imutáveis. Esta é também uma das razões para a distinção entre tuplas e listas.

Outra vantagem é que strings em Python são consideradas tão “elementares” quanto números. Nenhuma atividade alterará o valor 8 para qualquer outra coisa e, em Python, nenhuma atividade alterará a string “oito” para qualquer outra coisa.

Por que o ‘self’ deve ser usado explicitamente em definições de método e chamadas?

A ideia foi emprestada do Modula-3. Acontece dela ser muito útil, por vários motivos.

Primeiro, é mais óbvio que você está usando um método ou atributo de instância em vez de uma variável local. Ler self.x ou self.meth() deixa absolutamente claro que uma variável de instância ou método é usado mesmo se você não souber a definição da classe de cor. Em C++, você pode perceber pela falta de uma declaração de variável local (presumindo que globais são raras ou facilmente reconhecíveis) – mas no Python não há declarações de variáveis locais, então você teria que procurar a definição de classe para tenha certeza. Alguns padrões de codificação C++ e Java exigem que atributos de instância tenham um prefixo m_, portanto, essa explicitação ainda é útil nessas linguagens também.

Segundo, significa que nenhuma sintaxe especial é necessária se você quiser referenciar ou chamar explicitamente o método de uma classe específica. Em C++, se você quiser usar um método de uma classe base que é substituído em uma classe derivada, você deve usar o operador :: – no Python, você pode escrever baseclass.methodname(self, <lista de argumentos>). Isto é particularmente útil para métodos __init__() e, em geral, em casos onde um método de classe derivada deseja estender o método da classe base de mesmo nome e, portanto, precisa chamar o método da classe base de alguma forma.

Finalmente, por exemplo, variáveis, ele resolve um problema sintático com atribuição: uma vez que variáveis locais em Python são (por definição!) aquelas variáveis às quais um valor é atribuído em um corpo de função (e que não são explicitamente declaradas globais), é necessário deve haver alguma forma de dizer ao interpretador que uma atribuição deveria ser atribuída a uma variável de instância em vez de a uma variável local, e deve ser preferencialmente sintática (por razões de eficiência). C++ faz isso através de declarações, mas Python não possui declarações e seria uma pena ter que introduzi-las apenas para esse fim. Usar o self.var explícito resolve isso muito bem. Da mesma forma, para usar variáveis de instância, ter que escrever self.var significa que referências a nomes não qualificados dentro de um método não precisam pesquisar nos diretórios da instância. Em outras palavras, variáveis locais e variáveis de instância residem em dois espaço de nomes diferentes, e você precisa informar ao Python qual espaço de nomes usar.

Por que não posso usar uma atribuição em uma expressão?

A partir do Python 3.8, você pode!

Expressões de atribuição usando o operador morsa := atribuem uma variável em uma expressão:

while chunk := fp.read(200):
   print(chunk)

Veja PEP 572 para mais informações.

Por que o Python usa métodos para algumas funcionalidades (ex: lista.index()) mas funções para outras (ex: len(lista))?

Como Guido disse:

(a) Para algumas operações, a notação de prefixo é melhor lida do que as operações de prefixo (e infixo!) têm uma longa tradição em matemática que gosta de notações onde os recursos visuais ajudam o matemático a pensar sobre um problema. Compare a facilidade com que reescrevemos uma fórmula como x*(a+b) em x*a + x*b com a falta de jeito de fazer a mesma coisa usando uma notação OO bruta.

(b) Quando leio o código que diz len(x) eu sei que ele está perguntando o comprimento de alguma coisa. Isso me diz duas coisas: o resultado é um número inteiro e o argumento é algum tipo de contêiner. Pelo contrário, quando leio x.len(), já devo saber que x é algum tipo de contêiner implementando uma interface ou herdando de uma classe que possui um len() padrão. Veja a confusão que ocasionalmente temos quando uma classe que não está implementando um mapeamento tem um método get() ou keys(), ou algo que não é um arquivo tem um método write().

https://mail.python.org/pipermail/python-3000/2006-November/004643.html

Por que o join() é um método de string em vez de ser um método de lista ou tupla?

Strings se tornaram muito parecidas com outros tipos padrão a partir do Python 1.6, quando métodos que dão a mesma funcionalidade que sempre esteve disponível utilizando as funções do módulo de string foram adicionados. A maior parte desses novos métodos foram amplamente aceitos, mas o que parece deixar alguns programadores desconfortáveis é:

", ".join(['1', '2', '4', '8', '16'])

que dá o resultado:

"1, 2, 4, 8, 16"

Existem dois argumentos comuns contra esse uso.

O primeiro segue as linhas de: “Parece muito feio usar um método de uma string literal (constante de string)”, para o qual a resposta é que pode, mas uma string literal é apenas um valor fixo. Se os métodos devem ser permitidos em nomes vinculados a strings, não há razão lógica para torná-los indisponíveis em literais.

A segunda objeção é normalmente formulada como: “Na verdade, estou dizendo a uma sequência para unir seus membros com uma constante de string”. Infelizmente, você não está. Por alguma razão parece haver muito menos dificuldade em ter split() como um método string, já que nesse caso é fácil ver que

"1, 2, 4, 8, 16".split(", ")

é uma instrução para uma string literal para retornar as substrings delimitadas pelo separador fornecido (ou, por padrão, execuções arbitrárias de espaço em branco).

join() é um método de string porque ao usá-lo você está dizendo à string separadora para iterar sobre uma sequência de strings e se inserir entre elementos adjacentes. Este método pode ser usado com qualquer argumento que obedeça às regras para objetos sequência, incluindo quaisquer novas classes que você mesmo possa definir. Existem métodos semelhantes para bytes e objetos bytearray.

Quão rápidas são as exceções?

Um bloco try/except é extremamente eficiente se nenhuma exceção for levantada. Na verdade, capturar uma exceção é caro. Nas versões do Python anteriores à 2.0 era comum usar esta expressão:

try:
    value = mydict[key]
except KeyError:
    mydict[key] = getvalue(key)
    value = mydict[key]

Isso somente fazia sentido quando você esperava que o dicionário tivesse uma chave quase que toda vez. Se esse não fosse o caso, você escrevia desta maneira:

if key in mydict:
    value = mydict[key]
else:
    value = mydict[key] = getvalue(key)

Para este caso específico, você também pode usar value = dict.setdefault(key, getvalue(key)), mas apenas se a chamada getvalue() tiver pouco custosa o suficiente porque é avaliada em todos os casos.

Por que não existe uma instrução de switch ou case no Python?

Em geral, as instruções switch estruturadas executam um bloco de código quando uma expressão possui um valor ou conjunto de valores específico. Desde o Python 3.10 é possível combinar facilmente valores literais, ou constantes dentro de um espaço de nomes, com uma instrução match ... case. Uma alternativa mais antiga é uma sequência de if... elif... elif... else.

Para casos em que você precisa escolher entre um grande número de possibilidades, você pode criar um dicionário mapeando valores de caso para funções a serem chamadas. Por exemplo:

functions = {'a': function_1,
             'b': function_2,
             'c': self.method_1}

func = functions[value]
func()

Para chamar métodos em objetos, você pode simplificar ainda mais usando olá função embutida getattr() para recuperar métodos com um nome específico:

class MyVisitor:
    def visit_a(self):
        ...

    def dispatch(self, value):
        method_name = 'visit_' + str(value)
        method = getattr(self, method_name)
        method()

É sugerido que você use um prefixo para os nomes dos métodos, como visit_ neste exemplo. Sem esse prefixo, se os valores vierem de uma fonte não confiável, um invasor poderá chamar qualquer método no seu objeto.

Imitar o comportamento do switch no C, em que a execução atravessa as instruções de um case para outro caso não seja interrompida, é possível, mais difícil, e menos necessário.

Você não pode emular threads no interpretador em vez de confiar em uma implementação de thread específica do sistema operacional?

Resposta 1: Infelizmente, o interpretador envia pelo menos um quadro de pilha C para cada quadro de pilha Python. Além disso, as extensões podem retornar ao Python em momentos quase aleatórios. Portanto, uma implementação completa de threads requer suporte de thread para C.

Resposta 2: Felizmente, existe o Stackless Python, que tem um laço de interpretador completamente redesenhado que evita a pilha C.

Por que expressões lambda não podem conter instruções?

Expressões lambda no Python não podem conter instruções porque o framework sintático do Python não consegue manipular instruções aninhadas dentro de expressões. No entanto, no Python, isso não é um problema sério. Diferentemente das formas de lambda em outras linguagens, onde elas adicionam funcionalidade, lambdas de Python são apenas notações simplificadas se você tiver muita preguiça de definir uma função.

Funções já são objetos de primeira classe em Python, e podem ser declaradas em um escopo local. Portanto a única vantagem de usar um lambda em vez de uma função definida localmente é que você não precisa inventar um nome para a função – mas esta só é uma variável local para a qual o objeto da função (que é exatamente do mesmo tipo de um objeto que uma expressão lambda carrega) é atribuído!

O Python pode ser compilado para linguagem de máquina, C ou alguma outra linguagem?

O Cython compila uma versão modificada do Python com anotações opcionais em extensões C. Nuitka é um compilador emergente de Python em código C++, com o objetivo de oferecer suporte à linguagem Python completa.

Como o Python gerencia memória?

Os detalhes do gerenciamento de memória Python dependem da implementação. A implementação padrão do Python, CPython, usa contagem de referências para detectar objetos inacessíveis, e outro mecanismo para coletar ciclos de referência, executando periodicamente um algoritmo de detecção de ciclo que procura por ciclos inacessíveis e exclui os objetos envolvidos. O módulo gc fornece funções para realizar uma coleta de lixo, obter estatísticas de depuração e ajustar os parâmetros do coletor.

Outras implementações (como Jython ou PyPy), no entanto, podem contar com um mecanismo diferente, como um coletor de lixo maduro. Essa diferença pode causar alguns problemas sutis de portabilidade se o seu código Python depender do comportamento da implementação de contagem de referências.

Em algumas implementações do Python, o código a seguir (que funciona bem no CPython) provavelmente ficará sem descritores de arquivo:

for file in very_long_list_of_files:
    f = open(file)
    c = f.read(1)

Na verdade, usando o esquema de contagem de referências e destrutor de referências do CPython, cada nova atribuição a f fecha o arquivo anterior. Com um GC tradicional, entretanto, esses objetos de arquivo só serão coletados (e fechados) em intervalos variados e possivelmente longos.

Se você quiser escrever um código que funcione com qualquer implementação Python, você deve fechar explicitamente o arquivo ou usar a instrução with; isso funcionará independentemente do esquema de gerenciamento de memória:

for file in very_long_list_of_files:
    with open(file) as f:
        c = f.read(1)

Por que o CPython não usa uma forma mais tradicional de esquema de coleta de lixo?

Por um lado, este não é um recurso padrão C e, portanto, não é portátil. (Sim, sabemos sobre a biblioteca Boehm GC. Ela possui pedaços de código assembler para a maioria das plataformas comuns, não para todas elas, e embora seja em sua maioria transparente, não é completamente transparente; são necessários patches para obter Python para trabalhar com isso.)

O GC tradicional também se torna um problema quando o Python é incorporado em outros aplicativos. Embora em um Python independente seja bom substituir o padrão malloc() e free() por versões fornecidas pela biblioteca GC, uma aplicação que incorpora Python pode querer ter seu próprio substituto para malloc() e free(), e podem não querer Python. No momento, CPython funciona com qualquer coisa que implemente malloc() e free() corretamente.

Por que toda memória não é liberada quando o CPython fecha?

Os objetos referenciados nos espaço de nomes globais dos módulos Python nem sempre são desalocados quando o Python é encerrado. Isso pode acontecer se houver referências circulares. Existem também certos bits de memória alocados pela biblioteca C que são impossíveis de liberar (por exemplo, uma ferramenta como o Purify reclamará disso). Python é, no entanto, agressivo quanto à limpeza de memória na saída e tenta destruir todos os objetos.

Se você quiser forçar o Python a excluir certas coisas na desalocação, use o módulo atexit para executar uma função que forçará essas exclusões.

Por que existem tipos de dados separados para tuplas e listas?

Listas e tuplas, embora semelhantes em muitos aspectos, geralmente são usadas de maneiras fundamentalmente diferentes. Tuplas podem ser consideradas semelhantes a records de Pascal ou structs de C; são pequenas coleções de dados relacionados que podem ser de diferentes tipos e operados como um grupo. Por exemplo, uma coordenada cartesiana é representada apropriadamente como uma tupla de dois ou três números.

As listas, por outro lado, são mais parecidas com arrays em outras linguagens. Eles tendem a conter um número variável de objetos, todos do mesmo tipo e operados um por um. Por exemplo, os.listdir('.') retorna uma lista de strings representando os arquivos no diretório atual. Funções que operam nesta saída geralmente não seriam interrompidas se você adicionasse outro arquivo ou dois ao diretório.

As tuplas são imutáveis, o que significa que, uma vez criada uma tupla, você não pode substituir nenhum de seus elementos por um novo valor. As listas são mutáveis, o que significa que você sempre pode alterar os elementos de uma lista. Somente elementos imutáveis podem ser usados como chaves de dicionário e, portanto, apenas tuplas e não listas podem ser usadas como chaves.

Como as listas são implementadas no CPython?

As listas do CPython são, na verdade, vetores de comprimento variável, listas vinculadas não no estilo Lisp. A implementação usa um vetor contíguo de referências a outros objetos e mantém um ponteiro para esse vetor e o comprimento de vetor em uma estrutura de cabeçalho de lista.

Isso torna a indexação de uma lista a[i] uma operação cujo custo é independente do tamanho da lista ou do valor do índice.

Quando itens são anexados ou inseridos, o vetor de referências é redimensionado. Alguma inteligência é aplicada para melhorar o desempenho de anexar itens repetidamente; quando o vetor precisa ser aumentado, algum espaço extra é alocado para que as próximas vezes não exijam um redimensionamento real.

Como são os dicionários implementados no CPython?

Os dicionários do CPython são implementados como tabelas hash redimensionáveis. Em comparação com árvores B, isso oferece melhor desempenho para pesquisa (de longe a operação mais comum) na maioria das circunstâncias, e a implementação é mais simples.

Os dicionários funcionam calculando um código hash para cada chave armazenada no dicionário usando a função embutida hash(). O código hash varia amplamente dependendo da chave e da semente por processo; por exemplo, 'Python' poderia fazer hash para -539294296 enquanto 'python', uma string que difere por um único bit, poderia fazer hash para 1142331976. O código hash é então usado para calcular um local em um vetor interno onde o valor será armazenado. Supondo que você esteja armazenando chaves com valores de hash diferentes, isso significa que os dicionários levam um tempo constante – O(1), na notação Big-O – para recuperar uma chave.

Por que chaves de dicionário devem ser imutáveis?

A implementação da tabela hash de dicionários usa um valor hash calculado a partir do valor da chave para encontrar a chave. Se a chave fosse um objeto mutável, seu valor poderia mudar e, portanto, seu hash também poderia mudar. Mas como quem altera o objeto-chave não pode saber que ele estava sendo usado como chave de dicionário, ele não pode mover a entrada no dicionário. Então, quando você tentar procurar o mesmo objeto no dicionário, ele não será encontrado porque seu valor de hash é diferente. Se você tentasse procurar o valor antigo, ele também não seria encontrado, porque o valor do objeto encontrado naquele hash seria diferente.

Se você deseja que um dicionário seja indexado com uma lista, simplesmente converta primeiro a lista em uma tupla; a função tuple(L) cria uma tupla com as mesmas entradas da lista L. As tuplas são imutáveis e, portanto, podem ser usadas como chaves de dicionário.

Algumas soluções inaceitáveis que foram propostas:

  • Listas de hash por endereço (ID do objeto). Isto não funciona porque se você construir uma nova lista com o mesmo valor ela não será encontrada; por exemplo.:

    mydict = {[1, 2]: '12'}
    print(mydict[[1, 2]])
    

    levantaria uma exceção KeyError porque o id do [1, 2] usado na segunda linha difere daquele da primeira linha. Em outras palavras, as chaves de dicionário devem ser comparadas usando ==, não usando is.

  • Fazer uma cópia ao usar uma lista como chave. Isso não funciona porque a lista, sendo um objeto mutável, poderia conter uma referência a si mesma e então o código copiado entraria em um laço infinito.

  • Permitir listas como chaves, mas dizer ao usuário para não modificá-las. Isso permitiria uma classe de bugs difíceis de rastrear em programas quando você esquecesse ou modificasse uma lista por acidente. Também invalida uma importante invariante dos dicionários: todo valor em d.keys() pode ser usado como chave do dicionário.

  • Marcar listas como somente leitura quando forem usadas como chave de dicionário. O problema é que não é apenas o objeto de nível superior que pode alterar seu valor; você poderia usar uma tupla contendo uma lista como chave. Inserir qualquer coisa como chave em um dicionário exigiria marcar todos os objetos acessíveis a partir daí como somente leitura – e, novamente, objetos autorreferenciais poderiam causar um laço infinito.

Existe um truque para contornar isso se você precisar, mas use-o por sua própria conta e risco: você pode agrupar uma estrutura mutável dentro de uma instância de classe que tenha um método __eq__() e um método __hash__(). Você deve então certificar-se de que o valor de hash para todos os objetos invólucros que residem em um dicionário (ou outra estrutura baseada em hash) permaneça fixo enquanto o objeto estiver no dicionário (ou outra estrutura).

class ListWrapper:
    def __init__(self, the_list):
        self.the_list = the_list

    def __eq__(self, other):
        return self.the_list == other.the_list

    def __hash__(self):
        l = self.the_list
        result = 98767 - len(l)*555
        for i, el in enumerate(l):
            try:
                result = result + (hash(el) % 9999999) * 1001 + i
            except Exception:
                result = (result % 7777777) + i * 333
        return result

Observe que o cálculo do hash é complicado pela possibilidade de que alguns membros da lista possam ser não não-hasheável e também pela possibilidade de estouro aritmético.

Além disso, deve ser sempre o caso que se o1 == o2 (ou seja, o1.__eq__(o2) is True) então hash(o1) == hash(o2) (ou seja, o1.__hash__() == o2.__hash__()), independentemente de o objeto estar em um dicionário ou não. Se você não cumprir essas restrições, os dicionários e outras estruturas baseadas em hash se comportarão mal.

No caso de ListWrapper, sempre que o objeto invólucro estiver em um dicionário, a lista agrupada não deve ser alterada para evitar anomalias. Não faça isso a menos que esteja preparado para pensar muito sobre os requisitos e as consequências de não atendê-los corretamente. Considere-se avisado.

Por que lista.sort() não retorna a lista ordenada?

Em situações nas quais desempenho importa, fazer uma cópia da lista só para ordenar seria desperdício. Portanto, lista.sort() ordena a lista. De forma a lembrá-lo desse fato, isso não retorna a lista ordenada. Desta forma, você não vai ser confundido a acidentalmente sobrescrever uma lista quando você precisar de uma cópia ordenada mas também precisar manter a versão não ordenada.

Se você quiser retornar uma nova lista, use a função embutida sorted() ao invés. Essa função cria uma nova lista a partir de um iterável provido, o ordena e retorna. Por exemplo, aqui é como se itera em cima das chaves de um dicionário de maneira ordenada:

for key in sorted(mydict):
    ...  # use mydict[key] conforme quiser...

Como você especifica e aplica uma especificação de interface no Python?

Uma especificação de interface para um módulo fornecida por linguagens como C++ e Java descreve os protótipos para os métodos e funções do módulo. Muitos acham que a aplicação de especificações de interface em tempo de compilação ajuda na construção de programas grandes.

Python 2.6 adiciona um módulo abc que permite definir Classes Base Abstratas (ABCs). Você pode então usar isinstance() e issubclass() para verificar se uma instância ou classe implementa um ABC específico. O módulo collections.abc define um conjunto de ABCs úteis como Iterable, Container e MutableMapping

Para Python, muitas das vantagens das especificações de interface podem ser obtidas por uma disciplina de teste apropriada para componentes.

Um bom conjunto de testes para um módulo pode fornecer um teste de regressão e servir como uma especificação de interface do módulo e um conjunto de exemplos. Muitos módulos Python podem ser executados como um script para fornecer um simples “autoteste”. Mesmo módulos que usam interfaces externas complexas muitas vezes podem ser testados isoladamente usando emulações triviais da interface externa. Os módulos doctest e unittest ou estruturas de teste de terceiros podem ser usados para construir conjuntos de testes exaustivos que exercitam cada linha de código em um módulo.

Uma disciplina de teste apropriada pode ajudar a construir aplicações grandes e complexas no Python, assim como ter especificações de interface. Na verdade, pode ser melhor porque uma especificação de interface não pode testar certas propriedades de um programa. Por exemplo, espera-se que o método list.append() adicione novos elementos ao final de alguma lista interna; uma especificação de interface não pode testar se sua implementação list.append() realmente fará isso corretamente, mas é trivial verificar esta propriedade em um conjunto de testes.

Escrever conjuntos de testes é muito útil e você pode querer projetar seu código para torná-lo facilmente testável. Uma técnica cada vez mais popular, o desenvolvimento orientado a testes, exige a escrita de partes do conjunto de testes primeiro, antes de escrever qualquer parte do código real. É claro que o Python permite que você seja desleixado e nem escreva casos de teste.

Por que não há goto?

Na década de 1970, as pessoas perceberam que o goto irrestrito poderia levar a um código “espaguete” confuso, difícil de entender e revisar. Em uma linguagem de alto nível, também é desnecessário, desde que existam maneiras de ramificar (em Python, com instruções if e or, and e expressões if/else) e iterar (com instruções while e for, possivelmente contendo continue e break).

Também é possível usar exceções para fornecer um “goto estruturado” que funcione mesmo em chamadas de função. Muitos acham que as exceções podem convenientemente emular todos os usos razoáveis das construções go ou goto de C, Fortran e outras linguagens. Por exemplo:

class label(Exception): pass  # declara um label

try:
    ...
    if condition: raise label()  # goto label
    ...
except label:  # destino do goto
    pass
...

Isso não permite que você pule para o meio de um laço, mas isso geralmente é considerado um abuso de goto de qualquer maneira. Use com moderação.

Por que strings brutas (r-strings) não podem terminar com uma contrabarra?

Mais precisamente, eles não podem terminar com um número ímpar de contrabarras: a contrabarra não pareada no final escapa do caractere de aspa de fechamento, deixando uma string não terminada.

Strings brutas foram projetadas para facilitar a criação de entrada para processadores (principalmente mecanismos de expressão regular) que desejam fazer seu próprio processamento de escape de contrabarra. De qualquer forma, esses processadores consideram uma contrabarra incomparável como um erro, portanto, as strings brutas não permitem isso. Em troca, eles permitem que você transmita o caractere de aspas da string escapando dele com uma contrabarra. Essas regras funcionam bem quando r-strings são usadas para a finalidade pretendida.

Se você estiver tentando criar nomes de caminho do Windows, observe que todas as chamadas do sistema do Windows também aceitam barras:

f = open("/mydir/file.txt")  # funciona normalmente!

Se você estiver tentando construir um nome de caminho para um comando DOS, tente, por exemplo, algum desses

dir = r"\this\is\my\dos\dir" "\\"
dir = r"\this\is\my\dos\dir\ "[:-1]
dir = "\\this\\is\\my\\dos\\dir\\"

Por que o Python não tem uma instrução “with” para atribuição de atributos?

Python tem uma instrução with que envolve a execução de um bloco, chamando o código na entrada e na saída do bloco. Algumas linguagens têm uma construção desta forma:

with obj:
    a = 1               # equivalente a obj.a = 1
    total = total + 1   # obj.total = obj.total + 1

Em Python, tal construção seria ambígua.

Outras linguagens, como Object Pascal, Delphi, e C++, usam tipos estáticos, então é possível saber, de maneira não ambígua, que membro está sendo atribuído. Esse é o principal ponto da tipagem estática – o compilador sempre sabe o escopo de toda variável em tempo de compilação.

O Python usa tipos dinâmicos. É impossível saber com antecedência que atributo vai ser referenciado em tempo de execução. Atributos membro podem ser adicionados ou removidos de objetos dinamicamente. Isso torna impossível saber, de uma leitura simples, que atributo está sendo referenciado: um atributo local, um atributo global ou um atributo membro?

Por exemplo, pegue o seguinte trecho incompleto:

def foo(a):
    with a:
        print(x)

O trecho presume que a deve ter um atributo de membro chamado x. No entanto, não há nada em Python que diga isso ao interpretador. O que deveria acontecer se a for, digamos, um número inteiro? Se houver uma variável global chamada x, ela será usada dentro do bloco with? Como você pode ver, a natureza dinâmica do Python torna essas escolhas muito mais difíceis.

O principal benefício de with e recursos de linguagem semelhantes (redução do volume de código) pode, no entanto, ser facilmente alcançado em Python por atribuição. Em vez de:

function(args).mydict[index][index].a = 21
function(args).mydict[index][index].b = 42
function(args).mydict[index][index].c = 63

escreva isso:

ref = function(args).mydict[index][index]
ref.a = 21
ref.b = 42
ref.c = 63

Isso também tem o efeito colateral de aumentar a velocidade de execução porque as ligações de nome são resolvidas a tempo de execução em Python, e a segunda versão só precisa performar a resolução uma vez.

Propostas semelhantes que introduziriam sintaxe para reduzir ainda mais o volume do código, como o uso de um ‘ponto inicial’, foram rejeitadas em favor da explicitação (consulte https://mail.python.org/pipermail/python-ideas/2016-May/040070.html).

Por que os geradores não suportam a instrução with?

Por razões técnicas, um gerador usado diretamente como gerenciador de contexto não funcionaria corretamente. Quando, como é mais comum, um gerador é usado como um iterador executado até a conclusão, nenhum fechamento é necessário. Quando estiver, envolva-o como contextlib.closing(generator) na instrução with.

Por que dois pontos são necessários para as instruções de if/while/def/class?

Os dois pontos são obrigatórios primeiramente para melhorar a leitura (um dos resultados da linguagem experimental ABC). Considere isso:

if a == b
    print(a)

versus

if a == b:
    print(a)

Note como a segunda é ligeiramente mais fácil de ler. Note com mais atenção como os dois pontos iniciam o exemplo nessa resposta de perguntas frequentes; é um uso padrão em Português.

Outro motivo menor é que os dois pontos deixam mais fácil para os editores com realce de sintaxe; eles podem procurar por dois pontos para decidir quando a recuo precisa ser aumentada em vez de precisarem fazer uma análise mais elaborada do texto do programa.

Por que o Python permite vírgulas ao final de listas e tuplas?

O Python deixa você adicionar uma vírgula ao final de listas, tuplas e dicionários:

[1, 2, 3,]
('a', 'b', 'c',)
d = {
    "A": [1, 5],
    "B": [6, 7],  # a vírgula ao final é opcional, mas é bom estilo
}

Existem várias razões para permitir isso.

Quando você possui um valor literal para uma lista, tupla, ou dicionário disposta através de múltiplas linhas, é mais fácil adicionar mais elementos porque você não precisa lembrar de adicionar uma vírgula na linha anterior. As linhas também podem ser reordenadas sem criar um erro de sintaxe.

Acidentalmente omitir a vírgula pode levar a erros que são difíceis de diagnosticar. Por exemplo:

x = [
  "fee",
  "fie"
  "foo",
  "fum"
]

Essa lista parece ter quatro elementos, mas na verdade contém três: “fee”, “fiefoo” e “fum”. Sempre adicionar a vírgula evita essa fonte de erro.

Permitir a vírgula no final também pode deixar a geração de código programático mais fácil.