5
(1)

Desde Python 3, o strtipo usa representação Unicode. As strings Unicode podem ter até 4 bytes por caractere, dependendo da codificação, que às vezes pode ser cara da perspectiva da memória.

Para reduzir o consumo de memória e melhorar o desempenho, o Python usa três tipos de representações internas para cadeias de caracteres Unicode:

  • 1 byte por caractere (codificação Latin-1)
  • 2 bytes por caractere (codificação UCS-2)
  • 4 bytes por caractere (codificação UCS-4)

Ao programar em Python, todas as strings se comportam da mesma forma, e na maioria das vezes não percebemos nenhuma diferença. No entanto, a diferença pode ser notável e às vezes inesperada ao trabalhar com grandes quantidades de texto.

Para ver a diferença nas representações internas, podemos usar a sys.getsizeoffunção, que retorna o tamanho de um objeto em bytes:

>>>  import  sys 
>>>  string  =  'olá' 
>>>  sys . getsizeof ( string ) 
54 
>>>  codificação de 1 byte 
>>>  sys . getsizeof ( string + '!' ) - sys . getsizeof ( string ) 
1 
>>>  codificação de 2 bytes 
>>>  string2   =  '你' 
>>>  sys . getsizeof ( string2 + '好'. getsizeof ( string2 ) 
2 
>>>  sys . getsizeof ( seqüência2 ) 
76 
>>>  # 4 bytes codificação 
>>>  string3  =  '🐍' 
>>>  SYS . getsizeof ( string3 + '💻' ) - sys . getsizeof ( string3 ) 
4 
>>>  SYS . getsizeof ( string3 ) 
80

Como você pode ver, dependendo do conteúdo de uma string, o Python usa codificações diferentes. Observe que cada string no Python ocupa 49-80 bytes de memória adicionais, onde armazena informações suplementares, como hash, comprimento, comprimento em bytes, tipo de codificação e sinalizadores de string. É por isso que uma string vazia ocupa 49 bytes de memória.

Podemos recuperar a codificação diretamente de um objeto usando ctypes:

ctypes de importação

classe  PyUnicodeObject ( ctypes . Structure ): 
    # campos internos do objeto de string 
    _fields_  =  [( "ob_refcnt" ,  ctypes . c_long ), 
                ( "ob_type" ,  ctypes . c_void_p ), 
                ( "length" ,  ctypes . c_ssize_t ), 
                ( " hash " ,  ctypes . c_ssize_t ), 
                ( " interned " ,  ctypes . c_uint ,  2 ), 
                ("tipo" ,  ctypes . c_uint ,  3 ), 
                ( "compact" ,  ctypes . c_uint ,  1 ), 
                ( "ascii" ,  ctypes . c_uint ,  1 ), 
                ( "ready" ,  ctypes . c_uint ,  1 ), 
                # ... 
                # ... 
                ]


def  get_string_kind ( string ): 
    retorna  PyUnicodeObject . from_address ( id ( string )) . tipo
>>>  get_string_kind ( 'Hello' ) 
1 
>>>  get_string_kind ( '你好' ) 
2 
>>>  get_string_kind ( '🐍' ) 
4

Se todos os caracteres em uma cadeia puderem caber no intervalo ASCII, eles serão codificados usando a codificação Latin-1 de 1 byte. Basicamente, Latin-1 representa os primeiros 256 caracteres Unicode. Ele suporta muitos idiomas latinos, como inglês, sueco, italiano, norueguês e assim por diante. No entanto, ele não pode armazenar idiomas não latinos, como chinês, japonês, hebraico e cirílico. Isso ocorre porque seus pontos de código (índices numéricos) são definidos fora do intervalo de 1 byte (0-255).

>>>  ord ( 'a' ) 
97 
>>>  ord ( '你' ) 
20320 
>>>  ord ( '!' ) 
33

A maioria dos idiomas naturais populares pode caber na codificação de 2 bytes (UCS-2). A codificação de 4 bytes (UCS-4) é usada quando uma sequência contém símbolos especiais, emojis ou idiomas raros. Existem quase 300 blocos (intervalos) no padrão Unicode. Você pode encontrar os blocos de 4 bytes após o bloco 0xFFFF.

Vamos supor que temos 10GB de texto ASCII e queremos carregá-lo na memória. Se você inserir um único emoji em nosso texto, o tamanho de uma string aumentará pelo fator 4! Essa é uma enorme diferença que você pode encontrar na prática ao trabalhar com problemas de PNL.

Por que o Python não usa codificação UTF-8 internamente

A codificação Unicode mais conhecida e popular é a UTF-8, mas o Python não a usa internamente.

Quando uma sequência é armazenada na codificação UTF-8, cada caractere é codificado usando 1-4 bytes, dependendo do caractere que está representando. É uma codificação eficiente de armazenamento, mas tem uma desvantagem significativa. Como cada caractere pode variar em tamanho de bytes, não há como acessar aleatoriamente um caractere individual por índice sem verificar a sequência. Portanto, para executar uma operação simples, como string[5]no UTF-8, o Python precisaria verificar uma sequência até encontrar o caractere necessário. Codificações de comprimento fixo não têm esse problema. Para localizar um caractere pelo índice, o Python multiplica um número de índice pelo comprimento de um caractere (1, 2 ou 4 bytes).

Interno de strings

Ao trabalhar com cadeias vazias ou cadeias ASCII de um caractere, o Python usa internação de cadeias . Seqüências de caracteres internas atuam como singletons, ou seja, se você tiver duas seqüências de caracteres idênticas que são internadas, haverá apenas uma cópia delas na memória.

>>>  a  =  'olá' 
>>>  b  =  'mundo' 
>>>  a [ 4 ], b [ 1 ] 
( 'o' ,  'o' ) 
>>>  id ( a [ 4 ]),  id ( b [ 1 ]),  a [ 4 ]  é  b [ 1 ] 
( 4567926352 ,  4567926352 ,  True ) 
>>>  id ( '' )

ID da foto: 4545673904 >>>  id ( '' ) 
4545673904

Como você pode ver, as duas fatias de cadeia apontam para o mesmo endereço na memória. É possível porque as strings Python são imutáveis.

No Python, a cadeia interna de caracteres não se limita a caracteres ou cadeias vazias. Seqüências de caracteres criadas durante a compilação de código também podem ser internadas se o comprimento não exceder 20 caracteres.

Isso inclui:

  • nomes de função e classe
  • nomes de variáveis
  • nomes de argumentos
  • constantes (todas as strings definidas no código)
  • chaves de dicionários
  • nomes de atributos

Quando você pressiona enter no Python REPL, sua instrução é compilada no bytecode. É por isso que todas as sequências curtas no REPL também são internadas.

>>>  b  =  'teststring' 
>>>  id ( a ),  id ( b ),  a  é  b 
( 4569487216 ,  4569487216 ,  True ) 
>>>  a  =  'test' * 5 
> >>  b  =  'teste' * 5 
>>>  len ( a ),  id ( a ),  id ( b ), a  é  b
( 20 ,  4569499232 ,  4569499232 ,  Verdadeiro ) 
>>>  a  =  'teste' * 6 
>>>  b  =  'teste' * 6 
>>>  len ( a ),  id ( a ),  id ( b ),  a  é  b 
( 24 ,  4569479328 ,  4569479168 ,  falso )

Este exemplo não funcionará, porque essas seqüências de caracteres não são constantes:

>>>  aberto ( 'test.txt' , 'w' ) . escreva ( 'olá' ) 
5 
>>>  open ( 'test.txt' , 'r' ) . read () 
'olá' 
>>>  a  =  open ( 'test.txt' , 'r' ) . read () 
>>>  b  =  aberto ( 'test.txt' , 'r' ) . read () 
>>>  ( b ),  a  é  b 
( 4384934576 ,  4384934688 ,  Falso ) 
>>>  len ( a ),  id ( a ),  id ( b ),  a  é  b 
( 5 ,  4384934576 ,  4384934688 ,  False )

A técnica de internamento de strings salva dezenas de milhares de alocações de strings duplicadas. Internamente, a internação de strings é mantida por um dicionário global em que strings são usadas como chaves. Para verificar se já existe uma sequência idêntica na memória, o Python realiza uma operação de associação ao dicionário.

objeto unicode é quase 16.000 linhas de código C, portanto, há muitas otimizações pequenas que não são mencionadas neste artigo. Se você quiser aprender mais sobre Unicode em Python, recomendo que você leia PEPs sobre strings e verifique o código do objeto unicode.

Postagem original: https://rushter.com/blog/python-strings-and-memory/

O que você achou disso?

Clique nas estrelas

Média da classificação 5 / 5. Número de votos: 1

Nenhum voto até agora! Seja o primeiro a avaliar este post.

Compartilhar.

Sobre o autor

Tiago Silva

Full-Stack Developer e Software Engineer, especialista em Python e JavaScript, professor e palestrante de assuntos voltados à programação web, desktop e mobile, ciência de dados entre outros assuntos. Escritor dos Livros Python de A a Z e Flask de A a Z.

Deixe uma resposta