Para ser continuado. Começando em Python como o caso final de C ++. Parte 1/2 ".
Variáveis e tipos de dados
Agora que finalmente descobrimos a matemática, vamos decidir o que as variáveis devem significar em nossa linguagem.
No C ++, um programador tem uma opção: use variáveis automáticas colocadas na pilha ou mantenha valores na memória de dados do programa, colocando apenas ponteiros para esses valores na pilha. E se escolhermos apenas uma dessas opções para Python?
Obviamente, nem sempre podemos usar apenas os valores das variáveis, pois grandes estruturas de dados não cabem na pilha ou seu movimento constante na pilha cria problemas de desempenho. Portanto, usaremos apenas ponteiros no Python. Isso simplificará conceitualmente o idioma.
Então a expressão
a = 3
significa que criamos um objeto “3” na memória de dados do programa (a chamada “pilha”) e fizemos do nome “a” uma referência a ele. E a expressão
b = a
neste caso, significa que forçamos a variável "b" a se referir ao mesmo objeto na memória a que "a" se refere, ou seja, copiamos o ponteiro.
Se tudo é um ponteiro, quantos tipos de lista precisamos implementar em nosso idioma? Claro, apenas um é uma lista de indicadores! Você pode usá-lo para armazenar números inteiros, seqüências de caracteres, outras listas, o que for - afinal, esses são indicadores.
Quantos tipos de tabelas de hash precisamos implementar? (No Python, esse tipo é chamado de "dicionário" - dict
.) Um! Deixe associar ponteiros às chaves e ponteiros aos valores.
Portanto, não precisamos implementar em nossa linguagem uma grande parte da especificação C ++ - modelos, pois executamos todas as operações nos objetos, e os objetos sempre são acessíveis pelo ponteiro. Obviamente, os programas escritos em Python não precisam se limitar ao trabalho com ponteiros: existem bibliotecas como o NumPy, com as quais os cientistas trabalham com matrizes de dados na memória, como faria no Fortran. Mas a base da linguagem - expressões como "a = 3" - sempre funciona com ponteiros.
O conceito de "tudo é um ponteiro" também simplifica a composição dos tipos até o limite. Quer uma lista de dicionários? Basta criar uma lista e colocar dicionários lá! Você não precisa pedir permissão ao Python, não precisa declarar tipos adicionais, tudo funciona imediatamente.
Mas e se quisermos usar objetos compostos como chaves? A chave no dicionário deve ter um valor imutável; caso contrário, como procurar valores por ela? As listas estão sujeitas a alterações, portanto, não podem ser usadas nessa capacidade. Para tais situações, o Python possui um tipo de dados que, como uma lista, é uma sequência de objetos, mas, diferentemente de uma lista, essa sequência não muda. Esse tipo é chamado de tupla ou tuple
(pronunciada "tupla" ou "tupla").
As tuplas no Python resolvem um problema de longa data da linguagem de script. Se você não está impressionado com esse recurso, provavelmente nunca tentou usar linguagens de script para trabalhos sérios com dados, nos quais é possível usar apenas cadeias ou apenas tipos primitivos como chave nas tabelas de hash.
Outra possibilidade que as tuplas nos dão é retornar vários valores de uma função sem precisar declarar tipos de dados adicionais para isso, como você deve fazer em C e C ++. Além disso, para facilitar o uso desse recurso, o operador de atribuição recebeu a capacidade de descompactar automaticamente as tuplas em variáveis separadas.
def get_address(): ... return host, port host, port = get_address()
A descompactação tem vários efeitos colaterais úteis, por exemplo, a troca de valores de variáveis pode ser escrita da seguinte maneira:
x, y = y, x
Tudo é um ponteiro, o que significa que funções e tipos de dados podem ser usados como dados. Se você conhece o livro “Design Patterns” dos autores de “The Gang of Four”, lembre-se dos métodos complexos e confusos que ele oferece para parametrizar a escolha do tipo de objeto criado pelo seu programa em tempo de execução. De fato, em muitas linguagens de programação isso é difícil de fazer! No Python, todas essas dificuldades desaparecem, porque sabemos que uma função pode retornar um tipo de dados, que funções e tipos de dados são apenas links e links podem ser armazenados, por exemplo, em dicionários. Isso simplifica a tarefa até o limite.
David Wheeler disse: "Todos os problemas de programação são resolvidos criando um nível adicional de indireção". O uso de links no Python é o nível de indireção tradicionalmente usado para resolver muitos problemas em várias linguagens, incluindo C ++. Mas se for usado explicitamente lá, e isso complica os programas, no Python é usado implicitamente, de maneira uniforme em relação a dados de todos os tipos, e é fácil de usar.
Mas se tudo é um link, a que esses links se referem? Idiomas como C ++ têm muitos tipos. Vamos deixar no Python apenas um tipo de dados - um objeto! Especialistas no campo da teoria dos tipos balançam a cabeça com desaprovação, mas acredito que um tipo de dado de origem, do qual todos os outros tipos na linguagem são derivados, é uma boa idéia que garanta a uniformidade da linguagem e sua facilidade de uso.
Para conteúdos específicos da memória, várias implementações do Python (PyPy, Jython ou MicroPython) podem gerenciar a memória de maneiras diferentes. Mas, para entender melhor como a simplicidade e a uniformidade do Python são implementadas, para formar o modelo mental correto, é melhor recorrer à implementação de referência do Python em C chamada CPython, que pode ser baixada em python.org .
struct { struct _typeobject *ob_type; }
O que veremos no código fonte do CPython é uma estrutura que consiste em um ponteiro para informações sobre o tipo de uma determinada variável e uma carga útil que define o valor específico da variável.
Como as informações de tipo funcionam? Vamos nos aprofundar no código fonte do CPython novamente.
struct _typeobject { getattrfunc tp_getattr; setattrfunc tp_setattr; newfunc tp_new; freefunc tp_free; binaryfunc nb_add; binaryfunc nb_subtract; richcmpfunc tp_richcompare; }
Vemos indicadores de funções que fornecem todas as operações possíveis para um determinado tipo: adição, subtração, comparação, acesso a atributos, indexação, fatiamento etc. Essas operações sabem como trabalhar com a carga útil localizada na memória abaixo de um ponteiro para digitar informações, seja um número inteiro, sequência ou objeto de um tipo criado pelo usuário.
Isso é radicalmente diferente de C e C ++, no qual as informações de tipo estão associadas a nomes, não a valores de variáveis. No Python, todos os nomes estão associados aos links. O valor por referência, por sua vez, é do tipo. Essa é a essência das linguagens dinâmicas.
Para entender todos os recursos da linguagem, basta definir duas operações nos links. Um dos mais óbvios é copiar. Quando atribuímos um valor a uma variável, a um slot em um dicionário ou a um atributo de um objeto, copiamos os links. Esta é uma operação simples, rápida e completamente segura: copiar links não altera o conteúdo do objeto.
A segunda operação é uma chamada de função ou método. Como mostramos acima, um programa Python pode interagir com a memória apenas através de métodos implementados em objetos internos. Portanto, não pode causar um erro relacionado ao acesso à memória.
Você pode ter uma pergunta: se todas as variáveis contêm referências, como posso proteger o valor de uma variável das alterações passando sua função como parâmetro?
n = 3 some_function(n)
A resposta é que tipos simples em Python são imutáveis: eles simplesmente não implementam o método responsável por alterar seu valor. O imutável (imutável) int
, float
, tuple
ou str
fornece em linguagens como "tudo é um ponteiro" o mesmo efeito semântico que variáveis automáticas fornecem em C.
Tipos e métodos unificados simplificam o uso de programação generalizada, ou genéricos, tanto quanto possível. As funções min()
, max()
, sum()
e similares são incorporadas, não há necessidade de importá-las. E eles funcionam com todos os tipos de dados nos quais as operações de comparação para min()
e max()
são implementadas, adições para sum()
, etc.
Criar objetos
Descobrimos em termos gerais como os objetos devem se comportar. Agora vamos determinar como vamos criá-los. Esta é uma questão de sintaxe da linguagem. O C ++ suporta pelo menos três maneiras de criar um objeto:
- Automático, declarando uma variável desta classe:
my_class c(arg);
- Usando o
new
operador:
my_class *c = new my_class(arg);
- Factory, chamando uma função arbitrária que retorna um ponteiro:
my_class *c = my_factory(arg);
Como você provavelmente já adivinhou, tendo estudado a maneira de pensar dos criadores de Python nos exemplos acima, agora devemos escolher um deles.
Do mesmo livro, The Gangs of Four, aprendemos que uma fábrica é a maneira mais flexível e universal de criar objetos. Portanto, apenas esse método é implementado no Python.
Além da universalidade, esse método é bom, pois você não precisa sobrecarregar a linguagem com sintaxe desnecessária para garantir: uma chamada de função já está implementada em nossa linguagem e uma fábrica nada mais é do que uma função.
Outra regra para criar objetos em Python é a seguinte: qualquer tipo de dados é sua própria fábrica. Obviamente, você pode escrever qualquer número de fábricas personalizadas adicionais (que serão funções ou métodos comuns, é claro), mas a regra geral permanecerá válida:
Todos os tipos são chamados objetos e todos retornam valores de seu tipo, determinados pelos argumentos transmitidos na chamada.
Assim, usando apenas a sintaxe básica da linguagem, qualquer manipulação na criação de objetos, como os padrões “Arena” ou “Adaptação”, pode ser encapsulada, pois outra ótima idéia emprestada do C ++ é que o próprio tipo determina como isso acontece gerando seus objetos, como o new
operador trabalha para ele.
E quanto a NULL?
O manuseio de um ponteiro nulo aumenta a complexidade do programa, portanto, proibimos o uso de NULL. A sintaxe do Python torna impossível criar um ponteiro nulo. Duas operações elementares em ponteiros, sobre as quais falamos anteriormente, são definidas de tal maneira que qualquer variável aponta para algum objeto.
Como resultado, o usuário não pode usar o Python para criar um erro relacionado a um acesso à memória, como um erro de segmentação ou fora dos limites do buffer. Em outras palavras, os programas Python não são afetados pelos dois tipos de vulnerabilidades mais perigosos que ameaçam a segurança da Internet nos últimos 20 anos.
Você pode perguntar: "Se a estrutura das operações nos objetos não for alterada, como vimos anteriormente, como os usuários criarão suas próprias classes, com métodos e atributos não listados nessa estrutura?"
A mágica está no fato de que, para classes personalizadas, o Python possui uma "preparação" muito simples, com um pequeno número de métodos implementados. Aqui estão os mais importantes:
struct _typeobject { getattrfunc tr_getattr; setattrfunc tr_setattr; newfunc tp_new; }
tp_new()
cria uma tabela de hash para a classe de usuário, a mesma do tipo dict
. tp_getattr()
extrai algo dessa tabela de hash e tp_setattr()
, pelo contrário, coloca algo lá. Assim, a capacidade das classes arbitrárias de armazenar quaisquer métodos e atributos é fornecida não no nível das estruturas da linguagem C, mas em um nível superior - uma tabela de hash. (Obviamente, com exceção de alguns casos relacionados à otimização de desempenho.)
Modificadores de acesso
O que fazemos com todas as regras e conceitos criados em torno das palavras-chave C ++ private
e protected
? Python, sendo uma linguagem de script, não precisa deles. Já temos partes "protegidas" do idioma - esses são dados de tipos internos. Sob nenhuma circunstância o Python permitirá que um programa, por exemplo, manipule os bits de um número de ponto flutuante! Esse nível de encapsulamento é suficiente para manter a integridade do próprio idioma. Nós, criadores do Python, acreditamos que a integridade da linguagem é o único bom pretexto para ocultar informações. Todas as outras estruturas e dados do programa do usuário são considerados públicos.
Você pode escrever um sublinhado ( _
) no início de um nome de atributo de classe para avisar um colega: você não deve confiar nesse atributo. Mas o restante do Python aprendeu as lições do início dos anos 90: então muitos acreditavam que a principal razão pela qual escrevemos programas inchados, ilegíveis e com erros é a falta de variáveis privadas. Acho que os próximos 20 anos convenceram a todos na indústria de programação: variáveis privadas não são as únicas e estão longe de ser o remédio mais eficaz para programas inchados e com erros. Portanto, os criadores do Python decidiram nem se preocupar com variáveis privadas e, como você pode ver, elas não falharam.
Gerenciamento de memória
O que acontece com nossos objetos, números e seqüências de caracteres em um nível inferior? Como exatamente eles são armazenados na memória, como o CPython fornece acesso compartilhado a eles, quando e sob quais condições eles são destruídos?
E, nesse caso, escolhemos a maneira mais geral, previsível e produtiva de trabalhar com a memória: do lado do programa C, todos os nossos objetos são ponteiros compartilhados .
Com esse conhecimento em mente, as estruturas de dados que examinamos anteriormente na seção "Variáveis e tipos de dados" devem ser complementadas da seguinte maneira:
struct { Py_ssize_t ob_refcnt; struct { struct _typeobject *ob_type; } }
Portanto, todo objeto em Python (queremos dizer a implementação do CPython, é claro) tem seu próprio contador de referência. Depois de zero, o objeto pode ser excluído.
O mecanismo de contagem de links não depende de cálculos adicionais ou processos em segundo plano - um objeto pode ser destruído instantaneamente. Além disso, fornece alta localidade dos dados: geralmente, a memória começa a ser usada novamente imediatamente após ser liberada. O objeto recém-destruído provavelmente foi usado recentemente, o que significa que estava no cache do processador. Portanto, o objeto recém-criado permanecerá no cache. Esses dois fatores - simplicidade e localidade - tornam a contagem de links uma maneira muito produtiva de coleta de lixo.
(Como objetos em programas reais geralmente se referem um ao outro, o contador de referência em certos casos não pode cair para zero, mesmo quando os objetos não são mais usados no programa. Portanto, o CPython também possui um segundo mecanismo de coleta de lixo - um segundo, baseado em em gerações de objetos - aproximadamente tradução )
Erros de desenvolvedor Python
Tentamos desenvolver uma linguagem que fosse simples o suficiente para iniciantes, mas também atraente o suficiente para profissionais. Ao mesmo tempo, não fomos capazes de evitar erros no entendimento e no uso das ferramentas que nós mesmos criamos.
O Python 2, devido à inércia do pensamento associado às linguagens de script, tentou converter tipos de string, como faria uma linguagem com digitação fraca. Se você tentar combinar uma sequência de bytes com uma sequência em Unicode, o intérprete converte implicitamente a sequência de bytes em Unicode usando a tabela de códigos disponível no sistema e apresenta o resultado em Unicode:
>>> 'byte string ' + u'unicode string' u'byte string unicode string'
Como resultado, alguns sites funcionaram bem enquanto seus usuários usavam o inglês, mas eles produziram erros enigmáticos ao usar caracteres de outros alfabetos.
Este erro de design de linguagem foi corrigido no Python 3:
>>> b'byte string ' + u'unicode string' TypeError: can't concat bytes to str
Um erro semelhante no Python 2 estava relacionado à classificação "ingênua" de listas que consistem em elementos incomparáveis:
>>> sorted(['b', 1, 'a', 2]) [1, 2, 'a', 'b']
O Python 3, neste caso, deixa claro para o usuário que ele está tentando fazer algo não muito significativo:
>>> sorted(['b', 1, 'a', 2]) TypeError: unorderable types: int() < str()
Abusos
De vez em quando, os usuários às vezes abusam da natureza dinâmica da linguagem Python e, nos anos 90, quando as práticas recomendadas ainda não eram amplamente conhecidas, isso acontecia com frequência:
class Address(object): def __init__(self, host, port): self.host = host self.port = port
"Mas isso não é o ideal!" - Alguns disseram: - “E se a porta não diferir do valor padrão? De qualquer forma, gastamos um atributo de classe inteiro em seu armazenamento! ” E o resultado é algo como
class Address(object): def __init__(self, host, port=None): self.host = host if port is not None:
Portanto, objetos do mesmo tipo aparecem no programa, que, no entanto, não podem ser operados de maneira uniforme, pois alguns deles têm um determinado atributo, enquanto outros não! E não podemos tocar nesse atributo sem verificar sua presença com antecedência:
Atualmente, a abundância de hasattr()
, isinstance()
e outras introspecções é um sinal seguro de código incorreto e é considerado uma boa prática tornar atributos sempre presentes no objeto. Isso fornece uma sintaxe mais simples ao acessá-lo:
Portanto, os primeiros experimentos com atributos excluídos e adicionados dinamicamente terminaram, e agora analisamos as classes no Python da mesma maneira que no C ++.
Outro mau hábito do Python inicial era o uso de funções nas quais um argumento pode ter tipos completamente diferentes. Por exemplo, você pode pensar que pode ser muito difícil para o usuário criar uma lista de nomes de colunas a cada vez e permitir que ele os passe também como uma única linha, onde os nomes de colunas individuais são separados por, por exemplo, uma vírgula:
class Dataframe(object): def __init__(self, columns): if isinstance(columns, str): columns = columns.split(',') self.columns = columns
Mas essa abordagem pode dar origem a seus problemas. Por exemplo, e se um usuário acidentalmente nos fornecer uma linha que não se destina a ser usada como uma lista de nomes de colunas? Ou se o nome da coluna deve conter uma vírgula?
Além disso, esse código é mais difícil de manter, depurar e especialmente testar: nos testes, apenas um dos dois tipos suportados por nós pode ser verificado, mas a cobertura ainda será 100% e não testaremos o outro tipo.
Como resultado, chegamos à conclusão de que o Python permite que o usuário passe argumentos de qualquer tipo para funções, mas a maioria deles na maioria das situações usará uma função da mesma maneira que em C: passará um argumento do mesmo tipo para ela.
A necessidade de usar eval()
em um programa é considerada um erro de cálculo explícito da arquitetura. Provavelmente, você simplesmente não descobriu como fazer o mesmo de uma maneira normal. − , Jupyter notebook - − eval()
, Python ! , C++ .
, ( getattr()
, hasattr()
, isinstance()
) . , , , , : , , , !
: , . 20 , C++ Python. , , . .
, shared_ptr
TensorFlow 2016 2018 .
TensorFlow − C++-, Python- ( C++ − TensorFlow, ).

TensorFlow, shared_ptr
, . , .
C++? . , ? , , C++ Python!