Dataframes – Seleções e Ordenamento


Outras formas do construtor de dataframes

dataFrames

Se um dicionário aninhado (onde os valores associados às chaves externas são outros dicionários) é passado no construtor de um DataFrame o pandas interpretará as chaves externas como nomes das colunas e as chaves internas como índices das linhas. Na ausência de um par chave:valor em um ou mais dos dicionários o campo receberá o valor NaN.

» dic = {'Pedro': {'Prova 1': 5.4, 'Prova 3': 7.9},
                'Ana': {'Prova 1': 8.5, 'Prova 2': 9.7, 'Prova 3': 6.6},
               'Luna': {'Prova 2': 5.0, 'Prova 3': 7.0, 'Prova 4': 6.0}
             }
» dfNotas = pd.DataFrame(dic)
» dfNotas
↳
            Pedro    Ana   Luna
  Prova 1     5.4    8.5    NaN
  Prova 3     7.9    6.6    7.0
  Prova 2     NaN    9.7    5.0
  Prova 4     NaN    NaN    6.0

Se os nomes das linhas e das colunas forem fornecidos eles serão exibidos.

» dfNotas.index.name = 'Prova';
» dfNotas.columns.name = 'Aluno'
» dfNotas
↳
  Aluno      Pedro    Ana    Luna
  Prova             
  Prova 1     5.4     8.5     NaN
  Prova 3     7.9     6.6     7.0
  Prova 2     NaN     9.7     5.0
  Prova 4     NaN     NaN     6.0

Com frequência importamos de fontes externas, como faremos abaixo, uma fonte de dados e precisamos verificar sua integridade. Por ex., para encontrar elementos ausentes, preenchidos como NaN, usamos dataFrame.isnull() (o mesmo que pd.isnull(dataFrame)). Para saber quantos valores nulos existem usamos dataFrame.isnull().sum(), que fornece a soma dos campos True para cada campo.

» dfNotas.isnull()    # o mesmo que pd.isnull(dfNotas)
↳ Aluno       Pedro     Ana      Luna
  Prova             
  Prova 1     False     False    True
  Prova 3     False     False    False
  Prova 2     True      False    False
  Prova 4     True      True     False

» dfNotas.isnull().sum()
↳ Aluno
  Pedro    2
  Ana      1
  Luna     1
  dtype: int64

O método dataFrame.notna() (o mesmo que dataFrame.notnull() e o inverso de dataFrame.isnull()) retorna um dataframe booleano com True onde os campos não são nulos. Para inserir manualmente campos nulos usamos a constante pd.NaT e para eliminar linhas (ou colunas) contendo nulos aplicamos dataframe.dropna().

» # para eliminar linhas contendo nulos (o default é axis=0)
» dfNotas.dropna()
↳ Aluno     Pedro     Ana     Luna
  Prova             
  Prova 3     7.9     6.6      7.0
    
» # para eliminar colunas contendo nulos
» dfNotas.dropna(axis=1)
# todas são eliminadas pois existem NaN em todas as colunas

Evidentemente é necessário ter cuidado ao eliminar linhas ou colunas com NaN. Em muitos casos pode ser necessário substituir esses valores por outros, dependendo da aplicação. Para fazer a alteração no próprio frame use o parâmetro inplace = True.

Colunas e índices são objetos do tipo array e podem ser usados com alguns métodos de conjuntos.

» dfNotas.columns
↳ Index(['Pedro', 'Ana', 'Luna'], dtype='object', name='Aluno')

» dfNotas.index
↳ Index(['Prova 1', 'Prova 3', 'Prova 2', 'Prova 4'], dtype='object', name='Prova')

» 'Ana' in dfNotas.columns      # True
» 'Ann' in dfNotas.columns      # False
» 'Prova 5' in dfNotas.index    # False

O mesmo ocorre se o dicionário contiver Series como valores, sendo as chaves usadas como nomes das colunas e os índices das series usados como índices das linhas.

» serie1 = pd.Series([1, 2, 3, 4], index=['a', 'b', 'c', 'd'])
» serie2 = pd.Series([5, 6, 7, 8], index=['a', 'b', 'c', 'd'])
» serie3 = pd.Series([9, 0, -1, -2], index=['a', 'b', 'c', 'd'])

» dic = {'A':serie1, 'B':serie2, 'C':serie3 }
» pd.DataFrame(dic)
↳ 
      A   B   C
  a   1   5   9
  b   2   6   0
  c   3   7  -1
  d   4   8  -2

Dataframes podem ser criados recebendo Series no construtor.

» disciplinas = pd.Series(['Matemática', 'Física', 'História', 'Geografia'])
» notas = pd.Series([9.0, 5.4, 7.7, 8.9])
» df = pd.DataFrame({'Disciplina':disciplinas, 'Notas': notas})
» df
↳     Disciplina   Notas
  0   Matemática     9.0
  1       Física     5.4
  2     História     7.7
  3    Geografia     8.9

Outros objetos podem ser passados como argumento no construtor:

  • Ndarray (do NumPy) de 2 dimensões,
  • dicionário de arrays, listas ou tuples (todas as sequências devem ter o mesmo comprimento),
  • dicionários de arrays NumPy, de Series ou de outros dicionários,
  • listas de dicionários, Series, listas ou tuplas,
  • Series ou outro dataframe.

Tratamento de dados usando pandas.dataframe

Para os testes e demonstrações que se seguem vamos usar dados reais para demonstrar algumas funcionalidades úteis dos pandas.dataframes.

Fonte de dados

Para realizar os teste com dataframes vamos utilizar os dados encontrados no Gapminder nessa url: 08_gap-every-five-years.tsv. Esse é um arquivo contendo dados com um registro em cada linha e os valores na linha separados por tabs, (tabulação). Esse arquivo pode ser baixado para o seu computador e depois importado para um dataframe ou, como usamos abaixo, importada diretamente do site de Jennifer Bryan (jennybc): Gapminder, no Github.

O arquivo original tem o seguinte formato,

country      continent       year   lifeExp        pop     gdpPercap
Afghanistan       Asia       1952    28.801    8425333   779.4453145
Afghanistan       Asia       1957    30.332    9240934   820.8530296
Afghanistan       Asia       1962    31.997   10267083     853.10071
...

onde os espaços entre valores são tabulações (\t, no python). A primeira linha contém os ‘headers’ ou títulos das colunas. Traduziremos esses títulos da seguinte forma: country ⟼ pais, continent ⟼ continente, year ⟼; ano, lifeExp ⟼ expVida (expectativa de vida), pop ⟼ populacao, gdpPercap ⟼ pibPercap (produto interno bruto, percapita).

» import pandas as pd
» import numpy as np
» # Usando arquivo encontrado no Gapminder
» url =(
      'https://raw.githubusercontent.com/jennybc/'
      'gapminder/master/data-raw/08_gap-every-five-years.tsv'
       )
» url
↳ 'https://raw.githubusercontent.com/jennybc/gapminder/master/data-raw/08_gap-every-five-years.tsv'

» # criamos o dataframe dfPaises. O arquivo importado tem campos separados por tabs 
» dfPaises = pd.read_csv(url, sep='\t')

» # o dataframe tem 1704 linhas e 6 colunas
» dfPaises.shape
↳ (1704, 6)

» dfPaises.head()
↳
          country   continent    year   lifeExp        pop     gdpPercap
  0   Afghanistan        Asia    1952    28.801    8425333    779.445314
  1   Afghanistan        Asia    1957    30.332    9240934    820.853030
  2   Afghanistan        Asia    1962    31.997    10267083   853.100710
  3   Afghanistan        Asia    1967    34.020    11537966   836.197138
  4   Afghanistan        Asia    1972    36.088    13079460   739.981106
  1704 rows × 6 columns 
» # renomeando os campos para nomes em português
» dfPaises.rename(columns={'country':'pais',
                           'continent':'continente',
                           'year':'ano',
                           'lifeExp':'expVida',
                           'pop':'populacao',
                           'gdpPercap':'pibPercap',
                          }, inplace=True)
» # ficamos assim
» dfPaises.columns
↳ Index(['pais', 'continente', 'ano', 'expVida', 'populacao', 'pibPercap'], dtype='object')

» # para reordenar as colunas em sua exibição
» dfPaises = dfPaises[['continente', 'pais', 'ano', 'expVida', 'populacao', 'pibPercap']]

Podemos obter uma visão geral do conjunto de dados importados usando dois métodos. dataframe.info() retorno os nomes das colunas, quantos valores não nulos, seus dtypes, e memória usada nesse armazenamento. Por aí vemos que nossos dados não possuem valores nulos. Caso esses existissem eles teriam que ser localizados e tratados devidamente. O método df.describe() retorna um dataframe contendo a contagem count dos valores (nesse caso, o número de linhas), a média mean desses valores, o desvio padrão std, o valor mínimo e máximo, min, max e os quartis em 25%, 50%, 75%.

» dfPaises.info()
↳ <class 'pandas.core.frame.DataFrame'>
  RangeIndex: 1704 entries, 0 to 1703
  Data columns (total 6 columns):
   #   Column      Non-Null Count  Dtype  
  ---  ------      --------------  -----  
   0   continente  1704 non-null   object 
   1   pais        1704 non-null   object 
   2   ano         1704 non-null   int64  
   3   expVida     1704 non-null   float64
   4   populacao   1704 non-null   int64  
   5   pibPercap   1704 non-null   float64
  dtypes: float64(2), int64(2), object(2)
  memory usage: 80.0+ KB

» dfPaises.describe()
↳ 
                   ano          expVida         populacao         pibPercap
 count      1704.00000      1704.000000      1.704000e+03       1704.000000
  mean      1979.50000        59.474439      2.960121e+07       7215.327081
  std         17.26533        12.917107      1.061579e+08       9857.454543
  min       1952.00000        23.599000      6.001100e+04        241.165876
  25%       1965.75000        48.198000      2.793664e+06       1202.060309
  50%       1979.50000        60.712500      7.023596e+06       3531.846988
  75%       1993.25000        70.845500      1.958522e+07       9325.462346
  max       2007.00000        82.603000      1.318683e+09     113523.132900

Gravação e recuperação de dados em arquivos pickle

Após a verificação de integridade dos dados e a realização das alterações básicas necessárias é boa ideia salvar em disco o dataframe nesse momento. Para isso usamos pandas.to_pickle(dfFrame, 'nomeArquivo.pkl'), gravando um arquivopickle. Para recuperá-lo em qualquer momento usamos dfPaises = pandas.read_pickle('./dados/dataframePaises.pkl').

» # gravando um arquivo pickle
» pd.to_pickle(dfPaises, './dados/dataframePaises.pkl')    

» # mais tarde esse dataframe pode ser recuperado
» del dfPaises   # para limpar essa variável
» dfPaises = pd.read_pickle('./dados/dataframePaises.pkl')
» # o dataframe é recuperado

Seleção e filtragem

As principais formas de seleção de um ou mais valores de um dataframe são os métodos dataframe.loc(), dataframe.iloc(), dataframe.at e dataframe.iat. Um subconjunto de dados do dataframe, seja por seleção de linhas, colunas ou ambas, é denominado de fatia ou slice.

A principal diferença entre loc (at) e iloc (iat) é a seguinte: loc é baseado em labels ou nomes das linhas ou colunas, enquanto iloc é baseado nos índices numéricos (mesmo que tenham nomes) sempre com base 0.

  • dataframe.at[row_label, column_label]
  • dataframe.iat[row_position, column_position]
  • dataframe.loc[row_label, column_label]
  • dataframe.iloc[row_position, column_position]

Na tabela abaixo nos referiremos a um dataframe nomeado como df. (S) se refere a uma Series retornada, (D) a um dataframe.

Operações df.iat e df.at retorna: (índices são posições linhas/colunas)
df.iat[m,n] elemento da m-ésima linha, n-ésima coluna
df.at[lblLinha, lblColuna] elemento linha/coluna relativas aos labels lblLinha/lblColuna
Operações df.iloc retorna: (índices são posições das linhas/colunas)
df.iloc[n] n-ésima linha (S)
df.iloc[[n]] n-ésima linha (D)
df.iloc[-n] n-ésima linha, contando do final
df.iloc[i,j:, n] linhas i, até j (exclusive), coluna n (S)
df.iloc[[i,j,k]:[m,n,o]] linhas i, j, k, colunas m, n, o
df.iloc[:, n] n-ésima coluna (S)
df.iloc[:, [n]] n-ésima coluna (D)
df.iloc[:,-1] última coluna
df.iloc[i:j,m:n] linhas i até j (exclusive), colunas m até n (inclusive)
Operações df.loc retorna: (índices linhas/colunas se referem aos seus labels)
df.loc[n] linha de índice n (S)
df.loc[[n]] linha de índice n (D)
df.loc[:] todas as linhas e colunas (D)
df.loc[:, 'col'] todas as linhas, coluna ‘col’ (S)
df.loc[:, ['col']] todas as linhas, coluna ‘col’ (D)
df.loc[:, ['col1', 'col2']] todas as linhas, colunas ‘col1’ e ‘col2’ (D)
df.loc[i:j, ['col1', 'col2']] linhas com índices de i até i (inclusive), colunas ‘col1’ e ‘col2’ (D)
df.loc[[i,j,k] , ['col1', 'col2']] linhas com índices i, j, k, colunas ‘col1’ e ‘col2’ (D)
df.loc[i:j, 'col1':'coln']] linhas com índices i até j (inclusive), colunas ‘col1’ até ‘coln’ (inclusive) (D)
Atalhos o mesmo que
df['col1'] ou df.col1 df.loc[:, ‘col1’]] (S)
df[['col1', 'col2']] df.loc[:, [‘col1’, ‘col2’]] (D)

Em todos esses métodos uma exceção de KeyError é levantada se um índice ou label não existir na dataframe.

Se o index da linha coincidir com sua posição então df.loc[n] e df.iloc[n] serão as mesmas linhas. Isso nem sempre é verdade, como se verá abaixo com o reordenamento das linhas.

São incorretas as sintaxes: df.loc[-n], df.loc[:, n], df.loc[:, [n]] com n numérico pois os labels devem ser fornecidos.

Exemplos de consultas e seleções

dataframe.iloc()

Para outros exemplos vamos usar o dataframe já carregado, dfPaises, para fazer consultas e seleções, primeiro usando dataframe.iloc(). Lembramos que a contagem de índices sempre se inicia em 0:

» # lembrando que dfPaises.iloc[[0]] é um dataframe, dfPaises.iloc[0] é uma Series
» # primeira linha, pelo índice    
» dfPaises.iloc[[0]]
↳    continente            pais    ano    expVida    populacao     pibPercap
  0        Asia     Afghanistan   1952     28.801      8425333    779.445314

» # última linha, pelo índice    
» dfPaises.iloc[[-1]]
↳         continente          pais     ano    expVida    populacao     pibPercap
  1703        Africa      Zimbabwe    2007     43.487     12311143    469.709298    

» # linhas 15 até 20 (exclusive), colunas 2 até 5 (exclusive)
» dfPaises.iloc[15:20, 2:5]
↳      ano   expVida  populacao
  15  1967     66.22    1984060
  16  1972     67.69    2263554
  17  1977     68.93    2509048
  18  1982     70.42    2780097
  19  1987     72.00    3075321

» # linhas 1, 3, 5 , colunas 2, 5
» dfPaises.iloc[[1,3,5],[2,5]]
↳     ano   pibPercap
  1  1957   820.853030
  3  1967   836.197138
  5  1977   786.113360

» # linhas 1, 3, 5, última coluna
» dfPaises.iloc[[1,3,5],-1]
↳ 1    820.853030
  3    836.197138
  5    786.113360
  Name: pibPercap, dtype: float64

» # todas as linhas, coluna 3
» dfPaises.iloc[:, [3]].head()
↳     expVida
  0    28.801
  1    30.332
  2    31.997
  3    34.020
  4    36.088

» # linhas 0, 3, 6, 24; colunas 0, 3, 5
» dfPaises.iloc[[0,3,6,24], [0,3,5]]
↳    continente   expVida    pibPercap
  0        Asia    28.801   779.445314
  3        Asia    34.020   836.197138
  6        Asia    39.854   978.011439
  24     Africa    43.077  2449.008185

A seleção das linhas nos dois métodos é diferente. Em dataframe.loc[m,n] linhas com labels de m até n (inclusive) são selecionadas. Em dataframe.iloc[m,n] são selecionadas linhas com índices (numéricos) de m até n (exclusive).

» # iloc[m,n] exibe linhas m até n (exclusive)
» dfPaises.iloc[1:2]
↳   continente         pais   ano  expVida  populacao   pibPercap
  1       Asia  Afghanistan  1957   30.332    9240934   820.85303    

» # loc[m:n] exibe linhas m até n (inclusive)
» dfPaises.loc[1:2]
↳     continente          pais    ano   expVida   populacao    pibPercap
  1         Asia   Afghanistan   1957    30.332     9240934    820.85303
  2         Asia   Afghanistan   1962    31.997    10267083    853.10071
dataframe.loc()

Os próximos testes são feitos com dataframe.loc(), que deve receber os labels como índices.

» # todas as linhas, só colunas 'ano' e 'populacao' (limitadas por head())
» dfPaises.loc[:,['ano','populacao']].head()
↳        ano     populacao
  0     1952       8425333
  1     1957       9240934
  2     1962      10267083
  3     1967      11537966
  4     1972      13079460

» # linhas 3 até 6 (inclusive), só colunas 'ano' e 'expVida'
» dfPaises.loc[3:6,['ano', 'expVida']]
↳        ano     expVida
  3     1967      34.020
  4     1972      36.088
  5     1977      38.438
  6     1982      39.854

» # todas as linhas, só colunas 'ano' (restritas por head())
» dfPaises.loc[:, 'ano'].head()
↳ 0    1952
  1    1957
  2    1962
  3    1967
  4    1972

Métodos df.loc, df.iloc, df.at e df.iat

Para explorar um pouco mais a diferença no uso de df.loc e df.iloc vamos criar um dataframe bem simples e sem valores nulos.

» dic = {'Pedro': {'Prova 1': 5.4, 'Prova 2': 6.2, 'Prova 3': 7.9},
         'Ana':  {'Prova 1': 8.5, 'Prova 2': 9.7, 'Prova 3': 6.6},
         'Luna': {'Prova 1': 5.0, 'Prova 2': 7.0, 'Prova 3': 4.3}
        }
» dfNotas = pd.DataFrame(dic)
» dfNotas
↳           Pedro     Ana    Luna
  Prova 1     5.4     8.5     5.0
  Prova 2     6.2     9.7     7.0
  Prova 3     7.9     6.6     4.3

df.loc e df.at usa labels de linhas e colunas.
df.iloc e df.iat usa números (índices) de linhas e colunas.

Nos comentários listamos seleções usando df.iloc para se obter o mesmo retorno.

» dfNotas.loc['Prova 1','Luna']             # dfNotas.iloc[0,2]
↳ 5.0

» dfNotas.loc['Prova 1']                    # dfNotas.iloc[0] (Series)
↳ Pedro    5.4
  Ana      8.5
  Luna     5.0

» dfNotas.loc[['Prova 1']]                  # dfNotas.iloc[[0]] (dataframe)
↳           Pedro   Ana   Luna
  Prova 1     5.4   8.5    5.0

» dfNotas.loc[['Prova 1','Prova 2']]        # dfNotas.iloc[0:2] (dataframe)
↳           Pedro    Ana   Luna
  Prova 1     5.4    8.5    5.0
  Prova 2     6.2    9.7    7.0

» dfNotas.loc['Prova 1': 'Prova 3']         # dfNotas.iloc[0:3] (dataframe)
↳           Pedro    Ana   Luna
  Prova 1     5.4    8.5    5.0
  Prova 2     6.2    9.7    7.0
  Prova 3     7.9    6.6    4.3

» dfNotas.loc[['Prova 1'],['Ana','Luna']]   # dfNotas.iloc[[0],[1,2]]  (dataframe)
↳           Ana   Luna
  Prova 1   8.5    5.0

» dfNotas.loc['Prova 1':'Prova 3', 'Pedro':'Luna']   # dfNotas.iloc[0:3,0:3] (dataframe)
↳           Pedro    Ana    Luna
  Prova 1     5.4    8.5     5.0
  Prova 2     6.2    9.7     7.0
  Prova 3     7.9    6.6     4.3

» dfNotas.loc[:,['Luna']]                   # dfNotas.iloc[:,[2]]
↳           Luna
  Prova 1    5.0
  Prova 2    7.0
  Prova 3    4.3

Observe que em dfNotas.iloc[0:3,0:3] são selecionadas as linhas de índices 0, 1 e 2 e colunas 0, 1 e 2.

Análogos à df.loc e df.iloc temos, respectivamente, df.at[lblLinha, lblColuna] e df.iat[m,n] onde lblLinha, lblColuna se referem aos labels e m, n aos índices das linhas/colunas. Ambos recebem um par e retornam um único valor do dataframe. Quando aplicados em uma Series iat e at recebem um único índice/label localizador de posição.

» dfNotas.iat[2,1]
↳ 6.6
» dfNotas.iloc[0].iat[1]                 # o mesmo que dfNotas.loc['Prova 1'].iat[1]
↳ 8.5
» dfNotas.at['Prova 1', 'Luna']
↳ 5.0
» dfNotas.loc['Prova 1'].at['Ana']       # o mesmo que dfNotas.loc['Prova 1'].iat[1]
↳ 8.5

Nenhuma das duas formas de seleção de uma slice (.loc ou .iloc) copiam um dataframe por referência, como ocorre com numPy.ndarrays. Por exemplo, df = dfNotas.iloc[:,[2]] é uma cópia da 3ª coluna, e não uma referência ou view. Ela pode ser alterada sem que o dataframe original seja modificado. Se um novo valor for atribuído ao slice diretamente, no entanto, o dataframe fica alterado.

» df = dfNotas.iloc[:,[2]]
» df.Luna = 10
» display(df,dfNotas)
↳           Luna
  Prova 1     10
  Prova 2     10
  Prova 3     10

↳           Pedro   Ana   Luna
  Prova 1     5.4   8.5    5.0
  Prova 2     6.2   9.7    7.0
  Prova 3     7.9   6.6    4.3

» # no entanto se o slice receber atribuição direta o dataframe fica alterado
» dfNotas.iloc[:,[2]] = 10

» dfNotas
↳           Pedro    Ana    Luna
  Prova 1     5.4    8.5    10.0
  Prova 2     6.2    9.7    10.0
  Prova 3     7.9    6.6    10.0

» # para inserir valores diferentes outro dataframe de ser atribuído ao slice
» dic = {'Luna': {'Prova 1': 8.5, 'Prova 2': 7.9, 'Prova 3': 10}}
» dfLuna = pd.DataFrame(dic)
» dfNotas.iloc[:,[2]] = dfLuna

» dfNotas
↳           Pedro    Ana   Luna
  Prova 1     5.4    8.5    8.5
  Prova 2     6.2    9.7    7.9
  Prova 3     7.9    6.6   10.0

» # alternativamente, um np.array com shape apropriado pode ser atribuído ao slice
» arrLuna =np.array([2.3, 4.5, 5.6]).reshape(3,1)
» dfNotas.iloc[:,[2]] = arrLuna
» dfNotas
↳          Pedro   Ana   Luna
  Prova 1    5.4   8.5    2.3
  Prova 2    6.2   9.7    4.5
  Prova 3    7.9   6.6    5.6

Na atribuição dfNotas.iloc[:,[2]] = 10 houve o broadcasting de 10 para uma forma compatível com o slice.

Para que a atribuição seja bem sucedida, sem necessidade de broadcasting, um objeto de mesmo formato deve ser atribuído. No caso dfNotas.iloc[:,[2]].shape = dfLuna.shape = (3, 1) (3 linhas, 1 coluna). O mesmo ocorre com a atribuição de um array do numpy.

Manipulando linhas e colunas

Um array booleano pode ser passado como índice de um dataframe. Apenas as linhas correspondentes ao índice True será exibida. Alguns métodos de string estão disponíveis para testes em campos, como df['campo'].str.startswith('str') e df['campo'].str.endswith('str') (começa e termina com). O teste df['campo'].isin(['valor1', 'valor2'])] retorna True se os campos estão contidos na lista.

Para os exemplos usamos o dataframe dfPaises.

» # seleção por array booleano
» dfPaises.loc[dfPaises['ano'] == 2002].head(3)
↳      continente         pais   ano   expVida   populacao    pibPercap
  10         Asia  Afghanistan   2002   42.129    25268405   726.734055
  22       Europe      Albania   2002   75.651     3508512  4604.211737
  34       Africa      Algeria   2002   70.994    31287142  5288.040382

» # quais os paises tem nome começados com 'Al'
» dfPaises.loc[dfPaises['pais'].str.startswith('Al')]['pais'].unique()
↳ array(['Albania', 'Algeria'], dtype=object)

» # quais os paises tem nome terminados em 'm'
» dfPaises.loc[dfPaises['pais'].str.endswith('m')]['pais'].unique()
↳ array(['Belgium', 'United Kingdom', 'Vietnam'], dtype=object)

» # quantas linhas se referem à 'Europe' e 'Africa'
» dfPaises.loc[dfPaises['continente'].isin(['Europe', 'Africa'])].shape[0]
↳ 984

» dfPaises.loc[(dfPaises['continente']=='Africa') & (dfPaises['ano']==1957)].head(4)
↳       continente         pais     ano    expVida    populacao      pibPercap
  25        Africa     Algeria    1957     45.685     10270856    3013.976023
  37        Africa      Angola    1957     31.999      4561361    3827.940465
  121       Africa       Benin    1957     40.358      1925173     959.601080
  157       Africa    Botswana    1957     49.618       474639     918.232535


» # paises e anos com população < 7000 ou expectativa de vida > 82
» dfPaises.loc[(dfPaises['populacao'] < 70000) | (dfPaises['expVida'] > 82)][['ano','pais']]
↳          ano    pais
  420     1952    Djibouti
  671     2007    Hong Kong, China
  803     2007    Japan
  1296    1952    Sao Tome and Principe
  1297    1957    Sao Tome and Principe
  1298    1962    Sao Tome and Principe
Operador significa
& and, e
| or, ou
~ not, negação

O método arr.unique() acima foi aplicado para ver quais os países satisfazem as condições, sem repetições. arr.shape é uma tupla (número linhas, número colunas). Os últimos exemplos fazem testes compostos usando os operadores & (and, e lógico) e | (or, ou lógico).

Se nenhum campo for submetido ao teste lógico todos os valores do dataframe são usados. O mesmo ocorre com a aplicação de uma função, como mostrado para uma função lambda.

» # novos teste com loc e iloc
» dic = {'Pedro': {'Prova 1': 5.4, 'Prova 2': 6.2, 'Prova 3': 7.9},
         'Ana':  {'Prova 1': 8.5, 'Prova 2': 9.7, 'Prova 3': 6.6},
         'Luna': {'Prova 1': 5.0, 'Prova 2': 7.0, 'Prova 3': 4.3}
          }
» dfNotas = pd.DataFrame(dic)
» dfNotas
↳           Pedro     Ana    Luna
  Prova 1     5.4     8.5     5.0
  Prova 2     6.2     9.7     7.0
  Prova 3     7.9     6.6     4.3

» # o teste retorna um df com o mesmo shape que dfNotas
» dfNotas > 6
↳             Pedro    Ana    Luna
  Prova 1     False   True   False
  Prova 2      True   True    True
  Prova 3      True   True   False

» # os campos do df são filtrados pelo df booleano
» dfNotas[dfNotas > 6]
↳           Pedro     Ana    Luna
  Prova 1     NaN     8.5     NaN
  Prova 2     6.2     9.7     7.0
  Prova 3     7.9     6.6     NaN

Funções lambda

Uma função pode ser aplicada sobre elementos de uma coluna específica ou sobre todas as colunas. Veremos mais tarde detalhes sobre o uso de dataframe.apply().

» dfNotas
↳           Pedro     Ana    Luna
  Prova 1     5.4     8.5     5.0
  Prova 2     6.2     9.7     7.0
  Prova 3     7.9     6.6     4.3

» # uma função aplicada à todos os elementos do df
» dfNotas.apply(lambda x: x**2)
↳            Pedro     Ana     Luna
  Prova 1    29.16   72.25    25.00
  Prova 2    38.44   94.09    49.00
  Prova 3    62.41   43.56    18.49

Funções lambda que retornam valores booleanos podem ser usadas para filtragem dos campos de um dataframe. No exemplo dfPaises['pais'].apply(lambda x: len(x)) == 4 retorna True para as linhas onde o campo pais tem comprimento de 4 letras.

» dfPaises.loc[dfPaises['pais'].apply(lambda x: len(x)) == 4].head(2)
↳      continente    pais    ano   expVida   populacao     pibPercap
  264      Africa    Chad   1952    38.092     2682462   1178.665927
  265      Africa    Chad   1957    39.881     2894855   1308.495577

# são os países com nomes de 4 letras:
» set(dfPaises.loc[dfPaises['pais'].apply(lambda x: len(x)) == 4]['pais'])
↳ {'Chad', 'Cuba', 'Iran', 'Iraq', 'Mali', 'Oman', 'Peru', 'Togo'}

# o mesmo que
# dfPaises.loc[dfPaises['pais'].apply(lambda x: len(x)) == 4]['pais'].unique()  # (um array)

O seletor pode ser composto de mais testes, ligados pelos operadores lógicos & e |.

» # paises/anos com nomes compostos por mais de 2 palavras e população acima de 50 milhões
» dfPaises.loc[(dfPaises['pais'].apply(lambda x: len(x.split(' '))) > 2) &
               (dfPaises['populacao']>50_000_000)]

↳      continente    pais                  ano   expVida   populacao    pibPercap
  334      Africa    Congo, Dem. Rep.     2002    44.966    55379852   241.165876
  335      Africa    Congo, Dem. Rep.     2007    46.462    64606759   277.551859

Ordenamento com Sort

Para ordenar um dataframe podemos usar o método sort, com a seguinte sintaxe:

dataframe.sort_values(by=['campo'], axis=0, ascending=True, inplace=False)
onde
by pode ser uma string ou lista com o nome ou nomes dos campos, na prioridade de ordenamento,
axis{0 ou ‘index’, 1 ou ‘columns’} default 0, indica o eixo a ordenar,
ascending=True/False se ordenamento é crescente/decrescente.

Existem vários outros parâmetros para o controle de ordenamentos, como pode ser lido no API reference do pandas.

Muitas informações importantes sobre um conjunto de dados podem ser obtidas apenas pela inspecção dos dados. Por exemplo, podemos encontrar respostas para:

  • que país do mundo teve, em qualquer ano, o PIB percapita mais elevado?
  • no ano de 2007 (o último de nossa lista), quais são os 5 países com maior população, e quais são os 5 com PIB mais baixo, no mundo?
  • no ano de 2002, quantos países no mundo tinham PIB percapita acima e abaixo da média?
# encontramos o maior pib percapita e a linha que corresponde a ele   
» dfMax = dfPaises[dfPaises['pibPercap']==dfPaises['pibPercap'].max()]
» dfMax
↳     continente    pais   ano   expVida  populacao    pibPercap
  853       Asia  Kuwait  1957    58.033     212846  113523.1329

» # para formatar uma resposta amigável
» ano = dfMax['ano'].values[0]
» pais = dfMaxPib['pais'].values[0]
» pibP = dfMaxPib['pibPercap'].values[0]

» print('O PIB percapita máximo foi de {} e ocorreu no {} em {}.'.format(pibP, pais, ano))
↳ O PIB percapita máximo foi de 113523.1329 e ocorreu no Kuwait em 1957.

» # ordenando em ordem decrescente
» dfPaises.sort_values(by=['pibPercap'], ascending=False).iloc[[0]]
↳      continente     pais   ano  expVida  populacao     pibPercap
  853        Asia   Kuwait  1957   58.033     212846   113523.1329

Observe que dfMax['ano'] é uma Series que, se exposta diretamente, não contém apenas o ano. Por isso extraimos dele o valor, 1º campo: dfMax['ano'].value[0]. Idem para pais e pibPercap.

Claro que podemos também ordenar o dataframe em ordem descrecente no campo pibPercap e pegar apenas a 1ª linha.
dataframe.iloc[[0]] foi usado para pegar a 1ª linha, cujo índice é 853. A mesma linha seria retornada com dataframe.loc[[853]], o que mostra, mais uma vez, a diferença entre df.loc e df.iloc.

Para encontrar os 5 países com maior população em 2007 usamos a mesma técnica de ordenamento. Primeiro filtramos pelo ano = 2007, ordenamos por população, ordem inversa, e pegamos os 5 primeiros. Para exibir o resultado podemos transformar o dataframe em string, sem os índices.

Para encontrar os 5 países com maior população em 2007, e os 5 com menor PIB:

» # dataframe com 5 maiores populações em 2007
» popMax = dfPaises[dfPaises['ano']==2007].sort_values(by=['populacao'], ascending=False).head()

» print(popMax[['pais','populacao']].to_string(index=False))
↳          pais  populacao
          China 1318683096
          India 1110396331
  United States  301139947
      Indonesia  223547000
         Brazil  190010647

» # o 5 países com menor pib:
» # criamos um dataframe apenas do ano 2007 e acrescentamos o campo pib
» # pib = pibPercap * populacao
» df2007 = dfPaises[dfPaises['ano']==2007]
» df2007['pib'] = df2007['pibPercap'] * df2007['populacao']       

» # são os países com menor pib em 2007
» df2007.sort_values(by=['pib']).head()['pais']
↳ 1307    Sao Tome and Principe
  323                   Comoros
  635             Guinea-Bissau
  431                  Djibouti
  563                    Gambia
  Name: pais, dtype: object

# se não precisamos mais do df, podemos apagá-lo
» del df2007

Para saber quantos países tem PIB percapita acima e abaixo da média em 2002 primeiro encontramos essa média. Depois selecionamos as linhas que satisfazem com pibPercap >= media e pibPercap < media. Para saber quantas linhas restaram contamos, por exemplo, quantos elementos existem em seu index.

» # média do pibPercap em 2002 (um escalar)
» media2002 = dfPaises[dfPaises.ano==2002]['pibPercap'].mean()
» acima = dfPaises[(dfPaises.ano==2002) & (dfPaises.pibPercap ≥= media2002)].index.size
» abaixo = dfPaises[(dfPaises.ano==2002) & (dfPaises.pibPercap < media2002)].index.size

» print('[Dos {} países, {} tem PIB percapita acima da média, {} abaixo da média.'.format(acima+abaixo, acima, abaixo))
↳ Dos 142 países, 44 tem PIB percapita acima da média, 98 abaixo da média.

Obtenção e análise de um slice : Brasil

Em diversas circunstâncias queremos fazer análise de apenas um slice da dataframe geral. Além de simplificar o conjunto de campos podemos conseguir com isso um uso menor de espaço em memória e maior velocidade de processamento.
Podemos, por ex., obter um dataframe separado apenas com a os dados referentes ao Brasil. Passando como índice o array booleano dfPaises['pais'] == 'Brazil' apenas as linhas relativas a esse país serão retornadas.

» dfBrasil = dfPaises[dfPaises['pais'] == 'Brazil'][['ano', 'expVida', 'populacao', 'pibPercap']]
» dfBrasil.head()
↳ 
         ano  expVida   populacao     pibPercap
  168   1952   50.917    56602560   2108.944355
  169   1957   53.285    65551171   2487.365989
  170   1962   55.665    76039390   3336.585802
  171   1967   57.632    88049823   3429.864357
  172   1972   59.504   100840058   4985.711467

O dataframe dfBrasil tem os mesmos índices que aos do segmento de dfPaises, de onde ele foi retirado. Para restabelecer esses índices usamos dataFrame.reset_index(). Se utilizado com o parâmetro drop=True o índice antigo é excluído (e perdido), caso contrário é copiado como uma coluna do dataframe. Para atribuir um nome para o índice usamos dataframe.index.rename('novoNome', inplace=True).

» # os índices iniciais são
» dfBrasil.index
↳ Int64Index([168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179], dtype='int64')

» # resetamos os índices, abandonando a coluna de índices iniciais
» dfBrasil.reset_index(drop=True, inplace=True)
» # novos índices
» dfBrasil.index
↳ RangeIndex(start=0, stop=12, step=1)

» dfBrasil.index.rename('id', inplace=True)
» # o dataframe fica assim:
» dfBrasil.head(3)
↳       ano     expVida     populacao       pibPercap
  id
  0    1952      50.917      56602560     2108.944355
  1    1957      53.285      65551171     2487.365989
  2    1962      55.665      76039390     3336.585802

Podemos usar um campo qualquer como index, com qualquer dtype. No caso abaixo usamos o campo ano como índice.

» # vamos usar o campo ano como index
» dfBrasil.set_index('ano', inplace=True)
» dfBrasil.head(3)
↳           expVida      populacao       pibPercap
  ano             
  1952.0     50.917     56602560.0     2108.944355
  1957.0     53.285     65551171.0     2487.365989
  1962.0     55.665     76039390.0     3336.585802

» # agora os índices passam a ser o ano
» dfBrasil.loc[1997]            # é uma Series
↳ expVida      6.938800e+01
  populacao    1.685467e+08
  pibPercap    7.957981e+03
  Name: 1997.0, dtype: float64

» # dfBrasil.loc[[1997]]            # é um dataframe

Para restaurar a coluna ano copiamos o índice para essa coluna e restauramos o índice.

» # restauramos a coluna ano
» dfBrasil['ano'] = dfBrasil.index
» # e resetamos o indice
» dfBrasil.reset_index(drop=True, inplace=True)

» dfBrasil.head(3)
↳    expVida   populacao     pibPercap    ano
  0   50.917    56602560   2108.944355   1952
  1   53.285    65551171   2487.365989   1957
  2   55.665    76039390   3336.585802   1962

Linhas podem ser inseridas de várias formas. Um delas consiste em criar novos dataframes com as linhas a inserir e concatenar como a dataframe inicial. Para isso usamos pandas.concat(): pd.concat([dfInicio, dfFinal]).
Vamos inserir linhas com dados fictícios, apenas para efeito de aprendizado.

» colunas = ['expVida','populacao','pibPercap','ano']      # nomes das colunas, na ordem dos dados
» valores1 = [48.0,45000000,2000.0,1951]                   # valores a inserir no ínicio (ano 1951)
» valores2 = [75.0, 200000000, 9500.0, 2008]               # valores a inserir no final (ano 2008)
» dfP = pd.DataFrame([valores1], columns=colunas)          # df a inserir no ínicio
» dfU = pd.DataFrame([valores2], columns=colunas)          # df a inserir no final
» dfBrasil = pd.concat([dfP, dfBrasil])                    # 1ª linha + dfBrasil
» dfBrasil = pd.concat([dfBrasil, dfU])                    # dfBrasil + última linha

» # agora a 1ª linha é
» dfBrasil.iloc[[0]]
↳    expVida   populacao   pibPercap   ano
  0     48.0   45000000       2000.0  1951

» # a última linha é
» dfBrasil.iloc[[-1]]
↳    expVida   populacao  pibPercap   ano
  0     75.0   200000000     9500.0  2008

» # como os índices ficaram duplicados e desordenados fazemos um reordenamento
» dfBrasil.reset_index(drop=True, inplace=True)

» dfBrasil
↳      expVida     populacao       pibPercap      ano
  0     48.000      45000000     2000.000000     1951
  1     50.917      56602560     2108.944355     1952
  2     53.285      65551171     2487.365989     1957
  ------------ linhas 3 até 11 omitidas ----------------
  12    72.390     190010647     9065.800825     2007
  13    75.000     200000000     9500.000000     2008

Como essas linhas não contém dados corretos, vamos apagá-las. Para usamos dataframe.drop(linha, axis=0, inplace = True), onde linha é o label, que pode não ser numérico) da linha ou seu índice (numérico). Várias linhas podem ser apagadas com dataframe.drop([linha0,...,linhan], axis=0, inplace = True).

» # apagar linhas 0 e 13: axis = 0 se refere às linhas
» dfBrasil.drop([0,13], axis=0, inplace = True)

» # para reordenar os índices
» dfBrasil.reset_index(drop=True, inplace=True)

» # recolocar a coluna 'ano' no início
» dfBrasil = dfBrasil[['ano', 'expVida', 'populacao', 'pibPercap']]
» dfBrasil

# o estado do dataframe agora é
↳ dfBrasil
        ano    expVida     populacao       pibPercap
  0    1952     50.917      56602560     2108.944355
  1    1957     53.285      65551171     2487.365989
  2    1962     55.665      76039390     3336.585802
  ------------ linhas 3 até 8 omitidas ----------------
  9    1997     69.388     168546719     7957.980824
  10   2002     71.006     179914212     8131.212843
  11   2007     72.390     190010647     9065.800825

Vamos inserir uma coluna, atribuindo a ela um escalar (um valor único). Aqui ocorre, como nas Series, o broadcasting, onde o escalar é transformado em uma Series de tamanho apropriado antes de ser inserido na nova coluna. Todas as linhas terão o valor 42 no campo “novoCampo”.

Em seguida alteramos o valor dessa coluna em uma linha específica, usando dataframe.loc(númeroLinha, nomeColuna) ou dataframe.iloc(numeroLinha, numeroColuna). Depois, como essa é uma coluna indesejada, nos a apagamos usando dataframe.drop('nomeColuna', axis=1, inplace=True).

» dfBrasil['novoCampo'] = 42
» dfBrasil.head(3)
↳        ano     expVida     populacao       pibPercap   novoCampo
  0     1952      50.917      56602560     2108.944355          42
  1     1957      53.285      65551171     2487.365989          42
  2     1962      55.665      76039390     3336.585802          42

» # alteramos o 'novoCampo' na linha 1 (usando loc)
» # e a coluna 4 ('novoCampo') na linha 2 (usando iloc, fornecendo o índice)
» dfBrasil.loc[1,'novoCampo'] = 123456
» dfBrasil.iloc[2,4] = 22222

» dfBrasil.head(3)
↳      ano     expVida     populacao       pibPercap     novoCampo
  0   1952      50.917      56602560     2108.944355     42
  1   1957      53.285      65551171     2487.365989     123456
  2   1962      55.665      76039390     3336.585802     22222

» # apagamos essa coluna com drop
» dfBrasil.drop('novoCampo', axis=1, inplace=True)
» # o dataframe fica como no início

Um campo pode ser inserido como resultado de operações entre outros campos. No caso abaixo criamos uma coluna pib que é o produto das colunas populacao × pibPercap. O resultado é aplicado, em cada linha, à nova coluna, em notação científica. Na 1ª linha pib = 1.193716 × 1011.

Outra coluna marca a passagem de quando a expectativa de vida do brasileiro ultrapassa os 60 anos.

» dfBrasil.loc[:,'pib'] = dfBrasil['pibPercap'] * dfBrasil['populacao']
» dfBrasil.head(4)
↳        ano    expVida       populacao       pibPercap     pib
  0     1952     50.917      56602560.0     2108.944355     1.193716e+11
  1     1957     53.285      65551171.0     2487.365989     1.630498e+11
  2     1962     55.665      76039390.0     3336.585802     2.537119e+11
  3     1967     57.632      88049823.0     3429.864357     3.019989e+11

» # inserindo coluna 'acima60'(†)
» dfBrasil.loc[:,'acima60'] = dfBrasil['expVida'] > 60
» dfBrasil.loc[3:6,['ano','expVida','acima60']]
↳      ano   expVida  acima60
  3   1967    57.632    False
  4   1972    59.504    False
  5   1977    61.489     True
  6   1982    63.336     True
  
» dfBrasil[dfBrasil['acima60']]
» # todas as linhas com expVida > 60 são exibidas (output omitido)

» # as colunas podem ser removidas (para ficarmos com o dataframe original)
» dfBrasil.drop(['acima60', 'pib'], axis=1, inplace=True)

() dfBrasil['expVida'] > 60 é uma Series booleana.

Objetos de índices

Em um dataframe, assim como nas Series, a informação relativa aos índices e seus nomes (labels ), assim como os nomes dos eixos, são armazenados em objetos Index (índice). O objeto Index é imutável (não pode ser alterado após a construção).

» pdSerie = pd.Series(range(4), index=['a1', 'a2', 'a3', 'a4'])
» index = pdSerie.index
» index
↳ Index(['a1', 'a2', 'a3', 'a4'], dtype='object')
» # o índice é uma sequência (pode ser lido em slices)
» index[2]
↳ 'a3'
» index[2:]
↳ Index(['a3', 'a4'], dtype='object')

» # o index é imutável
» index[0] = 'A'
↳ TypeError: Index does not support mutable operations

» # já vimos que índices não fornecidos são preenchidos como um range
» pd.Series(range(4)).index
↳ RangeIndex(start=0, stop=4, step=1)
Uma UA é a distância média da Terra ao Sol.
1 UA ≈ 149,6 × 109 m.

No exemplo abaixo construimos primeiro um objeto Index usando pandas.Index(lista). Em seguida construimos uma Series usando esse index, contendo como valores as distâncias dos planeta até o Sol, em unidaddes astronômicas (UA). Com a Series inicializamos um dataframe com o mesmo index.

» # objeto index
» labels = pd.Index(np.array(['mercurio', 'venus', 'terra']))
» labels
↳ Index(['mercurio', 'venus', 'terra'], dtype='object')

» # Serie construída com esse index
» planetas = pd.Series([0.387, 0.723, 1], index=labels)
» planetas
↳ mercurio    0.387
  venus       0.723
  terra       1.000
  dtype: float64

» # o index da Series é o mesmo objeto que labels
» planetas.index is labels
↳ True

» # essa Series pode ser usada para construir um dataframe
» dfPlanetas = pd.DataFrame(planetas)
» dfPlanetas
↳             0
  mercurio    0.387
  venus       0.723
  terra       1.000

» # o index do dataframe é o mesmo que o da Series
» dfPlanetas.index is labels
↳ True

» # alteramos o nome da coluna
» dfPlanetas.rename(columns={0:'distancia'}, inplace=True)
» dfPlanetas
↳           distancia
  mercurio      0.387
  venus         0.723
  terra         1.000

Podemos inserir uma coluna, por exemplo, relativa ao diâmetro dos planetas (comparados ao diâmetro da Terra), atribuindo valores à uma nova coluna de nome ‘diametro’. O objeto atribuído deve ter o mesmo shape (ou passar por broadcasting). Alterar a ordem das colunas, o que pode ser feito com df.reindex(listaColunas), altera todo o dataframe (embora não inplace). O objeto retornado se ajusta de acordo com os índices fornecidos.

» # inserir uma nova coluna
» dfPlanetas['diametro'] = pd.Series([0.382, 0.949, 1], index=labels)
» dfPlanetas
↳         distancia   diametro
  mercurio    0.387      0.382
  venus       0.723      0.949
  terra       1.000      1.000

» # as colunas estão em um objeto Index
» dfPlanetas.columns
↳ Index(['distancia', 'diametro'], dtype='object')

» type(dfPlanetas.columns)
↳ pandas.core.indexes.base.Index

» 'distancia' in dfPlanetas.columns
↳ True

» # podemos alterar a ordem das colunas com reindex
» dfPlanetas.reindex(['venus','terra','mercurio'])
↳       distancia    diametro
  venus     0.723       0.949
  terra     1.000       1.000
  mercurio  0.387       0.382

» # podemos ordenar os índices para ordenar o dataframe
» idx = dfPlanetas.index
» idx = idx.sort_values()
» idx
↳ Index(['mercurio', 'terra', 'venus'], dtype='object')

» dfPlanetas.reindex(idx)
↳         distancia   diametro
  mercurio    0.387      0.382
  terra       1.000      1.000
  venus       0.723      0.949

Diferentes de um conjunto (set) objetos Index podem ter índices repetidos. Se índices inseridos não correspondem à dados existentes estes são preenchidos com NaN. Os parâmetros method='bfill' (ou “ffill” forçam as colunas (ou linhas) com NaN a serem preenchidos com valores das colunas (ou linhas) anteriores ou posteriores. Claro que reindexações podem ser também obtidas com df.loc e df.iloc.

» # índices de linhas repetidos
» duplicados = pd.Index(['mercurio', 'venus', 'terra', 'mercurio', 'marte'])
» duplicados
↳ Index(['mercurio', 'venus', 'terra', 'mercurio', 'marte'], dtype='object')

» dfPlanetas.reindex(duplicados)   # default é axis = 0
↳         distancia   diametro
  mercurio    0.387      0.382
  venus       0.723      0.949
  terra       1.000      1.000
  mercurio    0.387      0.382
  marte         NaN        NaN  

» # índices de colunas repetidos
» duplicados = pd.Index(['distancia', 'diametro', 'diametro', 'distancia', 'massa'])
» dfPlanetas.reindex(duplicados, axis=1)   # sobre colunas
↳          distancia  diametro  diametro  distancia  massa
  mercurio     0.387     0.382     0.382      0.387    NaN
  venus        0.723     0.949     0.949      0.723    NaN
  terra        1.000     1.000     1.000      1.000    NaN

» # method='bfill' lê valor da coluna anterior
» dfPlanetas.reindex(duplicados, axis=1, method='bfill')
↳          distancia  diametro  diametro  distancia    massa
  mercurio     0.387     0.382     0.382      0.387    0.387
  venus        0.723     0.949     0.949      0.723    0.723
  terra        1.000     1.000     1.000      1.000    1.000

» # use method='ffill' para copiar coluna posterior

» # reindexação com loc
» nCol = pd.Index(['diametro', 'distancia'])
» dfPlanetas.loc[['venus','terra'], ['diametro', 'distancia']]
↳        diametro   distancia
  venus     0.949       0.723
  terra     1.000       1.000
» # nCol pode ser uma lista: nCol = ['diametro', 'distancia']

De posse dos índices das linhas e colunas qualquer uma delas pode ser apagada com df.drop(lista, axis). As operações retornam o dataframe modificado, sem alterar o original, a menos que seja marcado o parâmetro inplace=True. Nesse caso os dados removidos serão perdidos.

» dfPlanetas
↳          distancia   diametro
  mercurio     0.387      0.382
  venus        0.723      0.949
  terra        1.000      1.000

» # apagando linhas (axis = 0 é default)
» dfPlanetas.drop(['venus', 'mercurio'])
↳     distancia     diametro
  terra     1.0         1.0

» # apagando colunas
» dfPlanetas.drop(['distancia'], axis=1)
↳           diametro
  mercurio     0.382
  venus        0.949
  terra        1.000

Os seguintes argumentos são usados com reindex

Argumento descrição
index Index ou sequência a ser usada como index,
method forma de interpolação: ‘ffill’ preenche com valor posterior, ‘bfill’ com valor anterior,
fill_value valor a usar quando dados não existentes são introduzidos por reindexing (ao invés de NaN),
limit quando preenchendo com valor anterior ou posterior, intervalo máximo a preencher (em número de elementos),
tolerance quando preenchendo com valor anterior ou posterior, intervalo máximo a preencher para valores inexatos (em distância numérica),
level combina Index simples no caso de MultiIndex; caso contrário seleciona subset,
copy se True, copia dados mesmo que novo índice seja equivalente ao índice antigo; se False, não copia dados quando índices são equivalentes.

Métodos e propriedades de Index

Método descrição
append concatena outro objeto Index objects, gerando novo Index
difference calcula a diferença de conjunto como um Index
intersection calcula intersecção de conjunto
union calcula união de conjunto
isin retorna array booleano indicando se cada valor está na coleção passada
delete apaga índice, recalculando Index
drop apaga índices passados, recalculando Index
insert insere índice, recalculando Index
is_monotonic retorna True se indices crescem de modo monotônico
is_unique returns True se não existem valores duplicados no Index
unique retorna índices sem repetições
🔺Início do artigo

Bibliografia

Consulte bibliografia completa em Pandas, Introdução neste site.

Nesse site:

NumPy, Álgebra Linear


Métodos de ordenamento

Um array do NumPy pode ser ordenado com o método array.sort(), que transforma o array inplace. Se o array que passa por ordenamento for uma seção (uma view ) de um array maior esse array original também será alterado. O método np.sort(array) retorna cópia ordenada, sem alterar o original. Não há um método ou parâmetro predefinido para fazer o ordenamento inverso. Para isso usamos a mesma sintaxe que retorna uma lista invertida, lista[::-1].

» # um array qualquer sem ordenamento
» arrOriginal = np.array([-1, 20, 13, -5, 1, 0, 9, 3, -3, 7])
» # uma cópia (não uma view)
» arr = arrOriginal.copy()

» # arr.sort ocorre inplace
» arr.sort()
» arr
↳ array([-5, -3, -1,  0,  1,  3,  7,  9, 13, 20])

» # para obter a lista ordenada invertida (sem alterar a original)
» arr[::-1]
↳ array([20, 13,  9,  7,  3,  1,  0, -1, -3, -5])

» # retorna para o array original (não ordenado)
» arr = arrOriginal.copy()
» np.sort(arr)
↳ array([-5, -3, -1,  0,  1,  3,  7,  9, 13, 20])

» # arr não foi alterado
» arr
↳ array([-1, 20, 13, -5,  1,  0,  9,  3, -3,  7])

» # o array ordenada em ordem inversa também pode ser obtido (inplace) da seguinte forma
» arr[::-1].sort()
» arr
↳ array([20, 13,  9,  7,  3,  1,  0, -1, -3, -5])

Para arrays como mais de um eixo podemos informar ao longo de qual deles queremos ordenar os valores. Em qualquer dos casos cada linha (ou cada coluna) será ordenada independentemente. Em qualquer dos casos, com o ordenamento das colunas, as linhas perdem seu alinhamento, caso exista. Por exemplo, se cada linha se referia à uma medida específica, no ordenamento os dados ficam desalinhados. Idem para ordenamento das linhas.

» # inicializamos um array 3 × 4
» lista = ([ [ 3,   2,  1,  -1],
             [-3,   4,  -6,  5],
             [ 3,   0,   -9,  15]
           ])
» arr = np.array(lista)
» arr
↳ array([[ 3,  2,  1, -1],
         [-3,  4, -6,  5],
         [ 3,  0, -9, 15]])

» # ordenamos ao longo das colunas
» arr.sort(0)
» arr
↳ array([[-3,  0, -9, -1],
         [ 3,  2, -6,  5],
         [ 3,  4,  1, 15]])

» # reconstituindo o array original
» arr = np.array(lista)

» # ordenamos ao longo das linhas
» arr.sort(1)
» arr
↳ array([[-1,  1,  2,  3],
         [-6, -3,  4,  5],
         [-9,  0,  3, 15]])

Gravação e leitura de arrays em arquivos


Em muitas situações é útil gravar resultados finais ou etapas intermediárias de cálculo para posterior finalização. NumPy permite a gravação de arquivos contendo os arrays em formato de texto ou binário. Os métodos principais são np.save() e np.load(). Por default um array é gravado em arquivo com extensão .npy em formato binário. Formatos mais sofisticados para textos e arrays tabulares são encontrados no pandas.

» # criando um array 
» arr = np.linspace(0, 22, 12).reshape(3,4)
» arr
↳ array([[ 0.,  2.,  4.,  6.],
         [ 8., 10., 12., 14.],
         [16., 18., 20., 22.]])
» # esse array será gravado em arrGravado.npy.
» # a extensão será acrescentada se não fornecida
↳ np.save('arrGravado', arr)

» # apagamos o array e depois o recarregamos
» del arr
» arr = np.load('arrGravado.npy')
» arr
↳ array([[ 0.,  2.,  4.,  6.],
         [ 8., 10., 12., 14.],
         [16., 18., 20., 22.]])

Mais de um array podem ser gravados no mesmo arquivos arq.npz. Os arrays são passados como argumentos de keyword. Na recuperação dos arrays um objeto tipo dicionário é carregado, tendo os arrays associados às chaves que foram as keywords passadas.

A mesma operação de armazenar vários arrays em arquivo pode ser realizada compactando-se os dados para que ocupem menos espaço em disco. Isso é feito com np.savez_compressed('nomeDoArquivoaCompactado.npz', a=arr1, b=arr2, ...).

» # definimos 2 arrays
» arr1 = np.array([1,2,3])
» arr2 = np.array([4,5,6])
» # e os gravamos em disco
» np.savez('variosArrays.npz', a1=arr1, a2=arr2)

» # apagamos e recuperamos os arrays
» del arr1, arr2
» arrays = np.load('variosArrays.npz')

» # o load carrega um objeto tipo dicionário
» arr1, arr2 = arrays['a1'], arrays['a2']

» display(arr1, arr2)
↳ array([1, 2, 3])
↳ array([4, 5, 6])

» # a mesma operação, amazenando os arrays em arquivo compactado
» np.savez_compressed('arraysCompactados.npz', a=arr1, b=arr2)

Métodos de conjuntos

Algumas operações básica podem ser aplicadas sobre arrays, tratando seus elementos como um conjunto. Uma delas, usada com frequência, é a seleção de elementos únicos no array, feita com np.unique. O método np.in1d(array, valores), testa se elementos de um array estão também em outro, retornando um array booleano. O objeto valores pode ser uma lista, tupla ou outro array unidimensional.

» arr = np.array([1,2,3,4,4,3,2,1])
» # elementos de arr sem repetições
» np.unique(arr)
↳ array([1, 2, 3, 4])

» arrString = np.array(['Ana','Luiz','Paulo','Ana','Otto','Paulo','Otto','Paulo'])
» np.unique(arrString)
↳ array(['Ana', 'Luiz', 'Otto', 'Paulo'], dtype='<U5')

» # quais dos elementos de arr estão em (1,3,5)
» np.in1d(arr, (1,3,5))
↳ array([ True, False,  True, False, False,  True, False,  True])

» # o argumento pode ser uma tupla, lista ou array
» teste = np.array([1,3,5])
» np.in1d(arr, teste)
↳ array([ True, False,  True, False, False,  True, False,  True])

» # o mesmo com array de strings
» txt = np.array(['Otto','Luiz','Ana'])
» np.in1d(arrString, txt)
↳ array([ True,  True, False,  True,  True, False,  True, False])

» arrString = np.array(['A','F','D','A','O','P','C','D'])
» lista = np.in1d(arrString, ['A', 'B', 'C'])

» # os seguintes elementos estão na lista 
» arrString[lista]
↳ array(['A', 'A', 'C'], dtype='<U1')

» # os seguintes elementos não estão na lista 
» arrString[~lista]
↳ array(['F', 'D', 'O', 'P', 'D'], dtype='<U1')


O método np.intersect1d(x, y) retorna a interseção entre x e y.
O método np.setdiff1d(x, y) retorna x-y.
setxor1d(x, y): elementos em x ou y, mas não em ambos.

» # para verificar intersect1d
» # múltiplos de 23 até 1000
» arr1 = np.arange(0,1000,23)
» # múltiplos de 27 até 1000
» arr2 = np.arange(0,1000,27)

» # múltiplos de 23 e 27 até 1000 (é a interseção entre os dois conjuntos)
» np.intersect1d(arr1, arr2)
↳ array([  0, 621])

» # para verificar setdiff1d
» arr1 = np.array([5, 3, 1, 9, 7, 6])
» arr2 = np.array([5, 4, 1, 8, 2, 3])
» np.setdiff1d(arr1,arr2)
↳ array([6, 7, 9])

» # para verificar setxor1d
» np.setxor1d(arr1,arr2)
↳ array([2, 4, 6, 7, 8, 9])

Funções de conjuntos em NumPy:

Método descrição
unique(x) conjunto de elementos únicos em x,
intersect1d(x, y) elementos comuns em x e y, (ordenados),
union1d(x, y) união dos elementos em x e y, (ordenados),
in1d(x, y) array booleano indicando se cada elemento de x está em y,
setdiff1d(x, y) conjunto diferença: elementos em x que não estão em y,
setxor1d(x, y) conjunto diferença simétrica: elementos em x ou y, mas não em ambos.

Numpy: Álgebra linear


A Álgebra Linear é uma parte da matemática muito importante nas aplicações científicas e da engenharia. Para o cálculo simbólico o módulo Sympy (Matemática Simbólica em Python) oferece muitos métodos interessantes e úteis, inclusive para a álgebra linear.

É uma notação útil denotar os arrays da seguinte forma:
Um array unidimensional (um vetor) é uma coleção de elementos \(A_M = \{a_{i}\}\), onde \(i = 0, …,M-1\) para um array de rank = 1. Diferente da notação matemática usual os índices são contados a partir de 0. Seu shape = (M,).
Um array bidimensional (uma matriz) é uma coleção de elementos \(A_{MN} = \{a_{ij}\}\) onde \(i = 0, …,M-1; j = 0, …,N-1; \) para um array de rank = 2. Seu shape=(M,N) e rank=2.
Arrays de ranks superiores são generalizações, com mais eixos acresentados. Em arrays 3-dimensionais, digamos arr3D.shape = (r,m,n), temos r matrizes m × n.
Arrays de ranks superiores são generalizações, com mais eixos acresentados. Em arrays 3-dimensionais, digamos arr3D.shape = (r,m,n), temos r matrizes m × n.

Alguns dos métodos mais comuns usados na álgebra linear estão no módulo numpy.linalg, descrito abaixo.

Produto Matricial

O produto de matrizes, que é diferente da operação * definida previamente e que consiste na mera multiplicação dos termos e/e, está definido em numpy. As dimensões devem ser compatíveis. Por exemplo, o produto
Am,n × Bn,p = Cm,p. A sintaxe do produto de matrizes A por B é np.dot(A,B) ou A.dot(B).

» # produto matricial
» A = np.arange(0, 9).reshape(3, 3)
» B = np.arange(0, 3).reshape(3, 1)
» A
↳ array([[0, 1, 2],
         [3, 4, 5],
         [6, 7, 8]])

» B
↳ array([[0],
         [1],
         [2]])

» A * B
↳ array([[ 0,  0,  0],
         [ 3,  4,  5],
         [12, 14, 16]])

» A + B
↳ array([[ 0,  1,  2],
         [ 4,  5,  6],
         [ 8,  9, 10]])

» A.dot(B)      # o mesmo que np.dot(A,B)
↳ array([[ 5],
         [14],
         [23]])

» # 6 *0 + 7*1 + 8*2 = 23   # é o elemento da 3º linha do produto
» # B.dot(A) não está definida
» # O quadrado da matriz A, A2 = A.dot(A)
» A.dot(A)
↳ array([[ 15,  18,  21],
         [ 42,  54,  66],
         [ 69,  90, 111]])

Matemáticamente a operação acima para \(A \cdot B\) (A.dot(B)) é representada como:
$$
\left[ \begin{array}{ccc}
0 & 1 & 2\\
3 & 4 & 6\\
6 & 7 & 8
\end{array} \right] \left[ \begin{array}{c}
0\\
1\\
2
\end{array} \right] = \left[ \begin{array}{l}
5\\
14\\
23
\end{array} \right] .
$$

Transposta e inversões de eixos

A tansposta de uma matriz é a matriz obtida da original trocando-se suas linhas por colunas. Essa é uma operação comum na análise de dados e na álgebra linear e pode ser obtida com o método transposta = array.transpose() ou seu atalho transposta = array.T. Em notação matemática, se \(A_{MN} = \{a_{ij}\}\) sua transposta é \(A{^T}_{NM} = \{a_{ji}\}\).

» import numpy as np
» # uma matriz (2 ×3 ) qualquer para exemplo
» arr = np.arange(0,6).reshape(2,3)
» arr
↳ array([[0, 1, 2],
         [3, 4, 5]])
       
» # sua transposta é (3 × 2) 
» transp = arr.T
» transp
↳ array([[0, 3],
         [1, 4],
         [2, 5]])

» # o produto matricial (dot) é (2 × 2) 
» np.dot(arr,transp)
↳ array([[ 5, 14],
         [14, 50]])

» # observe que o produto não é comutativo
» # (a ordem é relevante) transp.arr é (3 × 3)
» np.dot(transp, arr)
↳ array([[ 9, 12, 15],
         [12, 17, 22],
         [15, 22, 29]])

Em matrizes de ordem superior a operação de transposição permite que se informe quais os eixos serão transpostos. Um array arr3D do exemplo abaixo, com shape = (2,3,4), que pode ser vista como 2 matrizes 3 × 4 se torna um array com 3 matrizes 2 × 4 através da operação arr3D.transpose(1,0,2), onde o 1º eixo é permutado com o 2º (o 3º fica inalterado).

» arr3D = np.arange(24).reshape((2, 3, 4))
» arr3D
↳ array([[[ 0,  1,  2,  3],
          [ 4,  5,  6,  7],
          [ 8,  9, 10, 11]],

         [[12, 13, 14, 15],
          [16, 17, 18, 19],
          [20, 21, 22, 23]]])

» # permutando 1º eixo com o 2º
» arr3D.transpose(1,0,2)
↳ array([[[ 0,  1,  2,  3],
          [12, 13, 14, 15]],

         [[ 4,  5,  6,  7],
          [16, 17, 18, 19]],

         [[ 8,  9, 10, 11],
          [20, 21, 22, 23]]])

» # temos 3 matrizes 2 × 4
» arr3D.transpose(1,0,2).shape
↳ (3, 2, 4)

» # se permutarmos 2º com 3º eixo
» arr3D.transpose(0,2,1)
↳ array([[[ 0,  4,  8],
          [ 1,  5,  9],
          [ 2,  6, 10],
          [ 3,  7, 11]],

         [[12, 16, 20],
          [13, 17, 21],
          [14, 18, 22],
          [15, 19, 23]]])

No último caso, permutando 2º com 3º eixo e mantendo o 1º temos as 2 matrizes original transpostas.

A transposição é um caso particular da inversão mais geral de eixos. Isso pode ser feito com array.swapaxes(i,j), que recebe um par de índices referentes aos eixos e os permuta.

» # ainda usando a matriz já definida
» arr3D
↳ array([[[ 0,  1,  2,  3],
          [ 4,  5,  6,  7],
          [ 8,  9, 10, 11]],

         [[12, 13, 14, 15],
          [16, 17, 18, 19],
          [20, 21, 22, 23]]])

» arr3D.swapaxes(0,2)
↳ array([[[ 0, 12],
          [ 4, 16],
          [ 8, 20]],

         [[ 1, 13],
          [ 5, 17],
          [ 9, 21]],

         [[ 2, 14],
          [ 6, 18],
          [10, 22]],

         [[ 3, 15],
          [ 7, 19],
          [11, 23]]])

arr3D.swapaxes(0,2) é idêntica à arr3D.transpose(2,1,0). Quando as dimensões são altas pode ficar difícil visualizar e manipular os arrays. Em alguns casos quebrar o array em blocos pode ser a melhor prática.

Biblioteca numpy.linalg

numpy.linalg é uma subbiblioteca de NumpPy contendo métodos matriciais usuais as operações comuns na álgebra linear, como o cálculo de determinantes e de matrizes inversas similares àquelas usadas no MATLAB e R.

Alguns dos métodos mais comuns usados na álgebra linear:

Método descrição
diag elementos da diagonal (ou fora da diagonal) de matriz quadrada,
diag Se o argumento for array 1-D retorna o array na diagonal e zeros fora da diagonal,
dot multiplicação de matrizes,
trace traço: soma dos elementos da diagonal,
det determinante da matriz,
eig autovalores e autovetores (eigenvalues e eigenvectors) de uma matriz quadrada,
inv a inversa de uma matriz quadrada,
pinv a pseudo inversa de Moore-Penrose de uma matriz,
qr cálculo da decomposição QR,
svd calcula a decomposição de valor singular (SVD),
solve resolve o sistema linear Ax = b para x, sendo A uma matriz quadrada,
lstsq calcula a solução de mínimos quadrados para Ax = b.

A solução de sistemas lineares é uma aplicação comum da álgebra linear. Um exemplo bem simples com equações e 2 incógnitas, cuja solução pode ser vista em matrizes, é:
$$
\left\{ \begin{array}{l}
2 x + y = 5\\
x – 3 y = 6
\end{array} \right.
$$
Ele corresponde a busca do array x (um vetor de 2 variáveis) satisfazendo A x = B onde A e B são listados abaixo.

» A = np.array([[2, 1], [1, -3]])
» B = np.array([5, 6])
» x = np.linalg.solve(A, B)
» # a solução é
» x
↳ array([ 3., -1.])

Portanto a solução (única, nessa caso) é o vetor \(x = (3., -1.)\).

Dada uma matriz \(A\), por definição sua matriz inversa é \(A^{-1}\), satisfazendo \(A.A^{-1} = A^{-1}.A = I\), onde \(I\) é a matriz identidade. Observe que um sistema do tipo \(A.x = B\) fica resolvido se existe a inversa, \(A^{-1}\). Nesse caso basta multiplicar todo o sistema à esquerda (ou à direita) pela inversa: \(A^{-1}.A.x = A^{-1}.B\) que resulta na solução procurada \(x = A^{-1}.B\).

Para o mesmo sistema acima:

» from numpy.linalg import inv
» A = np.array([[2, 1], [1, -3]])
» B = np.array([5, 6])

» # a inversa de A é
» inv(A)
↳ array([[ 0.42857143,  0.14285714],
         [ 0.14285714, -0.28571429]])
       
» # por definição A . inv(A) = identidade †
» # (verificamos que essa é de fato a inversa)
» np.dot(A, inv(A)).round(2)
↳ array([[1., 0.],
         [0., 1.]])

» # a solução do sistema é
» np.dot(inv(A), B)
↳ array([ 3., -1.])

(†): Não se pode esperar que de fato o cálculo de A.inv(A) resulte exatamente na identidade. Devido à aproximações numéricas essa matriz apresentará com frequência elementos pequenos mas não nulos fora da diagonal. Daí o uso de .round(2).

Algumas matrizes não possuem inversas, sendo chamadas de matrizes singular. Seu determinante é \(det(A)= 0\) e, nesse caso, o sistema não tem solução.

» # resolvendo o sistema
» A = np.array([[2, 1], [6, 3]])
» B = np.array([5, 10])
» x = np.linalg.solve(A, B)
↳ LinAlgError: Singular matrix

# ocorre que a matriz A é singular, e o sistema não tem solução
» det(A)
↳ 0.0

O determinante de uma matriz \(A\), denotada por \(det(A)\) está definido no artigo sobre determinantes, nesse site. O exemplo abaixo está resolvido nessa página. Como \(det(A)\ne 0\) ela possui uma inversa \(A^{-1}\), definida de forma que \(A.A^{-1} = \mathbb{I}\), onde \(\mathbb{I}\) é a matriz identidade.

» arr = np.array([[1, -2, 3],[2, 1, -1], [-2, -1, 2]])
» arr
↳ array([[ 1, -2,  3],
         [ 2,  1, -1],
         [-2, -1,  2]])

» np.linalg.det(arr).round(2)
↳ 5.0

» np.linalg.inv(arr)
↳ array([[ 0.2,  0.2, -0.2],
         [-0.4,  1.6,  1.4],
         [ 0. ,  1. ,  1. ]])

» # A A-1 é a identidade
» np.dot(arr,inv(arr))
↳ array([[1., 0., 0.],
         [0., 1., 0.],
         [0., 0., 1.]])

Outra operação importante é a de se encontrar autovetores e autovalores de uma matriz quadrada. Ele consiste em encontrar os valores de \(\lambda\) (os autovalores) e os autovetores \(x\) que satisfazem à equação \(A.x = \lambda x\). Se consideramos a matriz \(A\) como a matriz correspondente a uma transformação linear então os autovetores são aquelas direções mantidas invariantes pela transformação e \(\lambda\) (os autovalores) são os fatores de escala nestas direções.

Por exemplo, no plano uma reflexão no eixo \(Ox\) corresponde à transformação \(R_x(x,y)=(x, -y)\). Ela pode ser escrita em forma matricial como
$$
r_x \left[ \begin{array}{r}
x\\
y
\end{array} \right] = \left[ \begin{array}{rr}
1 & 0\\
0 & – 1
\end{array} \right] \left[ \begin{array}{r}
x\\
y
\end{array} \right] = \left[ \begin{array}{r}
x\\
– y
\end{array} \right].
$$
Portanto queremos encontrar os autovetores e autovalores do array reflex:

» # a reflexão em Ox é descrita por
» reflx = np.array([[1, 0],[0,-1]])
» reflx
↳ array([[ 1,  0],
         [ 0, -1]])
» # seus autovalores e autovetores são
» auto = eig(reflx)
» auto
↳ (array([ 1., -1.]),
↳ array([[1., 0.],
         [0., 1.]]))

» # eig retorna um tupla com 2 elementos
» # o primeiro contem outra tupla com os autovalores (1, -1)
» auto[0]
↳ array([ 1., -1.])

» # o segundo contém outra tupla com os dois autovetores
» auto[1]
↳ array([[1., 0.],
         [0., 1.]])

» auto[1][0]
↳ array([1., 0.])

» auto[1][1]
↳ array([0., 1.])

Isso significa que no plano, a reflexão em torno do eixo \(Ox\) só deixa 2 direções inalteradas: a direção de x, sendo que todos os vetores \((x,0)\) ficam iguais (autovalor = 1), e o eixo \(Oy\). Vetores \((0,y)\) permanecem na mesa direção com o sentido invertido (autovalor = -1).

Brodcasting


Broadcasting se refere ao comportamento de arrays de diferentes dimensões quando operados entre si. Quando uma ou mais dimensões estão ausentes em um dos arrays e as dimensões presentes são compatíveis o array menor e replicado para preencher as dimensões ausentes de forma a que ambas tenham as mesmas dimensões.

Na figura a operação de soma é mostrada. O mesmo comportamento se dá para qualquer outras operação.

Bibliografia

🔺Início do artigo
  • Harrison, Matt: Learning Pandas, Python Tools for Data Munging, Data Analysis, and Visualization,
    Treading on Python Series, Prentiss, 2016.
  • McKinney, Wes: Python for Data Analysis, O’Reilly Media, Sebastopol CA, 2018.
  • McKinney, Wes & Pandas Development Team: pandas: powerful Python data analysis toolkit Release 1.2.1,
  • Miller, Curtis: Hands-On Data Analysis with NumPy and pandas, Packt Publishing, Birmingham, 2018.
  • NumPy, docs.
  • NumPy, Learn.
  • NumPy, linalg.

Sobre Sympy: Matemática Simbólica em Python

Nesse site:

NumPy, Introdução


Numpy

NumPy é uma biblioteca do python especializada em computação científica e análise de dados. Ela é usada em diversos tipos de operações que envolvem operações matriciais. Além disso suas matrizes formam a base para outros pacotes, como o pandas e outras voltadas para o cálculo matemático e científico. Numpy foi primeiro lançado por Travis Oliphant em 2006 e tem sido mantido por um grande número de colabores desde então, sob licença BSD.

NumPy, com suas matrizes, apresenta algumas vantagens sobre o cálculo usual com objetos do python, como listas e tuplas. Essas operações são mais rápidas e flexíveis e foram construídas de forma a evitar a necessidade da realização de laços (loops ). Ela contém métodos voltados para operações da álgebra linear, geração de números aleatórios e transformadas de Fourier, além da interface voltada para a conexão com as linguagens C, C++ e FORTRAN.

Um exemplo rápido pode mostrar como as rotinas do numpy são mais eficientes que as de objetos list do python.

» # comparação de velocidades
» import numpy as np
» # um array do numpy
» array = np.arange(1_000_000)
» # uma lista usual do python
» lista = list(range(1_000_000))

» %time for _ in range(100): arr2 = array * 2
↳ CPU times: user 145 ms, sys: 7.93 ms, total: 152 ms
  Wall time: 151 ms

» %time for _ in range(100): lista2 = [x * 2 for x in lista]
↳ CPU times: user 5.35 s, sys: 778 ms, total: 6.13 s
  Wall time: 6.12 s

» # 5.35/.145 ≈ 37 × mais rápido

Wall time é o tempo total gasto pelo código para ser executado. CPU time é uma medida do tempo gasto pelo processador apenas quando esteve operando sobre a tarefa específica. Dependendo do cálculo feito o numpy pode ser mais de 100 vezes mais rápido que uma operação similar em puro python.

Instalação


Em geral o módulo está presente como pacote na maioria das distribuições de Python. Se necessária a sua instalação em separado pode ser feita. Numpy e pandas são instalados juntos com o Anaconda.

# No Linux (Ubuntu and Debian):
» sudo apt-get install python-numpy
# No Linux (Fedora)
» sudo yum install numpy scipy
# No Windows com Anaconda:
» conda install numpy
# após a instalação o módulo deve ser importado:
» import numpy as np
# O aliás np é opcional e de escolha do programador.

Ndarray

O objeto básico da biblioteca Numpy é o ndarray (N-dimensional array). Ndarrays são matrizes multidimensionais com número determinado de dimensões e elementos. Seus elementos pertencem todos a um único tipo, chamado de dtype (data-type ou tipo de dado). Cada dimensão é denominada por axis (eixo) e o número de eixos é o rank do objeto. Diferente das listas do python, ndarrays têm dimensões fixas, definidas em sua construção.

Um ndarray possui os seguintes atributos referentes ao seu tipo de dado, tamanho e ordem:

Atributo descrição
array.dtype tipo de dado armazenado. (Veja lista abaixo),
array.ndim número de dimensões (que são eixos ou axis ); o mesmo que rank
array.size número de elementos em cada eixo,
array.shape (ou forma), tupla de N inteiros positivos com o comprimento de cada eixo.

Um ndarray de 1 dimensão é um objeto similar a um vetor (rank = 1): arr1D = ([a0,a1,...,aN-1,]), arr1D.size = N, arr1D.ndim = 1, arr1D.shape = (N,).

Um ndarray de 2 dimensões é um objeto similar a uma matriz (rank = 2): se ela possui M linhas, cada uma com N elementos então arr2D.size = M × N , arr2D.ndim = 2, arr2D.shape = (M,N).

Um ndarray de 3 dimensões é uma coleção de matrizes (rank = 3): se ela possui K matrizes de M linhas, cada uma com N elementos então arr3D.size = K × M × N , arr3D.ndim = 3, arr3D.shape = (K, M, N).

Ndarrays de ordem superior são generalizações desse processo, acrescentados novos eixos.

Os eixos são numerados para diversas operações. Em 2 dimensões axis=0 são as linhas, axis=1 as colunas, e assim consecutivamente para ordens superiores.

Tipos, dtypes

Além dos tipos usuais do python, a importação de Numpy disponibiliza um conjunto extendido de tipos ou dtypes.

dtype descrição
bool booleano (true ou false) armazenado como um byte
intX inteiro com sinal, X-bit (X=8,16,32, 64)
uintX inteiro sem sinal, X-bit (X=8,16,32, 64)
intc idêntical ao int C (em geral int32 ou int64)
intp inteiro usado para indexação (como C size_t; em geral int32 ou int64)
float_ o mesmo que float64
float16 meia precisão float: sign bit, 5-bit exponente, 10-bit mantissa
float32 simple precisão float: sign bit, 8-bit exponente, 23-bit mantissa
float64 dupla precisão float: sign bit, 11-bit exponente, 52-bit mantissa
complex_ o mesmo que complex128
complex64 complexo, representado por dois 32-bit floats (parte real e imaginária)
complex128 complexo, representado por dois 64-bit floats (parte real e imaginária)

Construção de um array

Um array do NumPy pode ser criado passando-se uma lista para construtor np.array(lista).

» import numpy as np

» lista = [123,234,345]
» arr = np.array(lista)
» arr
↳ array([123, 234, 345])

# o objeto criado é um ndarray do numpy
» type(arr)
↳ numpy.ndarray

» arr.dtype
↳ dtype('int64')

» arr.ndim
↳ 1
» arr.shape
↳ (3,)
» arr.size
↳ 3

» # uma lista de listas
» lista2 = [[123,234,345],
            [456,567,678],
            [789,890,901]]
» arr2 = np.array(lista2)
» arr2
↳ array([[123, 234, 345],
         [456, 567, 678],
         [789, 890, 901]])

» arr2.ndim
↳ 2
» arr2.size
↳ 9
» arr2.shape
↳ (3, 3)

» # lista de listas de listas
» lista3 =[ [ [1,2],[2,3] ], [ [3,4],[4,5] ],  [ [1,2],[2,3] ], [ [3,4],[4,5] ] ]
» arr3 = np.array(lista3)
» # o resultado é: 4 matrizes de 2 x 2  elementos
» arr3
↳ array([[[1, 2],
         [2, 3]],

        [[3, 4],
         [4, 5]],

        [[1, 2],
         [2, 3]],

        [[3, 4],
         [4, 5]]])

» arr3.ndim
↳ 3
» arr3.shape
↳ (4, 2, 2)
» arr3.size
↳ 16

» # a 1ª matriz
» arr3[0]

↳  array([[1, 2],
         [2, 3]])

» # a 2ª linha da 1ª matriz
» arr3[0,1]
↳ array([2, 3])

» # o 1º elemento da 2ª linha da 1ª matriz
» arr3[0,1,0]
↳ 2

# todos os arrays criados tem o mesmo dtype
» arr3.dtype
↳ dtype('int64')

Arrays podem ser de outros tipos, como um array de strings. No entanto devem ser homogêneos (todos os elementos do mesmo tipo). Uma tentativa de criar um array como em stArr2 causa uma tentativa de ajuste (cast), transformando os inteiros em string. O método array(listas, dtype) aceita o parâmetro dtype onde se pode informar o tipo de elemento que se pretende armazenar.

» stArr = np.array([['a', 'b'],['c', 'd']])
» stArr
↳ array([['a', 'b'],
       ['c', 'd']], dtype='<U1')

» stArr[0,1]
↳ 'b'
» stArr.dtype
↳ dtype('<U1')
» stArr.dtype.name
↳ 'str32'

» stArr2 = np.array([[1.01, 2.02],['h', 'i']])
» stArr2
↳ array([['1.01', '2.02'],
        ['h', 'i']], dtype='<U21')

» stArr3 = np.array([[True, False],['h', 'i']])
» stArr3
↳ array([['True', 'False'],
        ['h', 'i']], dtype='<U5')

» # parâmetro dtype
» cplx = np.array([[1, 2, 3],[4, 5, 6]], dtype=complex)
» cplx
↳ array([[1.+0.j, 2.+0.j, 3.+0.j],
        [4.+0.j, 5.+0.j, 6.+0.j]])               

Arrays podem ser transformados de um tipo para outro (quando possível). Para isso usamos array.astype(). Na transformação de floats para inteiros a parte decimal será truncada. Arrays de strings, desde que devidamente formatados, podem ser convertidos em numéricos.

» # criando um array de integers
» arr = np.array([1, 2, 3, 4, 5])
» arr.dtype
↳ dtype('int64')

» # cast para array de ponto flutuante
» floatArr = arr.astype(np.float64)
» floatArr.dtype
↳ dtype('float64')

» # floats para integers
» # criando um array de floats
» arr = np.array([1.9, -8.2, -9.6, 0.9, 2.3, 10.7])
» arr
↳ array([ 1.9, -8.2, -9.6, 0.9, 2.3, 10.7])

» # converte para inteiros (trunca parte inteira)
» arr.astype(np.int32)
↳ array([ 1, -8, -9, 0, 2, 10], dtype=int32)

» # um array de strings
» arrNumStrings = np.array(['0.0', '7.75', '-6.6', '100'], dtype=np.string_)
» arrNumStrings.astype(float)
↳ array([ 0., 7.75, -6.6 , 100.]

Métodos predefinidos de construção

O método np.arange(m,n,[p]) retorna um array de inteiros no intervalo (m, n], i.e., começando em m e terminando em n-1. Se o primeiro argumento for omitido m=0. Um terceiro argumento informa o p=passo, intervalo entre os valores da sequência. Em np.arange(m,n,p) m, n devem ser inteiros mas p pode ser um float.

O método np.linspace(m,n,p) retorna um array no intervalo (m, n), ambos os extremos incluídos, com p números igualmente espaçados.

np.random.random(n) retorna um array com n elementos aleatórios e np.random(m, n) retorna um array com shape = (m,n) e elementos aleatórios.

» # np.range(n)
» np.arange(10)
↳ array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

» # np.range(m,n)
» np.arange(5, 15)
↳ array([ 5,  6,  7,  8,  9, 10, 11, 12, 13, 14])

» # np.range(m,n,p)
» np.arange(10, 100, 10)
↳ array([10, 20, 30, 40, 50, 60, 70, 80, 90])

» np.arange(10, 20, .5)
↳ array([10. , 10.5, 11. , 11.5, 12. , 12.5, 13. , 13.5, 14. , 14.5, 15. ,
         15.5, 16. , 16.5, 17. , 17.5, 18. , 18.5, 19. , 19.5])

» # np.linspace(m,n,p)
» np.linspace(10, 20, 5)
↳ array([10. , 12.5, 15. , 17.5, 20. ])

» # a sequência pode ser decrescente
» np.linspace(20, 10, 5)
↳ array([20. , 17.5, 15. , 12.5, 10. ])

» # np.random.random(n)
» np.random.random(10)
↳ array([0.35433322, 0.54555179, 0.48783323, 0.5785414 , 0.76837232,
         0.69888297, 0.62492788, 0.33289321, 0.75068313, 0.95667854])

» # np.random.random(m,n)
» np.random.random((2,3))
↳ array([[0.11351481, 0.76831577, 0.27597676],
         [0.73130126, 0.7225559 , 0.54040225]])

Os métodos np.zeros((m,n)) e np.ones((m,n)) criam, respectivamente, ndarrays de zeros e uns com as dimensões dadas pela tupla (ou lista) no argumento. np.eye(m,n) gera um ndarray m × n com elementos 1 na diagonal, 0 fora dela. np.eye(n,n) é o mesmo que np.eye(n) ou np.identity(n), que são a matriz identidade de n dimensões.

» np.zeros((2,4))
↳ array([[0., 0., 0., 0.],
         [0., 0., 0., 0.]])

» np.ones((2,3))
↳ array([[1., 1., 1.],
         [1., 1., 1.]])

» np.eye(2,4)
↳ array([[1., 0., 0., 0.],
         [0., 1., 0., 0.]])

» np.identity(3)
↳ array([[1., 0., 0.],
         [0., 1., 0.],
         [0., 0., 1.]])

» np.eye(2,6)
↳ array([[1., 0., 0., 0., 0., 0.],
         [0., 1., 0., 0., 0., 0.]])

» np.eye(4,4)
↳ array([[1., 0., 0., 0.],
         [0., 1., 0., 0.],
         [0., 0., 1., 0.],
         [0., 0., 0., 1.]])

O método np.empty(m,n) permite a criação de arrays vazios, em geral destinados a serem preenchidos depois de sua criação por meio de algum cálculo ou leitura de dados. Nos exemplos criamos um array numérico vazio, arrN. Observe que não há garantia de que as entradas serão nulas. Em seguida criamos um array vazio de strings, com espaço para 3 caracteres (dtype='<U3′), e o preenchemos em um loop.

» # array numérico "vazio"
» arrN = np.empty((1,3))
» arrN
↳ array([[4.66896202e-310, 0.00000000e+000, 1.58101007e-322]])    

» # array de strings
» arr = np.empty((3,3), dtype='<U3')
» arr
↳ array([['', '', ''],
         ['', '', ''],
         ['', '', '']], dtype='<U3')

» for linha in range(arr.shape[0]):
»     for coluna in range(arr.shape[1]):
»         arr[linha,coluna] = 'a' + str(linha) + str(coluna)
» arr
↳ array([['a00', 'a01', 'a02'],
         ['a10', 'a11', 'a12'],
         ['a20', 'a21', 'a22']], dtype='<U3')

Alterando dimensões

Dado um array de uma única linha com r elementos podemos tranformá-lo em um array com shape = (m,n), desde que as dimensões sejam compatíveis, i.e., r = m × n. De fato, qualquer array pode ser transformado em outro se eles possuem o mesmo número de elementos (mesmo size ).

» arr = np.random.random(12)
» arr
↳ array([0.04276829, 0.76468762, 0.24807651, 0.75531679, 0.60327475,
         0.81704922, 0.08233836, 0.64112484, 0.55276595, 0.30669723,
         0.43989324, 0.60031761])

» # reshape
» arr.reshape(3,4)
↳ array([[0.04276829, 0.76468762, 0.24807651, 0.75531679],
         [0.60327475, 0.81704922, 0.08233836, 0.64112484],
         [0.55276595, 0.30669723, 0.43989324, 0.60031761]])

» np.linspace(0,10, 6).reshape(2,3)
↳ array([[ 0.,  2.,  4.],
         [ 6.,  8., 10.]])    

Indexação e fatiamento


Quando um array é criado ele recebe automaticamente um conjunto de índices. Um elemento pode ser lido ou alterado por meio de seu índice. Índices negativos contam de trás para frente, sendo arr[-1] o último elemento do array. Para selecionar (ou editar) vários elementos passamos uma lista de índices.

» arr = np.linspace(1,12, 12)
» arr
↳ array([ 1.,  2.,  3.,  4.,  5.,  6.,  7.,  8.,  9., 10., 11., 12.])

» # o 5º elemento
» arr[4]
↳ 5.

» # alterando o 5º elemento
» arr[4] = 100
» arr
↳ array([ 1.,  2.,  3.,  4.,  100.,  6.,  7.,  8.,  9., 10., 11., 12.])

» arr[-8]
↳ 100

» # lendo vários elementos
» arr[[0,2,5]]
↳ array([1., 3., 6.])

» # alterando vários elementos
» arr[[0,2,5]] = [10, 20, 30]
» arr
↳ array([ 10.,   2.,  20.,   4., 100.,  30.,   7.,   8.,   9.,  10.,  11.,  12.])

No caso de arrays bidimensionais os elementos do array são acessados pelos índices de suas linhas e colunas, sendo que arr[l,c] = al,c é o elemento da linha l e coluna c. Em objetos de ranks superiores cada índice se refere a um dos eixos.

Uma fatia ou slice do array é um subconjunto de elementos que pode ter o mesmo shape ou não. Para um vetor, digamos arr1D = [a0, a1, ..., aM] a fatia arr1D[m,n] = [am, ..., an-1], onde m ≥ 0, n ≤ M. Vale lembrar que o comprimento da fatia de um array unidimensional é arr1D[m,n].size = n-m.

» # outro teste para slices
» arr = np.arange(10)    # cria o array array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

» arr[6:9]
↳ array([6, 7, 8])

» arr[:3]
↳ array([0, 1, 2])

» arr[7:]
↳ array([7, 8, 9])

» # uma seção pode ser alterada
» arr[3:6] = -5
» arr
↳ array([ 0,  1,  2, -5, -5, -5,  6,  7,  8,  9])

» arr[7:] = -arr[7:]
» arr
↳ array([ 0,  1,  2, -5, -5, -5,  6, -7, -8, -9])

(†) Uma operação entre arrays de dimensões diferentes, como ocorre em arr[0:4] = -1 é chamado de propagação ou broadcasting. Para interagir com a 1ª parte da expressão a 2ª é transformada: -1 → arr[-1,-1,-1,-1]. Voltaremos a esse tópico.

Observe que uma fatia de um array é uma referência àquela parte do array e qualquer alteração feita na fatia se refletirá no array original. A notação arr[i:j] significa, claro, elementos de i-ésimo até (j-1)-ésimo. arr[:] significa todos os elementos do array.

» # criando um array de teste
» arr = (np.random.random(10)*10).round(1)
» arr
↳ array([3. , 1.5, 5.3, 1.3, 8.8, 9.8, 4.7, 0.1, 0.1, 0.6])
» fatia = arr[1:5]
» fatia
↳ array([1.5, 5.3, 1.3, 8.8])

# vamos alterar trecho da fatia
» fatia[1:3] = 0
» fatia
↳ array([1.5, 0. , 0. , 8.8])

» # o array original foi alterado
» arr
↳ array([3. , 1.5, 0. , 0. , 8.8, 9.8, 4.7, 0.1, 0.1, 0.6])

» # vamos alterar a fatia inteira
» fatia[:] = -10
» arr
↳ array([  3. , -10. , -10. , -10. , -10. ,   9.8,   4.7,   0.1,   0.1,  0.6])

» # valores específicos podem ser fornecidos (sem broadcast)
» fatia[:] = [-1,-2,-3,-4]
» arr
↳ array([ 3. , -1. , -2. , -3. , -4. ,  9.8,  4.7,  0.1,  0.1,  0.6])

Esse comportamento é útil quando se trabalha com array de dados muito grande e se deseja alterar apenas parte dele, lembrando que a biblioteca efetua suas operações mantendo os dados envolvidos na memória.

Em um array arr de 2 dimensões arr[m] é a m-ésima linha e arr[m,n] se refere ao elemento am,n, da m-ésima linha, n-ésima coluna. arr[m][n] é o mesmo am,n.

» # slices para dimensões mais altas
» arr = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
» arr
↳ array([[1, 2, 3],
         [4, 5, 6],
         [7, 8, 9]])

» # 2ª linha
» arr[1]
↳ array([4, 5, 6])

» arr[:1]
↳ array([[1, 2, 3]])

» # 1ª linha, 3º elemento
» arr[0,2]
↳ 3
» arr[0][2]
↳ 3

Uma cópia de um setor é uma referência para aquele setor, chamda de view ou visualização do segmento. Alterações feitas à view se refletam no array original, a menos que o método arr.copy() seja usado. Um array copiado dessa forma perde a referência com o array original e pode ser modificado independentemente.

» # slices em 3D:
» # criamos um array com shape (2,3,2)
» arr3D = np.arange(12).reshape(2,3,2)
» arr3D
↳ array([[[ 0,  1],
         [ 2,  3],
         [ 4,  5]],

        [[ 6,  7],
         [ 8,  9],
         [10, 11]]])

» # a 1ª matriz é
» arr3D[0]
↳ array([[0, 1],
        [2, 3],
        [4, 5]])
» # a 2ª linha da 1ª matriz é
» arr3D[0,1]
↳ array([2, 3])

» # seu 2º elemento
» arr3D[0,1,1] # o mesmo que arr3D[0,1][1]
↳ 1

» # copiamos um slice de 2 formas
» guardar = arr3D[0].copy()
» slice = arr3D[0]
» # ambos com os valores da  1ª matriz

» # alteramos toda a 1ª matriz
» arr3D[0] = 12
» # o array original foi alterado
» arr3D
↳ array([[[12, 12],
         [12, 12],
         [12, 12]],

        [[ 6,  7],
         [ 8,  9],
         [10, 11]]])
» # a cópia por referência foi alterada
» slice
↳ array([[12, 12],
         [12, 12],
         [12, 12]])

» # mas a cópia por valor não foi alterada
» guardar
↳ array([[0, 1],
         [2, 3],
         [4, 5]])

» # podemos restaurar o array aos seus valores originais
» arr3D[0] = guardar
» arr3D
↳ array([[[ 0,  1],
          [ 2,  3],
          [ 4,  5]],

         [[ 6,  7],
          [ 8,  9],
          [10, 11]]])


A notação de slice, idêntica à usada em listas do python, funciona em arrays. Para um array unidimensional arr1d[i:f] retorna do i-ésimo elemento até j-ésimo elemento (exclusive). Para um array de 2 dimensões arr2d[i:f] retorna i-ésima linha até j-ésima linha (exclusive). Se i é omitido o início é usado, se j é omitido o final é usado. Portanto arr2d[:2] significa as duas primeiras linhas do array (linha 0 e linha 1).

Slices ou segmentos múltiplos podem ser usados. Por exemplo, arr2d[m:n, r:s] são as linhas de m até n-1, colunas de r até s-1.

» # array 1d  (um vetor)  
» arr = np.array([1.2, 2.3, 3.4, 4.5, 5.6])
» arr[2:4]
↳ array([3.4, 4.5])

» # array 2d  (uma matriz)  
» arr2d = np.array([[1,2,3],[4,5,6],[7,8,9]])
» arr2d
↳ array([[1, 2, 3],
         [4, 5, 6],
         [7, 8, 9]])

» arr2d[0:2]
↳ array([[1, 2, 3],
         [4, 5, 6]])

» arr2d[:2]
↳ array([[1, 2, 3],
         [4, 5, 6]])
       
» # slices múltiplos
» arr2d[:2, 1:]
↳ array([[2, 3],
         [5, 6]])

arr2d[:2, 1:] são as linhas 0 e 1, colunas de 1 em diante.

O indexador pode ser um array booleano, e os valores serão filtrados apenas se o índice for True. Essa operação pode ser muito útil para a implementação de filtragens de tipos diversos.

» # o indexador pode ser booleano
» ar1 = np.array([-1,0, 3, 7, -2, 10])
» ar2 = np.array([-2,10, 2, 9, -1, 8])

» # extrair apenas 0º e 2º elemento
» ar1[[True, False, True, False, False, False]]
↳ array([-1,  3])

» # elementos de ar1 maiores que 2
» ar1[ar1>2]
↳ array([ 3,  7, 10])

» # elementos de ar1 maiores que os de ar2
» ar1[ar1>ar2]
↳ array([-1,  3, 10])

Suponha que temos dados sobre alguns países, armazenados em forma tabular. O primeiro array contém os nomes dos países, como se fosse um cabeçalho da tabela seguinte. O segundo array contém dados numéricos de qualquer natureza, com 5 linhas e 4 colunas, cada coluna contendo dados relativos ao país no cabeçalho. Para esse exemplo geramos esses dados aleatoriamente, apenas para exibir a operação.

» arrPais = np.array(['Brasil', 'Chile', 'Brasil', 'Peru'])
» arrDados = np.random.randn(5,4).round(2)  # 5 linhas e 4 colunas

» print(arrPais, '\n', arrDados)
↳ ['Brasil' 'Chile' 'Brasil' 'Peru'] 
   [[-0.71  0.42 -0.52  0.63]
   [-1.12 -0.29  0.03  1.43]
   [ 0.99  0.45  1.08  0.53]
   [-0.78  0.18 -0.07  0.28]
   [-2.03  0.44  0.07  1.28]]

» # podemos exibir todas as linhas das colunas 0 e 2
» arrDados[:,[0,2]]
↳ array([[-0.71, -0.52],
         [-1.12,  0.03],
         [ 0.99,  1.08],
         [-0.78, -0.07],
         [-2.03,  0.07]])

» # alternativamente, as colunas que correspondem ao Brasil
» arrBrasil = arrDados[:, arrPais=='Brasil']
» arrBrasil
↳ array([[-0.71, -0.52],
         [-1.12,  0.03],
         [ 0.99,  1.08],
         [-0.78, -0.07],
         [-2.03,  0.07]])

» # na tabela do Brasil, suponha que valores negativos sejam insignificantes
» # podemos eliminá-los com uma filtragem
» arrBrasil[arrBrasil < 0] = 0
» arrBrasil
↳ array([[0.  , 0.  ],
         [0.  , 0.03],
         [0.99, 1.08],
         [0.  , 0.  ],
         [0.  , 0.07]])

» # a soma desses dados é
» arrBrasil.sum()
↳ 2.17

» # para exibir os demais dados (não do Brasil)
» arrDados[:, arrPais!='Brasil'] # ou arrDados[:, ~(arrPais=='Brasil')]
↳ array([[ 0.42,  0.63],
         [-0.29,  1.43],
         [ 0.45,  0.53],
         [ 0.18,  0.28],
         [ 0.44,  1.28]])

Observe que arrPais!='Brasil' é a negação de arrPais=='Brasil'. O parênteses em ~(arrPais=='Brasil') é necessário pois a negação ~ tem precedência sobre o teste de igualdade. Sem o parênteses ~arrPais seria avaliado primeiro, o que resultaria em erro pois o array não é booleano.

As ferramentas do pandas facilitam operações como essas.

Operações matemáticas

Operações são realizadas elemento a elemento (que abreviaremos para “e/e” nesse texto). Operações usuais de um array por um escalar são são propagadas entre o escalar e cada elemento do array. Operações entre arrays são feitas e/e, ou seja, realizadas entre elementos na mesma posição. Os arrays devem ter as mesmas dimensões.

Operações de incremento, tais como array += 1 (que significa array = array + 1) ou array *= 2 são realizados inplace (alteram o próprio array). Diversas outras operações do NumPy (e do pandas) são realizadas inplace enquanto em várias delas existe o parâmetro inplace = True/False que permite a decisão de qual caso se deseja naquele momento.

Operações podem ser feitas entre arrays retornados por funções (que retornam arrays).

» # operações com escalares    
» ar1 =  np.linspace(10, 20, 5)
» ar1
↳ [10.  12.5 15.  17.5 20. ]

» ar1 + 10
↳ [20.  22.5 25.  27.5 30. ]

» ar1 * 10
↳ [100. 125. 150. 175. 200.]

» ar1 / 10
↳ [1.   1.25 1.5  1.75 2.  ]

» ar1**2
↳ [100.   156.25 225.   306.25 400.  ]

» 1/arr1
↳ array([0.1       , 0.08      , 0.06666667, 0.05714286, 0.05      ])

» # operações entre arrays
» ar2 =  np.linspace(10, 50, 5)
» print(ar1)
» print(ar2)
↳ [10.  12.5 15.  17.5 20. ]
↳ [10. 20. 30. 40. 50.]

» ar1 + ar2
↳ array([20. , 32.5, 45. , 57.5, 70. ])

» ar1 * ar2
↳ array([ 100.,  250.,  450.,  700., 1000.])

» ar2 / ar1
↳ array([1.        , 1.6       , 2.        , 2.28571429, 2.5       ])

» # operações com arrays (2 × 3)
» ar3 = np.linspace(0,5, 6).reshape(2,3)
» ar4 = np.linspace(0,10, 6).reshape(2,3)

» print(ar3)
↳ [[0. 1. 2.]
   [3. 4. 5.]]

» print(ar4)
↳ [[ 0.  2.  4.]
   [ 6.  8. 10.]]

» ar3 + ar4
↳ array([[ 0.,  3.,  6.],
         [ 9., 12., 15.]])

» ar3 - ar4
↳ array([[ 0., -1., -2.],
         [-3., -4., -5.]])

» ar3 * ar4
↳ array([[ 0.,  2.,  8.],
         [18., 32., 50.]])
       
» # operações de incremento são realizados inplace
» ar3 +=1
» ar3
↳ array([[1., 2., 3.],
         [4., 5., 6.]])

» # seno e cosseno em np retorna um array e/e
» ar1 * np.sin(ar2)
↳ array([ -5.44021111,  11.41181563, -14.82047436,  13.03948031,
          -5.24749707])

» ar3 * np.cos(ar4)
↳ array([[ 0.        , -0.41614684, -1.30728724],
         [ 2.88051086, -0.58200014, -4.19535765]])

Comparações entre arrays resultam em arrays booleanos.

» ar5 = np.linspace(0,5, 6).reshape(2,3)
» ar6 = np.random.random(6).reshape(2,3).round(2)*5

» ar5
↳ array([[0., 1., 2.],
         [3., 4., 5.]])
» ar6
↳ array([[3.  , 2.45, 1.2 ],
         [3.1 , 1.55, 1.75]])

» arrMaior= ar5 > ar6
» arrMaior
↳ array([[False, False,  True],
         [False,  True,  True]])

Broadcasting: A operações feitas acima, entre um array e um escalar, transformam o escalar em um array de dimensões apropriadas (de mesmo shape) antes de sua realização. Essa operação se chama broadcasting:. Por exemplo:

» ar1 = np.array([0,1,2,3])
» ar2 = np.array([4,4,4,4])

» # a soma com um escalar
» ar1 + 4
↳ array([4, 5, 6, 7])

» # é  mesmo que
» ar1 + ar2
↳ array([4, 5, 6, 7])

» # os elementos são iguais
» ar1 + ar2 == ar1 + 4
↳ array([ True,  True,  True,  True])

# o mesmo ocorre com comparações
» ar1 ≥ 2
↳ array([False, False,  True,  True])

Uma forma de seleção diferente consiste em passar listas de valores como índices. O array retornado depende de como essas listas são passadas. Essa é técnica é chamada de fancy indexing (indexação sofisticada). Em qualquer dos casos abaixo os índices podem aparecer em qualquer ordem.

O slice array[[i,j,...]] contém as linhas array[i], array[j], etc. (O mesmo que array[:,[i,j,...]]).
O slice array[:,[i,j,...]] contém as colunas array[:,i], array[:,j], etc.
O slice array[[i,j,...]:[r, s,...]] é uma linha contendo os elementos array[i,r], array[j,s], etc.

» # fancy indexing (passando arrays como indices)
» # vamos construir um array 6 × 5 e atribuir seus valores um a um
» arr = np.empty((6,5))

» for linha in range(6):
»     for coluna in range(5):
»         arr[linha,coluna] = linha * 10 + coluna
        
» # o array obtido é (uma forma de identificar facilmente de que elemento se trata)
» arr
↳ array([[ 0.,  1.,  2.,  3.,  4.],
         [10., 11., 12., 13., 14.],
         [20., 21., 22., 23., 24.],
         [30., 31., 32., 33., 34.],
         [40., 41., 42., 43., 44.],
         [50., 51., 52., 53., 54.]])

» # podemos selecionar linhas (em qualquer ordem)
» arr[[3,5,1]]       # o mesmo que arr[[3,5,1],:]
↳ array([[30., 31., 32., 33., 34.],
         [50., 51., 52., 53., 54.],
         [10., 11., 12., 13., 14.]]) 

» # ou colunas (em qualquer ordem)
» arr[:,[3,1]]
↳ array([[ 3.,  1.],
         [13., 11.],
         [23., 21.],
         [33., 31.],
         [43., 41.],
         [53., 51.]])

» # fornecer duas listas (que devem ter o mesmo tamanho) tem efeito diferente,
» # retornando array de i dimensão com os índices dados nas listas
» arr[[0,3,4,1],[0,3,4,1]]
↳ array([ 0., 33., 44., 11.])

» arr[[0,3,4,1],[1,0,4,2]]
↳ array([ 1., 30., 44., 12.])

Funções universais

Funções universais, ou ufunc, são funções que agem e/e, sobre todos os elementos de um array, retornando outra array de mesmas dimensões. Essas operações são também chamadas de operações vetorializadas. Embora envolvam laços (loops ) esses são realizados internamente e de forma eficiente, de modo a agilizar os processos.

Uma tabela das funções universais é encontrada abaixo.

Método retorna
abs, fabs valor absoluto inteiros, floats, ou complexos
sqrt raiz quadrada (equivale a arr**0.5)
square elementos elevados ao quadrado (equivale a arr**2)
exp exponencial de cada elemento (ex)
log, log10, logaritmos naturais (de base e e base 10var>
log2, log1p logaritmos de base 2 e log(1 + x)
sign sinal: (1, 0, -1) (positivo, zero, negativo)
ceil teto, menor inteiro maior ou igual
floor piso, maior inteiro menor ou igual
rint arredonda para o inteiro mais próximo, preservando dtype
modf partes inteiras e fracionárias do array, em e arrays
isnan array booleano, se o valor é NaN (Not a Number)
isfinite array booleano, se cada valor é finito (non-inf, non-NaN)
isinf array booleano, se cada valor é infinito
cos, sin, tan funções trigonométricas
cosh, sinh, tanh funções trigonométricas hiperbólicas
arccos, arcsin, arctan arcos de funções trigonométricas
arccosh, arcsinh, arctanh arcos de funções trigonométricas hiperbólicas
logical_not array booleano, negação do array (equivalent to ~arr).

Exemplos de uso:

» # Funções universais
» arrBool = np.array([True, False, False, True])
» arrBool
↳ array([ True, False, False,  True])
» np.logical_not(arrBool)
↳ array([False,  True,  True, False])

» arr = np.linspace(0, 10, 6)
» arr -=5
» arr
↳ array([-5., -3., -1.,  1.,  3.,  5.])

» np.abs(arr)
↳ array([5., 3., 1., 1., 3., 5.])

» # não altera arr
» np.sign(arr)
↳ array([-1., -1., -1.,  1.,  1.,  1.])

» arr = arr/10 +4
» arr = arr.reshape(2,3)
» arr
↳ array([[3.5, 3.7, 3.9],
         [4.1, 4.3, 4.5]])

» np.modf(arr)
↳ (array([[0.5, 0.7, 0.9],
          [0.1, 0.3, 0.5]]),
↳  array([[3., 3., 3.],
          [4., 4., 4.]]))

Funções de Agregação


Funções de agregação são funções que realizam operações em todos os elementos do array, retornando um escalar(um número). Em sua maioria elas retornam cálculos estatíscos sobre os dados.

» ag = np.array([3.3, 12.5, 11.2, 5.7, 0.3])
» ag
↳ array([ 3.3, 12.5, 11.2,  5.7,  0.3])

» # outputs nos comentários
» ag.sum()       # 33.0
» ag.min()       # 0.3
» ag.max()       # 12.5
» ag.mean()      # 6.6
» ag.std()       # 4.6337889464238655
» ag.var()       # 21.472
» ag.argmin()    # 4
» ag.argmax()    # 1

» # o mesmo vale para arrays com outros shapes
» ag23 =  np.random.random(6).reshape(2,3) -.5
» ag23
↳ array([[ 0.10425075, -0.29335437, -0.36814244],
         [ 0.32986805,  0.17289794, -0.4568041 ]])

» ag23.mean()
↳ -0.08521402850598175

Diversas das operações podem ser feitas sobre o array inteiro ou sobre linhas ou colunas. Para isso podemos especificar axis = 0 para operações sobre elementos das colunas, axis = 1 para operações sobre elementos das linhas.

» arr = np.random.randn(4,5).round(2)
» arr
↳ array([[ 0.45, -0.11, -0.54, -0.97, -0.23],
         [ 1.65,  0.76, -0.39, -1.83,  0.02],
         [ 0.45, -1.22,  1.93,  1.92, -0.43],
         [-0.39,  2.01,  0.04,  0.67, -1.1 ]])

» # a soma de todos os elementos
» arr.sum()
↳ 2.689999999999999

» # soma sobre elementos de cada coluna
» arr.sum(axis=0)       # ou arr.sum(0)
↳ array([ 2.17,  0.37,  0.17, -2.49, -3.13])

» # soma sobre elementos de cada linha
» arr.sum(axis=1)       # ou arr.sum(1)
↳ array([-0.42, -1.67,  0.6 , -1.42])

» # produtos dos elementos das linhas
» arr.prod(axis=1).round(2)
↳ array([0.01, 0.02, 0.87, 0.02])

» # produtos dos elementos das colunas
» arr.prod(axis=0).round(2)
↳ array([-0.13,  0.21,  0.02,  2.28, -0.  ])


Funções básicas de agregação:

Função retorna
np.all booleano, True se todos os elementos no array são não nulos
np.any booleano, True se algum dos elementos no array é não nulo
np.sum soma dos elementos do array ou sobre eixo especificado †.
np.mean Média aritmética; arrays de comprimento nulo têm média = NaN
np.std, np.var variância e desvio padrão
np.max, np.min valor máximo e mínimo no array
np.argmax, np.argmin índices do valor máximo e mínimo no array
np.cumsum soma cumulativa dos elementos, começando em 0
np.cumprod produto cumulativo dos elementos, começando em 1

(†) Para arrays de comprimento nulo tem soma np.sum = NaN.

🔺Início do artigo

Bibliografia

  • Blair, Steve: Python Data Science, edição do autor, 2019.
  • Harrison, Matt: Learning Pandas, Python Tools for Data Munging, Data Analysis, and Visualization,
    Treading on Python Series, Prentiss, 2016.
  • Johansson, Robert: Numerical Python, Scientific Computing and Data Science Applications with Numpy, SciPy and Matplotlib, 2nd., Chiba, Japan, 2019.
  • McKinney, Wes: Python for Data Analysis, O’Reilly Media, Sebastopol CA, 2018.
  • McKinney, Wes, Pandas Development Team: pandas: powerful Python data analysis toolkit Release 1.2.1,
  • Miller, Curtis: Hands-On Data Analysis with NumPy and pandas, Packt Publishing, Birmingham, 2018.
  • Nelli, Fabio: Python Data Analytics With Pandas, NumPy, and Matplotlib, 2nd., Springer, New York, 2018.
  • Site AI Ensina: Entendendo a biblioteca NumPy, acessado em julho de 2021.
  • Site GeeksforGeeks: Python NumPy, acessado em julho de 2021.
  • Site W3 Schools: NumPy Tutorial, acessado em julho de 2021.
  • NumPy, docs.
  • NumPy, Learn.

Nesse site:

Dataframes: Resumo


Atributos e métodos do pandas.dataFrame


Nas tabelas usamos df para referenciar um dataframe do pandas. Quando necessária a interação com um segundo dataframe ele é designado por dfOutro. A abreviação e/e significa “elemento a elemento”, utilizada quando a operação é aplicada entre todos os elementos de um e outro dataframe, usando elementos na posição (linha, coluna).

Atributos dos dataframes

Atributo descrição
df.at(m,n) valor correspondente à linha=m, coluna=n
df.axes lista representando os eixos do df
df.columns rótulos das colunas do df
df.dtypes dtypes no df
df.flags propriedades associadas ao df
df.iat valor para um par linha/coluna
df.iloc indexação baseada em localização puramente inteira para seleção por posição
df.loc grupo de linhas e colunas por rótulo(s) ou matriz booleana
df.ndim número (int) de eixos/dimensões da matriz
df.shape tupla com a dimensionalidade do df
df.size número (int) de elementos no objeto
df.style um objeto Styler
df.values representação Numpy do df

Métodos dos dataframes

Exibição e consulta ao dataframe

df.at(m,n) valor correspondente à linha=m, coluna=n
df.attrs dict de atributos globais do objeto (experimental)
df.head([n]) as primeiras n linhas
df.info([verbose, buf, max_cols, memory_usage,…]) resumo conciso do dataframe
df.tail([n]) últimas n linhas
df.items() iteração sobre (nome da coluna, Series)
df.iteritems() iteração sobre (nome da coluna, Series)
df.iterrows() iteração sobre linhas de df como pares (index, Series)
df.itertuples([index, nome]) iteração sobre linhas do df como pares nomeados
df.memory_usage([index, deep]) uso de memória de cada coluna em bytes
df.nlargest(n, columns[, keep]) n primeiras linhas ordenadas por colunas em ordem decrescente
df.nsmallest(n, colunas[, manter]) n primeiras n linhas ordenadas por colunas em ordem crescente
df.nunique([axis, dropna]) número de elementos distintos no eixo especificado

Operações matemáticas

Método descrição
df.abs() valor absoluto de cada elemento
df.add(dfOutro[, axis, level, fill_value]) adição de df e dfOutro
df.apply(func[, axis, raw, result_type, args]) aplica função ao longo de um eixo do df
df.applymap(func[, na_action]) aplica uma função a um df e/e
df.div(dfOutro[, axis, level, fill_value]) divisão flutuante de df por dfOutro
df.divide(dfOutro[, axis, level, fill_value]) divisão flutuante de df por dfOutro
df.dot(dfOutro) multiplicação de df por dfOutro
df.eval(expr[, inplace]) avalia a string ‘expr’ contendo operações sobre colunas do df
df.ewm([com, span, halflife, alpha,…]) função exponencial ponderada (EW)
df.floordiv(dfOutro[, axis, level, fill_value]) divisão inteira do df por dfOutro
df.mod(dfOutro[, axis, level, fill_value]) módulo de df por dfOutro e/e
df.mul(dfOutro[, axis, level, fill_value]) multiplicação de df por dfOutro, e/e
df.multiply(dfOutro[, axis, level, fill_value]) multiplicação de df por dfOutro, e/e
df.pow(dfOutro[, axis, level, fill_value]) exponencial do df por dfOutro e/e
df.prod([axis, skipna, level, numeric_only, ...]) produto dos valores sobre o eixo especificado
df.product([axis, skipna, level, numeric_only, ...]) produto dos valores sobre o eixo especificado
df.radd(dfOutro[, axis, level, fill_value]) adição de df e dfOutro e/e, com suporte a fill_na
df.rdiv(dfOutro[, axis, level, fill_value]) divisão float de df por dfOutro e/e
df.rfloordiv(dfOutro[, axis, level, fill_value]) divisão inteira do df e dfOutro e/e
df.rmod(dfOutro[, axis, level, fill_value]) módulo de df e dfOutro e/e
df.rmul(dfOutro[, axis, level, fill_value]) multiplicação de df por dfOutro e/e
df.round([decimals]) arredonda elementos de df para um número dado de casas decimais
df.rpow(dfOutro[, axis, level, fill_value]) exponencial do df por dfOutro e/e
df.rsub(dfOutro[, axis, level, fill_value]) subtração de df e dfOutro e/e
df.rtruediv(dfOutro[, axis, level, fill_value]) divisão float de df por dfOutro e/e
df.sub(dfOutro[, axis, level, fill_value]) subtração de df por dfOutro e/e
df.sum([axis, skipna, level, numeric_only, ...]) soma dos valores sobre o eixo especificado
df.transform(func[, axis]) executa func em elementos de df, sobre eixo especificado
df.truediv(dfOutro[, axis, level, fill_value]) divisão float de df por dfOutro e/e

Formatação, Transposição, Ordenação

Método descrição
Transposição Tij = Mji
df.T a transposta de df
df.transpose(* args[, copiar]) transpor índices e colunas (obter a transposta)
df.droplevel(nível[, axis]) remove colunas com index ou níveis solicitados
df.melt([id_vars, value_vars, var_name,…]) Unpivot dataFrame para um formato largo
df.pivot([index, columns, values]) refaz df organizado por valores de index/column fornecidos
df.pivot_table([values, index, columns,…]) cria tabela dinâmica no estilo planilha como um df
df.sort_index([axis, level, ascendente, ...]) classifica objeto por rótulos (ao longo de um eixo)
df.sort_values ​​(by[, axis, ascending, inplace, ...]) ordena por valores ao longo do eixo especificado
df.stack([level, dropna]) empilha os níveis prescritos das colunas para o índice
df.swapaxes(axis1, axis2[, copy]) inverte eixos com seus valores respectivos
df.swaplevel([i, j, axis]) inverte níveis i e j em umMultiIndex
df.unstack([level, fill_value]) pivot um level dos rótulos de índice (necessariamente hierárquicos)
df.explode(column[, ignore_index]) transforma cada elemento de uma lista em uma linha, mantendo os índices
df.squeeze([axis]) comprime valores de eixo unidimensional para escalares

Métodos estatísticos

Método descrição
df.corr([method, min_periods]) calcula correlação de pares de colunas, excluindo valores NA/nulos
df.corrwith(dfOutro[, axis, drop, method]) calcula correlação de pares
df.count([axis, level, numeric_only]) quantos valores não NA para cada coluna ou linha
df.cov([min_periods, ddof]) calcula covariância de pares de colunas, excluindo NA/nulos
df.cummax([axis, skipna]) máximo cumulativo em um df
df.cummin([axis, skipna]) mínimo cumulativo sobre um eixo do df
df.cumprod([axis, skipna]) produto cumulativo sobre um eixo do df
df.cumsum([axis, skipna]) soma cumulativa sobre eixo do df
df.describe([percentis, include, exclude, ...]) gera descrição estatística
df.diff([points, axis]) primeira diferença discreta do elemento
df.hist([column, by, grid, xlabelsize, xrot,…]) histograma das colunas do df
df.info([verbose, buf, max_cols, memory_usage,…]) resumo conciso do dataframe
df.kurt([axis, skipna, level, numeric_only]) curtose imparcial sobre o eixo especificado
df.kurtosis([axis, skipna, level, numeric_only]) curtose imparcial sobre o eixo especificado
df.mad([axis, skipna, level]) desvio absoluto médio sobre especificado
df.max([axis, skipna, level, numeric_only]) máximo dos valores sobre o eixo especificado
df.mean([axis, skipna, level, numeric_only]) média dos valores sobre o eixo especificado
df.median([axis, skipna, level, numeric_only]) mediana dos valores sobre o eixo especificado
df.min([axis, skipna, level, numeric_only]) mínimo dos valores sobre o eixo especificado
df.mode([axis, numeric_only, dropna]) moda(s) dos elemento ao longo do eixo selecionado
df.pct_change([periods, method, limit, frequency]) alteração percentual entre o elemento atual e o anterior
df.quantil([q, axis, numeric_only, interpolateion]) valores no quantil dado sobre o eixo especificado
df.sample([n, frac, replace, weight,…]) amostra aleatória de itens de um eixo
df.sem([axis, skipna, level, ddof, numeric_only]) erro padrão imparcial da média sobre o eixo especificado
df.std([axis, skipna, level, ddof, numeric_only]) desvio padrão da amostra sobre o eixo especificado
df.var([axis, skipna, level, ddof, numeric_only]) variação imparcial sobre o eixo especificado

Gerenciamento, filtragem e consulta ao dataframe

Método descrição
df.add_prefix(prefixo) acrecenta prefixo nos rótulos (labels)
df.add_suffix(sufixo) acrecenta sufixo nos rótulos (labels)
df.agg([function, axis]) agregar usando function no eixo especificado
df.aggregate([função, axis]) agregar usando function no eixo especificado
df.align(dfOutro[, junção, axis, nível, cópia, ...]) alinha dois objetos em seus eixos com o método especificado
df.append(dfOutro[, ignore_index,…]) anexa linhas de dfOutro ao final de df
df.asof(where[, subset]) última(s) linha(s) sem NaN antes de where
df.assign(** kwargs) atribui novas colunas a um df
df.clip([lower, upper, axis, inplace]) corta (trim) os valores no(s) limite(s) dados
df.combine(dfOutro, func[, fill_value, ...]) combina colunas com dfOutro
df.combine_first(dfOutro) atualiza elementos nulos com elementos de dfOutro, na mesma posição
df.compare(dfOutro[, align_axis, keep_shape,…]) compara com dfOutro> e exibe diferenças
df.copy([deep]) faz cópia do objeto com índices e dados
df.drop([labels, axis, index, columns, level, ...]) remove linhas ou colunas com os rótulos especificados
df.drop_duplicates([subset, keep, inplace, ...]) remove linhas duplicadas
df.expanding([min_periods, center, axis, method]) aplica transformações de expansão
df.filter([itens, like, regex, axis]) filtra linhas ou colunas do df de acordo com os índices especificados
df.first_valid_index() índice do primeiro valor não NA; None se nenhum for encontrado
df.get(key[, default]) item do dataframe correspondente à chave fornecida
df.groupby([by, axis, level, as_index, ...]) agrupa df usando um mapper ou Series de colunas
df.infer_objects() tentativa de inferir dtypes para colunas do objeto
df.insert(loc, column, value[, allow_duplicates]) insere coluna no df no local especificado
df.join(dfOutro[, on, how, lsuffix, rsuffix, sort]) junta colunas de df e dfOutro
df.lookup(row_labels, col_labels) (descontinuado) “indexação extravagante” baseada em rótulos para df
df.mask(cond[, dfOutro, local, axis, level, ...]) substitui valores onde a condição é True
df.merge(right[, how, on, left_on, …]) mesclar df usando database de estilo
df.pipe(func, * args, ** kwargs) aplicação de func(self, * args, ** kwargs)
df.pop(item) remove e retorna o item removido
df.query(expr[, inplace]) consulta colunas de df com uma expressão booleana
df.rank([axis, method, numeric_only, ...]) classificações de dados numéricos (1 a n) ao longo do eixo
df.replace([to_replace, value, inplace, limit,…]) substitui valores em to_replace com value
df.rolling(window[, min_periods, center,…]) cálculos de “rolling window”
df.select_dtypes([include, exclude]) subset de colunas do df com base nos dtypes da coluna
df.skew([axis, skipna, level, numeric_only]) inclinação imparcial sobre o eixo especificado
df.slice_shift([perids, axis]) (descontinuado) Equivalente a shift sem copiar dados
df.sparse alias de pandas.core.arrays.sparse.accessor.SparseFrameAccessor
df.take(indices[, axis, is_copy]) elementos nos índices posicionais fornecidos ao longo de um eixo
df.truncate([before, after, axis, copy]) truncar df antes e depois de valores de índices
df.update(dfOutro[, juntar, substituir,…]) modifique “no local” usando valores não NA de dfOutro
df.value_counts([subset, normalize, …]) retorna série contendo número de linhas exclusivas no df
df.where(cond[, dfOutro, inplace, axis, level, ...]) substitui valores em que a condição é falsa
df.xs(key[, axis, level, drop_level]) seção transversal de df

Testes e comparações

Método descrição
df.empty booleano: se o df está vazio
df.all([axis, bool_only, skipna, nível]) booleano: se todos os elementos são verdadeiros, sobre um eixo (se especificado)
df.any([axis, bool_only, skipna, nível]) booleano: se algum elemento é verdadeiro, sobre um eixo (se especificado)
df.bool() booleano, se um único elemento do df é True/False
df.duplicated([subset, keep]) série booleana marcando linhas duplicadas
df.eq(dfOutro[, axis, level]) boolena: se elementos são iguais a um escalar ou dfOutro, e/e
df.equal(dfOutro) booleano: se dois objetos contêm os mesmos elementos
df.ge(dfOutro[, axis, level]) dataframe booleano, maior ou igual entre df e dfOutro, e/e
df.gt(dfOutro[, axis, level]) dataframe booleano, maior que, entre df e dfOutro, e/e
df.isin(values) booleano: se cada elemento no df está contido em values
df.le(dfOutro[, axis, level]) booleano: menor ou igual entre elementos de df e dfOutro, e/e
df.lt(dfOutro[, axis, level]) booleano: menor dos elementos de df e dfOutro, e/e
df.ne(dfOutro[, axis, level]) booleano, diferente entre df e dfOutro, e/e

Operações com rótulos (labels) e índices

df.index índices do df
df.keys() retorna o index
df.last_valid_index() índice do último valor não NA; None se nenhum valor NA for encontrado
df.reindex([labels, index, columns, axis, ...]) substitue índices de df, com lógica de preenchimento opcional
df.reindex_like(dfOutro[, method, copy, limit, ...]) retorna objeto com índices correspondentes à dfOutro
df.rename([mapper, index, columns, axis, copy, ...]) renomear rótulos dos eixos
df.rename_axis([mapper, index, columns, axis, ...]) define o nome do eixo para o índices ou colunas
df.reorder_levels(order[, axis]) reorganiza níveis de índices usando a ordem em order
df.reset_index([level, drop, inplace, ...]) redefine um índice ou seu nível
df.set_axis(rótulos[, axis, local]) atribui o índice desejado a determinado eixo
df.set_flags(*[, copy, allow_duplicated_labels]) novo objeto com sinalizadores (flags) atualizados
df.set_index(keys[, drop, append, inplace, ...]) define índices de df usando colunas existentes
df.idxmax([axis, skipna]) índice da primeira ocorrência do máximo sobre o eixo especificado
df.idxmin([axis, skipna]) índice da primeira ocorrência do mínimo sobre o eixo especificado

Plots

Método descrição
df.boxplot([column, by, ax, font-size, rot, ...]) traça gráfico de caixa usando as colunas do df
df.plot o mesmo que pandas.plotting._core.PlotAccessor
df.hist([column, by, grid, xlabelsize, xrot,…]) histograma das colunas do df

Serialização e conversões

Método descrição
df.astype(dtype[, copy, errors]) transforma objeto para um dtype especificado
df.convert_dtypes([infer_objects,…]) converte colunas para os melhores dtypes usando dtypes com suporte parapd.NA
df.from_dict(data[, orient, dtype, columns]) constroi df a partir de dict ou similar
df.from_records(data[, index, exclusion,…]) converte ndarray ou dados estruturados em dataframe
df.to_clipboard([excel, sep]) copia objeto para a área de transferência do sistema
df.to_csv([path_or_buf, sep, na_rep,…]) grava objeto em um arquivo de valores separados por vírgula (csv)
df.to_dict([orient, into]) converte df em um dicionário
df.to_excel(excel_writer[, sheet_name, na_rep,…]) grava objeto em uma planilha Excel
df.to_feather(path, **kwargs) grava df no formato binário Feather
df.to_gbq(destination_table[, project_id,…]) grava df em uma tabela Google BigQuery
df.to_hdf(path_or_buf, key[, mode, complevel,…]) grava objeto em um arquivo HDF5 usando HDFStore
df.to_html([buf, columns, col_space, header,…]) renderiza df como uma tabela HTML
df.to_json([path_or_buf, orient, date_format,…]) converte objeto em uma string JSON
df.to_latex([buf, colunas, col_space, cabeçalho,…]) renderiza objeto em uma tabela LaTeX
df.to_markdown([buf, mode, index, ...]) imprima df em formato Markdown
df.to_numpy([dtype, copy, na_value]) converte df em uma matriz NumPy
df.to_parquet([path, engine, ...]) grave df em formato binário parquet
df.to_period([freq, axis, copy]) converte df de DatetimeIndex para PeriodIndex
df.to_pickle(path[, compression, protocol, ...]) serializa o objeto para o arquivo pickle
df.to_records([index, column_dtypes, index_dtypes]) converte df em uma matriz de registro NumPy
df.to_sql(name, con[, schema, if_exists,…]) grava registros em df em um banco de dados SQL
df.to_stata(path[, convert_dates, write_index,…]) exporta df para o formato Stata dta
df.to_string([buf, columns, col_space, header, ...]) renderiza df em uma saída tabular compatível com o console
df.to_timestamp([freq, how, axis, copy]) converte para DatetimeIndex de timestamps, no início do período
df.to_xarray() converte para objeto xarray
df.to_xml([path_or_buffer, index, root_name,…]) renderiza df em um documento XML

Gerenciamento de valores ausentes

Método descrição
df.backfill([axis, inplace, limit, reduction]) o mesmo que df.fillna() com method = 'bfill'
df.bfill([axis, inplace, limit, downcast]) o mesmo que df.fillna() com method = 'bfill'
df.dropna([axis, how, treshold, subset, inplece]) remove os valores ausentes
df.ffill([axis, inplace, limit, reduction]) o mesmo que df.fillna() com method = 'ffill'
df.fillna([value, method, axis, local, ...]) preenche campos com NA/NaN usando o método especificado
df.interpolate([method, axis, limit, inplace, ...]) substitui valores NaN usando método de interpolação
df.isna() detecta valores ausentes
df.isnull() detecta valores ausentes
df.notna() valores existentes (não ausentes)
df.notnull() valores existentes (não ausentes)
df.pad([axis, inplace, limit, downcast]) o mesmo que df.fillna() com method = 'ffill'

Séries temporais e dados com hora/data

Método descrição
df.asfreq(freq[, método, como, normalizar, ...]) converte série temporal para a frequência especificada
df.at_time(hour[, asof, axis]) seleciona valores em um determinado horário do dia
df.between_time(start_time, end_time[,…]) seleciona valores entre horários especificados
df.first(offset) seleciona períodos iniciais em série temporal usando deslocamento (offset)
df.last(offset) selecione períodos finais da série temporal com deslocamento (offset)
df.resample(rule[, axis, closed, label, ...]) reamostrar os dados de série temporal
df.shift([periods, freq, axis, fill_value]) desloca índices por número desejado de períodos com uma frequância opcional
df.tshift([períodos, freq, axis]) (descontinuado) altera índice de tempo, usando a frequência do índice, se dispolevel
df.tz_convert(tz[, axis, level, copy]) converte o eixo com reconhecimento de tz em fuso horário de destino
df.tz_localize(tz[, axis, level, copy, ...]) localiza índice tz-naive de df para o fuso horário de destino

Ordenamento com dataframes.sort_values

Para ordenar um dataframe podemos usar o método sort, com a seguinte sintaxe:

dataframe.sort_values(by=['campo'], axis=0, ascending=True, inplace=False)
onde
by pode ser uma string ou lista com o nome ou nomes dos campos, na prioridade de ordenamento,
axis{0 ou ‘index’, 1 ou ‘columns’} default 0, indica o eixo a ordenar,
ascending=True/False se ordenamento é crescente/decrescente.

dataframe.reindex

Alterações da ordem dos índices de um dataframe podem ser obtidos com:
dataframe.reindex(listaDeCampos)
Os seguintes argumentos são usados com reindex

Argumento descrição
index Index ou sequência a ser usada como index,
method forma de interpolação: ‘ffill’ preenche com valor posterior, ‘bfill’ com valor anterior,
fill_value valor a usar quando dados não existentes são introduzidos por reindexing (ao invés de NaN),
limit quando preenchendo com valor anterior ou posterior, intervalo máximo a preencher (em número de elementos),
tolerance quando preenchendo com valor anterior ou posterior, intervalo máximo a preencher para valores inexatos (em distância numérica),
level combina Index simples no caso de MultiIndex; caso contrário seleciona subset,
copy se True, copia dados mesmo que novo índice seja equivalente ao índice antigo; se False, não copia dados quando índices são equivalentes.

Métodos e propriedades de Index

Método descrição
append concatena outro objeto Index objects, gerando novo Index
difference calcula a diferença de conjunto como um Index
intersection calcula intersecção de conjunto
union calcula união de conjunto
isin retorna array booleano indicando se cada valor está na coleção passada
delete apaga índice, recalculando Index
drop apaga índices passados, recalculando Index
insert insere índice, recalculando Index
is_monotonic retorna True se indices crescem de modo monotônico
is_unique returns True se não existem valores duplicados no Index
unique retorna índices sem repetições

Bibliografia

  • Pandas Pydata.org pandas docs, acessado em julho de 2021.

Python: Expressões regulares


Expressões regulares, também chamadas de regex (de regular expression), são meios de descrever padrões que podem ser encontrados dentro de um texto. Os padrões podem ser simples como a busca de um ou dois dígitos especificados, ou padrões complexos que incluem a posição do padrão no texto, o número de repetições, etc.
piada regex
Regex são usados basicamente para a busca de um padrão, substituição de texto, validação de formatos e filtragem de informações. Praticamente todas as linguagens de programação possuem ferramentas de uso de regex, assim como grande parte dos editores de texto. As diferentes linguagens de programação e aplicativos que incorporam funcionalidade de regex possuem sintaxes ligeiramente diferentes, cada uma, mas há uma embasamento geral que serve a todas. Existem aplicativos de teste de regex em diversas plataformas e aplicativos online para o mesmo fim.

Uma descrição das expressões regulares e seu uso estão no artigo Expressões Regulares (regex) deste site.

Módulo re, expressões regulares

No Python o módulo re, parte da biblioteca padrão, carrega uma “mini-linguagem” com meios de especificar tais padrões e realizar essas buscas e sibstituições.

O módulo possui os métodos que podem ser usados para encontrar padrões, partir texto e compilar padrões:

Método retorna
re.search(padrao, texto) 1º texto casado e sua posição, em todas as linhas,
re.match(padrao, texto) 1º texto casado e sua posição, na 1ª linha,
re.findall(padrao, texto) retorna uma lista com todos os trechos encontrados,
re.split(padrao, texto) parte o texto na ocorrência do padrão e retorna as partes,
re.sub(padrao, sub, texto) substitue em texto o padrao por sub,
re.subn(padrao, texto) similar à sub mas retorna tupla com nova string e número de substituições
re.compile(padrao) compila e retorna um padrão regex pre-compilado

Método re.search

resultado = re.search(padrao, texto)
O método search procura por um padrão dentro de um texto alvo e retorna um objeto re.Match que contém a posição inicial e final do padrão encontrado. Se o padrão não for encontrado o método retorna None.

O objeto re.Match possui o método group() que retorna o trecho encontrado, sendo que apenas a primeira ocorrência é considerada. Os parâmetros são padrao, uma construção regex, e texto, o conjunto de caracteres onde se busca o padrão. Em um texto de muitas linhas search procura em todas as linhas até encontrar o padrão, diferente do método match que procura apenas na primeira linha.

Uma letra, dígito ou conjunto de caracteres é casado literalmente. Se não encontrado None é retornado.

» import re
» texto = 'Este é um texto de teste para testar o funcionamento das expressões regulares'
» # procuramos por 'exp' no texto
» padrao = 'exp'
» resultado = re.search(padrao, texto)

» # o padrão 'exp' é encontado na posição 57 até 60
» print(resultado)
↳ <re.Match object; span=(57, 60), match='exp'>

» print(resultado.group())
↳ exp

» # a busca retorna None se o padrão não é encontrado
» print(re.search('z', texto))
↳ None

» # apenas o primeira coincidência é casada
» print(re.search('ste', texto))
↳ <re.Match object; span=(1, 4), match='ste'>

Como search() retorna None se não houver um casamento, podemos usar o retorno do método como critério de sucesso da busca. O padrão A{3} significa 3 letras A maiúsculas consecutivas, o que não existe no texto.

» texto = 'American Automobile Association'
» busca = re.search('A{3}', texto)
» if busca:
»     print(busca.group())
» else:
»     print('não encontrado')
↳ não encontrado    

Além de caracteres simples e grupos de caracteres os metacaracteres permitem ampliar o poder de busca das regex. Os textos casados são representados como em texto. Na tabela abaixo x representa um padrão qualquer.

padrão significado exemplo: casa ou não com
a caracter comum a casa Afazer aaa
ab grupos de caracteres comuns ab absoluto abraço Abcd trabalho
. casa com qualquer caracteres único m.to mato, mito, m3to
x* 0, 1 ou várias ocorrências de x 13* 1, 13456, 133, 13333-0987
x? 0, 1 ocorrência de x 13? 1, 13456, 133, 13333-0987
x+ 1 ou mais ocorrências de x 13+ 1, 13456, 133, 13333-0987
» # . = qualquer caracter
» print(re.search('p.ata', 'pirata pata prata').group())
↳ prata

» # x* = 0, 1 ou várias repetições de x
» print(re.search('jo*e', 'jose joo joe').group())
↳ joe

» print(re.search('jo*e', 'jose joo jooooooe').group())
↳ jooooooe

» # x? = 0 ou 1 ocorrência de x
» print(re.search('jo?e', 'jose jooe je').group())
↳ je

» print(re.search('jo?e', 'jose jooe joe').group())
↳ joe

» # x+ = 1 ou mais ocorrências de x
» print(re.search('jo+e', 'jose jooe joe').group())
↳ jooe

As chaves são usadas para quantificar repetições de um padrão.

padrão significado exemplo: casa ou não com
{n} significa exatamente n repetições do padrão 9{3} 999, 1999-45, 9-999, 999-00, 9, 99
{n,} mínimo de n repetições do padrão 9{2,} 99, 1999-45, 9-9999, 99999-00, 9, 9-9
{n,m} mínimo de n, máximo de m repetições do padrão 9{2,4} 99, 1999-45, 9-9999, 99999-00, 9, 9-9

O objeto re.Match possui diversos métodos:

Método retorna
match.group() a parte do texto casada com o padrão,
match.start() índice do início da parte do texto casada com o padrão,
match.end() índice do fim da parte do texto casada com o padrão,
match.span() os índices do início e do fim da parte do texto casada com o padrão,
match.re() a expressão regular casada (o padrão),
match.string() o texto passado como parâmetro.
» texto = 'Telefone: 05 (61) 3940-35356 (casa da Dinda), CEP: 123456789'

» # 4 digitos, hifen, 5 dígitos
» padrao = '\d{4}-\d{5}'

» # a variável resultado contém um objeto Match
» resultado = re.search(pattern, texto) 

» if resultado:
»     print(resultado.group())
»     print(resultado.start())
»     print(resultado.end())
»     print(resultado.span())    
» else:
»     print('Padrão não encontrado!')

↳ 3940-35356
↳ 18
↳ 28
↳ (18, 28)

» texto = 'CEP do cliente: 72715-620, DF'
» busca = re.search('\d+', texto)

» print(busca.start(), busca.end())
↳ 16 21

» # o primeiro trecho casado é retornado
» print(texto[busca.start(): busca.end()], '=', busca.group())
↳ 72715 = 72715

» busca = re.search('-\d+', texto)
» print(busca.group())
↳ -620

match.group(), que é o mesmo que match.group(0), se refere a todos os grupos encontrados. Se o padrão contém apenas um grupo só uma combinação é encontrada. Podemos construir padrões com mais de um grupos usando os marcadores de grupos, os parênteses ().

» texto = 'Telefone: 05 (61) 3940-35356 (casa da Dinda), CEP: 123456789'

» # 4 digitos (1º grupo), hifen, 5 dígitos (2º grupo)
» padrao = '(\d{4})-(\d{5})'
» resultado = re.search(padrao, texto) 

» # o 1º grupo combina com
» print(resultado.group(1))
↳ 3940

» # o 2º grupo combina com
» print(resultado.group(2))
↳ 35356

» # ambos os grupos
» print(resultado.group())
↳ 3940-35356

Parênteses () indicam um grupo, a ser procurado como um bloco. O sinal |indica uma alternativa onde um ou outro grupo é procurado.

» # procurando por mato ou mito
» print(re.search('m(a|i)to', 'moto mato mito').group())
↳ mato

» # só a primeira ocorrência é retornada
» print(re.search('m(a|i)to', 'moto muto mito').group())
↳ mito

» print(re.search('q{3}', 'q qq qqq').group())
↳ qqq
» print(re.search('q{,2}', 'qqqqq qq qqq').group())
↳ qq
print(re.search('q{2,}', 'qqqqqqqq qq qqq').group())
↳ qqqqqqqq

Um colchete [] delimita um conjunto alternativo de caracteres. O sinal |indica uma alternativa, um ou outro grupo é casado.

» print(re.search('pr[ae]to', 'prato, preto').group())
↳ prato

» print(re.search('pr[ae]to', 'proto, preto').group())
↳ preto

» # [0-9] representa qualquer dígito. '[0-9]{3,}' é grupo com mais de 3 dígitos:
» print(re.search('[0-9]{3,}', '6-45-4567-345345').group())
↳ 4567

» # grupo com até 2 dígitos
» print(re.search('[0-9]{,2}', '45-4567-345345').group())
↳ 45
» # \d é o mesmo que [0-9]
» print(re.search('\d{,2}', '45-4567-345345').group())
↳ 45

No Python uma “raw string” é uma sequência de caracteres que ignoram especiais do texto demarcado com \. '\ttexto' é “texto” após um espaçamento de tabulação mas r'\ttexto' é uma string simples. Na montagem de padrões é comum se usar “raw strings”.

» texto = '(casa): 72715-620, (escritório): 74854-890'

» busca = re.search('\(casa\)', texto)
» busca.group()
↳ (casa)

» # \t é tab
» print('\ttexto')
↳       texto
» # raw strings ignoram o metacaracter
» print(r'\ttexto')
↳ \ttexto

O padrão usado abaixo, padrao = ‘\+\d{2}\(\d{2}\)\d{5}-\\d{4}’ significa um número escrito como um telefone no formato + cód país (cod área) 5 dígitos – 4 dígitos.

» texto = '''Suponha que temos um texto com um número de telefone 
»            Telefone do cliente: +55(21)92374-4782
»            mais texto irrelevante'''

» padrao = '\+\d{2}\(\d{2}\)\d{5}-\\d{4}'
» fone = re.search(padrao, texto).group()

» print(fone)
↳ +55(21)92374-4782

Método re.findall

re.findall(padrao, texto)
O método findall encontra todas as ocorrências de padrao em texto e retorna uma lista com os trechos encontrados.

» import re    
» texto = 'Hoje 1 estamos 23 procurando 456 por 7890 números'
» padrao = '\d'
» resultado = re.findall(padrao, texto) 

» # \d = qualquer um dígito
» print(resultado)
↳ ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0']

» # \d+ = qualquer um ou mais dígitos
» print(re.findall('\d+', texto))
↳ ['1', '23', '456', '7890']

» # \d{2} = grupos de 2 dígitos
» print(re.findall('\d{2}', texto))
↳ ['23', '45', '78', '90']

» # \d{3} = grupos de 3 dígitos
» print(re.findall('\d{3}', texto))
↳ ['456', '789']

» # \d{3,} = grupos de 3 ou mais dígitos
» print(re.findall('\d{3,}', texto))
↳ ['456', '7890']

» # \D+ = grupos de 1 ou mais não-dígitos
» print(re.findall('\D+', texto))
↳ ['Hoje ', ' estamos ', ' procurando ', ' por ', ' números']

» # caracteres na faixa de a até d (a, b, c, d)
» print(re.findall('[a-d]', texto))
↳ ['a', 'c', 'a', 'd']

» # dígitos na faixa de 1 a 4 (1,2 ,3, 4)
» print(re.findall('[1-4]', texto))
↳ ['1', '2', '3', '4']

» # texto 'oje' ou 'ando'
» print(re.findall('oje|ando', texto))
↳ ['oje', 'ando']

» # texto 'oje' ou 'ando' seguindos de qualquer sequência de caracteres
» print(re.findall('oje.*|ando.*', texto))
↳ ['oje 1 estamos 23 procurando 456 por 7890 números']

» # Obs. em qualquer busca o trecho casado é excluído de buscas posteriores.
» # o padrão 'ando.*' é ignorando

» # para encontrar no texto um padrão que contém metacaracteres usamos "raw strings"
» texto = 'Podemos usar \n para quebra de linha e \t para tabulações.'

» print(re.findall(r'[\n\t]', texto))
↳ ['\n', '\t']

Método re.split

resultado = re.split(padrao, texto, [maxsplit])
O método de split parte o texto em todas as ocorrências de padrao: e retorna uma lista com os trechos encontrados. Se o padrão não for encontrado uma lista com o texto inteiro é retornada. O parâmetro maxsplit (opcional) especifica o número máximo de cortes devem ser feitos no texto. O default é maxsplit = 0, signicando que todos os cortes possíveis serão feitos.

» import re
» texto = 'Hoje 1 estamos 23 procurando 456 por 7890 números'
» padrao = '\d+'
» resultado = re.split(padrao, texto) 

» # texto picado em toda ocorrência de 1 ou mais dígitos
» print(resultado)
↳ ['Hoje ', ' estamos ', ' procurando ', ' por ', ' números']

» # texto picado em toda ocorrência de espaços (\s)
» print(re.split('\s', texto))
↳ ['Hoje', '1', 'estamos', '23', 'procurando', '456', 'por', '7890', 'números']

» # padrão não encontrado
» print(re.split('w', texto))
↳ ['Hoje 1 estamos 23 procurando 456 por 7890 números']

» # especificando maxsplit = 2 (fazer apenas 2 cortes no texto)
» print(re.split('\d+', texto, 2))
↳ ['Hoje ', ' estamos ', ' procurando 456 por 7890 números']

Método re.sub

resultado = re.sub(padrao, subst, texto, [quantos])
O método re.sub procura um padrão e o substitui por um texto. A variável resultado é uma string com padrao substituído por subst. Se o padrão não é encontrado o texto original é retornado. O parâmetro opcional quantos indica quantas substituições devem ser feitas. O default é quantos = 0, o que significa que todas as ocorrências do padrão devem set substituídas.

» # remover todos os espaços em branco
» import re
» # texto com várias linhas e espaços em branco
» texto = 'Nome: Pedro \nSobrenome: Alvarez\nCabral'
» # padrão para casar com espaços (troca espaços por '')
» padrao = '\s'
» sub = ''
» resultado = re.sub(padrao, sub, texto) 

» print(resultado)
↳ Nome:PedroSobrenome:AlvarezCabral

» # padrão para substituir 1 ou mais espaços por espaço único
» texto = 'É comum  ter   textos  com dois ou mais espaços   inseridos onde  se deseja     apenas um!'

» print(texto)
↳ É comum  ter   textos  com dois ou mais espaços   inseridos onde  se deseja     apenas um!

» print(re.sub('\s+', ' ', texto) )
↳ É comum ter textos com dois ou mais espaços inseridos onde se deseja apenas um!

» # usando o parâmetro quantos
» texto = 'Esse texto possui 4 ocorrências de 3 dígitos repetidos: 012, 123, 234 e 345.'

» # Substituindo apenas as 2 primeiras ocorrências de 3 dígitos por ###
» print(re.sub('\d{3}', '###', texto, 2))
↳ Esse texto possui 4 ocorrências de 3 dígitos repetidos: ###, ###, 234 e 345.

Método re.subn

resultado = re.subn(padrao, subst, texto, [quantos])
O método re.subn é similar à re.sub mas retorna uma tupla de 2 itens, contendo a string modificada e o número de substituições feitas.

» texto = 'Temos as seguintes permutações de {a, b, c}: abc, acb, bac, bca, cab, cba.'
» resulta = re.subn('[abc]{3}', '|||', texto)
» print(resulta)
» print('Foram feitas {} substituições'.format(resulta[1]))

↳ ('Temos as seguintes permutações de {a, b, c}: |||, |||, |||, |||, |||, |||.', 6)
↳ Foram feitas 6 substituições

O método re.search recebe dois argumentos: um padrão e o texto a ser modificado. O método procura apenas pela primeira ocorrência do padrão. Se existe um casamento o método retorna um objeto match que contém a posição da coincidência (início e final) e a parte do texto que combina com o padrão. Se não houver nenhum casamento o método retorna None.

Método re.compile

padraoCompilado = re.compile(padrao, flags = 0)
O método re.compile() é especialmente útil quando o mesmo padrão será usado muitas vezes. Ele prepara um padrão através de uma pré-compilação e as armazena em cache que torna mais rápidas as buscas.

O método retorna um objeto re.Pattern que representa o padrao compilado sobre efeito dos parâmetros opcionais flags. Um exemplo é flag = re.I que determina que a busca será “insensível ao caso”. O objeto possui métodos que permitem as buscas pelo padrão dentro de um texto, tal como padrao.findall(texto), que retorna uma lista, ou padrao.finditer(texto) que retorna um iterável com os casamentos encontrados.

O padrão patt = ‘(xa|ma){2}’ significa um dos dois grupos, “xa” ou “ma”, repetidos 2 vezes.

» # 2 ocorrências de "xa" ou "ma"
» patt = '(xa|ma){2}'
» padrao = re.compile(patt)
» texto = 'xa, xaxado, ma, mamata, errata'
» busca = padrao.findall(texto)
» print(busca)
↳ ['xa', 'ma']

» # ocorrência de 5 dígitos juntos
» padrao = re.compile('\d{5}')
» texto = '12345 543213 858 9658 96521'
» busca = padrao.finditer(texto)
» for t in busca:
»     print(t.group())
↳ 12345
↳ 54321
↳ 96521

O objeto retornado, representado pela variável padraoCompilado acima, tem vários atributos, que podem ser vistos com a função dir(). Entre eles temos:

Flags ou sinalizadores

Os métodos do módulo re admitem um parâmetro extra chamado de flag (sinalizador ou marcador). Eles modificam o significado do padrão que se pretende buscar.

Os sinalizadores podem ser qualquer um dos seguintes:

Abreviado longo integrado (inline) significado
re.I re.IGNORECASE (?i) ignorar maiúsculas e minúsculas.
re.M re.MULTILINE (?n) força os localizadores ^ $ a considerarem uma linha inteira.
re.S re.DOTALL (?s) força . a casar com a newline, \n.
re.U re.UNICODE (?u) força \w, \W, \b, \B} a seguirem regras Unicode.
re.L re.LOCALE (?L) força \w, \W, \b, \B} a seguirem regras locais.
re.X re.VERBOSE (?x) permite comentários no regex.
» txt = 'estado, Estudo, estrume, ESTATUTO'
» r1 = re.findall('est[a-z]+', txt)
» r2 = re.findall('est[a-z]+', txt, flags=re.IGNORECASE)

» print(r1)
↳ ['estado', 'estrume']

» print(r2)
↳ ['estado', 'Estudo', 'estrume', 'ESTATUTO']

» # o mesmo resultado pode ser obtido com a notação inline
» re.findall('(?i)est[a-z]+', txt)
↳ ['estado', 'Estudo', 'estrume', 'ESTATUTO']

» re.findall('[a-z]+[dt]o', txt, flags=re.I)
↳ ['estado', 'Estudo', 'ESTATUTO']

Para usar mais de uma flag é possível separá-las com uma barra vertical (ou pipe). Por exemplo para uma busca multiline, insensível ao caso e com comentário:

re.findall(pattern, string, flags=re.I|re.M|re.X)

» texto = """
» Gato é um bicho engraçado.
» gato não é como cachoroo.
» Gato mia!
» """

» # a 1&orf; linha não começa com 'gato'
» re.findall("^gato", texto, flags=re.IGNORECASE)
↳ []

» # procurando em todoas as linhas
» re.findall("^gato", texto, flags=re.M)
↳ ['gato']

» # procurando em todoas as linhas, insensível ao caso
» re.findall("^gato", texto, flags=re.I | re.M)
↳ ['Gato', 'gato', 'Gato']

» # o mesmo resultado pode ser conseguido com flags inline
» re.findall("(?i)(?m)^gato", text)
↳ ['Gato', 'gato', 'Gato']

Exemplos

Um exemplo simples de remoção de tags aplicado a um texto HTML pode ser o seguinte: O padrão padrao = '<.*?>|[\n]' apenas casa com qualquer conteúdo dentro de <>, não guloso ou um sinal de quebra de linha, [\n]. Usando o método re.sub removemos todos os trechos que casam com esse padrão.

» html = '''
» <html>
» <body>
» <p>Parágrafo um.</p>
» <p>Parágrafo dois.</p>
» </body>
» </html>
» '''
» padrao = '<.*?>|[\n]'
» textoSemTags = re.sub(padrao, '', html)
» print(textoSemTags)    
↳ Parágrafo um.Parágrafo dois.

Existem bibliotecas sofisticadas para web scrapping, como Beautiful Soup que permite a busca, modificação e completa navegação de um documento extraído de uma página HTML. Buscas podem ser feitos por elementos de css, ids e classes e tags.

Padrões muito complexos são difíceis de serem lidos e alterados. Para quem programa em Python as buscas regex são geralmente ferramentas auxiliares que podem ser complementadas com manuseios do texto feitos em código.

Suponha que temos um texto no formato *.csv (valores separados por vírgulas) com 5 colunas. Na quarta coluna existe uma data com formato nem sempre consistente, como 26/06/2021 onde o ano pode ter apenas 2 dígitos e o separador pode ser um barra ou hífen. Queremos extrair o valor da quinta coluna quando o ano for posterior a 2015.

» csv = '''
» col1, col2, col3, data, valor
» a1  , a2   , a3  , 01/06/01, 1000
» b1  , b2   , b3  , 06/05/2016, 1000
» c1  , c2   , c3  , 4/3/17, 2000
» d1  , d2   , d3  , 14-12-2018, 600
» e1  , e2   , e3  , 19-09-19, 600
» '''

» for t in csv.split('\n'):
»     data  = t.split(',')
»     if len(data) != 5: continue
»     dt = data[3].strip()    
»     if not re.match('\d{1,2}[/|-]\d{1,2}[/|-]\d{2,4}', dt):  continue
»     ano = int(re.split('[/|-]',dt)[2])
»     ano = ano + 2000 if ano < 100 else ano
»     if ano > 2015:
»         print(ano, data[4])
↳ 2016  1000
↳ 2017  2000
↳ 2018  600
↳ 2019  600 

O texto é partido em linhas, cada linha em campos separados por vírgula. Como existem linhas vazias só são aproveitadas aquelas com 5 campos. Formatos de data não admissíveis são excluídos e uma correção para anos com apenas dois dígitos inserida.

Bibliografia

Python: Biblioteca Padrão


Vimos que um usuário pode gravar suas classes em módulos para depois importá-las para dentro de seu código. Nas instalações de Python um conjunto de módulos vem instalado por padrão, outros podem ser baixados e instalados pelo usuário.

A biblioteca padrão do Python é extensa e inclui recursos diversos voltados para muitas áreas de aplicação. Alguns desses módulos são escritos em C para garantir velocidade de execução, outros em Python. Uma lista completa desses módulos pode ser encontrada em Python Docs: Standard Library. Muitos outros módulos podem ser encontrados em Pypi: The Python Package Index. Esse últimos devem ser baixados e instalados antes de sua utilização.

Lista de alguns módulos da biblioteca padrão:

Módulo Descrição (clique no ícone 📖 para ler a seção)
argparse processamento de argumentos em comando de linhas,
datetime 📖 classes para manipulação de datas e horas,
decimal tipo de dados decimais para aritmética de ponto flutuante,
doctest ferramenta para validar testes embutidos em docstrings de um módulo,
glob 📖 manipulação de listas de arquivos e pastas usando coringas,
io 📖 gerenciamento de IO (input/output),
math 📖 matemática de ponto flutuante,
os 📖 funções para a interação com o sistema operacional,
pprint habilidade pretty print para estruturas de dados,
random 📖 geração de números e escolhas aleatórios ,
re 📖 processamento de expressões regulares,
reprlib versão de repr() personalizada para exibições de contêineres grandes,
shutil 📖 interface com arquivos e coleções de arquivos,
statistics 📖 funções estatísticas (média, mediana, variância, etc.),
string 📖 métodos para formatação de strings,
textwrap formata parágrafos de texto para determinada largura de tela,
threading executa tarefas em segundo plano,
time 📖 acesso ao clock do computador,
timeit 📖 medidas de intervalo de tempo,
unittest conjunto de testes que podem ser mantidos em arquivo separado,
zlib, gzip, bz2, lzma, zipfile, tarfile suporte a formatos de compactação e arquivamento de dados.

Vamos exibir alguns dos métodos disponibilizados por esses módulos.

Módulo os

O módulo os foi tratado na seção Arquivos e pastas dessas notas.

Módulo io

Um buffer de dados (ou apenas buffer) é uma região de um armazenamento da memória física usado para armazenar dados temporariamente, antes de serem processados e gravados em memória permanente. Geralmente buffers são implementados pelo software na memória RAM e são normalmente usados ​​quando os dados são recebidos em taxa superior àquela em que podem ser processados. É o que ocorre em um spooler de impressora ou em streaming de vídeo online.

Os métodos read() e write(), que permitem a abertura de arquivos foram vistos em Arquivos e Pastas. No entanto os métodos em io são mais poderosos e flexíveis.

Arquivos binários e de texto

Arquivos podem ser abertos para escrita, para leitura e para acrescentar dados. O Python permite especificar dois tipos de arquivos: binário e texto. O modo binário é usado para lidar com todos os tipos de dados não textuais, como arquivos de imagem e arquivos executáveis. Qualquer arquivo é armazenado em forma binária. Mas, quando um arquivo de texto é aberto (e o comando de abertura informa que é um texto) esses bytes são decodificados para retornar objetos string. Já os binários retornam o conteúdo como sequências de bytes, sem decodificação.

Por exemplo, podemos abrir um arquivo binário e gravar nele a sequência algunsBytes = b'\xC3\xA9'. O prefixo b indica que essa é uma sequência binária.

# ilustrar gravação de arquivo binário e texto
» algunsBytes = b'\xC3\xA9'
  
» # abra um arquivo no modo 'wb' para escrever, no modo binário
» with open('./dados/arquivo.txt', 'wb') as arquivoBinario:
»     # escreve os bytes no arquivo
»     arquivoBinario.write(algunsBytes)
    
» # lendo esse arquivo no modo binário
» with open('./dados/arquivo.txt', 'rb') as f:
»     print(f.read())
↳ b'\xc3\xa9'

» # lendo esse arquivo no modo texto  (o código corresponde à letra "é")
» with open('./dados/arquivo.txt', 'r') as f:
»     print(f.read())
↳ é

Abrindo o arquivo no modo binário encontramos nossa sequência b’\xC3\xA9′. Quando verificamos esse arquivo no modo texto, usando o mode r ou abrindo em um editor de texto verificamos que está gravada a letra é (e com acento agudo). Fisicamente esse caracter fica gravado em disco (ou armazenado em RAM) como a sequência binária 11000011 10101001.

Outro exemplo curto consiste na leitura de um arquivo de imagem ./dados/Anaconda.png no modo binário, de leitura. O arquivo Anaconda.png deve estar na pasta indicada. Esses dados são depois escritos em ./dados/Novo.png, aberto no mode binário, para escrita. O resultado é a criação de Novo.png, uma imagem idêntica à original.

» with open('./dados/Anaconda.png', 'rb') as f:
»     with open('./dados/Novo.png', 'wb') as novo:
»         novo.write(f.read())

O módulo io é usado para gerenciar streams (fluxo de dados, I/O ou E/S em português). Esses fluxos podem ser, basicamente, de três tipos: dados de textos, binários ou RAW. Concretamente esses fluxos transferem objetos denominados arquivos.

Todos esses três tipos de objetos possuem atributos: podem ser só de leitura, gravação ou leitura e somente gravação. Eles podem ser acessados de modo de acesso aleatório (onde buscas são feitas em qualquer sentido, procurando por partes em qualquer local; ou de acesso apenas acesso sequencial, onde passos para trás não são permitidos, dependendo do tipo de dados lido.

As duas classes de uso mais comum são:

  • StringIO, operações em fluxos de strings e
  • BytesIO operações em fluxos de bites.

Encoding

Encoding ou codificação de texto é uma forma de representação de dados que contém letras, símbolos, números e marcações especiais (como quebra de linhas e tabulações). O forma de codificação mais comum para conversão de caracteres é a American Standard Code for Information Interchange (ASCII), desenvolvida no início da computação. Essa tabela contém apenas 128 posições, o que engloba apenas caracteres da língua inglesa. Para representar outros caracteres, com acentuação, de outras línguas e generalizar os símbolos se criou a codificação Unicode, que engloba todos os caracteres usados mundialmente. O UTF-8 (8-bit Unicode Transformation Format) é um tipo de codificação compacta para o código Unicode. Ele pode representar qualquer caractere universal padrão do Unicode, sendo também compatível com o ASCII. Por esta razão, está lentamente a ser adaptado como tipo de codificação padrão para e-mail, páginas web, e outros locais onde os caracteres são armazenados.

Exemplos de UNICODE e utf-8:

UNICODE caracter utf-8
U+00E0 à c3 a0
U+00E1 á c3 a1
U+00E2 â c3 a2
U+00E3 ã c3 a3
U+00E4 ä c3 a4

Quando se usa open() (e TextIOWrapper) para ler um arquivo é usada uma codificação especificada como sistema local. Sistemas Unix e seus derivados (como o Linux) usam por default a codificação UTF-8. No entanto existem sistemas (como o Windows) que não o fazem. Por isso é importante especificar explicitamente a codificação.

import io
» with io.open(filename,'r',encoding='utf8') as f:
»     text = f.read()

Para deixar o arquivo lido com a codificação local use encoding='locale'.

Classe io.StringIO()

O construtor de io.StringIO() permite a construção de um stream de dados tipo texto.

» import io
» arq = io.ioStringIO("Visitação aberta para o site\nPhylos.net")
» type(arq), type(arq.getvalue())
↳ (_ioStringIO, str)

» print(arq.getvalue()) # ou arq.read()
↳ Visitação aberta para o site
↳ Phylos.net

» # o objeto pode ser sobreescrito
» arq.write('Não deixem de visitar o')
↳ 23

» print(arq.getvalue())
↳ Não deixem de visitar o site
↳ Phylos.net

» arq.close()
» print(arq.getvalue()) 
↳ ValueError: I/O operation on closed file

StringIO constroi um objeto similar a um arquivo, residente na memória. Esse objeto pode ser usado como input ou output para funções que recebem arquivos padrões como argumentos. io.StringIO() inicializa um objeto vazio. Em qualquer dos casos o cursor sobre o arquivo está na posição 0. Os dados podem ser sobrepostos com io.StringIO('novo texto') que será inserido na posição do cursor (0, no caso). Após a inserção o cursor ficará na posição final do texto inserido, como mostrado no exemplo.

Depois do uso o objeto deve ser fechado com ioStr.close(). O objeto tipo arquivo é iterável como um arquivo usual. O fechamento após o uso garante a liberação de recursos usados. Nenhuma operação pode ser realizada sobre um arquivo fechado.

» f = io.StringIO()
» f = io.StringIO('linha 1\nlinha 2\nlinha 3')
» while True:
»     line = f.readline()
»     if (not line): break
»     else:print(line, end='')
» f.close() 

O método seek(n) permite apontar o cursor para o n-ésimo caracter. No código lemos apenas a primeira linha. Mesmo após o esgotamento das linhas seek(0) pode retornar o cursor para o início.

» with io.StringIO('linha 1\nlinha 2\nlinha 3') as f:
»     f.seek(3)
»     print(f.readline())
↳ ha 1
    
» with io.StringIO('linha 1\nlinha 2\nlinha 3') as f:
»     while True:
»         line = f.readline()
»         if (not line):
»             f.seek(0)
»             print(f.readline())  # só a 1ª linha é impressa
»             break    
↳ linha 1

O método arquivo.tell() informa o posição atual do cursor.

» with io.StringIO('Um cavalo! Um cavalo! Meu reino por um cavalo!') as f:
»     f.seek(22)
»     print('Da posição', f.tell(), 'em diante está o texto:', f.readline())
↳ Da posição 22 em diante está o texto: Meu reino por um cavalo!

O método file.getvalue() lê o arquivo inteiro independentemente da posição do cursor, enquanto file.read() lê à partir da posição do cursor até o final. O método file.truncate(n) corta o arquivo, mantendo apenas os caracteres de 0 até n, exclusive.

» file = io.StringIO('I\'ll be back!')
» print('1', file.getvalue())
» print('2', file.read())
» print('3', file.read())
» file.seek(0)
» print('4', file.read())

↳ 1 I'll be back!
↳ 2 I'll be back!
↳ 3 
↳ 4 I'll be back!

» file.truncate(7)
» print(file.getvalue())
↳ I'll be

Na linha 1 o arquivo foi lido mas o cursor continua no início. Em 2 o conteúdo é exibido e o arquivo é esgotado (cursor no final). Em 3 não há o que exibir. Em 4 o cursor é retornado ao início e o arquivo todo exibido.

O objeto StringIO tem os seguintes métodos e propriedade booleanas:

Métodos significado
StringIO.isatty() True se o arquivo é interativo,
StringIO.readable() True se o arquivo é está em modo leitura (readable),
StringIO.writable() True se o arquivo está no modo de escrita,
StringIO.seekable() True se o arquivo está no modo de acesso aleatório (random access),
Propriedade significado
StringIO.closed True se o arquivo está fechado.

A expressão arquivo na tabela significa um objeto tipo-arquivo, que não precisa estar gravado em disco. Um objeto que não está no modo de acesso aleatório não admite reposicionamento de cursor com o método seek(n).

» import io
» file = io.StringIO('Um texto qualquer de teste!')
» print("O arquivo é interativo?", file.isatty())
» print("O arquivo é de leitura?", file.readable())
» print("O arquivo está no modo de escrita?", file.writable())
» print("O arquivo admite buscas?", file.seekable())
» print("O arquivo está fechado?", file.closed)
» file.close()
» print("O arquivo foi fechado?", file.closed)
​
↳ O arquivo é interativo? False
↳ O arquivo é de leitura? True
↳ O arquivo está no modo de escrita? True
↳ O arquivo admite buscas? True
↳ O arquivo está fechado? False
↳ O arquivo foi fechado? True

Concluindo: io.StringIO fornece um meio conveniente de trabalhar com texto na memória usando métodos de arquivos como read(), write(). Com ele se pode construir strings longa com economia de desempenho em relação a outras técnicas de strings. Buffers de fluxo na memória também são úteis para testes uma vez que suas operações são mais ágeis que a gravação em um arquivo em disco.

Classe io.BytesIO

A classe io.BytesIO permite a inicialização de objetos de stream, puramente binários. Um arquivo aberto no modo binário não fará nenhuma conversão para caracteres de texto codificados, como o fazem os editores de texto, ou os arquivos abertos com io.StringIO. Eles são úteis para a manipulação de arquivos gerais, tais como imagens ou sons.

Um string prefixado com o caracter b é um string de bytes. O argumento de BytesIO deve ser sempre texto puro, em ASCII, ou um erro é lançado.

» import io
» arq = io.BytesIO(b'ç')
↳ SyntaxError: bytes can only contain ASCII literal characters.

» # usada com um argumento válido
» arq = io.BytesIO(b'Este texto, em bytes, sobre Python: \x00\x01')

» # o conteúdo é um string de bytes
» type(arq.getvalue())
↳ bytes

» # os caracteres \x00\x01 não são decodificados
» print(arq.getvalue())
↳ b'Este texto, em bytes, sobre Python: \x00\x01'

» # um arquivo fechado não pode ser manipulado
» arq.close()
» arq.getvalue()
↳ ValueError: I/O operation on closed file.

Um objeto BytesIO pode ser criado vazio e preenchido depois. BytesIO.write(b_str) insere b_str no final do objeto (faz um append). No exemplo abaixo dois strings comuns são transformados em bytes: '中文'.encode('utf-8') e 'criação'.encode('utf-8'), ambos envolvendo caracteres fora da tabela ASCII pura.

» arq = io.BytesIO()
» type(arq)
↳ io_BytesIO

» arq.write('中文'.encode('utf-8'))
» print(arq.getvalue())
↳ b'\xe4\xb8\xad\xe6\x96\x87'

» # encode() transforma em bytes os caracteres extendidos
» # no caso '中文' = 'chinês'
» type('中文'.encode('utf-8'))
↳ bytes

» # removendo o conteudo de arq e inserindo novo b_string
» arq = io.BytesIO()
» arq.write('criação'.encode('utf-8'))
» print(arq.getvalue())
↳ b'cria\xc3\xa7\xc3\xa3o'

» # write faz o append
» arq.write(b' <---')
» print(arq.getvalue())
↳ b'cria\xc3\xa7\xc3\xa3o <---'

Operações com io.BytesIO() são similares às de uma gravação de arquivo obtidas com file.open() e file.write(). Mas, embora criando um objeto com características de um arquivo, eles não precisam ser gravados em disco (ou outro meio de armazenamento) o que torna essas operações muito mais rápidas.

Uma comparação de velocidade entre gerar e manipular uma string com conteúdo binário e um objeto io.BytesIO() é feita abaixo.

» import io
» import time

» start = time.time()
» buffer = b''
» for i in range(0,100_000):
»     buffer += b'Oi Mundo!'
» print('1:', time.time() - start)

» start = time.time()
» buffer = io.BytesIO()
» for i in range(0,100_000):
»     buffer.write(b'Oi Mundo!')
» print('2:', time.time() - start)

» # o output será
↳ 1: 6.659011363983154
↳ 2: 0.012261390686035156

Se necessário podemos passar o conteúdo de um objeto io.BytesIO() para um arquivo ordinário e gravá-lo em disco.

    
» # criamos um buffer vazio e depois o preenchemos
» buffer = io.BytesIO()
» for i in range(0,100_000):
»     buffer.write(b'Oi Mundo!')
» # gravamos no disco esse buffer
» with open('./dados/Buffer.bin', 'wb') as arquivo:
»     arquivo.write(buffer.getbuffer())
» # um arquivo com 100000 linhas de 'Oi Mundo' é gravado no disco    

io.open()

Já vimos e usamos o método open() do módulo os. O módulo io também possui um método open().

» import io
» arquivo = io.open('./dados/Anaconda.png', 'rb', buffering = 0)
» print(arquivo.read())
» # o resultado está exibido abaixo (truncado)
↳ b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x01\x1d\x00\x00\x01\x14\x08\x06\x00\x00\x00\x16e\xe2\xb3\x00\x00\x01\x84iCCPICC profile\x00\x00(\x91}\x91=H\xc3@\x1c\xc5_[E\xd1\xfa\x01f\x10q\xc8P\x9d,\x88\x8a8j\x15\x8aP!\xd4\n\xad:\x98\\\xfa\x05M\x1a\x92\x14\x17G\xc1\xb5\xe0\xe0\xc7b\xd5\xc1\xc5YW\x07WA\x10\xfc\x00qquRt\x91\x12\xff\x97\x14Z\xc4xp\xdc\x8fw\xf7\x1ew\xef\x80`\xad\xc44\xabm\x1c\xd0t\xdbL\xc6cb:\xb3*v\xbc\xa2\x1b\xbd\x10\xd0\x8f\x88\xcc,cN\x ...

O método io.open() é preferido para operações de E/S por ser interface de alto nível. Na verdade ele é um wrapperque se utiliza de os.open() mas retorna um objeto com mais propriedades e métodos.

RAW

O modo RAW (também chamado de IO sem buffer) é usada como um bloco de construção de baixo nível para fluxos binários e de texto. Esse modo raramente é útil manipular diretamente um fluxo do código do usuário. Se necessário um fluxo bruto pode ser criado abrindo-se um arquivo no modo binário e com o buffer desativado:

» f = open('myfile.jpg', 'rb', buffering=0)

Módulo shutil

O módulo shutil contém vários métodos que operam em arquivos e pastas, usados para copiar, apagar, entre outras funcionalidades. Vamos carregar os módulos os e shutil para experimentar alguns desses métodos. No Linux o sinal ‘./’ indica a pasta atual, ‘./Temp’, é uma subpasta de nome Temp.

Método significado
os.system() Executa um comando shell,
shutil.copy(origem, destino) copia arquivos,
shutil.copy2(origem, destino) copia arquivos com metadados,
shutil.move(origem, destino) move arquivos,
shutil.copyfile(origem, destino) copia arquivos,
shutil.copytree(pastaOrigem, pastaDestino) copia pasta e seu conteúdo
shutil.rmtree(pastaDestino) paga pastas recursivamente,
shutil.disk_usage(caminho) dados de uso do disco,
shutil.which(app) caminho de um aplicativo no path.

Exemplos de uso:
shutil.copy(origem, destino)
O método shutil.copy copia o arquivo origem para o destino, retornando uma exceção se a pasta de destino não existir.

shutil.copy2(origem, destino)
O método shutil.copy2 tem o mesmo efeito que o anterior, mas tenta copiar junto com o arquivo origem os seus metadados para o destino.

shutil.move(origem, destino)
O método shutil.move move o arquivo origem para o destino, retornando uma exceção se a pasta de destino não existir. O arquivo original deixa de existir.

» import os
» import shutil

» origem = './renomear.py'
» destino = './temp/renomear.py'

» # Vamos copiar o arquivo renomar.py para a paste temp de destino
» dest = shutil.copy(origem, destino)

» # um arquivo pode ser copiado em sua própria pasta
» shutil.copy('nomeAntigo.txt','nomeNovo.txt')

» # um arquivo pode ser renomeado
» shutil.move('nomeAntigo.txt','nomeNovo.txt')

» # o método retorna a pasta e arquivo de destino
» print('Pasta de destino:', dest)
↳ Pasta de destino: ./temp/renomear.py

» shutil.move('nomeAntigo.txt', , 'nomeNovo.txt')
↳ 'nomeNovo.txt'

» # considerando que nomeNovo.txt é o único arquivo na pasta
» os.listdir('./temp')
↳ ['nomeNovo.txt']

No Windows os caminhos devem ser escritos de forma compatível com o sistema.

» import shutil
» import os

» caminho = 'C:\\temp1\\temp2'
» print('A pasta atual contém os arquivos:') 
» print(os.listdir(caminho))
» # os arquivos são listados

» destino = 'C:\\temp1\\temp3'
» shutil.move('arquivo.txt',destino + '\\temp3\\arquivo.txt')

» print('A pasta de destino passa a ter os arquivos:') 
» print(os.listdir(destino))
» # uma lista é impressa, incluindo arquivo.txt

shutil.copyfile(origem, destino)
Um arquivo pode ser copiado com shutil.copyfile, origem é o caminho relativo ou absoluto da arquivo a ser copiado, destino o caminho do arquivo copiado.

shutil.copytree(pastaOrigem, pastaDestino)
O método shutil.copytree copia uma pasta inteira juntamente com todos os seus arquivos e outras pastas, de pastaOrigem para pastaDestino. A pasta de destino é criada na cópia ou uma exceção é lançada se a pasta ou os arquivos já existirem no destino.

» # copiando um arquivo da pasta ativa para Temp do usuário
» origem = './teste.txt'
» destino = '/home/usuario/Temp/testeNovo.txt'
» shutil.copytree(origem, destino)
	
» origem = '/home/usr/Projetos/'
» destino = '/home/usr/Projetos/temp2'

» shutil.copytree(origem, destino)
» print(os.listdir(destino))
» # uma lista dos arquivos no destino é exibida

shutil.rmtree(pastaDestino)
Para apagar pastas recursivamente podemos usar shutil.rmtree. Um erro será lançado de
a pasta pastaDestino não existir ou se não for uma pasta (se for um arquivo).

» print(os.listdir('./'))
↳ ['texto1.txt', 'alunos.csv', 'temp', 'temp2']

» # apagar pasta temp2 e todos os seus arquivos
» shutil.rmtree('./temp2')

» print(os.listdir('./'))
↳ ['texto1.txt', 'alunos.csv', 'temp']	

shutil.disk_usage(caminho)
shutil.which(app)
O método shutil.disk_usage informa dados de uso do disco sendo acessado e shutil.which(app) informa o caminho completo do aplicativo app que seria executado se estiver no PATH acessável no momento. O método retorna None se o app não existir ou não estiver no PATH.

» caminho = '/home/usuario/'
» estatistica = shutil.disk_usage(caminho)
» print(estatistica)
↳  usage(total=234684264448, used=172971024384, free=49720573952)

» comando = 'python'
» local = shutil.which(comando) 
» print(local)
↳ /home/guilherme/.anaconda3/bin/python

» comando = 'firefox'
» locate = shutil.which(comando) 
» print(local)
↳ /usr/bin/firefox

Biblioteca glob

O módulo glob fornece métodos para a busca de arquivos e pastas usando padrões de construção de nomes semelhantes aos usados pelo shell do Unix.

Método Descrição
glob.glob(padrao) retorna lista de arquivos que satisfazem o padrão em padrao,
glob.iglob(padrao) retorna um iterador contendo os nomes dos arquivos individuais,
glob.escape(padrao) Usado nos casos em que os nomes de arquivos usam caracteres especiais.

Usando o módulo glob é possível pesquisar por nomes de arquivo literais ou usar caracteres especiais, similares aos das expressões regulares (mas bem mais simples).

Sinal Descrição
* Asterisco: corresponde a zero ou mais caracteres,
? Interrogação: corresponde exatamente a um caractere,
[] Colchetes: intervalo de caracteres alfanuméricos,
[!] negativa: não contém os caracteres listados.

A sintaxe de glob é:

glob.glob(caminho, recursive = False)
glob.iglob(caminho, recursive = False)
onde caminho é o nome dos arquivo ou arquivos, usando os sinais acima e caminho relativo ou absoluto. Se recursive = True a busca é realizada recursivamente dentro das subpastas. O método iglob tem o mesmo efeito mas retorna um objeto iterável. Alguns exemplos são:

» import glob
» import os

» # retorna todos os arquivos na pasta /home/usuario/Temp/
» glob.glob('/home/usuario/Temp/*')
↳ ['/home/usuario/Temp/musica02.mp4',
↳  '/home/usuario/Temp/Musica01.mp3',
↳  '/home/usuario/Temp/teste.txt',
↳  '/home/usuario/Temp/Programa.py']

» # arquivos com nomes começados com letra minúscula 
» glob.glob('/home/usuario/Temp/[a-z]*')
↳ ['/home/usuario/Temp/musica02.mp4',
↳  '/home/usuario/Temp/teste.txt']

» # arquivos com nomes começados com letra maiúscula 
» glob.glob('/home/usuario/Temp/[A-H]*')
↳ ['/home/usuario/Temp/Programa.py',
↳  '/home/usuario/Temp/Musica01.mp3']

» # arquivos com extensão mp(0 até 9)
» glob.glob('/home/usuario/Temp/*.mp[0-9]')
↳ ['/home/usuario/Temp/Musica01.mp3',
↳   '/home/usuario/Temp/musica02.mp4']

» # podemos trocar a pasta ativa
» os.chdir('/home/usuario/Temp/')
» glob.glob('*')
» # retorna todos os arquivos na pasta

» # iglob retorna um iterável
» it = glob.iglob('*')
» for t in it:
»     print(t)
↳ /home/usuario/Temp/musica02.mp4
↳ /home/usuario/Temp/Musica01.mp3
↳ /home/usuario/Temp/teste.txt
↳ /home/usuario/Temp/Programa.py

» # usando a negativa
» # arquivos que não começam com m, M ou t.
» glob.glob('[!mMt]*')
↳ ['/home/usuario/Temp/Programa.py']

glob.escape(caminho, recursive = False)
O método escape permite o uso de caracateres especiais como _, #, $ no nome dos arquivos. O caracter especial é inserido por meio de glob.escape(char).

» caminho = '*' + glob.escape('#') + '*.jpeg'
» glob.glob(caminho)
↳ ['Foto#001.jpeg']

» caminho = 'A*' + glob.escape('_') + '*.gif'
» glob.glob(caminho)
↳ ['Abcd_001.gif']

Um exemplo de uso pode ser dado no código que procura e apaga arquivos temporários, com a extensão .tmp

» import glob
» import os

» # apague arquivos temporários, tipo *.tmp
» for t in (glob.glob('*.tmp')):
»     print('Apagando ', t)
»     os.remove(t)

Módulo math

O módulo math do Python contém diversas funções matemáticas, especialmente úteis em projetos científicos, de engenharia ou financeiros. Eles são um acréscimo aos operadores matemáticos básicos do módulo básico, como os operadores matemáticos integrados, como adição (+), subtração (-), divisão (/) e multiplicação (*).

As seguintes funções são encontradas no módulo:

Método Descrição
Funções trigonométricas e geométricas
math.sin() seno, ângulo em radianos,
math.cos() cosseno,
math.tan() tangente,
math.asin() arco seno,
math.acos() arco cosseno,
math.atan() arco tangente em radianos,
math.atan2() arco tangente de y/x em radianos,
math.sinh() seno hiperbólico,
math.cosh() cosseno hiperbólico,
math.tanh() tangente hiperbólica,
math.asinh() inverso do seno hiperbólico,
math.acosh() inverso do cosseno hiperbólico,
math.atanh() inversa da tangente hiperbólica,
math.degrees() ângulo convertido de radianos para graus,
math.radians() ângulo convertido de grau para radianos,
math.dist() distância euclidiana entre 2 pontos p e q,
math.hypot() norma euclidiana, distância da origem,
Exponencial e logarítmica
math.pow() potência, xy,
math.erf() função de erro de um número,
math.erfc() a função de erro complementar de um número,
math.exp() exponencial de x, ex,
math.expm1() exponencial de x menos 1, ex-1,
math.frexp() mantissa e expoente de número especificado,
math.log() logaritmo natural, ln(x),
math.log10() logaritmo de base 10, log10(x),
math.log1p() logaritmo natural de 1 + x, , ln(1+x),
math.log2() logaritmo de base 2, log2(x),
math.ldexp() inverso de math.frexp() que é x×2i,
math.gamma() função gama,
math.lgamma() gama do log de x,
Arredondamentos e truncamentos
math.ceil() número arredondado para o maior inteiro mais próximo,
math.floor() número arredondado para o menor inteiro mais próximo,
math.copysign() float, valor do primeiro parâmetro com o sinal do segundo parâmetro,
math.trunc() número truncado, parte inteira,
math.fabs() valor absoluto,
Análise combinatória
math.comb() quantas formas de k itens de n sem repetição e ordem,
math.perm() permutações, maneiras de escolher k itens de n com ordem, sem repetição,
math.factorial() fatorial,
Funções sobre iteráveis
math.prod() produto dos elementos de um iterável,
math.fsum() soma dos elementos de um iterável,
Outras funções
math.fmod() resto da divisão x/y,
math.remainder() resto da divisão x/y,
math.gcd() mdc, maior divisor comum de inteiros,
math.lcm() mdc, mínimo múltiplo comum de inteiros,
math.sqrt() raiz quadrada,
math.isqrt() raiz quadrada arredondada para o menor inteiro mais próximo,
Testes Lógicos
math.isclose() booleano, se dois valores estão próximos,
math.isfinite() booleano, se um número é finito,
math.isinf() booleano, se um número é infinito,
math.isnan() booleano, se um valor é NaN.

As seguintes constantes são carregadas:

Constante valor
math.e número de Euler (2.7182…),
math.inf infinito positivo de ponto flutuante,
math.nan NaN positivo de ponto flutuante,
math.pi π = 3.1415…,
math.tau τ = 2 π.

Alguns exemplos de operações:

» import math
» math.cos(0)        # 1.0
» math.sin(0)        # 0.0
» math.ceil(1.17)    # 2
» math.floor(1.75)   # 1
» math.fabs(-1.75)   # 1.75
» math.comb(7,3)     # 35,   comb(n,k) = n! / (k! * (n - k)!) se k <= n, 0 se  k > n
» math.copysign(3.7,-4.5) # -3.7
» math.fmod(125,3)   # 2.0
» math.fsum([1.2, 2.3, 3.4, 4.5]) # 11.4  (qualquer iterável no argumento)
» math.gcd(12, 8)    # 4 mdc (máximo divisor comum)
» math.pow(2,15)     # 32768.0
» math.sqrt(135)     # 11.61895003862225
» p, q = [1,2,3], [3,2,1]
» math.dist(p,q)     # 2.8284271247461903
» math.hypot(1,2,3)  # 3.741657386773941

Módulos cmath e Numpy

As funções do módulo math são todos definidas para parâmetros “reais” (números de ponto flutuante, pra ser mais preciso). Elas não podem receber números complexos como parâmetros. Para operações com complexos existe o módulo cmath que implementa muitas das mesmas funções. Mais informações sobre esse módulo em Python Docs.


Para operações matemáticas mais sofisticadas existem várias bibliotecas de uso específico. Uma das mais usadas é Numerical Python ou NumPy, usado principalmente na computação científica e no tratamento de dados. NumPy não faz parte da biblioteca padrão e deve ser instalado separadamente.

Além de diversas funções semelhantes às do módulo math, NumPy é capaz de realizar operações alto desempenho com matrizes n-dimensionais. Frequentemente NumPy é utilizado junto com o pandas (veja artigo) para a análise de dados, treinamento de máquina e inteligência artificial.

Módulos statistics e random

O módulo statistics contém diversos métodos para o cálculo de funções estatísticas sobre um conjunto de dados numéricos. A maioria dessas funções podem receber como parâmetros dados int, float, Decimal e Fraction.

Método descrição
Médias e medidas de localização central
mean() Média aritmética dos dados,
fmean() Média aritmética rápida, de ponto flutuante,
geometric_mean() Média geométrica dos dados,
harmonic_mean() Média harmônica dos dados,
median() Mediana,
median_low() Mediana baixa,
median_high() Mediana alta,
median_grouped() Mediana ou 50-ésimo percentil de dados agrupados,
mode() Moda (o valor mais comum) de dados discretos,
multimode() Lista de modas (valores mais comuns) de dados discretos,
quantiles() Divide dados em intervalos de igual probabilidade,
Medidas de espalhamento
stdev() Desvio padrão,
pstdev() Desvio padrão,
variance() Variância da amostra,
pvariance() Variância de toda a população.

Alguns exemplos simples de cálculos estatísticos:

» from statistics import *
» mean([1.2, 2.3, 3.4, 4.5, 5.6])
↳ 3.4

» mode([1.2,2.1,2.1,3.8,4.6])
↳ 2.1

» # mode pode ser usada com dados nominais
» mode(['carro', 'casa', 'casa', 'carro', 'avião', 'helicóptero', 'casa'])
↳ 'casa'

» # multimode retorna lista com valores mais frequentes, na ordem em que aparecem 
» multimode('aaaabbbccddddeeffffgg')
↳ ['a', 'd', 'f']

» multimode([1,1,1,2,3,3,3,4,5,5,6,6,6])
↳ [1, 3, 6]

» dados = [1.5, 2.5, 2.5, 2.75, 3.25, 4.75]
» pstdev(dados)
↳ 0.986893273527251

» # statistics.pvariance(data, mu=None): variação populacional de dados
» # se mu (ponto central da média) não for fornecido ele é calculado como a média
» # se for conhecido pode ser passado como parâmetro

» mu = mean(dados)
» pstdev(dados, mu)
↳ 0.986893273527251

» from fractions import Fraction as F
» dados = [F(1, 4), F(5, 4), F(1, 2)]
» pvariance(dados)
↳ Fraction(13, 72)

» pvariance([1/4, 5/4, 1/2])   # o mesmo resultado seria obtido com floats:  13/72 =
↳ 0.18055555555555555

» # desvio padrão
» dados = [1.25, 3.12, 2.15, 5.68, 7.23, 3.01]
» stdev(dados)
↳ 2.2622643523691037	

Muitos outras bibliotecas existem para cálculo estatístico no Python, como NumPy e SciPy, alémm de outros aplicativos como Minitab, SAS, Matlab e R (nesse site).

Módulo random

Números aleatórios ou randômicos gerados em computadores são números produzidos à partir de uma semente ( ou seed) por meio de algoritmos que visam produzir valores o mais espalhados e imprevisíveis quanto possível. Como os algoritmos são todos determinísticos a distribuição não será de fato aleatória, a menos que o computador possa estar conectado a um evento externo realmente randômico (como o decaimento de uma substância radioativa).

Para fins práticos, tais como uso em jogos, teste e geração de chaves criptográficas, a pseudo aleatoriedade pode ser suficiente.

O módulo random contém diversos métodos para geração desses números e de escolhas entre listas.

Método descrição
seed() inicilaliza o gerador de números aleatórios,
getstate() retorna o estado atual do gerador,
setstate() restaura o estado do gerador,
getrandbits() retorna um número representando bits aleatórios,
randrange() retorna um número aleatório dentro do intervalo,
randint() retorna um inteiro aleatório dentro do intervalo,
choice() retorna um elemento aleatório da sequência dada,
choices() retorna uma lista de elementos aleatórios da sequência dada,
shuffle() embaralha os elementos de lista, retorna None,
sample() retorna uma amostra da sequência,
random() retorna número de ponto flutuante entre 0 e 1,
uniform() retorna float no intervalo,
triangular() retorna float no intervalo, com possibilidade de determinar o ponto central.

Os métodos abaixo retornam um número de ponto flutuante (float) entre 0 e 1 baseado na distribuição mencionada:

Método distribuição
betavariate() Beta,
expovariate() Exponential,
gammavariate() Gamma,
gauss() Gaussiana,
lognormvariate() log-normal,
normalvariate() normal,
vonmisesvariate() de von Mises,
paretovariate() de Pareto,
weibullvariate() de Weibull.

O método random() retorna um número randômico entre 0 e 1 (no intervalo [0, 1)).

seed(semente) é usado para inicializar o gerador. A partir de uma determinada inicialização os números gerados sempre se repetem, o que é útil para testes, em execuções repetidas do código. Se nenhuma semente for fornecida a semente default é a hora atual do sistema.

» # número entre 0 (inclusive) e 1 (exclusive)
» random.random()
↳ 0.15084917392450192

» # a semente inicializa o gerador
» # o bloco seguinte sempre gera os mesmos números
» random.seed(10)
» for t in range(5):
»     print(random.random(), end=' ')
↳ 0.5714025946899135 0.4288890546751146 0.5780913011344704 0.20609823213950174 0.81332125135732     	

» # inteiros entre 1 e 10 (inclusive)
» for t in range(10):
»     print(int(random.random()*10+1), end=' ')
↳ 3 9 8 9 10 3 9 8 5 4

» # o mesmo se pode conseguir com randint
» for t in range(10):
»     print(random.randint(1,10), end=' ')
↳ 4 1 6 1 5 3 5 2 7 10 

O método choice(lista) retorna um ítem escolhido aleatoriamente entre membros de uma lista, tupla ou string. sample(lista, k) escolhe k elementos da lista.

» import random
» lista = [123, 234, 345, 456, 567, 678,789]
» print(random.choice(lista))
↳ 789

» lista = ['banana', 'laranja', 'abacaxi', 'uva', 'pera']
» print(random.choice(lista))
↳ abacaxi

» # escolhe um elemento de uma string
» texto = 'Phylos.net'
» print(random.choice(texto)	
↳ h

» # podemos simular o experimento de 1000 dados jogados
» milDados = {1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0}
» for t in range(1000):
»     milDados[random.choice([1,2,3,4,5,6])] += 1
» for t in range(1,7,2):
»     print('Lado {}: {} \t Lado {}: {}'.format(t, milDados[t], t+1, milDados[t+1]))
↳ Lado 1: 178 	 Lado 2: 171
↳ Lado 3: 160 	 Lado 4: 166
↳ Lado 5: 158 	 Lado 6: 167

» # o método sample(lista, n) escolhe n elementos da lista
» lista = [i for i in range(10,100,5)]
» print(lista)
↳ [10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95]
» print(random.sample(lista, 4))
↳ [20, 90, 95, 55]

» lista = ['banana', 'laranja', 'abacaxi', 'uva', 'pera']
» random.sample(lista, 2)
↳ ['abacaxi', 'banana']

randrange(inicio, fim, passo) retorna um aleatório entre inicio (inclusive) e fim (exclusive) com passos de passo, um parâmetro opcional com default de passo = 1. Todos os parâmetros são inteiros.

uniform(inicio, fim) retorna um float aleatório entre inicio e fim (ambos inclusive). Os parâmetros podem ser floats.

» # randrange(5, 10, 2) só pode retornar 5, 7 ou 9
» print(randrange(5, 10, 2))
↳ 9

» for t in range(5):
»     print(random.randrange(0, 4), end=' ')
↳ 0 3 3 1 2     

» # uniform lida com floats
» for t in range (5):
»     print(random.uniform(3.14, 3.16), end=' ')
↳ 3.1561382140695495 3.1525027738382234 3.152682503779535 3.1532559087053946 3.1411872204770708     

Aviso importante: Os pseudo geradores desse módulo não devem ser usados para gerar números (pseudo) aleatórios para fins de segurança, tais como a geração de chaves de criptografia, gerenciamento de segurança de passwords e autenticação e tokens de segurança. Para essa finalidade use o módulo secrets.

Datas e calendários

Para trabalhar com datas podemos importar o módulo datetime.

Uma das classes definidas nesse módulo é datetime que tem os métodos datetime.now(), a data e hora do momento, e e datetime.today(), a data apenas, ambas de acordo com o ajuste do computador. Um objeto de data contém o ano (year), mês (month), dia (day), hora (hour), minuto (minute), segundo (second), e microsegundo (microsecond).

O módulo datetime contém as classes date, time, datetime e timedelta.

» import datetime as dt

» # datetime.now() retorna a data e hora do momento
» d = dt.datetime.now()
» print(d)
↳ 2021-06-05 16:27:46.444763

» print('%d/%d/%d' % ( x.day, x.month, d.year))
↳ 5/6/2021

» # usando o método format de strings
» print('Ano: {}, Mẽs: {}, Dia: {}'.format(d.day, d.month, d.year))
↳ Ano: 7, Mẽs: 6, Dia: 2021

# ou
» print('{d.day}/{d.month}/{d.year}'.format(d=d))
↳ 7/6/2021

» print('minutos: %d, segundos:%d,%d' % ( d.minute, d.second, d.microsecond))
↳ minutos: 27, segundos:46,444763

» # usando o método format de strings:
» print('minutos: {}, segundos:{},{}'.format(d.minute, d.second, d.microsecond))
↳ minutos: 1, segundos:56,516050

» # ou
» print('{d.minute}min:{d.second},{d.microsecond}s'.format(d=d))
↳ 1min:56,516050s

Para inicializar uma variável com um valor qualquer usamos o construtor da classe datetime. Horas, minutos e seguntos, se não fornecidos, são ajustados para zero por default.

Uma data pode ser criada à partir um timestamp, que é o número de segundos decorridos desde 01 de janeiro de 1970 (UTC), usando o método fromtimestamp().

» # inicializando uma data
» d2 = dt.datetime(1996, 6, 14)
» print(d2)
↳ 1996-06-14 00:00:00

» # data de um timestamp
» ttp = dt.datetime.fromtimestamp(1613000000)
» print(ttp)
↳ 2021-02-10 20:33:20

Para conseguir uma formatação mais minuciosa de datas e hotas podemos usar o método strftime que converte um data em string de acordo com alguns parâmetros de valores pré-estabelecidos. Objetos das classes date, datetime e time todos possuem o método strftime(padrao) para criar uma representação de string representando seu valor.

Por outro lado o método datetime.strptime() constroi um objeto datetime partindo de uma string, representada em um padrão.

» d = dt.datetime.now()
» print(d.strftime('%d/%m/%y'))
↳ 07/06/21

» print(d.strftime('%d/%m/%Y')) 
↳ 07/06/2021

» print(d.strftime('%b'))   # sistema em inglês
↳ Jun

» print(d.strftime('%A, %d de %B, dia %j do ano de %Y')) 
↳ Monday, 07 de June, dia 158 do ano de 2021

A representação de horas segue o mesmo padrão:

» from datetime import time
» # time(hour = 0, minute = 0, second = 0) default
» a = time()
» print('a =', a)
↳ a = 00:00:00

» # time(hour, minute and second)
» b = time(8, 14, 15)
» print('b =', b)
↳ b = 08:14:15

» # time(hour, minute and second)
» c = time(hour = 8, minute = 14, second = 15)
» print('c =', c)
↳ c = 08:14:15

» # time(hour, minute, second, microsecond)
» d = time(8, 14, 15, 34567)
» print('d =', d)
↳ d = 08:14:15.034567

» print(b.strftime('Hora: %I%p, Minuto:%M, Segundo: %S '))
↳ Hora: 08AM, Minuto:14, Segundo: 15 

A diferença entre datas, calculadas com objetos datetime ou date é um objeto timedelta. O número de segundos em um intervalo é dado pelo método total_seconds().

» # diferença entre datas feitas entre objetos datetime e date 
» from datetime import datetime, date

» t1 = date(year = 2021, month = 6, day = 7)
» t2 = date(year = 1957, month = 6, day = 28)
» t3 = t1 - t2
» print('Diferença entre t1 e t2 =', t1 - t2)
↳ Diferença entre t1 e t2 = 23355 days, 0:00:00

» t4 = datetime(year = 2018, month = 7, day = 12, hour = 3, minute = 19, second = 3)
» t5 = datetime(year = 2021, month = 2, day = 12, hour = 1, minute = 15, second = 1)
» print('Diferença entre t4 e t5 =', t4 - t5)
↳ Diferença entre t4 e t5 = -946 days, 2:04:02

» print("type of t3 =", type(t3))
↳ type of t3 = <class 'datetime.timedelta'>
» print("type of t6 =", type(t6))
↳ type of t3 = <class 'datetime.timedelta'>

A diferença entre datas e horas pode ser calculada diretamente entre objetos timedelta.

» from datetime import timedelta
» t1 = timedelta(weeks = 2, days = 5, hours = 1, seconds = 33)
» t2 = timedelta(days = 4, hours = 11, minutes = 4, seconds = 54)
» deltaT = t1 - t2
» print("Diferença: ", deltaT)
↳ Diferença:  14 days, 13:55:39

» print("Segundos decorridos =", deltaT.total_seconds())

O método datetime.strptime(formato, data) tem o efeito oposto. Dada uma string contendo uma data escrita de acordo com formato ele retorna a data correspondente.

» from datetime import datetime
» data_string = '21 jun, 18'
» data1= datetime.strptime(data_string, '%d %b, %y')
» print('data =', data1)
↳ data = 2018-06-21 00:00:00

» data_string = '21 June, 2018'
» data2 = datetime.strptime(data_string, '%d %B, %Y')
» print('data =', data2)        
↳ data = 2018-06-21 00:00:00

Abaixo uma lista com os valores de parâmetros de formatação de strftime e strptime. Os exemplos são para a data datetime.datetime(2013, 9, 30, 7, 6, 5).

%Znome do fuso horário.,%jdia do ano, número preenchido com zero.273,
%-jdia do ano, número.(p) 273,%Unúmero da semana no ano (domingo é dia 1), número preenchido com zero.39,%Wnúmero da semana do ano (segunda-feira é dia 1), número.39,%crepresentação completa de data e hora (l).Seg, 30 de setembro 07:06:05 2013,%xrepresentação completa de data apenas (l).30/09/13,%Xrepresentação completa de tempo (l).07:06:05,%% o caracter literal ‘%‘.

Código Significado Exemplo
%a dia da semana abreviado (l). seg,
%A dia da semana completo (l). Segunda-feira,
%w dia da semana, numerado: 0 é domingo e 6 é sábado. 1,
%d dia do mês preenchido com zero. 30,
%-d dia do mês como um número decimal. (p) 30,
%b nome do mês abreviado (l). Set,
%B nome do mês, completo (l). setembro,
%m número do mês preenchido com zero. 09,
%-m número do mês. (p) 9,
%y ano sem século como um número decimal preenchido com zero. 13,
%Y ano com século como um número decimal. 2013,
%H hora (em 24 horas) como um número decimal preenchido com zero. 07,
%-Hv hora (em 24 horas) como um número decimal. (p) 7,
%I hora (em 12 horas) como um número decimal preenchido com zero. 07,
%-I hora (em 12 horas) como um número decimal. (p) 7,
%p AM ou PM (l). AM,
%M minuto, número preenchido com zero. 06,
%-M minuto, número. (p) 6,
%S segundos, número preenchido com zero. 05,
%-S segundo como um número decimal. (p) 5,
%f microssegundo, número preenchido com zeros à esquerda. 000000,
%z deslocamento UTC no formato + HHMM ou -HHMM.

Códigos marcados com (l) tem resultado dependente do ajuste local para datas. Os marcados com (p) são variáveis de acordo com a a plataforma.

Módulo timeit

O módulo timeit contém métodos úteis para medir o tempo decorrido na execução de um bloco de código. Diferentes modos de implementação de um código podem ser executados em tempos muito diferentes e medir esse tempo, em execuções mais longas, pode ser uma ótima forma de decidir por um otimização.

O método principal é
timeit.timeit(stmt=’codigo1′, setup=’codigo2′, timer=<timer>, number=1000000, globals=None)
onde codigo1 é uma string com as linhas de cógigo cujo tempo de execução se quer medir, codigo2 é string com o cógigo a ser executado previamente, <timer> é o timer a ser usado, number é o número de repetições da execução, e globals especifica o namespace onde será rodado o código.

No exemplo abaixo timeit.timeit('123456 + 654321', number=100_000) mede o tempo de execução da soma 123456 + 654321, repetida 100_000 vezes. O resultado da medida é retornado em segundos, no caso de t = 8.7 x 10-9 seg.

» import timeit
» timeit.timeit('123456 + 654321', number = 100_000)
↳ 0.000865324996993877
» timeit.timeit('123456 + 654321', number = 1_000_000)
↳ 0.008846134001942119

» # o código é realmente executado (evite outputs extensos)
» timeit.timeit('print("-", end=" ")', number = 10)
↳ - - - - - - - - - - 
↳ 0.0006212080006662291

Lembrando, podemos usar o underline como separador de milhares, apenas para facilitar a leitura de números longos. Vemos que realizar a mesma soma 10 vezes mais aumento o tempo de execução.

Suponha que queremos comparar o tempo de construção de uma tupla e uma lista usando o mesmo procedimento de listas de compreensão.

» # lista e tupla com inteiros de 0 a 1000, apenas múltiplos de 5
» lista = [i for i in range(1000) if i%5==0]
» tupla = (i for i in range(1000) if i%5==0)
» # qual deles é mais rápido?

» codigo1 = '''
» lista = [i for i in range(1000) if i%5==0]
» '''
» codigo2 = '''
» tupla = (i for i in range(1000) if i%5==0)
» '''
» # medimos o tempo de construção desses 2 objetos
» t1 = timeit.timeit(codigo1, number = 1000)
» print(t1)
↳ 0.07810732900179573

» t2 = timeit.timeit(codigo2, number = 1000)
» print(t2)
↳ 0.000657964003039524

» # comparando
» print('t1 = {} t2'.format(t1/t2))
» t1 = 118.71064167792143 t2

Vemos pelo último teste que a construção da tupla é mais de 100 vezes mais rápida que a da lista.

O tempo medido varia entre experimentos diferentes pois depende de uma série de fatores tais como quais os processos o computador já tem em execução e qual tempo está disponivel para o processamento do Python. É claro que varia também para máquinas diversas com velocidades de processador e memória disponível diferentes.

Qualquer bloco de código pode ser inserido para parâmetro de timeit, transformado em uma string. Também podemos passar funções como parâmetro.

» timeit.timeit('''
» import math
» listaFat = []
» def fatoriais(n):
»     for t in range(n):
»         listaFat.append(math.factorial(t))
»     print(listaFat)
» ''', number = 1_000_000)
↳ 0.28315910099991015

» # passando funções como parâmetros
» # soma dos 1000 primeiros numeros

» def soma1():
»     soma=0
»     cont=0
»     while cont ≤ 1000:
»         soma +=cont
»         cont +=1

» def soma2():
»     soma=0
»     for i in range(1001):
»         soma +=i

» timeit.timeit(soma1, number = 10000)
↳ 0.9234309990024485

» timeit.timeit(soma2, number = 10000)
↳ 0.6420390400016913

» import math
» def soma3():
»     soma = math.fsum(range(1001))    

» timeit.timeit(soma3, number = 10000)
↳ 0.2940528209983313

Vemos que usar range() para gerar uma sequência é mais rápido do que somar um contador um a um. O método math.fsum() é otimizado para velocidade e roda mais rápido de que as operações anteriores.

No exemplo abaixo um bloco de código é executado antes do código cuja execução é medida. Ele é usado para inicializar variáveis e preparar um cabeçalho para a exibição do resultado e seu tempo de execução não é incluído na medida.

» pre='a=10;conta=0;print("Conta |Valor de a");print("----------------")'
» cod='''
» for t in range(100):
»     conta +=1
»     a+=t
» print("{}   |  {}".format(conta, a))
» '''
» t = timeit.timeit(stmt = cod, setup = pre, number=5)
» print('O tempo de execução do código foi de \nt = {} seg'.format(t))
↳ Conta |Valor de a
↳ ----------------
↳ 100   |  4960
↳ 200   |  9910
↳ 300   |  14860
↳ 400   |  19810
↳ 500   |  24760
↳ O tempo de execução do código foi de 
↳ t = 0.0005647910002153367 seg

timeit no Jupyter Notebook


No Jupyter Notebook existe um método mágico para aplicar timeit a um bloco de código sem necessidade de se importar o módulo. Usamos %timeit comando para medir o tempo de execução do comando, e %%timeit célula para medir o tempo de execução em toda a célula.

No modo de linha várias comandos podem ser separados por ponto e vírgula, (;). Assim como na execução com timeit importado, como várias rodadas de execução do código são medidas, o código é efetivamente executado e, por isso, se deve evitar inserir partes com outputs extensos.

» %timeit r = range(10)
↳ 199 ns ± 9.94 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

» %timeit r = range(1000)
↳ 270 ns ± 18.7 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

» # o código é avaliado em namespace separado
» print(r)
↳ NameError: name 'r' is not defined

» # comandos alinhados com ;
» %timeit import math; n=10; m=100; math.pow(n,m)
↳ 303 ns ± 16.6 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

» # exemplo de exibição longa:
» %timeit for t in range(100): print(t)

» # 81125 linhas são exibidas

Para medida do tempo de execução de uma célula inteira usamos %%timeit.

» def fatorial(n):
»     if n <= 1: return 1
»     else: return n*fatorial(n-1)

-----------------------------------(início da célula)-------
» %%timeit
» n = 10
» fatorial(n)
-----------------------------------(  fim da célula )-------
↳ 1.54 µs ± 77 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

-----------------------------------(início da célula)-------
» %%timeit
» import math
» n=10
» math.factorial(n)
-----------------------------------(  fim da célula )-------
↳ 250 ns ± 8.43 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

» # em linha
» %timeit math.factorial(10)
↳ 110 ns ± 2.85 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)    	

Para medir o tempo de execução de um bloco de código também podemos usar o método timeit.default_timer() para obter um marco de tempo antes e após sua execução.

» import timeit
» import math

» def funcao_1():
»     soma = 0
»     t = 0
»     while t < 1_000_000:
»         soma += t
»         t += 1
»     return soma

» def funcao_2():
»     nums = [i for i in range(1_000_000)]
»     return math.fsum(nums)
    
» inicio = timeit.default_timer()
» funcao_1()
» intervalo_1 = timeit.default_timer() - inicio

» inicio = timeit.default_timer()
» funcao_2()
» intervalo_2 = timeit.default_timer() - inicio

» print('A função 1 demorou {} seg'.format(intervalo_1))
» print('A função 2 demorou {} seg'.format(intervalo_2))
↳ A função 1 demorou 0.12041138499989756 seg
↳ A função 2 demorou 0.07828983699982928 seg

O método timeit admite as seguintes opções:
Em modo de linha:
%timeit [-n N -r R [-t|-c] -q -p P -o]
Em modo de célula:
%%timeit [-n N -r R [-t|-c] -q -p P -o]
onde as opções são:

opção descrição
-n n executa o bloco n vezes em cada loop. Se não fornecido um valor de ajuste é escolhido,
-r r repete o loop r vezes e toma o melhor resultado. Default: r = 3,
-t usa time.time para medir o tempo, que é o default no Unix,
-c usa time.clock para medir o tempo, que é o default no Windows,
-p p usa precisão de p dígitos para exibir o resultado. Default: p = 3,
-q modo silencioso, não imprime o resultado,
-o retorna TimeitResult que pode ser atribuído a uma variável.

O módulo timeit é considerado mais preciso que time, descrito a seguir. Ele executa, por default, o bloco de código 1 milhão de vezes, retornando o menor tempo de execução. Esse número pode ser alterado com o parâmetro number.

Módulo time

O módulo time possui o método time.time() que retorna o momento de sua execução como um timestamp do Unix. The ctime() transforma esse timestamp em um formato padrão de ano, mês, dia e hora.

» import time
» t = time.time()
» print('Tempo decorrido desde a "época" {} segundos'.format(t))
↳ Tempo decorrido desde a "época" 1625697356.5482924 segundos

» horaLocal = time.ctime(t)
» print("Hora local: {}".format(horaLocal))
↳ Hora local: Wed Jul  7 19:35:56 2021

O método time.sleep(n) pausa a execução do código por n segundos, onde n pode ser um inteiro ou float.

» import time
» ti = time.time()
» time.sleep(3.5)
» tf = time.time()

» print('Pausa de {delta:.2f} segundos'.format(delta=tf - ti))
↳ Pausa de 3.50 segundos

Também existe, no Jupiter Notebook, o método mágico %time que mede o tempo decorrido na execução de uma função, similar ao comando time do Unix. Diferentemente de %timeit esse método também exibe o resultado do cálculo.

-----------------------------------(início da célula)-------	
» %time sum(range(100000))
-----------------------------------(  fim da célula )-------
↳ CPU times: user 2.1 ms, sys: 0 ns, total: 2.1 ms
↳ Wall time: 2.1 ms
↳ 4999950000

-----------------------------------(início da célula)-------
» %%time
» soma = 0
» for t in range(1000):
»     soma+=t
» soma
-----------------------------------(  fim  da célula)-------
↳ CPU times: user 150 µs, sys: 7 µs, total: 157 µs
↳ Wall time: 160 µs
↳ 499500 

» # w está disponível após a operação
» %time w = [u for u in range(1000)]
↳ CPU times: user 36 µs, sys: 8 µs, total: 44 µs
↳ Wall time: 47 µs

» print(w)
↳ [0,1,2,...,999]

Módulo string

O Python é notoriamente bom para o gerenciamento de texto. Uma das extensões dessas funcionalidades está no módulo string que contém uma única função e duas classes. A função é capwords(s, sep=None) que parte a string usando str.split() no separador sep; em seguida ela torna o primeiro caracter de cada parte em maiúsculo, usando str.capitalize(); finalmente junta as partes com str.join(). O separador default é sep=’ ‘ (espaço).

» # usando separador default    
» frase = 'nem tudo que reluz é ouro'
» maiuscula = string.capwords(frase)
» print(maiuscula)
↳ Nem Tudo Que Reluz É Ouro

# usando separador '\n' (quebra de linha)
» frase = 'mais vale um pássaro na mão\nque dois voando'
» maiuscula = string.capwords(frase, sep='\n')
» print(maiuscula)
↳ Mais vale um pássaro na mão
↳ Que dois voando    

Um número de constantes são carregadas com o módulo. Elas são especialmente úteis no tratamento de texto, por exemplo quando se deseja remover toda a pontuação de um texto base.

» # string module constants
» print('1.', string.ascii_letters)
» print('2.', string.ascii_lowercase)
» print('3.', string.ascii_uppercase)
» print('4.', string.digits)
» print('5.', string.hexdigits)
» print('6.', string.whitespace)  # ' \t\n\r\x0b\x0c'
» print('7.', string.punctuation)
↳ 1. abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
↳ 2. abcdefghijklmnopqrstuvwxyz
↳ 3. ABCDEFGHIJKLMNOPQRSTUVWXYZ
↳ 4. 0123456789
↳ 5. 0123456789abcdefABCDEF
↳ 6. 
↳ 
↳ 7. !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~

Um exemplo simples de remoção de pontuação de um texto.

» # removendo pontuação de um texto
» txtBasico = 'Partimos de um texto com pontuação, queremos remover essas marcações!'
» pontuacao = string.punctuation
» filtrado =''
» for t in txtBasico:
»     if not t in pontuacao:
»         filtrado += t
» print(filtrado)
↳ Partimos de um texto com pontuação queremos remover essas marcações

O módulo string possui duas classes: Formatter e Template.

Formatter funciona da mesma forma que a função str.format(). A utilidade dessa classe está na possibilidade de derivar dela subclasses para a definição customizada de formatações.

» from string import Formatter
» formatador = Formatter()

» print(formatador.format('{} {}.{}', 'Recomendo o site','phylos', 'net'))
↳ Recomendo o site phylos.net

» print(formatador.format('{site}', site='phylos.net'))
↳ phylos.net

» print(formatador.format('{} {site}', 'Visite o site', site='phylos.net'))
↳ Visite o site phylos.net

» # A função format() tem o mesmo comportamento
» print('{} {website}'.format('Visite o site', website='phylos.net'))
↳ Visite o site phylos.net

A classe Templater é usada para criar templates para substituições em strings. Essa funcionalidade é útil, por exemplo, na criação de aplicativos internacionalizados (contendo mais de uma língua como opção na interface).

» from string import Template
» t = Template('$txt $sobrenome, $nome $sobrenome!')
» ing = t.substitute(txt='My name is', nome='James', sobrenome='Bond')
» pt = t.substitute(txt='Meu nome é', nome='Quim', sobrenome='Joah')

» print(ing)
↳ My name is Bond, James Bond!

» print(pt)
↳ Meu nome é Quim, Joah Quim!

São constantes de Strings:

Constante Significado
string.ascii_letters Concatenação de ascii_lowercase e ascii_uppercase descritos abaixo,
string.ascii_lowercase Caracteres minúsculos ‘abcdefghijklmnopqrstuvwxyz’,
string.ascii_uppercase Caracteres maiúsculos ‘ABCDEFGHIJKLMNOPQRSTUVWXYZ’,
string.digits Dígitos: a string ‘0123456789’,
string.hexdigits Dígitos hexadecimais: a string ‘0123456789abcdefABCDEF’,
string.octdigits Dígitos octais: string ‘01234567’,
string.punctuation Caracteres ASCII considerados pontuação local: !”#$%&'()*+,-./:;<=>?@[\]^_`{|}~,
string.printable Caracteres ASCII imprimíveis: digits, ascii_letters, punctuation, e whitespace,
string.whitespace String com todos os caracteres ASCII impressos como espaços em branco (space, tab, linefeed, return, formfeed, vertical tab).
🔺Início do artigo

Bibliografia

Consulte a bibliografia no final do primeiro artigo dessa série.

Python: Escopos e namespaces


Escopos no Python

O escopo de uma variável é todo trecho de execução do código onde aquela variável pode ser encontrada, lida e alterada. Uma vez criada a variável fica disponível dentro desses trechos e só será apagada quando nenhuma referência á feita a ela. Fora de seu escopo a variável não existe e, por isso, o mesmo nome pode ser atribuído à outra variável, sem conflito.

A definição de uma função cria um novo escopo. Alguns exemplos abaixo demonstram escopos em relação a funções definidas na área principal do código (que já definiremos com mais rigor). Os casos são explicados depois do código.

» # Caso 1:
» def teste():
»     print(i)
» i = 42
» teste()
↳ 42

» #  Caso 2:
» def identidade(i):
»     return i
» i = 42
» print(identidade(5))
↳ 5

» # a variável externa à função não foi alterada
» print(i)
↳ 42

» #  manipulando i internamente
» def soma10(i):
»     i += 10
»     print(i)
» i = 42
» soma10(5)
↳ 15

» print(i)
↳ 42

» #  Caso 3:
» del h        # caso h já exista, delete a variável
» def func():
»     h=200
»     print(h)
» func()
↳ 200
» print(h)
↳ NameError: name 'h' is not defined

» # função definida dentro de outra (aninhada)
» def func():
»     h=200
»     def func2():
»         print(h)
» func2()
↳ NameError: name 'func2' is not defined

» # mas pode ser chamada na área onde foi definida
» def func():
»     h=800
»     def func2():
»         print(h)
»     func2()

» func()
↳ 800
  • Caso 1: a variável i definida fora da função pode ser lida internamente à função teste.
  • Caso 2: se o mesmo nome i é usado como parâmetro da função uma nova variável independente é criada. identidade.i não é a mesma variável que i fora da função. Aterações dentro do corpo da função não se propagam para fora dela.
  • Caso 3: uma variável ou uma função definida dentro de uma função não estão disponíveis fora dela.

Namespaces

“Namespaces are one honking great idea. Let’s do more of those!”
— The Zen of Python, Tim Peters
Em um projeto com código razoavelmente complexo muitos objetos são criados e destruídos. Namespaces é a forma usada para que conflitos entre esses nomes não ocorram. Definiremos como código, bloco ou programa principal a parte da execução do código por onde o interpretador (ou compilador) se inicia.

Um conceito importante, associado ao de escopo, é o de namespaces. Namespaces são as estruturas usadas para organizar os nomes simbólicos que servem para referenciar objetos em um programa. Uma atribuição cria um objeto na memória e associa a ele seu nome simbólico dado à variável, função ou método. Namespaces são coleções de nomes associados às informações sobre o objeto referenciados. Uma analogia pode ser feita com um dicionário onde os nomes dos objetos são as chaves os valores são os próprios objetos. Ele é um mapeamento entre nome e objeto na memória.

A existência de vários namespaces distintos significa que o mesmo nome pode ser usado em locais diferentes do código, desde que esteja em um namespace diferente.

Existem quatro tipos de namespaces no Python:

  • Interno (built-in),
  • global,
  • envolvente e
  • local.

Cada um deles é criado e existe por um tempo próprio, e destruído quando não mais necessário.

Namespace Interno (built-in)

O Namespace Interno (built-in) contém nomes e objetos criados internamente, antes mesmo que nada tenha sido importado e nem definido pelo usuário. Os nomes de váriáveis, funcões e métodos nele contidos podem ser vistos com o comando dir(__builtins__). Esse espaço é criado pelo interpretador quando é iniciado e existe enquanto ele não for encerrado.

» # No output abaixo (que está truncado) estão incluídos muitos nomes que já conhecemos
» dir(__builtins__)
↳ [ ...
↳    'chr', 'complex', 'dict', 'dir', 'divmod', 'enumerate', 'float', 'format', 'frozenset',
↳   'help', 'input', 'int', 'len', 'list', 'map', 'max', 'min', 'next', 'object', 'open', 'ord',
↳    'pow', 'print', 'range', 'reversed', 'round', 'set', 'slice', 'sorted', 'str', 'sum',
↳    'tuple', 'type', 'vars', 'zip'
↳ ]

Esses objetos estão disponíveis no ambiente mais geral e, portanto, em todos os setores do código, mesmo que distribuído em vários módulos.

Para ver onde residem esses valores em __builtins__ usamos:

» __builtins__.str.__module__
↳ 'builtins'

Isso significa que todas essas definições estão no módulo builtins.py.

Namespace global

O namespace global contém todos os nomes definidos no nível básico, abaixo apenas do namespace buil-in. Ele é criado quando o módulo é iniciado e só deixa de existir quando o interpretador é encerrado. Quando um novo módulo é importado com a instrução import um novo namespace é designado para ele. Além desses é possível criar nomes globais através palavra chave global, dentro da cada módulo.

Para ver o conteúdo do namespace global inciamos uma nova sessão do Jupiter Notebook, teclando 0-0 em qualquer célula, no modo de controle, para zerar as referências já criadas.

» # a função globals() retorna um dicionário
» type(globals())
» dict
» # para ver o conteúdo desse dicionário (output truncado)
» print(globals())
↳ {'__name__': '__main__', '__doc__': 'Automatically created module for IPython interactive environment', ...}

» # inserir uma variável é o mesmo que inserir um par no dicionário
» i = 23
» print(globals())
↳ {'__name__': '__main__', '__doc__': 'Automatically created module for IPython interactive environment', ..., 'i': 23}

» # a variável 'i' pode ser acessada e alterada diretamente no dicionário
» globals()['i']
↳ 23

» globals()['i'] = 789
» globals()['i']
↳ 789

# uma nova variável pode ser inserida no dicionário
» globals()['letras'] = 'Aa'
» print(letras)
↳ Aa

Os exemplos acima mostram que manipular o dicionário acessado por globals() é o mesmo que inserir, editar ou remover variáveis no namespaces global. Se um módulo externo for importado uma referência é feita à esse módulo, mas suas propriedades e métodos não são incluídos no dicionário.

» # importando o módulo datetime    
» import datetime
» globals()
↳ {'__name__': '__main__',  '__doc__': 'Automatically created module for IPython interactive environment', ...,
↳   'datetime': <module 'datetime' from '/home/guilherme/.anaconda3/lib/python3.8/datetime.py'>, ...}

Namespaces envolventes e locais

Quando uma nova função ou classe é inicializada o interpretador (ou compilador) cria num novo namespace reservado para aquela função ou classe. Objetos criados dentro deles não estarão disponíveis no ambiente externo (global), nem dentro de outras funcões no escopo global.

Isso significa que variáveis e funções podem ser definidas e usadas dentro de uma função com o mesmo nome de objetos em outras funções ou no programa principal, não ocorrendo confusão ou interferência porque são mantidos em namespaces separados. Isso contribui para que menos erros ocorram no código.

No exemplo seguinte o código exibe 4 diferentes namespaces:

» a = 1
» def f():
»     b = 2
»     c = 3
»     global g
»     def g():
»         c = 4
»         d = 5
»         print('a =%d, b=%d, c=%d, d=%d.'% (a, b, c, d))
        
» # f retorna None mas deve ser executada para que g() seja definida
» f()
» c = 3
» g()
↳ a = 1 , b = 2 , c = 4, d = 5

Em cada ambiente estão disponíveis:

  • built-in: a função print(), (por exemplo),
  • global: variável a, a função f() e a função g(),
  • ambiente de f(): as variáveis a, b, c e a função g(),
  • ambiente de g(): as variáveis a, b, c, d.

A função f() é global. A função g() foi tornada global devido à atribuição global.

Quando o módulo chama f() um novo namespace é criado. De dentro de f() a função g() é chamada, com seu namespace próprio.

O nome (ou identificador) g foi definido como global. Desta forma a função g(), embora tenha sido criada no namespace de f é global e pode ser chamada do módulo principal. Apesar disso sua variável interna d continua tendo escopo restrito a g. A variável c dentro de g não é a mesma que c em f, como se vê nos outputs do código.

Análogo ao dicionário para namespace global, podemos acessar o dicionário local através da função locals(). O código abaixo mostra o estado do namespace local para pontos diferentes de um código com a função g() aninhada em f().

» def f(x,y):
»     a = 'boa'
»     print('posição 1: ',locals())
»     def g():
»         b = 'tarde'
»         print('posição 2: ',locals())
»     g()
»     print('posição 3: ',locals())

» # executamos a função com x = 10, y = 55
» f(10, 55)
↳ posição 1:  {'x': 10, 'y': 55, 'a': 'boa'}
↳ posição 2:  {'b': 'tarde'}
↳ posição 3:  {'x': 10, 'y': 55, 'a': 'boa', 'g': <function f.<locals>.g at 0x7fc174050a60>}

No entanto, se tentarmos usar a variável a dentro de g() o interpretador a busca no namespace superior. Se ela for modificada dentro de namespace de g() uma nova variável é criada com aquele nome, preservando a variável do ambiente superior.

» def f(x,y):
»     a = 'boa'
»     def g():
»         b = 'tarde'
»         print('posição 1: ', a , b)
»         print('posição 2: ', locals())
»     g()
    
» f(10,55)
↳ posição 1:  boa tarde
↳ posição 2:  {'b': 'tarde', 'a': 'boa'}   

Uma forma diferente de se criar um novo namespace é através da criação de classes. Cada classe carrega seu próprio ambiente e é, portanto, seu próprio escopo.

» um = 1000
» tres = 3
» class Unidade:
»     um = 1
»     dois = 2000

» class Dezena:
»     um = 10
 
» class Soma:
»     um = 1 + tres

» print(um)
↳ 1000

» print(Unidade.um)
↳ 1

» print(Dezena.um)
↳ 10

» # a variável global tres existe dentro da classe Soma
» print(Soma.um)
↳ 4

Ao encontrar a requisição de um nome (uma referência à um objeto) o interpretador procura primeiro no ambiente local. Se não encontrar passa consecutivamente para os namespaces local, envolvente, global e built-in. Variáveis definidas em níveis mais externos podem ser acessadas nos níveis mais internos, mas não vice-versa. Em inglês é costume se referir a esse comportamento como Regra LEGB (Local, Enclosing, Global, Built-in).

A terminologia de namespaces local e envolvente não aparecem nas especificações oficiais do Python mas tem sido usadas em manuais e cursos. Ele simplesmente busca expressar o fato de que cada ambiente pode abrigar um ambiente aninhado criando uma hierarquia de precedências onde um nome será buscado.

Escopo:

Como vimos uma variável criada no corpo principal do código de Python está no global namespace e é chamada de variável global. Essas variáveis estão disponíveis em qualquer escopo, global e local.

Se, dentro de um escopo restrito, o programador necessite criar uma variável global (que possa ser acessada em todas as partes do código) dentro de uma área restrita podemos usar a palavra chave global.

Embora funções carreguem seu próprio escopo local, laços for ou while não criam uma área própria de escopo. Variáveis definidas dentro do laço continuam existindo depois de seu término.

» # r, definida dentro do laço, continua existindo após o seu final
» for t in range(10):
»     r = t
» print('Último t = %d' % r)
↳ Último t = 9

» # idem para um laço while
» while True:
»     j = 0
»     while j < 9:
»         j+=1
»     break
» 
» print(j)
↳ 9
    
» # global keyword: G fica acessível de fora de seu escopo de definição
» def func3():
»     global G
»     G = 300
» 
» func3()
» print(G) 
↳ 300

» # um variável 
» k = 1000
» def func4():
»     k = k + 23
»     print(k)
» 
» func4()
↳ UnboundLocalError: local variable 'k' referenced before assignment

» # no entanto ela pode ser acessada de dentro da função
» k = 1000
» def func4():
»     w = k + 23
»     print(w)
» 
» func4()
↳ 1023

# o mais seguro a fazer é passar um parâmetro que será confinado ao escopo da função
» w = 1000
» def func6(i):
»     return(i + 23)
» print(func6(w))
↳ 1023

Da mesma forma a função g() a seguir só poderia ser acessada de dentro de f() se não fosse usada a declaração global.

» def externa():
»     print('dentro de externa')
»     # global interna
»     def interna():
»         print('dentro de interna') 

» # executamos interna() para que ocorra a definição de interna()
» externa()
↳ dentro de externa

» interna()
↳ NameError: name 'interna' is not defined

» # mas, se definimos global interna
» def externa():
»     print('dentro de externa')
»     global interna
»     def interna():
»         print('dentro de interna') 

» externa()
↳ dentro de externa

» interna()
↳ dentro de interna

Sublinhados do Python

Sublinhados (_, __, underscores) tem significados específicos e diferentes no Python, de acordo com sua utilização.

Sublinhado simples:

No interpretador, seja no Idle, no prompt do Phyton ou no Jupyter Notebook, um sublinhado simples isolado (_) significa o valor da última expressão avaliada.

» pow(2,3)
↳ 8
» pow(_,3)
↳ 512

» 'cat' + 'egoria'
↳ 'categoria'
» _
↳ 'categoria'

» _ + 's'
↳ 'categorias'

Também, _ pode ser usado em lugar de uma variável que será ignorada. Isso pode tornar o código mais legível por mostrar que a tal variável não terá nenhum papel nas linhas a seguir. Outro uso é o de utilizar palavras chaves como nomes de variáveis, seguidos de um sublinhado.

Por exemplo:

» _, y = (1, 2)
» # ambos os valores são armazenados mas só y será usado (por convenção)
» print(y)
↳ 2

» for _ in range(10):
»     print('+', end='')
↳ ++++++++++

» # uma função pode retornar 4 valores mas apenas os 2 primeiros serão usados
» a, b, _, _ = funcao(parametro)

» # uma palavra chave (com sublinhado no final) pode ser usada como nome
» if_ = 1234
» print(if_)
↳ 1234

Essa última possibilidade pode tornar o código mais confuso e de difícil leitura.

Um sublinhado inicial, antes do nome da variável, função ou método é uma convenção usada para indicar que aquele objeto é apenas para uso interno e só deve ser acessado dentro da classe. Isso não previne, de fato, que ele seja acessado de fora da classe e, por isso o objeto é chamado de privado fraco. Mas, se o módulo em que a classe reside for importado com import * os nomes iniciados com _ não serão importados.

» # dentro de um módulo (gravado no arquivo classe.py) criamos 2 classes
» def metodo_publico():
»     print ("método público")
» def _metodo_privado():
»     print ("método privado")
» # ------ fim do módulo ------

» # importamos esse módulo
» from classe import *
» # uma chamada ao método público funciona normalmente
» metodo_publico()
↳ método público

» # uma chamada ao método privado não funciona
» _metodo_privado()
↳ NameError: name '_metodo_privado' is not defined
  
» # se o módulo inteiro for importado a classe _metodo_privado() pode ser chamada
» import classe
» classe._metodo_privado()
↳ método privado

Sublinhado duplo:

Uma variável ou método com duplo sublinhado inicial (__), como vimos antes, tem a função de reescrever em tempo de interpretação (ou compilação) o nome do objeto para evitar conflito com os mesmos nomes em subclasses.

» # O seguinte módulo está gravado como modulo.py
» class MinhaClasse():
»     def __init__(self):
»         self.__variavel = 987
» # ------ fim do módulo ------

» # importamos esse módulo e tentamos usar o acesso direto à variável __variavel
» import modulo
» obj = modulo.MinhaClasse()
» obj.__variavel
↳ AttributeError: Myclass instance has no attribute '__variavel'

» # para acessar a variável é necessário escrever um acesso público
» # e alterar o conteúdo de classe.py
» # O seguinte módulo está gravado como modulo.py
» class MinhaClasse():
»     def __init__(self):
»         self.__variavel = 987
»     def funcPublica(self)
»         print(self.___variavel)
» # ------ fim do módulo ------
» import modulo
» obj = modulo.MinhaClasse()
» obj.funcPublica()
↳ 987

Nomes iniciados e terminados com sublinhado duplo:

Métodos com nomes cercados por um duplo sublinhado, como __len__ e __init__, são considerados especiais no Python e servem para que o programador possa fazer sobrecarga ou overloading de métodos especiais das classes. Como vimos na seção sobre Métodos Especiais o método add() pode ser alterado pelo programador para executar função diferente da original.

» # O módulo seguinte está gravado no arquivo modulo2.py
» class MinhaClasse():
»     def __add__(self,a,b):
»         print (a*b)
» # ------ fim do módulo ------        

» import modulo2
» obj = modulo2.MinhaClasse()
» obj.__add__(5,2)
↳ 10

» # o operador + pode ser overloaded
» class novaSoma():
»     def __init__(self, valor):
»         self.valor = valor
»     def __add__(self, other):
»         return self.valor * other.valor

» a, b = novaSoma(56), novaSoma(10)
» print(a+b)
↳ 560

Resumindo

sublinhados em seu nome. Estas são as possibilidades:

  • Pública: significa que o membro pode ser acessado fora da classe onde foi definido, por outras instâncias ou objetos da mesma classe. Esses são nomes sem sublinhados. Por ex.: quantosAlunos = 367.
  • Protegida: o membro pode ser acessado pela classe onde ela está definida e seus filhos, outras classes que herdam dessa classe. Esses nomes são definidos iniciando com um sublinhado. Por ex.: _quantosAlunos = 367.
  • Privada: o membro só está acessível dentro da classe onde ela está definida. Esses são nomes iniciados com dois sublinhados. Por ex.: __quantosAlunos = 367.

Por default todos os membros de uma classe são públicos.

Argumentos de funções passados por valor e por referência

Terminologia:

Os valores usados na definição de uma função e manipulados por ela são chamados de parâmetros da função. Na chamada à função valores são fornecidos, normalmente chamado de argumentos. Na programação em geral (e não apenas no Python) argumentos podem ser passados por referência e por valor.

Um argumento passado por valor pode ser manipulado internamente na função e não tem seu valor alterado fora do escopo da função. Isso acontece porque a função manipula uma nova variável inicializada com aquele valor.

Argumentos passados por referência, se alterados no escopo interno da função, será alterado também fora do escopo da função. Nesse caso a função atua sobre o objeto em si, que é o mesmo que aquele do escopo de nível superior ao da função.

O comportamento dessas variáveis no Python é diferente se são referências a objetos mutáveis ou imutáveis.

São objetos imutáveis:

  • Números (Inteiros, Racionais, Ponto flutuante, Decimais, Complexos e Booleanos)
  • Strings
  • Tuplas
  • Conjuntos congelados (Frozen Sets)
  • Classes do usuário (desde que definida como imutável)

São objetos mutáveis:

  • Listas
  • Conjuntos (Sets)
  • Dicionários
  • Classes do usuário (desde que definida como mutável)

Um aspecto que pode ser difícil de debugar, em caso de erros, são as formas de tratamento dos parâmetros de uma função. Funções tratam de modo diferente argumentos mutáveis e imutáveis.

No Python, como as variáveis são nomes que fazem referências à objetos, toda variável é passada a uma função por referência. Se o objeto é mutável a variável original, fora do escopo da função, é alterado. Se o objeto é imutável a variável original fica inalterada e a função age sobre uma nova variável no seu próprio escopo, deixando inalterada a variável original.

Portanto, em comparação com outras linguagens, as funções agem como se variáveis mutáveis fossem passadas por referência e imutáveis por valor.

» # uma função que recebe um valor imutável trata seu parâmetro como passado por valor
» p = 'explícita'
» def concatena(p):
»     p = p.replace('í','i')
»     p +='mente'
»     return p

# a variável dentro da função está em escopo interno e não altera p global 
» print(concatena(a))
↳ explicitamente

» print(a)
↳ explícita

» # se o valor for mutável a função trata seu parâmetro como passado por referência
» alunos = {'Ana':28,'Julia':25,'José':32}
» def insere(alunos):
»     novos = {'Otto':30,'Mario':28}
»     alunos.update(novos)
»     print('Dentro da função:\n', alunos)

» insere(alunos)

» print('Fora da função:\n', alunos)
↳ Dentro da função:
↳  {'Ana': 28, 'Julia': 25, 'José': 32, 'Otto': 30, 'Mario': 28}
↳ Fora da função:
↳  {'Ana': 28, 'Julia': 25, 'José': 32, 'Otto': 30, 'Mario': 28}

» # forçando a função a se comportar "por valor"
» alunos = {'Ana':28,'Julia':25,'José':32}
» def byValue(alunos):
»     alunos2 = alunos.copy()
»     alunos2.update({'Otto':30,'Mario':28})
»     print("Dentro da função:\n", alunos2)
    
» byValue(alunos)
» print("Fora da função:\n", alunos)
↳ Dentro da função:
↳  {'Ana': 28, 'Julia': 25, 'José': 32, 'Otto': 30, 'Mario': 28}
↳ Fora da função:
↳  {'Ana': 28, 'Julia': 25, 'José': 32}

» # forçando função a se comportar "por referência"
» a = 34
» def byRef(a):
»     a = 78
»     print('Dentro da função: a=', a)
»     return a
» a = byRef(a)
↳ Dentro da função: a= 78

» print('Fora da função: a=', a) 
↳ Fora da função: a= 78

No penúltimo exemplo reconstruimos a função de modo a não alterar o objeto alunos no escopo global. Para isso criamos uma cópia de alunos em alunos2 = alunos.copy(), cuja alteração não implica em alteração na variável global.

Em seguida usamos uma solução paliativa para o caso de querermos tratar um valor imutável como passado por referência. Ele consiste em retornar o valor alternar e reatribuir a variável a.

Gerenciamento de memória

Cada um desses namespaces existe na memória até que sua função termine. O Python possui um processo interno recuperar a memória neles alocada. Mesmo que essa limpeza não seja imediata para esses namespaces quando suas funções terminam, mas todas as referências aos objetos que eles contêm deixam de ser válidas.

Estritamente dizendo, Python não possui variáveis e sim nomes (names) que são referências para objetos. Um objeto pode ter mais de um nome. Por exemplo, no código abaixo, experimento 1, a e b são referências para o mesmo objeto, o inteiro 1. A função id(objeto) retorna um id único para o objeto especificado. Todo objeto possuem seu próprio id que é atribuído a ela em sua criação. No teste vemos que a e b se referem ao mesmo objeto que tem id = 94153176063296. No experimento 2 o nomes, x e y, referenciam o mesmo objeto, a string “algo”. Mesmo sem associar as duas diretamente elas têm o mesmo id e x is y = True.

» # experimento 1
» a = 1
» b = a
» print(id(a))
↳ 94153176063296

» print(id(b))
↳ 94153176063296

# experimento 2
» x = 'algo'
» y = 'algo'

» print(id(x))
↳ 139723955294064

» print(id(y))
↳ 139723955294064

» print(x is y)
↳ True

Um nome é um label (uma etiqueta) que serve para disponibilizar no código um objeto, com suas propriedades e métodos. Mas nem todo objeto tem um nome. Existem objeto simples, como um inteiro ou uma string, e objetos compostos de outros objetos como containeres, como dicionários, listas e classes definidas pelo usuário, que armazenam referências para vários objetos simples ou mesmo outros containeres. Definimos como referência um nome, ou um objeto conteiner que apontam para outros objetos.

O Python usa um sistema de contagem de referências (reference counter) que mantém uma tabela atualizada de quantos nomes (ou referências) são associados a cada instante com um objeto. Quando um nome é associado à outro objeto a contagem decresce. Também podemos desfazer a ligação entre nome e objeto com o comando del.

» # instanciamos um objeto
» x = 700          # referências para o objeto 700 = 1
» y = 700          # referências para o objeto 700 = 2
» z = [700, 700]   # referências para o objeto 700 = 4
»                  # duas novas referências foram criadas: z[0] e z[1]
                 
» # a contagem decresce quando um nome passa a se referir a outo objeto
» x = None         # referências para o objeto 700 = 3
» y = 1            # referências para o objeto 700 = 2

# também podemos usar o comando del
» del z            # referências para o objeto 700 = 0

No código seguinte criamos duas variáveis com o mesmo valor (ou, melhor dizendo, associamos dois nomes com o mesmo objeto, 700) o verificamos seus ids, que são os mesmos. O teste x is y testa se são o mesmo objeto. No segundo teste a variável x é incrementada de 1. Isso faz com que novo objeto seja criado na memória (701) e a contagem de 700 seja diminuída de 1.

» def mem_test():
»     x = 700
»     y = 700
»     print(id(x))
»     print(id(y))
»     print(x is y)

» mem_test()
↳ 139800599228816
↳ 139800599228816
↳ True

» def mem_test2():
»     x = 700
»     y = 700
»     x += 1
»     print(id(x))
»     print(id(y))
»     print(x is y)

» mem_test()
↳ 139800599228688
↳ 139800599228656
↳ False

Objetos do Python, além de ter suas propriedades e código de seus métodos, possui sempre uma tabela com sua contagem de referência e seu tipo.

Como mencionamos o comando del desfaz a referência entre nome e objeto, reduzindo o contador de referências. Mas ele não apaga instantaneamente o objeto da memória.

A figura ilustra o ciclo de vida de um objeto criado dentro de uma função e referenciado uma vez. Quando a função termina sua contagem de referência está zerada e ele pode ser apagado.

No entanto, um objeto no namespace global não sai do escopo enquanto o programa inteiro não termina. Sua contagem de referência não se anula e, por isso, ele não será apagado, mesmo que se torne obsoleto. Essa é uma justificativa válida para não se usar variáveis globais em um projeto, exceto quando são realmente necessárias. E certamente se deve evitar referências para objetos grandes (em termos de requisição de memória) e complexos no escopo global.

Quando o contador de referências atinge o valor 0, significando que nenhuma referência está ativa para aquele objeto, ele pode ser apagado, liberando espaço de memória. Essa função é executada pelo coletor de lixo (garbage collector) que é uma forma de liberar memória para outros usos. Com frequência o uso total de memória de um programa em Python, visto pelo sistema, não decresce quando o coletor apaga objetos. No entanto a memória interna livre, alocada para aquele programa, aumenta. Não é muito difícil depararmos com situações de esgotamento de memória, o que causa lentidão do computador e eventual travamento.

Coletor de lixo geracional:

Um problema existente com o coletor de lixo ocorre com as chamadas referências cíclicas. Temos uma referência cíclica se um objeto faz referência a si mesmo, ou, por ex, um objeto A faz referência à B, que faz referência à C, que por sua vez contém referência à A. Mesmo que os objetos sejam extintos o contador de referências não fica zerado e esses objetos não são apagados, mesmo que nenhum nome se refira a eles.

Referência Cíclica

Por esse motivo o coletor de lixo geracional (generational garbage collector, GGC) foi inserido. Esse mecanismo faz uma verificação de objetos que não são acessíveis por nenhuma parte do código e os remove, mesmo que ainda estejam referenciados. O GGC é mais lento que o simples apagamento quando o contador de referências é zerado e por isso ele não é executado imediatamente após a remoção de uma referência.

Cada objeto no Python pertence a uma de três gerações de objetos. Ao ser criado ele é de primeira geração (geração 0). A cada execução do GGC, se o objeto não for removido, ele é promovido para a geração seguinte. Quando o objeto atinge a última geração (geração 2) ele permanece lá até ser alcançado pela remoção.

Configurando o GGC


A ação do GGC é configurável. Podemos especificar o número máximo de objetos que podem permanecer na geração 0 e quantos devem existir nas gerações 1 e 2 antes que a coleta seja ativada. É possível determinar o número de coletas que devem ser executadas sem que o processamento atinja geração 1 e 2.

Uma coleta de lixo é executada quando o número de objetos na geração 0 atinje o limite, removendo objetos inacessíveis e promovendo os demais para a próxima geração. Nesse caso o coletor de lixo atua apenas sobre geração 0. Quando o segundo limite é alcançado o coletor processa os objetos da geração 0 e 1. Quando o limite da geração 2 é alcançado, o coletor processa todos os objetos, das três gerações. Isso, no entanto, não acontece todas as vezes. A geração 2 (final) tem um limite adicional. Um GGC completo só ocorre quando o número total de objetos na primeira e segunda geração excede 25% de todos os objetos na memória (um limite não configurável).

Esses parâmetros foram inseridos para impedir a execução muito frequente do coletor, pois sua execução paraliza a execução do programa, tornando-os muito mais lentos. A maioria dos aplicativos usa objetos de “vida curta”, por exemplo criados e destruídos dentro de uma função, que nunca são promovidos à próxima geração. O programador pode usar desse fato para impedir “memory leaks” em seu código.

Por que isso importa no Jupyter Notebook

No Jupyter Notebook as células usam o escopo global. Variáveis criadas dentro de uma célula continuam existindo durante todo o ciclo de vida do próprio notebook. Desta forma ele não perde sua contagem de referência e não é excluída da memória, exceto se o programador atribuir aquele mesmo nome à outro objeto ou apague sua referência com o comando del. É fácil ter problemas com memória se você criar variáveis diferentes ​​para as etapas intermediárias de um processamento de dados.

Exemplos comuns desse procedimento ocorrem quando carregamos um dataframe de tamanho razoável (veja o artigo sobre dataframes). Em seguida podemos fazer várias etapas de tratamento de dados, tais como a remoção de valores inválidos, a inserção ou remoção de colunas, atribuindo o resultado de cada etapa à um novo nome.

Como já vimos uma solução seria o apagamento da referência com del, o que não é recomendado porque não há garantias de que o apagamento seria feito no momento esperado. Outra solução, mais eficaz, é a de só usar novos nomes para objetos dentro de funções ou classes, de forma que a referência é extinta ao fim do escopo. Um exemplo é mostrado no código a seguir. Se você não conhece pandas e dataframes simplesmente pule este exemplo.

» import pandas as pd

» def processar_dados(dados_brutos):
»     '''
»     codigo de remoção de valores nulos
»     alteração de nomes de colunas
»     inserção e remoção de dados
»     '''
»     return dados_depurados

» dados = pd.read_csv('file.csv')
» dados_processdos = processar_dados(dados)

Em linguagens de programação mais antigas o programador tinha que alocar um espaço de memória declarando a variável e seu tipo. Após o uso ele devia promover o apagamento da variável. Isso cria dois tipos de problemas: (a) o esquecimento de uma limpeza apropriada leva ao acúmulo de memória usada, particularmente em programas que rodam por longo tempo; (b) o apagamento prematuro de um recurso que ainda pode ser necessário, causando queda do programa. Por esses motivos as linguagens modernas usam o gerencimento automático de memória.

No lado negativo, o coletor de lixo deve armazenar suas informações (o contador de referência, no caso do Python) em algum lugar e precisa usar recursos de processamento para sua função, o que onera o sistema tanto em memória usada quanto de tempo de preocessamento. Ainda assim o gerenciamento automático torna mais fácil para o programador a sua tarefa.

🔺Início do artigo

Bibliografia

Consulte a bibliografia no final do primeiro artigo dessa série.

Python: Classes, métodos especiais


Métodos especiais, ou métodos mágicos, em Python são métodos predefinidos em todos os objetos, com invocação automática sob circunstâncias especiais. Eles normalmente não são chamados diretamente pelo usuário mas podem ser overloaded (sobrescritos e alterados). Seus nomes começam e terminam com sublinhados duplos chamados de dunder (uma expressão derivada de double underscore). As operações abaixo são exemplos de métodos mágicos e como acioná-los, com o operador + e a função len().

» x, y = 34, 45
» x+y
↳ 79
» x.__add__(y)
↳ 79
» 
» l = [4,67,78]
» len(l)
↳ 3
» l.__len__()
↳ 3

Vemos que somar dois números usando o operador + aciona o método __add __ e calcular o comprimento de um objeto usando a função len() equivale a usar seu método __len__().

Uma das principais vantagens de usar métodos mágicos é a possibilidade de elaborar classes com comportamentos similares ou iguais aos de tipos internos.

Atributos especiais das classes

Vimos na seção anterior sobre Classes no Python que a função dir() exibe todos os atributos de uma classe. Muitos deles são built-in, herdados da classe object que é a base de todas as classes, portanto de todos os objetos. Em outras palavras object é uma superclasse para todos os demais objetos.

Podemos listar esses atributos criando uma classe T sem qualquer atributo e examinando o resultado de print(dir(T)).

» class T:
»     pass

» print(dir(T))
↳ ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__',
↳  '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__',
↳  '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__',
↳  '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']

Já vimos e usamos os métodos __init__(), para inicializar objetos, e __str__(), para retornar uma representação de string do objeto, acessada por dir(objeto). Também fizemos o overloading de __eq__() e __add__().

Também usamos a possibilidade de sobrescrever os métodos __len__, que ativa a função len(), __str__, associado às chamadas de print() e __eq__ usado em comparações com ==. Para lembrar esse processo de overload observe os métodos na classe abaixo.

» class Comprimento:
»     def __init__(self, fim = 0):
»         self.minhaLista = list(range(fim))
»     def __len__(self):
»         return len(self.minhaLista)
»     def __eq__(self, outro):
»         return self.minhaLista == outro.minhaLista
»     def __str__(self):
»         return '%s \nlen = %d' % (str(self.minhaLista), len(self.minhaLista))

» comp1 = Comprimento(fim=9)
» comp2 = Comprimento(fim=9)

» print(comp1)
↳ [0, 1, 2, 3, 4, 5, 6, 7, 8] 

» len = 9
» comp1==comp2
↳ True

O método len() se refere à contagem de elementos discretos e só pode ser definido para retornar um número inteiro.

Em outro exemplo definimos na classe Ponto os operadores de comparação __gt__ e __ne__, respectivamente > e != da seguinte forma: consideramos, para esse nosso caso, que um ponto é “maior” que outro se estiver mais afastado da origem de coordenadas, o ponto (0, 0). Para isso definimos o método distancia() que calcula essa distância. O operador __ne__ retorna True se uma ou ambas coordenadas dos dois pontos testados forem diferentes. Para usar a função sqrt(), (raiz quadrada) temos que importar o módulo math.

» from math import sqrt
» class Ponto:
»     def __init__(self, x, y):
»         self.x, self.y = x, y
» 
»     def distancia(self):
»         return sqrt(self.x**2 + self.y**2)
»     
»     def __gt__(self, other):
»         return self.distancia() > other.distancia()
»     
»     def __ne__(self, other):
»         x, y, w, z = self.x, self.y, other.x, other.y
»         return x != w or y != z
»     
»     def __str__(self):
»         return 'Ponto com coordenadas (%d, %d)' % (self.x, self.y)

» p1 = Ponto(4,5)
» p2 = Ponto(1,2)

» p1 != p2
↳ True

» p1 > p2
↳ True

De forma análoga podemos fazer o overload e utilizar os aperadores __eq__, ==, __ge__, >=, __gt__, >, __le__, <=, __lt__, < e __ne__, !=.

Todas as comparações se baseiam no método __cmp __(self, other) que deve retornar um inteiro negativo se self < other, zero se self == other e um inteiro positivo se self > other.

Geralmente é melhor definir cada uma das comparações que serão utilizadas. Mesmo assim a definição do método __cmp__ pode ser uma boa maneira de economizar repetição e melhorar a clareza quando você precisa que todas as comparações sejam implementadas com critérios semelhantes.

Método __init__()

Já vimos na seção anterior o funcionamento do método __init__, e extendemos aqui a descrição de sua funcionalidade. Sabemos que podemos inicializar um objeto sem qualquer referência às propriedades de que ele necessita e inserir mais tarde, dinamicamente, essas propriedades.

» class Area:
»     ''' Área de um retângulo '''
» 
»     def area(self):
»         return self.altura * self.largura
»
» a = Area()
» a.altura = 25
» a.largura = 75
» a.area()
↳ 1875

Uma classe definida dessa forma não deixa claro quais são as propriedades que ele deve usar. Considerando que o método __init__() é acionado internamente, podemos tirar vantagem desse método fazendo seu overload e inicializando as propriedades explicitamente.

» class Area:
»     def __init__(self, altura, largura):
»         self.altura = altura
»         self.largura = largura
» 
»     def area(self):
»         return self.altura * self.largura
» 
» b = Area(123, 90)
» b.area()
↳ 11070

A segunda definição é considerada um melhor design uma vez que torna mais clara a leitura do código e seu uso durante a construção de um aplicativo. A própria definição da classe informa quais são os parâmetros usados pelos objetos dela derivados.

Para o próximo exemplo suponha um jogo do tipo RPG onde os jogadores são criados com uma determinada quantidade de energia e inteligência e que esses valores são incrementados ou decrementados de acordo com as escolhas feitas pelo jogador. A partir desses valores se calcula vida e poder, significando quanto tempo a personagem tem de vida e quanto poder de destruição ela possui em seus golpes.

Para representar as dois tipos possíveis de personagens no jogo definimos a superclasse Personagem com duas variáveis de classe (energia e inteligência) e dois atributos calculados: vida e poder. Duas subclasses Cientista e Estudante herdam da primeira, todas as variáveis e métodos, inclusive __init__(), mas sobreescrevem os métodos _estado(), usado na inicialização, e __str()__.

» class Personagem:
»     def __init__(self, energia, inteligencia):
»         self.energia = energia
»         self.inteligencia = inteligencia
»         self.vida, self.poder = self._estado()
» 
»     def _estado(self):
»         ''' Retorna uma tupla '''
»         return int(self.energia + self.inteligencia), int(self.energia * 10)
»     
»     def __str__(self):
»         return 'Vida = %d Poder = %d' % (self.energia , self.inteligencia)
» 
» class Cientista(Personagem):
»     def _estado(self):
»         return  int(self.energia + self.inteligencia *10), int(self.energia * 5)
»      
»     def __str__(self):
»         return 'Cientista: Vida = %d Poder = %d' %  (self.vida , self.poder)
» 
» class Estudante(Personagem):
»     def _estado(self):
»         return  int(self.energia + self.inteligencia * 5), int(self.energia * 10)
» 
»     def __str__(self):
»         return 'Estudante: Vida = %d Poder = %d' % (self.vida , self.poder)
» 
» p = Personagem(10,10)
» c = Cientista(10,10)
» e = Estudante(10,10)
» 
» print(p)
↳ Vida = 10 Poder = 10
» 
» print(c)
↳ Cientista: Vida = 110 Poder = 50
» 
» print(e)
↳ Estudante: Vida = 60 Poder = 100

Esse é um exemplo de polimorfismo pois cada subclasse possui seu próprio método _estado(). É importante lembrar que __init__() sempre retorna None pois não possui (nem admite) o comando return. A notação com sublinhado em _estado() sugere que o método é de uso interno na classe e seus objetos.

Exibindo um objeto, __str__, __repr__, __format__

Temos usado o método __str__ que retorna uma string com dados sobre o objeto que é exibida com print(objeto). Usando esse método esperamos obter uma descrição amigável e legível do objeto, contendo todos os dados ou que julgamos mais relevantes.

Outro método, __repr__, também retorna uma representação de string, mais técnica e em geral usando uma expressão completa que pode ser usada para reconstruir o objeto. Ele é acionado pela função repr(objeto). Essa representação, se passada como argumento para a função eval(), retorna um objeto com as mesmas características do original. A função eval() interpreta a string passada como argumento e a interpreta como código, executando esse código como uma linha de programação python. Outros exemplos são dados a seguir.

» # __repr__
» class Bicho:
»     def __init__(self, especie, habitat):
»         self.especie = especie
»         self.habitat = habitat
» 
»     def __repr__(self):
»         return 'Bicho("%s", "%s")' % (self.especie, self.habitat)
» 
»     def __str__(self):
»         return 'O bicho %s com habitat: %s' % (self.especie, self.habitat)
»     
» peixe = Bicho('peixe', 'rios')
» 
» # usando o método __repr__
» print(repr(peixe))
↳ Bicho("peixe", "rios")
» 
» # usando o método __str__
» print(peixe)
↳ O bicho peixe com habitat: rios
» 
» # usando eval()
» bagre = eval(repr(peixe))
» print(bagre)
↳ O bicho peixe com habitat: rios

O retorno de repr(peixe) pode ser usado para reconstruir o objeto peixe. Caso o método __str__ não tenha sido sobrescrito sua execução chama o método __repr__ e a saída é idêntica. A expressão bagre = eval(repr(peixe)) é idêntica à bagre = Bicho("peixe", "rios").

» # outros exemplos de uso de eval()
» txt = 'x+x**x'.replace('x','3')
» eval(txt)
↳ 30
» 
» txt = "'casa da mãe joana'.title()"
» eval(txt)
↳ Casa Da Mãe Joana
» 
» for t in [a + ' * ' + b for a in '67' for b in '89']:
»     print(t, '=', eval(t))
↳ 6 * 8 = 48
↳ 6 * 9 = 54
↳ 7 * 8 = 56
↳ 7 * 9 = 63


A função eval() não deve ser aplicada diretamente a dados digitados pelo usuário devido ao risco de que se digite uma expressão que delete dados ou cause qualquer outro dano ao computador ou rede onde o código é executado.

Função e método format

Já vimos o método de formatação de strings usando format(), que é uma funcionalidade mais moderna e poderosa que a notação de % para inserir campos. Recapitulando e expandindo um pouco vamos listas mais alguns exemplos desse método.

» # marcadores nomeados são alimentados por format
» txt1 = '1. Eu me chamo {nome} e tenho {idade} anos.'.format(nome = 'João', idade = 36)
» 
» # os campos podem ser marcados numericamente
» txt2 = '2. Moro em {0}, {1} há {2} anos.'.format('Brasília', 'DF', 20)
» txt3 = '3. Há {2} anos moro em {0}, {1}.'.format('Brasília', 'DF', 20)
» 
» # ou marcados apenas por sua ordem de aparecimento
» txt4 = "4. {} são convertidos em {}. Exemplo: {}.".format('Dígitos', 'strings', 100)
» 
» # controle do número de casas decimais
» txt5 = '5. Esse livro custa R$ {preco:.2f} com desconto!'.format(preco = 49.8)
» 
» print(txt1)
↳ 1. Eu me chamo João e tenho 36 anos.
» 
» print(txt2)
↳ 2. Moro em Brasília, DF há 20 anos.
» 
» print(txt3)
↳ 3. Há 20 anos moro em Brasília, DF.
» 
» print(txt4)
↳ 4. Dígitos são convertidos em strings. Exemplo: 100.
» 
» print(txt5)
↳ 5. Esse livro custa R$ 49.80 com desconto!
» 
» # argumento de format é "unpacking" de sequência
» print('{3}{2}{1}{0}-{3}{0}{1}{2}-{1}{2}{3}{0}-{0}{3}{2} '.format(*'amor'))
↳ roma-ramo-mora-aro
» 
» #indices podem ser repetidos
» print('{0}{1}{0}'.format('abra', 'cad'))
↳ abracadabra

padrao.format(objeto) pode conter um objeto com propriedades que serão lidas em {objeto.propriedade}. Por ex., o módulo sys contém os atributos sys.platform e sys.version.

» import sys
» # sys possui atibutos sys.platform e sys.version
» # no caso abaixo sys é o parâmetro 0
» print ('Platform: {0.platform}\nPython version: {0.version}'.format(sys))
↳ Platform: linux
↳ Python version: 3.8.5 (default, Sep  4 2020, 07:30:14) 
↳ [GCC 7.3.0]

O parâmetro pode ser um objeto construído pelo programador.

» class Nome:
»     nome='Silveirinha'
»     profissao='contador' 
» 
» n = Nome()
» idade = 45
» print('Empregado: {0.nome}\nProfissão: {0.profissao}\nIdade: {1} anos'.format(n, idade))
↳ Empregado: Silveirinha
↳ Profissão: contador
↳ Idade: 45 anos

Uma especificação de formato mais precisa pode ser incluída cim a sintaxe de vírgula seguida da especificação do formato.
{0:30} significa, campo 0 preenchido com 30 espaços, e {1:>4} campo 1 com 4 espaços, alinhado à direita. O alinhamento à esquerda é default.

» # Campo 0: justificado à esquerda (default), preenchendo o campo com 30 caracteres
» # Campo 1: justificado à direita preenchendo o campo com 4 caracteres
» linha = '{0:30} R${1:>4},00'
» 
» print(linha.format('Preço para alunos', 35))
» print(linha.format('Preço para professores', 115))
↳ Preço para alunos              R$  35,00
↳ Preço para professores         R$ 115,00

Os campos usados em format() podem ser aninhados (um campo dentro do outro). No caso abaixo a largura do texto é passada como campo 1, que está dentro do campo 0, como comprimento.

» # campos aninhados. campo 0 é o texto a ser formatado pelo padrão. Campo 1 é a largura do texto
» padrao = '|{0:{1}}|'
» largura = 30
» txt ='conteúdo de uma célula'
» print(padrao.format(txt, largura))
↳ |conteúdo de uma célula        |

Esse processo pode ser muito útil quando se monta um texto com formatação, como em html. No preencimento de texto delimitado por tags um padrão pode incluir trechos de início e fim. Na construção de um tabela, por ex., as tags de abertura e fechamento de uma célula de uma tabela são <td></td>.

» padrao = '{inicio}{0:{1}}{fim}'
» txt ='conteúdo da célula'
» larg = len(txt)
» print(padrao.format(txt, larg, inicio = '', fim = ''))
↳ conteúdo da célula

Lembrando: os parâmetros posicionais 0, 1 devem vir antes dos nomeados.

Outro exemplo é a montagem de uma tabela com valores organizados por colunas. O padrão abaixo estabelece que cada número deve ocupar 6 espaços. Observe que ‘{:6d} {:6d} {:6d} {:6d}’ é o mesmo que ‘{0:6d} {1:6d} {2:6d} {3:6d}’.

» for i in range (3, 8):
»     padrao = '{:6d} {:6d} {:6d} {:6d}'
»     print(padrao.format(i, i ** 2, i ** 3, i ** 4))
↳      3      9     27     81
↳      4     16     64    256
↳      5     25    125    625
↳      6     36    216   1296
↳      7     49    343   2401

Os seguintes sinais podem são usados para alinhamento:

Especificadores de alinhamento
< alinhado à esquerda (default),
> alinhado à direita,
^ centralizado,
= para tipos numéricos, preenchimento após o sinal.

Os seguintes sinais são usados para especificação de formato:

Especificadores de formato
b Binário. Exibe o número na base 2.
c Caracter. Converte inteiros em caractere Unicode.
d Inteiro decimal. Exibe o número na base 10.
o Octal. Exibe o número na base 8.
x Hexadecimal. Exibe número na base 16, usando letras minúsculas para os dígitos acima de 9.
e Expoente. Exibe o número em notação científica usando a letra ‘e’ para o expoente.
g Formato geral. Número com ponto fixo, exceto para números grandes, quando muda para a notação de expoente ‘e’.
n Número. É o mesmo que ‘g’ (para ponto flutuantes) ou ‘d’ (para inteiros), exceto que usa a configuração local para inserir separadores numéricos.
% Porcentagem. Multiplica número por 100 e exibe no formato fixo (‘f’), seguido de sinal %.

A função format() aciona o método __format__() interno ao objeto. Uma classe do programador pode ter esse método overloaded e customizado. Ele deve ser chamado como __format__(self, format_spec) onde format_spec são as especificações das opções de formatação.

As seguintes expressões são equivalentes:
format(obj,format_spec) <=> obj.__format__(obj,format_spec) <=> "{:format_spec}".format(obj)

No próximo exemplo construímos uma classe para representar datas com os atributos inteiros dia, mês e ano. O método __format() recebe um padrão e retorna a string de data formatada de acordo com esse padrão. Por exemplo, se padrao = 'dma' ela retorna '{d.dia}/{d.mes}/{d.ano}'.format(d=self). O método __str__() monta uma string diferente usando um dicionário que associa o número ao nome do meses.

» class Data:
»     def __init__(self, dia, mes, ano):
»         self.dia, self.mes, self.ano = dia, mes, ano
»     
»     def __format__(self, padrao):
»         p = '/'.join('{d.dia}' if t=='d' else ('{d.mes}' if t=='m' else '{d.ano}') for t in padrao)
»         return p.format(d=self)
»
»     def __str__(self):
»         mes = {1:'janeiro', 2:'fevereiro', 3:'março', 4:'abril', 5:'maio', 6:'junho',
»          7:'julho', 8:'agosto', 9:'setembro', 10:'outubro', 11:'novembro', 12:'dezembro'}
»         m = mes[self.mes]
»         return '{0} de {1} de {2}'. format(self.dia, m, self.ano)
» 
» d1, d2, d3 = Data(31,12,2019), Data(20,7,2021), Data(1,7,2047)
» 
» print('Usando função: format(objeto, especificacao)')
» print(format(d1,'dma'))
» print(format(d2,'mda'))
» print(format(d3,'amd'))
↳ Usando função: format(objeto, especificacao)
↳ 31/12/2019
↳ 7/20/2021
↳ 2047/7/1
»
» print('\nUsando formatação de string: padrao.format(objeto)')
» print('dia 1 = {:dma}'.format(d1))
» print('dia 2 = {:mda}'.format(d2))
» print('dia 3 = {:amd}'.format(d3))
↳ Usando formatação de string: padrao.format(objeto)
↳ dia 1 = 31/12/2019
↳ dia 2 = 7/20/2021
↳ dia 3 = 2047/7/1
» 
» print('\nUsando método __str__()')
» print(d3)
↳ Usando método __str__()
↳ 1 de julho de 2047

Função property() e decorador @property

Vimos anteriormente que pode ser útil isolar uma propriedade em uma classe e tratá-la como privada. Isso exige a existência de getters e setters, tratados na seção anterior.

No código abaixo definimos uma classe simples com métodos getter, setter e outro para apagar uma propriedade.

» class Pessoa:
»     def __init__(self):
»         self._nome = ''
»     def setNome(self,nome):
»         self._nome = nome.title()
»     def getNome(self):
»         return 'Não fornecido' if not self._nome else self._nome
»     def delNome(self):
»         self._nome = ''
»    
» # inicializamos um objeto da classe e atribuimos um nome
» p1 = Pessoa()
» p1.setNome('juma jurema')
» 
» print(p1.getNome())
↳ Juma Jurema
» 
» # apagando o nome
» p1.delNome()
» print(p1.getNome())
↳ Não fornecido

A função property facilita o uso desse conjunto de métodos. Ela tem a seguinte forma:

property(fget=None, fset=None, fdel=None, doc=None)

onde todos os parâmetros são opcionais. São esses os parâmetros:

  • fget representa o método de leitura,
  • fset método de atribuição,
  • fdel método de apagamento e
  • doc de uma docstring para o atributo. Se doc não for fornecido a função lê o docstring da função getter.

Ela é usada da seguinte forma:

» class Pessoa:
»     def __init__(self, nome=''):
»         self._nome = nome.title()
»
»     def setNome(self, nome):
»         self._nome = nome.title()
»
»     def getNome(self):
»         return 'Não informado' if self._nome=='' else self._nome
»
»     def delNome(self):
»         self._nome = ''
» 
»     nome = property(getNome, setNome, delNome, 'Propriedade de nome')
 
» p = Pessoa('juma jurema')
» print(p.nome)
↳ Juma Jurema
 
» p.nome = 'japira jaciara'
» print(p.nome)
↳ Japira Jaciara

» del p.nome
» print(p.nome)
↳ Não informado

Com a linha nome = property(getNome, setNome, delNome, 'Propriedade de nome') se adiciona um novo atributo nome associada aos métodos getNome, setNome, delNome. Fazendo isso os seguintes comandos são equivalentes:

  • p1.nomep1.getNome(),
  • p1.nome = 'novo nome'p1.setNome('novo nome') e
  • del p1.nomep1.delNome().

Ao invés de usar essa sintaxe também podemos usar o decorador @property.

» class Pessoa:
»     def __init__(self, nome):
»         self._nome = nome.title()
» 
»     @property
»     def nome(self):
»         return 'Não informado' if self._nome=='' else self._nome
» 
»     @nome.setter
»     def nome(self, nome):
»         self._nome = nome.title()
» 
»     @nome.deleter
»     def nome(self):
»         self._nome = ''

» p = Pessoa('adão adâmico')
» print(p.nome)
↳ Adão Adâmico

» p.nome = 'arthur artemis'
» print(p.nome)
↳ Arthur Artemis

» del p.nome
» print(p.nome)
↳ Não informado

Observer que o decorador @property define a propriedade nome e portanto deve aparecer antes de nome.setter e nome.deleter.

Se mais de um atributo deve ser decorado o processo deve ser repetido para cada atributo.

» class Pessoa:
»     def __init__(self):
»         self._nome = ''
»         self._cpf = ''
» 
»     @property
»     def nome(self):
»         return self._nome
» 
»     @property
»     def cpf(self):
»         return self._cpf
»  
»     @nome.setter
»     def nome(self, nome):
»         self._nome = nome
»         
»     @cpf.setter
»     def cpf(self, cpf):
»         self._cpf = cpf
        
» p = Pessoa()
» p.nome = 'Albert'
» p.nome
↳ 'Albert'

» p.cpf = '133.551.052-12'
» p.cpf
↳ '133.551.052-12'

O decorador @property permite que um método seja acessado como se fosse um atributo. Ele é particularmente útil quando já existe código usando um acesso direto à propriedade na forma de objeto.atributo. Se a classe é modificada para usar getters e setters o uso de @property dispensa que todo o restante do código seja alterado..

Método __getattr__

A função interna getattr é usada para ler o valor de um atributo dentro de um objeto. Além de realizar essa leitura ela permite que se retorne um valor especificado caso o atributo não exista. Ela tem a seguinte sintaxe:

getattr(objeto, atributo[, default])

onde os parâmetros são:

  • objeto (obrigatório), um objeto qualquer;
  • atributo (obrigatório), um atributo do objeto;
  • default (opcional), o valor a retornar caso o atributo não exista.

Ela retorna o valor de objeto.atributo.
Com funcionalidades associadas temos as seguintes funções, exemplificadas abaixo:

  • hasattr(objeto, atributo), que verifica se existe o atributo,
  • setattr(objeto, atributo), que insere um valor nesse atributo,
  • delattr(objeto, atributo), que remove o atributo desse objeto.
» class Pessoa:
»     nome = 'Einar Tandberg-Hanssen'
»     idade = 91
»     pais = 'Norway'

» p = Pessoa
» p.pais='Noruega'
» print(getattr(p, 'nome'))
↳ Einar Tandberg-Hanssen

» print(getattr(p, 'idade'))
↳ 91

» print(getattr(p, 'pais'))
↳ Noruega

» print(getattr(p,'profissao','não encontrado'))
↳ não encontrado

» # funções hasattr, setattr e delattr
» hasattr(p,'pais')
↳ True

» hasattr(p,'profissao')
↳ False

» setattr(p, 'idade', 28)
» p.idade
↳ 28

» delattr(p,'idade')
» hasattr(p,'idade')
↳ False

O método __getattr__ permite um overload da função getattr. Ele é particularmente útil quando se deseja retornar muitos atributos derivados dos dados fornecidos, seja por cálculo, por composição ou modificação.

» class Pessoa:
»     def __init__(self, nome, sobrenome):
»         self._nome = nome
»         self._sobrenome = sobrenome
» 
»     def __getattr__(self, atributo):
»         if atributo=='nomecompleto':
»             return '%s %s' % (self._nome, self._sobrenome)
»         elif atributo=='nomeinvertido':
»             return '%s, %s' % (self._sobrenome, self._nome)
»         elif atributo=='comprimento':
»             return len(self._nome) + len(self._sobrenome)
»         else:
»             return 'Não definido'

» p = Pessoa('Albert','Einsten')

» p.nomecompleto
↳ 'Albert Einsten'

» p.nomeinvertido
↳ 'Einsten, Albert'

» p.comprimento
↳ 13

» p.idade
↳ 'Não definido'

Função e método hash

O método __hash__ é invocado quando se aciona a função hash().

chave = hash(objeto)

Hash usa um algoritmo que transforma o objeto em um número inteiro único que identifica o objeto. Esses inteiros são gerados de forma aleatória (tanto quanto possível) de forma que diversos objetos no código não repitam o mesmo código. O hash é mantido até o fim da execução do código (ou se o objeto for reinicializado). Só existem hashes de objetos imutáveis, como inteiros, booleanos, strings e tuplas e essa propriedade pode ser usada para testar se um objeto é ou não imutável.

Essa chave serve como índice que agiliza a localização de objetos nas coleções e é usada internamente pelo Python em dicionários e conjuntos.

» # o hash de um inteiro é o  próprio inteiro
» hash(12345)
↳ 12345

» hash('12345')
↳ -918245046130431123

» # listas não possuem hashes
» hash([1,2,3,4,5])
↳ TypeError: unhashable type: 'list'

» # o hash de uma função
» def func(fim):
»     for t in range(fim):
»         print(t, end='')
» a = func
» print(hash(a))
↳ 8743614090882

» # todos os elementos de uma tupla devem ser imutáveis
» print(hash((1, 2, [1, 2])))
↳ TypeError: unhashable type: 'list'

Em uma classe podemos customizar __hash__ e __eq__ de forma a alterar o teste de igualdade, ou seja, definimos nosso próprio critério de comparação.

As implementações default de __eq__ e __hash__ nas classes usam id() para fazer comparações e calcular valores de hash, respectivamente. A regra principal para a implementação customizada de métodos __hash__ é que dois objetos iguais devem ter o mesmo valor de hash. Por isso se __eq__ for alterada para um teste diferente de igualdade, o que pode ser o caso dependendo de sua aplicação, o método __hash__ também deve ser alterado para ficar em acordo com essa escolha.

Métodos id, hash e operadores == e is

Três conceitos são necessários para entender id, hash e os operadores == e is que são, respectivamente: identidade, valor do objeto e valor de hash. Nem todos os objetos possuem os três.

Objetos têm uma identidade única, retornada pela função id(). Se dois objetos têm o mesmo id eles são duas referências ao mesmo objeto. O operador is compara itens por identidade: a is b é equivalente a id(a) == id(b).

Objetos também têm um valor: dois objetos a e b têm o mesmo valor se a igualdade pode ser testada e a == b. Objetos container (como listas) têm valor definido por seu conteúdo. Objetos do usuário tem valores baseados em seus atributos. Objetos de diferentes tipos podem ter os mesmos valores, como acontece com os números: 0 == 0.0 == 0j == decimal.Decimal ("0") == fraction.Fraction(0) == False.

Se o método __eq__ não estiver definido em uma classe (para implementar o operador ==), seus objetos herdarão da superclasse padrão e a comparação será feita entre seus ids.

Objetos distintos podem ter o mesmo hash mas objetos iguais devem ter o mesmo hash. Armazenar objetos com o mesmo hash em um dicionário é muito menos eficiente do que armazenar objetos com hashes distintos pois a colisão de hashes exige processamento extra. Objetos do usuário são “hashable” por padrão pois seu hash é seu id. Se um método __eq__ for overloaded em uma classe personalizada, o hash padrão será desativado e deve também ser overloaded se existe intenção de manter a classe imutável. Por outro lado se você deseja forçar uma classe a gerar objetos mutáveis (sem valor de hash) você pode definir seu método __hash__ para retornar None.

Exibiremos dois exemplos para demonstrar esses conceitos. Primeiro definimos uma classe com sua implementação default de __equal__ e __hash__. Em seguida alteramos hash e __eq__ fazendo o teste de igualdade concordar com o teste de hash. Observe que em nenhum caso dois objetos diferentes satisfazem b1 is b2 já que o id é fixo e não pode ser alterado pelo usuário.

» # ----------- Exemplo 1 ---------------    
» class Bar1:
»     def __init__(self, numero, lista):
»         self.numero = numero
»         self.lista = lista

» b1 = Bar1(123, [1,2,3])
» b2 = Bar1(123, [1,2,3])
» b3 = b1

» b1 == b2                      # (o mesmo que b1 is b2)
↳ False
» b1 == b3                      # (o mesmo que b1 is b3)
↳ True
» id(b1) == id(b2)
↳ False
» hash(b1) == hash(b2)
↳ False

» # ----------- Exemplo 2 ---------------
» class Bar2:
»     def __init__(self, numero, lista):
»         self.numero = numero
»         self.lista = lista
»     def __hash__(self):
»         return hash(self.numero)
»     def __eq__(self, other):
»         return self.numero == other.numero and self.lista == other.lista
        
» b1 = Bar2(123, [1,2,3])
» b2 = Bar2(123, [1,2,3])
» b3 = Bar2(123, [4,5,6])

» b1 == b2
↳ True
» id(b1) == id(b2)
↳ False
» hash(b1) == hash(b2)
↳ True
» b1 == b3
↳ False
» hash(b1) == hash(b3)
↳ True

No segundo caso a alteração de __hash__ faz com que dois objetos diferentes, de acordo com o teste is, possam ter o mesmo código hash, o que pode ser útil, dependendo da aplicação.

Uma tabela que associa objetos usando o hash de uma coluna como índice, agilizando a busca de cada par é chamada de hashtable. No Python os dicionários são exemplos dessas hashtables.

O processo de hashing é usado em encriptação e verificação de autenticidade. O Python oferece diversos módulos para isso, como hashlib, instalado por padrão, e cryptohash disponível em Pypi.org.

Métodos __new__ e __del__

Vimos que o método __init__ é adicionado automaticamente quando uma classe é instanciada e que podemos passar valores em seus parâmetros para inicializar o objeto. Antes dele outro método é acionado automaticamente, o método __new__, acessado durante a criação do objeto. Ambos são acionados automaticamente.

Além dele o método __del__ é ativado quando o objeto é destruído (quando a última referência é desfeita e o objeto fica disponível para ser coletado pela lixeira).

O parâmetro cls representa a superclasse que será instanciada e seu valor é provido pelo interpretador. Ele não tem acesso a nenhum dos atributos do objeto definidos em seguida.

» class A:
»     def __new__(cls):
»         print("Criando novo objeto") 
»         return super().__new__(cls)
»     def __init__(self): 
»         print("Dentro de __init__ ")
»     def __del__(self):
»         print("Dentro de __del__ ")

» # instanciando um objeto
» a = A()
↳ Criando novo objeto
↳ Dentro de __init__ 

» # finalizando a referência ao objeto
» del a
↳ Dentro de __del__ 

A função super(), que veremos como mais detalhes, é usada para acessar a superclasse de A, que no caso é object, a classe default da qual herdam todos os demais objetos. Nessa linha se retorna o método __new__ da superclasse e, sem ela, não teríamos acesso à A.__init__.

Como não há uma garantia de que o coletor de lixo destruirá o objeto imediatamente após a referência ser cortada (veja a seção sobre o coletor de lixo) método __del__ tem utilidade reduzida. Ele pode ser útil, no entanto e por ex., para desfazer outra referência que pode existir para o mesmo objeto. Também podem ser usadas em conjunto __new__ e __del__ para abir e fechar, respectivamente, uma conexão com um arquivo ou com um banco de dados.

Se __init__ demanda por valores de parâmetros esses valores devem ser passados para __new__. O exemplo abaixo demostra o uso de __new__ para decidir se cria ou não um objeto da classe Aluno. Se a nota < 5 nenhum objeto será criado.

» class Aluno:
»     def __new__(cls, nome, nota):
»         if nota >= 5:
»             return object.__new__(cls)
»         else:
»             return None
» 
»     def __init__(self, nome, nota):
»         self.nome = nome
»         self.nota = nota
» 
»     def getConceito(self):
»         return 'A' if self.nota >= 8.5 else 'B' if self.nota >= 7.5 else 'C'
» 
»     def __str__(self):
»         return 'Classe = %s\nDicionário(%s) Conceito: %s %s' % (self.__class__.__name__,
»                                                    str(self.__dict__),
»                                                    self.getConceito(),
»                                                    '-'*60 )
                                                   
» a1, a2, a3 = Aluno('Isaac Newton', 9), Aluno('Forrest Gump',4), Aluno('Mary Mediana',7)

» print(a1)
» print(a2, '<---- Objeto não criado' + '-'*60)
» print(a3)

↳ Classe = Aluno
↳ Dicionário({'nome': 'Isaac Newton', 'nota': 9}) Conceito: A
↳ ------------------------------------------------------------
↳ None <---- Objeto não criado
↳ ------------------------------------------------------------
↳ Classe = Aluno
↳ Dicionário({'nome': 'Mary Mediana', 'nota': 7}) Conceito: C
↳ ------------------------------------------------------------

A classe Aluno usa no método __str__ dois atributos das classes: __class__ e __dict__. __class__ retorna um objeto descritor, que é de leitura apenas (não pode ser modificado) que contém dados sobre a superclasse, entre eles o atributo __name__, o nome da classe.

» a1.__class__==Aluno
↳ True

» print(a1.__class__.__name__)
↳ Aluno

» a1.__dict__
↳ {'nome': 'Isaac Newton', 'nota': 9}

» print(dir(a1))
↳ ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__',
↳ '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__',
↳  '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__',
↳ '__weakref__', 'getConceito', 'nome', 'nota']

A função dir retorna todos os seus métodos e propriedades. Como todos os objetos do Python, a classe Aluno possui o atributo __dict__ que é um dicionário contendo suas propriedades. As propriedades podem ser apagadas, editadas ou inseridas diretamente nesse dicionário.

» # inserindo campo 'nascimento'
» a1.__dict__['nascimento'] = '21/03/2001'
» a1.nascimento
↳ '21/03/2001'

» # apagando campo 'nota'
» del(a1.__dict__)['nota']
» a1.nota
↳ AttributeError: 'Aluno' object has no attribute 'nota

Funções e Classes Fábrica (Factory Classes, Functions)


Na programação orientada a objetos uma fábrica é um objeto usado para a criação de outros objetos. Ela pode ser uma função ou método que retorna objetos derivados de um protótipo ou de uma classe e suas subclasses, à partir de parâmetros que permitem a decisão sobre qual objeto retornar.

Isso é particularmente útil quando um grande número de objetos devem ser criados. Esse construtor generalizado pode ter um processo de decisão sobre que subclasse usar. No exemplo seguinte usamos a classe Personagem e suas subclasses Cientista e Estudante definidas anteriormente para construir um exemplo que gera alguns objetos.

» # uma função "factory"
» def criaPersonagem(qual, energia, inteligencia):
»     if qual == 0:
»         return Personagem(energia, inteligencia)
»     elif qual ==1:
»         return Cientista(energia, inteligencia)
»     elif qual ==2:
»         return Estudante(energia, inteligencia)
»     else:
»         print('Personagem = 0, 1, 2')

» # Constroi 3 personagens de tipos diferentes e os armazena em um dicionário
» personagens = {}
» for t in range(3):
»     personagens[t] = criaPersonagem(t, 100, 100)

» # temos um dicionário com os personagens
» for t in range(3):
»     print(personagens[t])
↳ Vida = 100 Poder = 100
↳ Cientista: Vida = 1100 Poder = 500
↳ Estudante: Vida = 600 Poder = 1000

Um design interessante para evitar um código repleto de ifs e, ao mesmo tempo, forçar uma padronização da informação consiste em usar códigos associados ao dado que se quer informar. Para o exemplo seguinte suponha que estamos interessados em classificar livros em uma biblioteca. Para isso criamos um dicionário que associa um código a cada gênero de livros. Com isso obrigamos o preenchimento de dados a se conformar com a inserção correta do código e evitar erros tais como escrever de modo diferente o mesmo gênero (como Poesias e Poemas).

Digamos que em uma biblioteca existem livros dos seguintes gêneros:

  • Romance
  • Poesia
  • Ficção Científica
  • Divulgação Científica
  • Política
  • Biografias

Claro que em um caso mais realista teríamos muito mais gêneros e subgêneros, tipicamente armazenados por código em um banco de dados.

Iniciamos por criar um dicionário (categoria) usando código do gênero como chave. A definição da classe Livro testa na inicialização se o código fornecido existe e, caso afirmativo, substitui self.genero com o gênero correspondente. Se o código não existe fazemos self.genero = "Não Informado".

» # armazenamos a relação codigo - gênero
» categoria = {1 : 'Romance',
»              2 : 'Poesia',
»              3 : 'Ficção Científica',
»              4 : 'Divulgação Científica',
»              5 : 'Política',
»              6 : 'Biografias'
»              }
» 
» # definimos a classe livro
» class Livro:
»     def __init__(self, codigoGenero, titulo, autor):
»         if codigoGenero in categoria.keys():
»             self.genero = categoria[codigoGenero]
»         else:
»             self.genero = 'Não informado'
»         self.titulo = titulo
»         self.autor = autor
»         
»     def __str__(self):
»         txt = 'Gênero: %s\n' % self.genero
»         txt += 'Título: %s\n' % self.titulo
»         txt += 'Autor: %s\n' % self.autor
»         return txt

» # Inicializamos livro com código existente
» livro1 = Livro(1, 'Anna Karenina','Leo Tolstoy')
» print(livro1)
↳ Gênero: Romance
↳ Título: Anna Karenina
↳ Autor: Leo Tolstoy

» # Inicializamos livro com código inexistente
» livro2 = Livro(9, 'Cosmos','Carl Sagan')
» print(livro2)
↳ Gênero: Não informado
↳ Título: Cosmos
↳ Autor: Carl Sagan

Outra abordagem seria armazenar o próprio código transformando-o em texto apenas no momento de uma consulta ou impressão. Esse tipo de design é particularmente útil quando a quantidade de dados mapeados é grande e a consulta a eles é frequente.

Já vimos que uma subclasse pode fazer poucas modificações na classe base, aproveitando quase todo o seu conteúdo mas customizando alguns atributos.

» class Biografia(Livro):
»     def __init__(self, titulo, autor, biografado):
»         self.biografado = biografado
»         super().__init__(6, titulo, autor)
»     def __str__(self):
»         return '%sBiografado: %s' % (super().__str__(), self.biografado)

» bio1 = Biografia('Um Estranho ao Meu Lado','Ann Rule','Ted Bundy')
» print(bio1)
↳ Gênero: Biografias
↳ Título: Um Estranho ao Meu Lado
↳ Autor: Ann Rule
↳ Biografado: Ted Bundy

Suponha que tenhamos criado subclasses especializadas para cada gênero: Biografia, Politica, Poesia, …, etc. Uma factory de classes pode usar um dicionário que associa diretamente um código à classe. No exemplo temos, além da classe Biografia, criamos outras duas apenas para efeito de demonstração.

» class Politica(Livro):
»     pass
»
» class Poesia(Livro):
»     pass
    
» # uma factory de classe (retorna a classe apropriada)
» class FactoryLivro:
»     def __init__(self, i):
»         classe = {6: Biografia, 5: Politica, 2: Poesia}
»         self.essaClasse = classe.get(i,Livro)
»     def getClasse(self):
»         return self.essaClasse

» # Inicializa com livro do gênero 6 (biografia, único que defimos corretamente)
» fact = FactoryLivro(6)
» # classBio vai receber a classe Biografia
» ClassBio = fact.getClasse()

» # instancia um objeto de ClassBio
» bio2 = ClassBio('Vivendo na Pré-história','Ugah Bugah','Fred Flistone')
» print(bio2)
↳ Gênero: Biografias
↳ Título: Vivendo na Pré-história
↳ Autor: Ugah Bugah
↳ Biografado: Fred Flistone

Relembrando, usamos acima o método de dicionários dict.get(key, default), que retorna o valor correspondente à key se ela existe, ou default, se não existe.

Claro que, ao invés de criar subclasses para cada gênero, também poderíamos ampliar a generalidade da própria superclasse Livro inserindo campos flexíveis capazes de armazenar as especificidades de cada gênero, tal como as propriedades Livro.nomeDoCampo e Livro.valorDoCampo.

Classes servidoras de dados: Podemos definir uma classe que não recebe dados do usuário e apenas é usada para preparar e retornar dados em alguma forma específica. A classe Baralho abaixo representa um baralho construído na inicialização, juntando naipes com números de 2 até 10, adicionados de A, J, Q, K. Um coringa C é adicionado a cada baralho. Um objeto da classe tem acesso ao método Baralho.distribuir(n) que seleciona e retorna aleatoriamente n cartas. Para embaralhar as cartas usamos o método random.shuffle que mistura elementos de uma coleção (pseudo) aleatoriamente.

» import random
» class Baralho:
»     def __init__(self):
»         naipes = ['♣', '♠', '♥','♦']
»         numeros = list(range(1, 14))
»         numeros = ['A' if i==1
»                    else 'K' if i==13
»                    else 'Q' if i==12
»                    else 'J' if i==11
»                    else str(i) for i in numeros
»                   ]
»         deck = [a + b for a in numeros for b in naipes]
»         deck += ['C']
»         random.shuffle(deck)
»         self.deck = deck
» 
»     def distribuir(self, quantas):
»         if quantas > len(self.deck):
»             return 'Só restam %d cartas!' % len(self.deck)
»         mao = ''
»         for t in range(quantas):
»             mao += '[%s]' % self.deck.pop()
»         return mao

» jogo = Baralho()
» player1 = jogo.distribuir(11)
» player2 = jogo.distribuir(11)

» print(player1)
↳ [7♥][5♠][6♥][3♦][6♠][8♣][4♥][3♣][9♠][Q♦][7♣]

» print(player2)
↳ [4♦][A♦][J♣][J♥][8♠][9♦][8♦][10♦][10♥][5♥][3♥]


O método lista.pop() retorna o último elemento da lista, removendo o elemento retornado, como uma carta retirada de um baralho.

Para tornar esse processo ainda mais interessante poderíamos criar a classe Carta que gera objetos contendo separadamente seu naipe e valor para facilitar as interações com outras cartas durante o jogo. Esses objetos poderiam, inclusive, armazenar o endereço de imagens de cada carta do baralho, gerando melhores efeitos visuais. Com isso o método Baralho.distribuir() poderia retornar uma coleção de cartas e não apenas strings com uma represntação simples das cartas do baralho.

🔺Início do artigo

Bibliografia

Consulte a bibliografia no final do primeiro artigo dessa série.

Python: Funções, Decoradores e Exceções


Funções e decoradores

No Python tudo é um objeto, inclusive as funções. Isso significa que elas podem ser atribuidas a uma variável ou serem retornadas por outra função. Na programação em geral uma função é considerada um objeto de primeira classe se:

  • é uma instância do tipo Object,
  • pode pode ser armazenada em uma variável,
  • pode ser passada como um parâmetro para outra função,
  • pode ser obtida no retorno de outra função,
  • pode ser armazenada em estruturas de dados, como listas e dicionários.

No Python funções podem ser atribuídas a variáveis.

# uma variável pode armazenar uma função interna
» p = print
» p(1234)
↳ 1234

# ou uma do usuário
» def funcao():
»     print('Tô aqui!')

» a = funcao
# a é uma função
» print(a)
↳ <function __main__.funcao()>

# a função é executada com colchetes
» a()
↳ Tô aqui!

# outra função recebe uma string como parâmetro
» def funcao(texto):
»     print(texto)

» a = funcao

» a('Meu nome é Enéas!')
↳ 'Meu nome é Enéas!

Funções podem ter outras funções definidas dentro de seu corpo. No caso abaixo temos o cálculo da função composta \(f(x) = \sqrt(x^2+1)\).

» import math

» def funcaoComposta(x):
»     def funcaoInterna(i):
»         return i**2 + 1
»     return math.sqrt(funcaoInterna(x))

» funcaoComposta(7)
↳ 7.0710678118654755

Funções podem ser passadas como argumentos para outras funções. A função digaOla(arg) recebe outras duas funções como argumento.

# funções como argumento de outras funções
» def falaAlto(texto):
»     return texto.upper()

» def falaBaixo(texto):
»     return texto.lower()

» def digaOla(func):
»     # variável oi armazena o retorno (string) das funções no argumento func
»     oi = func('Olá, texto usado como argumento da função parâmetro!')
»     print (oi)

» digaOla(falaBaixo)
↳ olá, texto passado como argumento da função parâmetro!

» digaOla(falaAlto)
↳ OLÁ, TEXTO PASSADO COMO ARGUMENTO DA FUNÇÃO PARÂMETRO!

A função funcOla é chamada de decoradora. A função funcNome, que é passada como argumento para o decorador, é chamada de função decorada.

» # exemplo 1
» def funcOla(varFuncao):
»     def funcInterna():
»         print('Olá ', end='')
»         varFuncao()
»     return funcInterna

» def funcNome():
»     print('Assurbanipal, rei da Assíria')

» obj = funcOla(funcNome)
» obj()
↳ Olá Assurbanipal, rei da Assíria

# exemplo 2
» def func1(txt):
»     print(txt)

» def func2(funcao, txt):
»     funcao(txt)

» func2(func1, 'Libbali-sharrat, esposa de Assurbanipal')
↳ Libbali-sharrat, esposa de Assurbanipal

# exemplo 3
» def decoradora(func):
»     def interna():
»         print("Ocorre antes da função parâmetro ser executada.")
»         func()
»         print("Ocorre depois da função parâmetro ser executada.")
»     return interna

» def digaUau():
»     print("Uau!!!!")

» f = decoradora(digaUau)    #   <---- f é uma função composta

» f()                        #   <---- executar a função f
↳ Ocorre antes da função parâmetro ser executada.
↳ Uau!!!!
↳ Ocorre depois da função parâmetro ser executada.

Funções compostas são chamadas de objetos de segunda classe ou funções de ordem superior. Decoradores envolvem uma função, modificando seu comportamento. Quando executamos f = decoradora(digaUau) estamos executando interna() tendo em seu corpo func=digaUau().

O Python fornece uma forma simplificada de usar decoradores, usando o sinal @.

» def funcaoDecoradora(funcaoArg):
»     def interna():
»         # corpo de interna usando funcaoArg()
»     return interna

» @funcaoDecoradora
» def funcaoDecorada:
»     # corpo de decorada #

» # essa sintaxe é equivalente à
» funcaoComposta = funcaoDecoradora(funcaoDecorada)
» # para executá-la
» funcaoComposta()

No caso do último exemplo 3 podemos apenas fazer

» def decoradora(func):
»     def interna():
»         print("Ocorre antes da função decorada ser executada.")
»         func()
»         print("Ocorre depois da função decorada ser executada.")
»     return interna

» @decoradora
» def digaUau():
»     print("Uau!!!!")

» digaUau()
↳ Ocorre antes da função decorada ser executada.
↳ Uau!!!!
↳ Ocorre depois da função decorada ser executada.

Se a função a ser decorada possuir parâmetros, a função interna (que envolve a decorada) deve possuir os mesmos parâmetros, que devem ser fornecidos quando se invoca a função decorada.

» def produtoDecorado(func):
»     def interna(a,b):
»         print('%d x %d = ' % (a,b), end='')
»         return func(a,b)
» 
»     return interna

» @produtoDecorado
» def produto(a,b):
»     print(a * b)

» produto(55,33)
↳ 55 x 33 = 1815

Vale lembrar que se desejamos passar um número qualquer de parâmetros podemos usar *args e *kwargs, que representam respectivamente um número arbitrário de argumentos e de argumentos com palavras chaves.

» def produtoDecorado(func):
»     def interna(*args):
»         print('O produto %s = ' % str(args).replace(', ',' x '), end='')
»         return func(*args)
» 
»     return interna

» @produtoDecorado
» def produtorio(*args):
»     prod = 1
»     for t in args:
»         prod *= t
»     print(prod)

» produtorio(1,2,3,4,5,6,7,8,9)
↳ O produto de (1 x 2 x 3 x 4 x 5 x 6 x 7 x 8 x 9) = 362880
: time.time() retorna a hora em segundos, como um número de ponto flutuante, lida no relógio interno do computador. Ela é o número de segundos decorridos desde a época, 1 de Janeiro de 1970, 00:00:00 (UTC), também chamada de (Unix time).

Um uso comum para um decorador é o de medir o tempo de execução de um bloco de código qualquer. Isso pode ser útil na otimização de um programa. Para isso usamos o módulo time, e suas funções time.time(), que lê a hora em segundos , e time.sleep(n), que suspende a execução do código por n segundos.

Para isso envolvemos o bloco de código a ser medido, no caso a função que é decorada, com o contador. O instante inicial é armazenado e comparado com o final, após a execução, a a diferença é exibida.

» # decorador para medir o tempo de execução de um bloco de código
» import time

» def cronometro(func):
»     def interna(*arg):
»         inicio = time.time()
»         func(*arg)
»         print('O código levou %s segundos para rodar.' % str(time.time()-inicio))
»     return interna

» @cronometro
» def funcaoTeste(n):
»     time.sleep(n)

» funcaoTeste(1.5)
↳ O código levou 1.5015053749084473 segundos para rodar.

» # outro teste, com um laço for
» @cronometro
» def laco():
»     soma = 0
»     for t in range(10000):
»         soma += t
»     print('soma =',soma)

» laco()
↳ soma = 49995000
↳ O código levou 0.0010344982147216797 segundos para rodar.

Erros, Exceções e tratamento de erros

No Python existem dois tipos de erros que são denominados erros de sintaxe e exceções.

Erros de sintaxe são comandos escritos incorretamente, a ausência ou excesso de parênteses, chaves ou colchetes ((, {, [,), delimitadores incorretos de strings, vírgulas ausentes ou postas em local incorreto, etc. Quando encontra esses erros o interpretador interrompe a execução e retorna uma instrução de onde o erro ocorreu e, em alguns casos, uma sugestão de como consertá-lo. Nessas mensagens um sinal de ^ indica o local onde o erro foi notado. Se o código foi lido em um arquivo.py o nome do arquivo é indicado e a linha do erro é indicada. Essas mensagens são fornecidas pela função Traceback.

» print 'sem parênteses'
↳   File "<ipython-input-6-cfe4fc7e6b4d>", line 1
↳     print 'sem parênteses'
↳           ^
↳ SyntaxError: Missing parentheses in call to 'print'. Did you mean print('sem parênteses')?

» print('parênteses excessivos'))
↳  File "<ipython-input-7-1c97f0f5b744>", line 1
↳     print('parênteses excessivos'))
                                  ^
↳ SyntaxError: unmatched ')'

» dicionario = {1:'um', 2:'dois' 3:'três'}
↳   File "<ipython-input-12-60359adab8df>", line 1
↳     dicionario = {1:'um', 2:'dois' 3:'três'}
↳                                    ^
↳ SyntaxError: invalid syntax

Esses são, quase sempre, os erros mais fáceis de serem encontrados e corrigidos. Observe que, no Python 2, o comando print 'sem parênteses' estava correto. No Python 3 print() se tornou uma função e os parênteses passaram a ser obrigatórios.

Vimos que o Python usa indentações (que podem ser espaços ou tabs) para delimitar eus blocos de código. Erros desses tipos são capturados como IndentationError e TabError.

Excessões: Uma exceção é um evento que ocorre durante a execução de um programa que interrompe o fluxo das instruções, além dos erros de sintaxe. Quando o interpretador encontra uma situação com esse tipo de erro ele levanta uma exceção, instanciando uma das classes derivadas da superclasse exception. Exceções levantadas devem ser tratadas para que a execução do código não termine de forma indesejada. Uma lista completa de exceções pode ser encontrada no artigo Python, Resumo.

Um exemplo de exceção é a tentativa de dividir por zero.

» for i in range(4):
»     v = 10/(2-i)
»     print(v)
↳ 5.0
↳ 10.0
↳ ---------------------------------------------------------------------------
↳ ZeroDivisionError                         Traceback (most recent call last)
↳ <ipython-input-14-b8aab2286d16> in <module>
↳             1 for i in range(4):
↳ ---->       2     v = 10/(2-i)
↳             3     print(v)
↳ ZeroDivisionError: division by zero

No exemplo é claro que quando i = 2 o denominador será nulo e a divisão por 0 não é definida. Por isso ZeroDivisionError foi lançada. Podemos corrigir esse erro simplesmente testando o denomidor e pulando o valor problemático. Mas denominadores nulos podem surgir de forma inesperada de muitas formas, tais como em dados lidos automaticamente ou inseridos pelo usuário. Por isso precisamos de um tratamento de erros. Para esse fim temos os blocos try, except e finally ou else.

  • try: verifica se há um erro no bloco seguinte de código,
  • except 1: recebe fluxo de execução em caso de exceção 1,
  • … : (podem existir várias capturas de exceções),
  • except n: recebe fluxo de execução em caso de exceção n,
  • else: código executado se nenhum erro for encontrado,
  • finally: código executado em ambos os casos.

Portanto, se suspeitamos que há possibilidade de um erro ser lançado envolvemos partes do código nesses blocos.

» for i in range(4):
»     try:
»         v = 10/(2-i)
»         print('i = %d, v = %d' % (i,v))
»     except:
»         print('Erro em i = %d' % i)

» # no caso de i=2 o primeiro comando print não é executado
↳ i = 0, v = 5
↳ i = 1, v = 10
↳ Erro em i = 2
↳ i = 3, v = -10

No caso acima except captura qualquer erro que tenha acontecido. Blocos grandes de código podem estar dentro de um try com captura genérica. Isso não é muito bom em muitos casos pois não saberíamos que tipo de de erro foi lançado. Ao invés disso podemos capturar um erro específico.

» # supondo que a variável w não está definida
» try:
»     print(w)
» except NameError:
»     print("A variável w não está definida")
» except:
»     print("Outro erro ocorreu")
» A variável w não está definida

O opção else ocorre se nenhuma exceção foi capturada. finally ocorre em ambos os casos e pode ser útil para a execução de alguma finalização ou limpeza.

Suponha que existe o arquivo arquivoTeste.txt na pasta de trabalho atual mas ele está marcado como read only (somente de leitura).

» try:
»     f = open('arquivoTeste.txt')
»     f.write('Lorum Ipsum')
» except:
»     print('Aconteceu alguma coisa errada com esse arquivo!')
» else:
»     print('Operação bem sucedida!')
» finally:
»     f.close()
»     print('* conexão fechada')
↳ Aconteceu alguma coisa errada com esse arquivo!
↳ * conexão fechada

Se o arquivo arquivoTeste2.txt não existe na pasta de trabalho outro erro será lançado:

» try:
»     f = open('arquivoTeste2.txt')
» except FileNotFoundError:
»     print('Esse arquivo não existe!')
» except:
»     print('Aconteceu alguma coisa errada com esse arquivo!')
» finally:
»     f.close()
↳ Esse arquivo não existe!

Suponha que na atual pasta de trabalho existe uma subpasta dados. Se tentarmos abrir essa pasta como se fosse um arquivo teremos uma exceção.

» try:
»     arq = 'dados'
»     f = open(arq)
» except FileNotFoundError:
»     print('Esse arquivo não existe!')
» except IsADirectoryError:
»     print('"%s" é uma pasta e não um arquivo!' % arq)
» else:
»     f.close()
↳ "dados" é uma pasta e não um arquivo!

As exceções FileNotFoundError e IsADirectoryError são ambas subclasses de OSError. As duas exceções são capturadas por essa superclasse.

» try:
»     arq = 'dados'
»     f = open(arq)
» except OSError:
»     print('"%s" é uma pasta e não um arquivo!' % arq)
↳ "dados" é uma pasta e não um arquivo!

» try:
»     arq = 'arquivoNaoExistente'
»     f = open(arq)
» except OSError:
»     print('"%s" não existe!' % arq)
↳ "arquivoNaoExistente" não existe! 

Diversos erros podem ser capturados em um bloco.

» try:
»     lunch()
» except SyntaxError:
»     print('Fix your syntax')
» except TypeError:
»     print('Oh no! A TypeError has occured')
» except ValueError:
»     print('A ValueError occured!')
» except ZeroDivisionError:
»     print('Did by zero?')
» else:
»     print('No exception')
» finally:
»     print('Ok then')

Segue uma lista parcial de erros e sua descrição. Uma lista completa de exceções pode ser encontrada no artigo Python, Resumo.

Exceção Ocorre quando
AsserationError na falha de uma instrução assert
AttributeError em erro de atribuição de atributo
FloatingPointError erro em operação de ponto flutuante
MemoryError ocorre falta de memória para realizar a operação
IndexError há uma chamada à índice fora do intervalo existente
NotImplementedError erro em métodos abstratos
NameError não existe uma variável com o nome no escopo local ou global
KeyError chave não encontrada no dicionário
ImportError tentativa de importar módulo não existente
ZeroDivisorError tentativa de divisão por 0 (zero)
GeneratorExit um gerador é abandonado antes de seu final
OverFlowError uma operação aritmética resulta em número muito grande
IndentationError indentação incorreta
EOFError uma função como input() ou raw_input() retorna end-of-file (EOF, fim de arquivo)
SyntaxError um erro de sintaxe é levantado
TabError espaço ou tabulações inconsistentes
ValueError uma função recebe um argumento com valor incorreto
TypeError tentativa de operação entre tipos incompatíveis
SystemError o interpretador detecta erro interno

É possível capturar o erro lançado com a expressão except Exception as varExcecao: de forma a exibir a mensagem embutida no objeto.

» x, y = 2, '3'
» try:
»     y + x
» except TypeError as t:
»     print(t)
↳ can only concatenate str (not "int") to str

Vários tipos de exceções podem ser capturadas simultaneamente.

try:
    <código que pode conter as exceções>
    ......................
except(Exception1[, Exception2[,...ExceptionN]]]):
    <tratamento das exceções, caso ocorram>
    ......................
else:
    <código executado caso nenhuma das exceções ocorra>
    ......................    

Além das diversas exceções built-in lançadas automaticamente o usuário pode lançar suas próprias exceções. Isso é feito com raise.

» x = 'um'
» if not isinstance(x, int):
»     raise ValueError("Tipo incorreto")
» else:
»     print(34/x)
↳ ValueError: Tipo incorreto

No exemplo acima isinstance(x, int) testa se x é uma instância de int, ou seja, se x é um inteiro.

O usuário pode definir suas próprias exceções, lembrando que devem ser todas derivadas da classe Exception. No exemplo as classes ValorMuitoBaixoError e ValorMuitoAltoError herdam todos os atributos da superclasse, sem acrescentar nenhuma cacterística própria.

» class ValorMuitoBaixoError(Exception):
»     """Erro lançado quando a tentativa é um valor muito baixo"""
»     pass

» class ValorMuitoAltoError(Exception):
»     """Erro lançado quando a tentativa é um valor muito alto"""
»     pass

» # Você deve adivinhar esse número
» numero = 10

» # Loop enquanto o número não for correto
» while True:
»     try:
»         num = int(input("Digite um número: "))
»         if num < numero:
»             raise ValorMuitoBaixoError
»         elif num > numero:
»             raise ValorMuitoAltoError
»         else:
»             print('Acertou!')
»             break
»     except ValorMuitoBaixoError:
»         print("Valor muito pequeno. Tente de novo!\n")
»     except ValorMuitoAltoError:
»         print("Valor muito alto. Tente de novo!\n")

» # ao ser executado o código abre um diálogo para input do usuário
» # suponha que as tentativas feitas são: 2, 55, 10
↳ Digite um número: 2
↳ Valor muito pequeno. Tente de novo!

↳ Digite um número: 55
↳ Valor muito alto. Tente de novo!

↳ Digite um número: 10
↳ Acertou!        

Além de simplesmente herdar da superclasse as classes de erros customizadas podem fazer o overload de seus métodos para realizar tarefas específicas. No caso abaixo usamos apenas uma classe indicativa de erro e alteramos a propriedade message da classe e da superclasse para informar se o erro foi para mais ou menos. Por default o método __str__ retorna essa mensagem.

No trecho abaixo fazemos o overload também de __str__ para incluir uma mensagem mais completa, mantendo igual todo o restante do código.

» # Você deve adivinhar esse número, entre 0 e 100
» numero = 50

» class ValorIncorretoError(Exception):
»     """Exceção lançada para erro de valor """
» 
»     def __init__(self, valor):
»         message='Valor %d é muito %s' % (valor,'baixo' if valor < numero else 'alto')
»         self.message = message
»         super().__init__(self.message)

» # Loop enquanto o número não for correto
» while True:
»     try:
»         num = int(input('Digite um número entre 0 e 100: '))
»         if num != numero:
»             raise ValorIncorretoError(num)
»         else:
»             print('Acertou!')
»             break
»     except ValorIncorretoError as vi:
»         print('%s. Tente de novo!\n' % str(vi))

↳ Digite um número entre 0 e 100: 34
↳ 34. Tente de novo!

↳ Digite um número entre 0 e 100: 89
↳ 89. Tente de novo!

↳ Digite um número entre 0 e 100: 50
↳ Acertou!

Assert e AssertionError

A instrução assert fornece um teste de uma condição. Se a condição é verdadeira o código continua normalmente sua execução. Se for falsa a exceção AssertionError é lançada, com uma mensagem de erro opcional. Ela deve ser usada como um auxiliar na depuração do código, informando o desenvolvedor sobre erros irrecuperáveis ​​em um programa. Asserções são autoverificações internas do programa e funcionam através da declaração de condições que não deveriam ocorrer de forma alguma. O lançamento de uma exceção AssertionError deve indicar que há um bug no código e sua ocorrência deve informar qual condição inaceitável foi violada.

» # forçando o levantamento de AssertionError
» a, b = 2, 3
» assert a==b
↳ AssertionError

Suponha que uma loja monta um sistema para gerenciar suas vendas. Em algum momento o vendedor pode oferecer um desconto na compra mas o gerente determinou que o desconto não pode ser superior a 50%. Definimos uma função de cálculo do valor final da venda que impede que o preço final seja menor que metade do preço original, o maior que ele.

» def precoComDesconto(preco, desconto):
»     try:
»         precoFinal = preco * (1-desconto/100)
»         assert .5 <= precoFinal/preco <= 1
»     except AssertionError:
»         return 'Desconto inválido!'
»     else:    
»         return precoFinal

» print(precoComDesconto(120,50))
↳ 60.0

» print(precoComDesconto(120,55))
↳ Desconto inválido!

O último exemplo mostra que um AssertionError pode ser capturado como qualquer outra exceção lançada.

Exceções do tipo AssertionError não devem ser usadas em produtos finais, no código depois de todos os testes de erros foram executados. Parcialmente porque é possível executar o código desabilitando todas as instruções assert. Suponha que um desenvolvedor quer evitar que um usuário, que não o administrador do sistema, apague registros em um banco de dados.

» # não faça isso!
» def apagarRegistros(usr):
»     assert usr.isAdmin()
»     < código de apagamento >

Se o sistema for executado com desabilitação de assert qualquer usuário tem acesso ao apagamento de dados!

Erros lógicos

Outro tipo de erro são os erros lógicos que, provavelmente, ocupam a maior parte do tempo de debugging dos desenvolvedores. Eles ocorrem quando o código não tem erros de sintaxe nem exceções de tempo de execução mas foram escritos de forma que o resultado da execução é incorreto. Um exemplo simples seria de uma lista que começa a ler os elementos com índice i=1, o que faz com que o primeiro elemento seja ignorado. Esses erros podem ser complexos e difíceis de serem encontrados e corrigidos pois não causam a interrupção do programa nem lançam mensagens de advertência.

Os três exemplos abaixo mostram casos de erros lógicos.

» # 1) queremos o produto dos 9 primeiros números
» produto = 1
» for i in range(10):
»     produto *= i
» print(produto)
↳ 0

» # 2) queremos soma dos 9 primeiros números
» num = 0
» for num in range(10):
»     num += num
» print(num)
↳ 18

» # 3) queremos a soma dos quadrados dos 9 primeiros números
» soma_quadrados = 0
» for i in range(10):
»     iquad = i**2
» soma_quadrados += iquad
» print(soma_quadrados)
↳ 81

É muito difícil ou impossível escrever um codigo mais complexo sem cometer erros de lógica. Algumas sugestões podem ajudar a minorar esse problema:

  • Planeje antes de começar a escrever código:
    • Faça diagramas deixando claro quais são os dados de entrada do código e o que se espera obter. Tenha clareza sobre o objetivo de seu projeto.
    • Fluxogramas e pseudocódigo ajudam nesse aspecto.
  • Comente o código e use docstrings corretos para suas funções e classes:
    • Um código bem documentado é mais fácil de ser compreendido não só por outros programadores que talvez trabalhem em seu projeto como para você mesmo, quando tiver que rever um bloco algum tempo após tê-lo idealizado.
    • Docstrings podem ser acessados facilmente pelo desenvolvedor e usado por várias IDEs para facilitar seu acesso.
  • Escreva primeiro blocos de código de funcionamento geral, depois os detalhes, testando sempre cada etapa.
  • Teste o produto final com dados válidos e dados inválidos:
    • Faça testes usando valores esperados. É particularmente importante testar o código com valores limítrofes, tais como o mais baixo e o mais alto aceitável. Teste também usando valores incorretos que podem ser, inadvertidamente, pelo usuário final. Um exemplo comum, o usuário pode digitar a letra l ou invés do dígito 1. Valores fora da faixa esperada e de tipos diferentes devem ser experimentados.
    • Para aplicativos usados por muitos usuários finais, particularmente os executados na internet, use testadores que não aqueles que desenvolveram o código.

Instrumentos de depuração (debugging)

Uma das formas simples de encontrar erros no código consiste em escrever instruções print() nas partes onde desejamos observar um valor intermediário de alguma variável. Existem IDEs que permitem o acompanhamento em tempo real de cada valor, na medida em que ocorrem na execução do código. E, finalmente, existem programas auxiliares para a localização de erros. Algumas dessas ferramentas verificam a sintaxe do código escrito marcando os erros e sugerindo melhor estilo de programação. Outras nos permitem analisar o programa enquanto ele está em execução.

Pyflakes, pylint, PyChecker e pep8

Descritos na documentação do Python esses quatro utilitários recebem os arquivos *.py como input e analisam o código em busca de erros de sintaxe e alguns de erros de tempo de execução. Ao final eles imprimem avisos sugerindo melhor estilo de codificação e códigos ineficientes e potencialmente incorretos como, por exemplo, variáveis ​​e módulos importados que nunca são usados.

Pyflakes analisa as linhas de código sem importá-lo. Ele detecta menos erros que os demais aplicativos mas é mais seguro pois não há risco de executar código defeituoso. Ele também é mais rápido que as demais ferramentas aqui descritas.

Pylint e PyChecker importam o código e produzem listas mais extensas de erros e advertências. Eles são especialmente importantes quando se considera a funcionalidade de pyflakes muito básica.

Pep8 faz uma análise do código procurando por trechos com estilo de codificação ruim, tendo como padrão o estilo proposto na Pep 8 que é o documento de especificação para um bom estilo de codificação no Python.

Todos eles são usados com os comandos de comando, no prompt do sistema:

> pyflakes meuCodigo.py
> pylint meuCodigo.py
> pychecker meuCodigo.py
> pep8 meuCodigo.py

pdb


pdb é um módulo built-in, usado para depurar código enquanto ele está em execução. É possível usá-lo invocando-o como um script enquanto o código é executado ou importar o módulo e usar suas funções junto com o código do desenvolvedor. pdb permite que o código seja executado uma linha de cada vez, ou em blocos, inspeccionando a cada passo o estado do programa. Ele também emite um relatório de problemas que causam o término da execução por erros.

> import pdb
» def funcaoComErro(x):
»     ideia_ruim = x + '4'

» pdb.run('funcaoComErro(3)')
↳ > <string>(1)>module>()

Como script pdb pode ser executado da seguinte forma:
python3 -m pdb myscript.py.

Uma descrição mais completa de pdb pode ser encontrada em Python Docs: Pdb.

🔺Início do artigo

Bibliografia

Consulte a bibliografia no final do primeiro artigo dessa série.

Python: Classes, variáveis do usuário

Programação Orientada a Objetos

Apesar de que o Python é uma linguagem multiparadigma ele dá suporte para a maioria das técnicas empregadas na Programação Orientada a Objetos, POO. Os tipos de dados que encontramos no Python, tais como strings, números inteiros, listas e dicionários são objetos. Eles possuem propriedades e métodos próprios, pré-programados. É frequente, no entanto, que coisas da vida real, sobre as quais queremos usar métodos de computação e análise, exijam uma modelagem mais complexa. Para isso podemos criar objetos que são os tipos definidos pelo usuário, denominados classes. Além de ser uma ferramenta útil para quem cria um programa, essa possibilidade foi explorada por outros programadores que disponibilizam seu código através de módulos disponíveis em um grande número de bibliotecas que expandem o poder do Python.

A programação orientada a objetos faz uso das seguintes técnicas:

  • Encapsulamento de dados : a restrição de que dados e métodos só possam ser acessados de dentro do objeto, com acesso vedado a chamadas externas.
  • Herança : a possibilidade de reutilizar e estender a definição de uma classe, alterando-a para especializações.
  • Polimorfismo : a possibilidade de que várias classes usem os mesmos nomes de métodos gerais.

No Python o encapsulamento de dados não é obrigatório nem automático, mas pode ser implementado. Herança e polimorfismo são partes naturais de sua sintaxe.

Assim como em outras partes desse texto, os exemplos dados são simples e pouco realistas. Casos complexos são compostos de um grande número de partes simples, portanto entender o simples é um grande passo para o gerenciamento dos casos gerais. Além disso nossas classes não são devidamente documentadas para diminuir o tamanho do texto e facilitar a leitura, o que deve ser evitado na prática.

Classes: Tipos de dados criados pelo usuário

Suponha que desejamos elaborar um programa para controle de uma escola. Um elemento básico desse programa seria, por ex., a descrição dos alunos. Um aluno pode ser descrito por uma série de dados de tipos diversos, como uma string para armazenar seu nome, inteiros para sua idade, floats para suas notas, datas para a data de nascimento, etc. É claro que podemos configurar listas ou dicionários complexos que contenham toda essa informação. No entanto temos uma ferramenta mais sofisticada e poderosa: as classes.

Definições

Uma classe é uma abstração de alguma entidade que se deseja modelar. Um objeto é um caso particular da entidade representada pela classe. Dizemos que o objeto é uma instância da classe. A classe possui atributos que podem ser propriedades ou métodos. Propriedades são o conjunto de valores (dados) que descrevem a entidade. Métodos são funções definidas na classe. Esses métodos contém instruções para as tarefas que esperamos que sejam executadas por aquela entidade.

Os métodos mais frequentes são aqueles que realizam operações CRUD (Create, Read, Update e Delete, ou seja, criar, ler, atualizar e apagar). Mas eles não se retringem a isso e podem fazer operações com os dados armazenados em propriedades, retornar resultados, imprimí-los ou executar qualquer tarefa disponível para a máquina que executa o código.

Uma classe é definida com a palavra chave class seguida de seu nome (por convenção iniciado por maiúscula). A partir da classe objetos são instanciados, ou seja, criados sob o molde de sua classe geradora. Um objeto instanciado de uma classe possui as suas propriedades e métodos.

A classe mais simples é aquela que não contém em sua definição nenhuma propriedade nem método. A palavra chave pass marca a posição, sem executar nenhuma tarefa. Propriedades podem ser inseridas, alteradas e lidas com a notação de ponto, objeto.propridade.

# Uma classe simples, sem propriedades ou métodos
» class Simples:
»     pass

# instanciando um objeto da classe
» s1 = Simples()
# inserindo 2 propriedades
» s1.propriedade1 = 'A solução para a questão da vida, do universo e tudo mais'
» s1.propriedade2 = 42

# examinando o estado das propriedades
» print(s1.propriedade1, s1.propriedade2)
↳ A solução para a questão da vida, do universo e tudo mais 42

# atributos são listados com dir
» print(dir(s1))
↳ ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', ↳ '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__',
↳ '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__',
↳ 'propriedade1', 'propriedade2']

# uma propriedade pode ser removida
» del s1.propriedade2

# a classe (modelo para os objetos) pode ser alterada após sua definição
» Simples.valor = 9
» s2 = Simples()
» s2.valor
↳ 9
» s1.valor
↳ 9

: A função dir lista os atributos da classe ou dos objetos, entre elas as que definimos acrescidas de muitas outras geradas automaticamente. Voltaremos a isso mais tarde. Vemos acima que uma alteração na classe gera alterações nas instâncias, mesmo aquelas criadas antes dessa alteração.

Propriedades podem ser definidas na classe, sendo herdadas por suas instâncias.

» class Aluno():
»     nome = ''
»     nota = 0

# instanciamos 2 objetos da classe
» aluno1 = Aluno()
» aluno2 = Aluno()

# as propriedades podem ser acessadas para leitura e alteração
» aluno1.nome = 'Ana Anésia'
» aluno1.nota = 79

» print(aluno1.nome, aluno1.nota) 
↳ Ana Anésia  79

# a propriedade __class__ indica a que classe pertence esse objeto
» aluno1.__class__
↳ __main__.Aluno

» aluno2.nome = 'Pedro Paulo Passos'
» aluno2.nota = 45

# propriedades podem ser usadas como variáveis
» print('Média dos alunos: ',  (aluno1.nota + aluno2.nota)/2)
↳ Média dos alunos:  62.0

Uma alteração na classe Aluno altera as propriedades dos objetos dela derivados e que ainda não foram modificados. Propriedades já modificadas em código não são alteradas. Novas instâncias da classe também são criadas com a nova propriedade.

# uma alteração na classe    
» Aluno.nota = 50
» aluno3 = Aluno()
» print(aluno3.nota)
↳ 50

» Aluno.nota = 65
» print(aluno3.nota)
↳ 65

# propriedades já alteradas não são afetadas
» print(aluno1.nota)
↳ 79


O parâmetro self serve como uma referência para o próprio objeto (como veremos, para cada uma das instâncias da classe) e é usado para acessar os atributos daquele objeto. Qualquer nome de variável pode ser usado, embora seja uma convenção chamá-lo de self. Não é recomendado usar outro nome se queremos que nosso código seja de fácil leitura para outros programadores. Ele deve ser sempre o primeiro parâmetro de todas as funções e mesmo os métodos que não recebem outros parâmetros devem conter self.

Além de propriedades uma classe pode ter métodos que são funções executadas dentro da classe. A classe Calculadora que se segue contém apenas um método e nenhuma propriedade acessada pelo usuário. Ela recebe dois números e o tipo de operação desejada, + – * / e retorna uma string com a operação e seu resultado.

» class Calculadora:
»     def calcular(self, x, y, operacao = '+'):
»         if operacao == '+':   resultado = '%.1f + %.1f = %.1f' % (x, y, x + y)
»         elif operacao == '-': resultado = '%.1f - %.1f = %.1f' % (x, y, x - y)
»         elif operacao == '*': resultado = '%.1f x %.1f = %.1f' % (x, y, x * y)
»         elif operacao == '/':
»             if y == 0:  resultado = 'Erro: divisão por zero!'
»             else:       resultado = '%.1f / %.1f = %.1f' % (x, y, x / y)
»         else:
»             resultado = 'Nenhuma operação conhecida (+ - * /) foi requisitada!'
»         print(resultado)

# instancia um objeto
» calc = Calculadora()
# a operação de soma é default
» calc.calcular(123,123)
↳ 123.0 + 123.0 = 246.0

» calc.calcular(213,432, '-')
↳ 213.0 - 432.0 = -219.0

» calc.calcular(213,43, '*')
↳ 213.0 x 43.0 = 9159.0

» calc.calcular(213,0, '/')
↳ Erro: divisão por zero!

» calc.calcular(213,23, 'r')
↳ Nenhuma operação conhecida foi definida!

# calc é uma variável do tipo
» type(calc)
↳ __main__.Calculadora

Os Métodos __init__() e __str__()


Todas as classes possuem um método chamado __init __(), mesmo que não explicitamente definido, que é sempre executado durante sua inicialização. O nome __init__, iniciado e terminado por 2 sublinhados (em inglês chamado de dunder, double underscore), é chamado de construtor da classe.

Também existe o método __repr__, similar ao __str__, tratado no capítulo Métodos especiais.

Esse método é usado para fazer as atribuições de valor às propriedades do objeto e executar outras operações que sejam necessárias quando o objeto está sendo criado. É considerada uma boa prática de programação não definir propriedades fora da inicialização, exceto quando propriedades de classe são necessárias. Após a inicialização o método não deve ser usado novamente.

Ao ser instanciado um objeto os parâmetros de __init__ devem ser fornecidos ou um erro é lançado.

» class Aluno:
»     def __init__(self, nome, nota):
»         self.nome = nome
»         self.nota = nota        
    
# Inicializamos um objeto com seus valores iniciais
» aluno1 = Aluno('Joana Paraíba', 67)
» print(a1.nome, a1.nota)
↳ Joana Paraíba  67

# como antes, a propriedade pode ser alterada
» aluno1.nome = 'Joana Pessoa'
» aluno1.nome
↳ 'Joana Pessoa'

# um atributo pode ser removido do objeto
» del aluno1.nome
» print(aluno1.nome)
↳ AttributeError: type object 'a1' has no attribute 'nome'

Após a definição da classe dois objetos foram instanciados: aluno1 e aluno2 têm as mesmas propriedades que a classe Aluno. Essas propriedades podem ser modificadas com a notação de ponto, objeto.propriedade, e acessadas como variáveis comuns, como foi feito no cálculo da média das notas.

Como no caso da calculadora acima, classes contém propriedades e métodos, ou funcionalidades. Métodos do objetos são funções definidas dentro de um objeto. Na nossa classse Aluno, vamos inserir vários métodos: get_sobrenome(), que retorna a última string após partir o nome nos espaços, set_nota() que insere as notas do aluno, get_media() que calcula e retorna a média das 3 notas, e o método especial __str()__ que retorna uma representação de string contendo os dados que julgamos representativos do objeto.

» class Aluno:
»     def __init__(self, nome, nota1=0, nota2=0, nota3=0):
»         self.nome = nome
»         self.nota1 = nota1
»         self.nota2 = nota2
»         self.nota3 = nota3

»     def get_sobrenome(self):
»         return self.nome.split()[-1]

»     def set_nota(self, n, nota):
»         if n==1:
»             self.nota1=nota
»         if n==2:
»             self.nota2=nota
»         if n==3:
»             self.nota3=nota

»     def get_media(self):
»         return (self.nota1 + self.nota2 + self.nota3)/3
        
»     def aprovado(self):
»         return self.get_media() >= 50

»     def __str__(self):
»         txt = 'Nome: ' + self.nome
»         txt += '\nNotas: %2.1f, %2.1f, %2.1f' % (self.nota1, self.nota2, self.nota3) 
»         txt += '\nMédia: %2.1f' % self.get_media()
»         txt +=  '  ** %s **\n' % ('aprovado' if self.aprovado() else 'reprovado')
»         return txt

Acessar um método de instância, objeto.metodo() é o mesmo que chamar o método da classe passando o próprio objeto (a instância) como argumento self.

# instanciando um aluno e suas notas
» a1 = Aluno('Mário Leibniz', 34, 45, 76)
# usar o método de instância
» a1.get_media()
↳ 51.666666666666664

# é o mesmo que chamar o método
» Aluno.get_media(a1)
↳ 51.666666666666664

# 1 aluno inicializado com as notas default    
» pedro = Aluno('Pedro Álvarez')
# pedro tem a propriedade nome
» print(pedro.nome)
↳ Pedro Álvarez

# as notas de pedro podem ser alteradas
» pedro.set_nota(1,84)
» pedro.set_nota(2,89)
» pedro.set_nota(3,97)
# e sua média pode ser calculada
» pedro.get_media()
↳ 90.0

Uma classe pode ter em suas propriedades dados de qualquer tipo. No exemplo seguinte criamos a classe Escola contendo apenas nome e uma lista de alunos, inicialmente por uma lista vazia e depois preenchida com objetos da classe Aluno.

# usando 2 alunos inicializados
» a1 = Aluno('Mário Leibniz', 34, 45, 76)
» a2 = Aluno('Joana Frida', 10, 28, 16)    
    
# definimos a class Escola
» class Escola:
»     def __init__(self, nome):
»         self.nome = nome
»         self.alunos = []

»     def insere_aluno(self, aluno):
»         self.alunos.append(aluno)

»     def __str__(self):        
»         txt = 'Nome: ' + self.nome
»         txt += '\nTem %d alunos' % len(self.alunos)
»         txt += '\nAlunos:\n'
»         for a in self.alunos:
»             txt += '-' * 38 + '\n'
»             txt += str(a)
»         return txt    

# criamos uma instância da classe
» escola1 = Escola('Caminhos da Luz Albert Einstein')

# inserimos os dois alunos já definidos
» escola1.insere_aluno(a1)
» escola1.insere_aluno(a2)

# usamos o método Escola.__str__ para listar o estado de escola1
» print(escola1.__str__())
↳ Nome: Caminhos da Luz Albert Einstein
↳ Tem 2 alunos
↳ Alunos:
↳ --------------------------------------
↳ Nome: Mário Leibniz
↳ Notas: 34.0, 45.0, 76.0
↳ Média: 51.7   **aprovado**
↳ --------------------------------------
↳ Nome: Joana Frida
↳ Notas: 10.0, 28.0, 16.0
↳ Média: 18.0   ** reprovado**

Embora não obrigatório, é uma convenção útil usar o nome da classe com primeira letra maiúscula. Esse código define a classe Aluno que tem as propriedades nome, nota1, nota2 e nota3, e os métodos set_nota(), que atribue valor a uma das 3 notas; get_sobrenome(), que retorna o último nome registrado; get_media(), que retorna a média das 3 notas; aprovado() que retorna um booleano (False ou True) informando se a aluno foi aprovado. O conjunto das propriedades de um objeto consistem em seu estado. Métodos são as funções que o objeto pode executar, em geral associadas ao seu estado.

Um método retorna um valor de tipo especificado ou None se é terminado por return vazio ou mesmo sem nenhum return.

Nas classes Aluno e Escola usamos também o método especial __str__(self) que retorna uma representação de string do objeto. Ele definido internamente de modo a ser acionado quando fazemos uma chamada a print(objeto). São equivalentes as instruções:
print(objeto.__str__()), print(str(objeto)), print(objeto).

» print(pedro)        # ou print(str(pedro)) ou  print(pedro.__str__())
↳ Nome: Pedro Álvarez
↳ Notas: 84.0, 89.0, 97.0
↳ Média: 90.0

# idem
» print(escola1)
↳ Nome: Caminhos da Luz Albert Einstein
↳ Tem 2 alunos
↳ Alunos:
↳ --------------------------------------
↳ Nome: Mário Leibniz
↳ Notas: 34.0, 45.0, 76.0
↳ Média: 51.7   **aprovado**
↳ --------------------------------------
↳ Nome: Joana Frida
↳ Notas: 10.0, 28.0, 16.0
↳ Média: 18.0   ** reprovado**

Vemos, dessa forma, que uma classe é um modelo geral que pode ser usado para a criação de várias casos particulares dos objetos que ela modela. Uma instância da classe, como pedro, tem todas as propriedades e métodos da classe.

Encapsulamento

Uma discussão mais completa de escopo e namespaces pode ser lida na seção Variavéis de Classe e de Instância e no artigo seguinte, neste site, Escopos e namespaces.

Encapsulamento é uma forma de trabalhar com uma variável dentro de um bloco, como um objeto ou função, sem tornar aquela variável disponível para o restante do código. Isso pode ser necessário por motivos de segurança ou para realizar uma verificação antes que uma alteração seja feita a essa variável. Por ex., se um campo de CPF vai ser modificado é sempre bom testar se um número válido está sendo inserido.

Se uma propriedade é definida no método de inicialização com dois sublinhados como __variavel ela não pode ser acessada fora do objeto diretamente por objeto.__variavel. Nesse caso métodos devem ser definidos para o seu acesso. Métodos privados, construídos com a mesma técnica, também só podem ser acessados de dentro da classe.


A classe seguinte representa uma porta que pode estar fechada e trancada. Métodos são definidos para abrir, fechar, trancar, destrancar a porta. Todos eles verificam o parâmetro fornecido em relação ao estado da porta para decidir qual é procedimento correto. Por ex., uma porta trancada não pode ser aberta. (Claro que uma classe mais enxuta poderia ser escrita mas seria, provavelmente, um pouco mais díficil de ler. Da mesmo forma o método __geraTexto() está aí apenas para demonstrar o funcionamento da função privada, pois poderia ser incorporado ao __str__().)

» class Porta:
»     def __init__(self, fechada, trancada):
»         self.__fechada = trancada or fechada
»         self.__trancada = trancada
 
»     def abre(self):
»         if self.__trancada:
»             print('A porta está trancada e não pode ser aberta!')
»         else:
»             print('A porta foi aberta!' if self.__fechada else 'A porta já está aberta!')
»             self.__fechada = False
 
»     def fecha(self):
»         print('A porta já está fechada!' if self.__fechada else 'A porta foi fechada!')
»         self.__fechada = True
 
»     def tranca(self):
»         if not self.__fechada:
»             print('A porta está aberta, não pode ser trancada!')
»         else:
»             print('A porta já está trancada!' if self.__trancada else 'A porta foi trancada!')
»             self.__trancada = True        
 
»     def destranca(self):
»         print('A porta foi destrancada!' if self.__trancada else 'A porta já está destrancada!')
»         self.__trancada = False
 
»     def __geraTexto(self):
»         txt = 'A porta está ' + ('fechada' if self.__fechada else 'aberta')
»         txt += (' e trancada' if self.__trancada else ' mas destrancada') if self.__fechada else ''
»         return txt
     
»     def __str__(self):
»         return self.__geraTexto()


Na inicialização a linha self.__fechada = trancada or fechada impede que o estado da porta seja definida como aberta e trancada simultaneamente. Lembrando, foi usada a forma de if else ternário: variavel = valor1 if condicao else valor2, que significa que variavel assume o valor1 se condicao for verdadeira, caso contrário, o valor2.

Observe também que nos métodos, como em destranca(), a mensagem foi gerada antes da troca de valor do estado, pois depende do valor antigo e não no novo.

Agora podemos inicializar um objeto porta e interagir com seus métodos (mas não com seus atributos diretamente). Os atributos privados não podem ser acessados diretamente, nem o método __geraTexto() que só pode ser chamado de dentro da classe, no caso de __str__.

# um atributo privado não pode ser acessado diretamente, nem o método __geraTexto()
» print(porta.__fechada)
↳ AttributeError: 'Porta' object has no attribute '__fechada'

» porta = Porta(True, True)
» print(porta)
↳ A porta está fechada e trancada

» porta.abre()
↳ A porta está trancada e não pode ser aberta!

» porta.destranca()
↳ A porta foi destrancada!

» porta.abre()
↳ A porta foi aberta!

» print(porta)
↳ A porta está aberta

» porta.tranca()
↳ A porta está aberta, não pode ser trancada!

» print(porta)
↳ A porta está aberta

Com encapsulamento conseguimos fazer com que o efeito de um método ou a atribuição de valores às propriedades seja dependente do estado do objeto. É o caso do método porta.abre() que impede que a porta seja aberta se estiver trancada.

A interface de uma classe ou de um módulo são as partes expostas ao usuário do objeto, sejam dados ou métodos.

O encapsulamento favorece a criação de classes que expõe uma interface simples com o código externo (e portanto para o programador), sem a necessidade de exibir possíveis complicações internas. Isso torna esse código mais fácil de reutilizar, em acordo com o princípio DRY do Python (don’t repeat yourself). Ele contribui para a legibilidade do código, pois expõe com clareza o que deve ser fornecido e o que pode ser extraído na classe, impedindo o acesso acidental à informações que não se pretendia tornar acessíveis.

Herdando propriedades e métodos

Além de permitir a criação de objetos como instâncias dessa classe, também é possível construir outras classes herdando e modificando suas propriedades e métodos. Isso é útil quando se quer escrever uma classe que é uma especialização de outra já definida. Quando uma classe herda de outra ela contém todos os métodos e propriedades da primeira classe e pode ter seus atributos alterados e ampliados.

A sintaxe para criar classes derivadas de outra já existente é a seguinte:

# ex. 1: herdando de classe base
» class NomeDaClasseFilha(NomeDaClasseBase):
»     <propriedades e métodos modificados>

# ex. 2: Classe base em outro módulo        
» class NomeDaClasseFilha(Modulo.NomeDaClasseBase):
»     <propriedades e métodos modificados>

Todas as classes herdam de alguma outra. Se NomeDaClasseBase não é especificado a classe herda de object. A classe básica deve estar no mesmo escopo que sua derivada ou, caso contrário, seu módulo deve ser citado, como no exemplo 2.

Na modelagem das pessoas associadas à uma escola encontramos funcionários, professores e alunos. Todos eles partilham características que reuniremos na classe Pessoa.

» class Pessoa(object):
»     def __init__(self, nome, cpf):
»         self.nome=nome
»         self.cpf=cpf
         
»     def __str__(self):
»         return 'Nome: %s; CPF: %s' %(self.nome, self.cpf)
 
» p1 = Pessoa('Ricardo Dalquins','123-456-789-00')
» print(p1)
↳ Nome: Ricardo Dalquins; CPF: 123-456-789-00

As declarações class Pessoa(object):, class Pessoa(): e class Pessoa: são equivalentes.

Os alunos, além de serem pessoas, possuem características especificas como número de matrícula, notas, situação de regularidade quanto às mensalidades, etc. Para demonstrar a criação de uma subclasse usaremos apenas a matrícula.

# definindo uma subclasse    
» class Aluno(Pessoa):
»     def __init__(self, nome, cpf, matricula):
»         super().__init__(nome, cpf)
»         self.matricula = matricula
 
»     def __str__(self):
»         txt =  'Nome: %s \nCPF: %s' %(self.nome, self.cpf)
»         txt += '\nMatricula ' + self.matricula
»         return txt

# inicializando instância da subclasse Aluno
» p2 = Aluno('Uiliam Cheiquispir', '321-654-987-55', '321.654')
 
» print(p2)
↳ Nome: Uiliam Cheiquispir 
↳ CPF: 321-654-987-55
↳ Matricula 321.654

super() é uma função especial que faz a conexão entre a classe mãe e a filha. Ela representa a classe mãe, de onde a classe atual é derivada. Desta forma super().__init__() chama a inicialização de Pessoa e preenche os atribulos nela existentes. O nome super vem da convenção muito usada de chamar a classe mãe de superclasse e a filha de subclasse. A subclasse, por sua vez, pode servir de base para a geração de outra subclasse, abaixo dela. Encontramos também a nomeclatura classe base e classe derivada.

Vamos lembrar que print(p2) aciona o método __str()__ que existe na super e na subclasse. Como ele foi definido na subclasse Aluno este método é executado e o método na superclasse é ignorado. Esse processo, de subreescrever o método da superclasse se chama overriding.

Na prática, quando tentamos modelar um objeto de uso do mundo real, é muito possível que o número de atributos se torne muito grande gerando classes longas e de difícil manuseio. Nesses casos pode ser interessante a criação de classes auxiliares que são usadas como variáveis dentro da classe principal. No caso dos alunos o tratamento de suas notas pode ser gerenciado à parte.

# define uma classe para as notas de um aluno    
» class Nota:
»     def __init__(self, matricula):
»         self.notas = []
»         self.matricula = matricula
»         
»     def insereNota(self,nota):
»         self.notas.append(nota)
»         
»     def getMedia(self):
»         return sum(self.notas)/len(self.notas)
» 
# instancia objeto
» n2 = Nota('321.654')
# insere 3 notas
» n2.insereNota(89)
» n2.insereNota(78)
» n2.insereNota(90)

# acessa propriedade de n2
» n2.getMedia()
↳ 85.66666666666667

É claro que na caso real deve haver um tratamento para evitar erros e conflitos. Se o método Nota.getMedia() for chamado antes da inserção de uma nota um erro será lançado. Isso pode ser contornado capturando essa exceção ou com uma modificação do código.

# modificando o método getMedia()
»     def getMedia(self):
»         if not self.notas:
»             return 0
»         else:
»             return sum(self.notas)/len(self.notas) 

if not self.notas retorna True se a lista estiver vazia. É o mesmo que if len(self.notas)==0 ou simplesmente if len(self.notas) uma vez que if 0 retorna False.

Modificamos agora a classe Aluno para conter uma propriedade Aluno.notas que armazena um objeto Nota que, por sua vez, contém a matricula do alunos e uma lista com suas notas.

# Altera a classe aluno para usar a classe Nota
» class Aluno(Pessoa):
»     def __init__(self, nome, cpf, matricula):
»         super().__init__(nome, cpf)
»         self.matricula = matricula
»         self.notas = Nota(matricula)

»     def __str__(self):
»         txt =  'Nome: %s \nCPF: %s' %(self.nome, self.cpf)
»         txt += '\nMatricula ' + self.matricula
»         txt += '\nMédia de Notas: %.1f' % self.notas.getMedia()
»         return txt

# cria um aluno específico
» bob = Aluno('Roberto Gepeto', '741-852-963-55', '654.987')
# insere suas notas
» bob.notas.insereNota(86)
» bob.notas.insereNota(88)
» bob.notas.insereNota(87)

# acessa bob.__str__() 
» print(bob)
↳ Nome: Roberto Gepeto 
↳ CPF: 741-852-963-55
↳ Matricula 654.987
↳ Média de Notas: 87.0

Na linha bob.notas.insereNota(86) estamos acessando o objeto bob.notas que é uma instância de Nota e que, portanto, possui o método insereNota().

Polimorfismo e Overloading

A palavra polimorfismo significa literalmente “muitas formas”. No Python polimorfismo significa que uma função pode receber parâmetros de tipos diferentes e ainda assim agir como esperado com esses tipos. A função len(), por exemplo, pode receber parâmetros diversos desde que sejam sequências (string, bytes, tupla, lista or range) ou a coleções (dicionário, conjunto, …). Idem, o operador + pode agir sobre objetos de tipos diferentes, gerando resultados diferentes.

# string
» len('Entrou mudo e saiu calado.')
↳ 26
# lista
» len([12,23,34,45,56,67])
↳ 6
# dicionário
» len({1:'1', 2:'2'})
↳ 2

# operador "+"
» 345 + 543
↳ 888
» 'ama' + 'ciante'
↳ 'amaciante'

Classes diferentes podem ter métodos com o mesmo nome. Ao ser chamado em um objeto o método específico de sua classe é executado.

» class Cavalo:
»     def som(self):
»         print('relincha')
 
» class Galinha:
»     def som(self):
»         print('cacareja')
 
» giselda = Galinha()
» corisco = Cavalo()

» print(giselda.som())
↳ cacareja

» print(corisco.som())
↳ relincha       

Overloading de operadores

Operadores pré-definidos em tipos criados pelo usuário, tal como o operador de soma + podem ser sobrescritos, overloaded (ou sobrecarregados).

No exemplo seguinte, definimos uma classe para um “vetor” com dois componentes, (x, y) e a soma de dois vetores, compatível com a soma matemática de vetores, que consiste em somar os componentes x, y, separadamente de cada vetor, e retornar outro vetor (que é instanciado dentro do método __add__().

Para definir essa soma sobreescrevemos o método __add__() que é internamente usado quando se opera com +. Observe que na definição de __add__() nos referimos ao segundo operando da soma como other (outro). Se esta definição não for feita explicitamente a soma entre dois vetores não estará definida.

Também inserimos o overloading do método __eq__ que define como se testa dois objetos para a igualdade, de forma que dois vetores sejam considerados iguais se seus dois componentes são iguais.

# nova definição de vetor
» class Vetor:
»     def __init__(self, x, y):
»         self.x = x
»         self.y = y
     
»     def __add__(self, other):
»         return Vetor(self.x + other.x, self.y + other.y)
 
»     def __eq__(self, other):
»         return self.x == other.x and self.y == other.y
 
»     def __str__(self):
»         return 'Objeto da Classe Vetor: (%d, %d)' % (self.x, self.y)
 
# instanciamos 2 vetores
» v1 = Vetor(34,74)
» v2 = Vetor(16, 26)

# somamos e exibimos a soma
» v3 = v1 + v2
» print(v3)
↳ Objeto da Classe Vetor: (50, 100)

# v3 é uma instância de Vetor
» isinstance(v3, Vetor)
↳ True

# para verificar o teste de igualdade
» v3 = Vetor(3,7)
» v4 = Vetor(3,7)
» print(v3 == v4)
↳ True

A classe Vetor, definida dessa forma, mostra que um objeto pode retornar outro objeto de seu próprio tipo. De fato funções e métodos podem receber e retornar parâmetros que são objetos de qualquer tipo, inclusive os tipos de usuário. A função interna isinstance(objeto, classe) retorna True se objeto é membro da classe, caso contrário retorna False.

Um outro exemplo mostra o overloading de len().

# controle de compras online
» class Compra:
»     def __init__(self, cesta, usuario):
»         self.cesta = cesta
»         self.usuario = usuario
        
»     def __len__(self):
»         return len(self.cesta)

» compra = Compra(['sapato', 'camisa', 'gravata'], 'Pedro Paulo Pizo')
» print('%s possui %d itens em sua cesta' % (compra.usuario, len(compra)))
↳ Pedro Paulo Pizo possui 3 itens em sua cesta

Observe que o “comprimento” de um objeto de Compra não estaria definido se não inseríssemos a definição de __len__() na classe.

Para fazer overloading em uma classe (ou função) definida pelo usuário é necessário testar que tipo de argumento foi passado para a função. No caso de class Ola o método trata diferentemente argumentos passados como None (o que ocorre se o parâmetro for omitido) ou passados como string.

# overloading na classe
» class Ola:
»     def digaOla(self, nome=None):
»         if nome is None:
»             print('Insira o seu nome ')
»         else:
»             print('Olá ' + nome)

» o1 = Ola()
» o1.digaOla()
↳ Insira o seu nome 

» o1.digaOla('Olavo')
↳ Olá Olavo

isinstance() foi usada na classe MontaTexto que espera receber uma string com itens separados por ; ou uma lista. Ela trata cada uma delas de modo diferente. Naturalmente este requisito deveria estar documentado junto à definição da classe.

» class MontaTexto:
»     def montaTexto(self, obj):
»         conta = 0
»         txt = ''
»         if isinstance(obj, str):
»             partes = obj.split(';')
»         elif isinstance(obj, list):
»             partes = obj
»         for item in partes:
»             conta +=1
»             txt += 'Item %d: %s\n' % (conta, item)
»         print(txt)            
    
» txt = ['abóbora', 'brócolis', 'couve', 'alface']
» texto = MontaTexto()
» texto.montaTexto(txt)
↳ Item 1: abóbora
↳ Item 2: brócolis
↳ Item 3: couve
↳ Item 4: alface

» txt = 'lâmpada;fio;alicate;parafuso'
» texto.montaTexto(txt)
↳ Item 1: lâmpada
↳ Item 2: fio
↳ Item 3: alicate
↳ Item 4: parafuso

Polimorfismo

Figura 1

Em termos de classes, polimorfismo signica que podemos, em uma subclasse, usar o mesmo nome de método que já existe na superclasse, alterando seu comportamento. Uma subclasse é sempre um caso particular da superclasse e, portanto, possui características comuns com ela (ou não seria boa ideia herdar da classe mãe). No entanto ela pode modificar as faixas válidas de valores das propriedades e o resultado da atuação de métodos, além de inserir novos atributos. Essa tipo de herança com customização favorece a reutilização de código pronto, alterando classes prontas para especializá-las ao caso desejado.

Considerando as classes herdadas de superclasses, polimorfismo signica que ao chamar um atributo em um objeto da classe 3 (na figura 1) esse atributo será procurado primeiro em (3), depois em (2) e finalmente em (1), e ler ou executar o que encontrar primeiro.

Uma classe pode herdar de mais de uma classe, carregando os atributos de ambas.

# define classes C1 e C2
» class C1:
»     c0='C1.c0'
»     c1='C1.c1'
    
» class C2:
»     c0='C2.c0'
»     c2='C2.c2'
»     c3='C2.c3'
   
# define classe C3 que herda de C1 e C2
» class C3(C1, C2):    
»     c3='C3.c3'
 
# instancia objeto de C3 e imprime suas propriedades
» obj3 = C3()
» print('%s - %s - %s - %s' % (obj3.c0, obj3.c1, obj3.c2, obj3.c3))
↳ C1.c0 - C1.c1 - C2.c2 - C3.c3

Como se vê no resultado impresso, a variável c0, que não existe em C3 foi lida em C1 pois essa classe está inserida antes (à esquerda) de C2.

No código abaixo queremos modelar as pessoas que trabalham em uma empresa. A classe mais geral, Pessoa, possui atributos que todas as pessoas na empresa (ou fora dela) possuem. O único método de nosso exemplo é __str__().

Dessa classe se deriva Funcionario que tem a propriedade extra, salario, e um método, dar_aumento(porcento). Funcionario.__str__() busca a representação de string de super() e acrescenta a informação do salário.

A última classe é um caso especial de funcionário que são os gerentes. A classe Gerente, além de receber os aumentos regulares de todos os funcionários recebe um bônus. O método dar_aumento(porcento, bonus) sobreescreve o método de mesmo nome na superclasse para adicionar um bônus. Para isso ele primeiro dá o aumento regular de todos os funcionários, super().dar_aumento(porcento) (que altera o self.salario) para depois somar a ele o bônus.

# define uma classe geral
» class Pessoa:
»     def __init__(self, nome, cpf):
»         self.nome = nome
»         self.cpf = cpf
»     def __str__(self):
»         return 'Nome: %s\nCPF: %s' % (self.nome, self.cpf)

» p1 = Pessoa('Maria Quiri', '555-444-888-99')

» print(p1)
↳ Nome: Maria Quiri
↳ CPF: 555-444-888-99
 
# define classe especializada de Pessoa
» class Funcionario(Pessoa):
»     def __init__(self, nome, cpf, salario):
»         super().__init__(nome, cpf)
»         self.salario = salario
»     def dar_aumento(self, porcento):
»         self.salario *= (1 + porcento/100)

»     def __str__(self):
»         return '%s \nSalário: %.2f '  % (super().__str__(), self.salario)

» f1 = Funcionario('Joana Darcos', '111-222-333-44', 1000)

» print(f1)
↳ Nome: Joana Darcos
↳ CPF: 111-222-333-44 
↳ Salário: 1000.00

» f1.dar_aumento(10)
» print(f1)
↳ Nome: Joana Darcos
↳ CPF: 111-222-333-44 
↳ Salário: 1100.00 

# define um tipo especial de Funcionario
» class Gerente(Funcionario):
»     def __init__(self, nome, cpf, salario):
»         super().__init__(nome, cpf, salario)
     
»     def dar_aumento(self, porcento, bonus):
»         super().dar_aumento(porcento)
»         self.salario += bonus

» g1 = Gerente('Isaque Nilton','999-888-777-22',2000)

» print(g1)
↳ Nome: Isaque Nilton
↳ CPF: 999-888-777-22 
↳ Salário: 2000.00 

» g1.dar_aumento(10, 800)
» print(g1)
↳ Nome: Isaque Nilton
↳ CPF: 999-888-777-22 
↳ Salário: 3000.00 

Como vemos, classes podem herdar atributos de uma superclasse, alterá-los para seu funcionamento específico ou inserir novos atributos.

Frequentemente esses dados, no caso de uma empresa real, estão gravados em bancos de dados que são lidos e armazenados nas propriedades dessas classes. Também existem formas de gravar o estado de um objeto em disco para reutilização posterior, como veremos.

Importando Classes

Uma vez que as classes estão bem definidas e funcionando adequadamente, temos a opção de armazená-las em módulos e utilizá-las como fazemos com qualquer outra classe importada do Python. Podemos gravar um arquivo com o nome escola.py que contém as definições das classes Pessoa, Aluno e Nota já definidas.

# arquivo escola.py    
» class Pessoa:
»     def __init__(self, nome, cpf):
»         self.nome=nome
»         self.cpf=cpf
        
»     def __str__(self):
»         return 'Nome: %s; CPF: %s' %(self.nome, self.cpf)

# classe aluno
» class Aluno(Pessoa):
»     def __init__(self, nome, cpf, matricula):
»         super().__init__(nome, cpf)
»         self.matricula = matricula
»         self.notas = Nota(matricula)
        
»     def __str__(self):
»         txt =  'Nome: %s \nCPF: %s' %(self.nome, self.cpf)
»         txt += '\nMatricula ' + self.matricula
»         txt += '\nMédia de Notas: %.1f' % self.notas.getMedia()
»         return txt

# classe nota
» class Nota:
»     def __init__(self, matricula):
»         self.notas = []
»         self.matricula = matricula
        
»     def insereNota(self,nota):
»         self.notas.append(nota)

»     def getMedia(self):    
»         if not self.notas:
»             return 0
»         else:
»             return sum(self.notas)/len(self.notas)         

Para usar essas classes temos que importar o módulo e as classes necessárias. Para a importação temos que fornecer o caminho completo de onde está o módulo, caso ele não esteja na pasta em uso ou no PATH do Python.

# importa módulo e classes
from escola import Aluno
# inicializa aluno
» novoAluno = Aluno('Homero Poeta', '234-456-656-56', '346.345')
#insere suas notas
» novoAluno.notas.insereNota(34)
» novoAluno.notas.insereNota(45)
» novoAluno.notas.insereNota(41)

» print(novoAluno)
↳ Nome: Homero Poeta 
↳ CPF: 234-456-656-56
↳ Matricula 346.345
↳ Média de Notas: 40.0

A importação de Aluno tornou disponível Pessoa e Nota que são por ela utilizados, uma vez que estão todas no mesmo escopo, de modo que não é necessário importar as 3 classes.

Você pode importar todos as classes de um módulo simultaneamente de duas formas.

» from escola import *
» novoAluno = escola.Aluno('Homero Poeta', '234-456-656-56', '346.345')

O primeiro método apenas estabelece um caminho de busca para as classes usadas, e não onera o código em termos de memória, durante sua execução. No entanto ele não é recomendado pois é útil poder ver nas primeiras linhas do código as classes que serão usadas. Também, ele pode gerar confusão com os nomes do código e os do módulo caso você importe outro módulo com nomes coincidentes. Isso pode gerar erros difíceis de serem encontrados e corrigidos.

A segunda abordagem é mais interessante: o módulo é importado sem menção às classes ou funções usadas. Em seguida classes e funções são acessadas como componentes desse módulo.

# importamos o modulo que contém classe nomeDaClasse e função fc()
» import modulo

# classes e funções são chamadas
» objeto = modulo.nomeDaClasse
» h = modulo.fc(parametro)


Desta forma os nomes de classes não estarão listados no topo do código mas aparecem claramente no corpo do programa, onde as classes e funções são usadas.

Se o número de classes for muito grande elas podem ser gravadas em arquivos diferentes e importadas dentro do módulo que necessita de outro módulo. Suponha que gravamos Aluno e Pessoa nos arquivos aluno.py e pessoa.py. Como a classe Aluno necessita de Pessoa para sua definição ela deve conter em seu cabeçalho a linha import pessoa from Pessoa.

Gravar os módulos e suas classes em arquivos separados é uma maneira eficaz de manter um código limpo e mais fácil de gerenciar. Cada classe carrega a lógica relativa aquele objeto e o programa central fica encarregado de juntar as partes e executar a lógica principal.


No Jupyter Notebook podemos reinicializar o kernel limpando todas as definições prévias. Para isso selecione menu | Kernel | Restart ou pressione 0, 0 na célula, modo de controle.Além disso podemos escrever o nome na variável seguido de um ponto, e apertar tab. Isso faz com com métodos e propriedades sejam exibidos, como mostrado na figura.

Variavéis de Classe e de Instância

Getters e Setters

Métodos que visam ler o estado de alguma propriedade do objeto, sem realizar alterações, são denominados getters (assessors ou leitores). Já os que alteram propriedades são setters, (mutators ou definidores).

Diferente de outras linguagens POO, variáveis ou métodos no Python não podem ser declaradas como públicas ou privadas, que são aqueles que só podem ser acessados de dentro da classe. Depois de definido em objeto instância de Aluno (da classe já definida) podemos acessar suas propriedades e métodos livremente no objeto. Podemos definir ana.nome='Ana Marta' e recuperar esse nome através de print(ana.nome).

Apesar disso há uma convenção seguida pela maioria dos programadores: um nome (de variável ou método) prefixado com um sublinhado (como _variavel) deve ser tratado como parte não pública daquele bloco de código que pode ser uma função, um método ou uma classe. Além disso um duplo sublinhado (como __variavel) tem o efeito de tornar privada aquela variável. Sublinhados possuem signficados especiais no Python, que já exploraremos.

Como vimos, existe no Python o chamado ocultamento de nome (name muting) que permite a definição de nomes válidos para membros de uma classe privada, o que é útil para evitar conflitos de nomes com outros nomes definidos em subclasses (classes que se baseiam nesta para sua construção). Um identificador (nome de variável ou função) com dois sublinhados iniciais (pode ter um sublinhado final), como por ex. __nome é substituído por _nomeDaClasse.__nome, onde nomeDaClasse é o nome da classe onde está o objeto, com um sublinhado removido. Esse ocultamento é útil para permitir que as subclasses sobrescrevam (override) os métodos sem impedir que os métodos da classe mãe continuem funcionando.

Vejamos o exemplo seguinte para entender como esse duplo sublinhado funciona:

» class Publico:
»     __privadaDeClasse = 66
»     def __init__(self):
»         self.__privadaDeInstancia = 17;

»     def __metodoPrivado(self):
»         print('Dentro do método privado')
»     def metodoPublico(self):
»         print('Essa classe tem uma variável privada com valor: ', self.__privadaDeInstancia)

»     def getPrivada(self):
»         print(self.__privadaDeClasse)

»     def setPrivada(self, k):
»         self.__privadaDeClasse = k

# instancia um objeto da classe
» caso = Publico()

# usando um método público
» caso.metodoPublico()
↳ Essa classe tem uma variável privada com valor:  17

# tentativa de usar um método privado
» caso.metodoPrivado()
↳ AttributeError: 'Publico' object has no attribute 'metodoPrivado'

# tentativa de ler diretamente uma variável privada
» caso.__privadaDeInstancia
↳ AttributeError: 'Publico' object has no attribute '__privadaDeInstancia'

# a variável privada só pode ser acessada de dentro do objeto
» caso.getPrivada()
↳ 66

# para alterar a variável privada usamos método público
» caso.setPrivada(9)
» caso.getPrivada()
↳ 9

A propriedade __privadaDeClasse, além se ser privada, é chamada de variável de classe. Embora ela possa ser alterada após a criação todos os objetos instanciados terão esse valor inicial. Da mesma forma métodos privados só podem ser chamados de dentro do objeto. Dessa forma se impede que partes do código não sejam expostas ao módulo mais geral. Essas partes não aparecem nos chamados à help(), nem nas caixas dropdown do Jupyter Notebook. Ao atribuir um valor a uma variável se pode, por ex., fazer testes de verificação se um tipo correto foi enviado e se o valor está dentro da faixa esperada, enviando mensagens apropriadas de erro, quando for o caso.

A classe a seguir define uma variável quantos que pertence à definição da classe. Ela pode ser alterada e todo objeto instanciado a partir dela terá essa mesma propriedade. Já a variável id pertence à cada objeto individual e não é compartilhada com outros membros da classe.

Essa classe está definida da seguinte forma: a cada nova inicialização, que corresponde à inserção de um novo funcionário, a variável quantos é incrementada de 1, e a variável id copia esse valor. Na criação de novo Funcionario o atributo de classe quantos é alterado para todos os funcionários, enquanto seu id permanece o mesmo.

Observe que, dentro de __init__, a referência foi feita à Funcionario.quantos e self.id respectivamente. Lembrando, self é uma forma de referenciar o objeto instância da classe.

# declare uma classe
» class Funcionario:
»     # atributo de classe
»     quantos = 0
    
»     def __init__(self):
»         Funcionario.quantos += 1
»         self.id = Funcionario.quantos
    
»     def __str__(self):
»         return 'Funcionário %d de %d' % (self.id, Funcionario.quantos) 

A variável quantos é de classe, enquanto id é uma variável de instância.

O método __self__() retorna uma string com o id do funcionário e o número de cadastrados.

# cria um Funcionario
» f1 = Funcionario()
# use print acionar o método __str__ e ver atributos de instância e de classe
» print(f1) 
↳ Funcionário  1 de 1

# novos funcionários
» f2 = Funcionario()
» f3 = Funcionario()

# agora existem 3 funcionários
» print(f2) 
↳ Funcionário  2 de 3

» print(f3) 
↳ Funcionário  3 de 3

# o estado de f1 foi alterado
» print(f1)
↳ Funcionário  1 de 3

# o estado da classe também foi alterado
» Funcionario.quantos
↳ 3

Esse exemplo também ilustra o fato de que uma variável de classe pode ser acessada e modificada de dentro de cada instância. A modificação da classe geradora modifica suas instâncias, o que mostra que a execução de cada instância acessa o codigo da classe.

🔺Início do artigo


Classes, variáveis do Usuário

Bibliografia

Consulte a bibliografia no final do primeiro artigo dessa série.