Pandas dataframes: atributos e métodos

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, mantando 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

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.

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
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 permite a interação com o sistema operacional, executando várias tarefas. Ele contém funções para criar ou remover uma pasta, fazer pesquisa em seu conteúdo, alterar e identificar a pasta atual, etc.

» # importar o módulo os
» import os

» # mudar a pasta atual para /home/usr/Temp
» os.chdir('/home/usr/Temp')
» os.getcwd()
↳ '/home/usr/Temp'

» # lembrando: no Windows o caminho dever ser compatível
» # os.chdir("C:\\Projetos")

» # retornar para a pasta acima
» os.chdir('..')
» os.getcwd()
↳ '/home/usr'

» # retornando para a pasta temp
» pasta = '/home/usr/Temp'
» os.chdir(pasta)

» # criar uma pasta em temp
» os.mkdir('temp2')

» # alternativamente, de qualquer pasta
» novaPasta = '/home/usr/Temp/temp3'
» os.mkdir(novaPasta)

» # para remover a última pasta
» os.rmdir('temp3')

» # listdir retorna uma lista com os arquivos (e pastas) na pasta atual
» lista = os.listdir('/home/guilherme/Public/Programação/Python')
» for t in lista:
»     print(t)
↳ temp.html
↳ python-01.html
↳ python-02.html
↳ python-03.html    
↳ ...
# lista truncada

O módulo os possui algumas propriedades úteis, entre elas os.environ, que contém informação sobre o sistema operacional, e os.environ que armazena um dicionário com informações sobre o ambiente (environment).

» os.system
↳ <function posix.system(command)>
        
» os.environ
↳ environ{'QT_SCALE_FACTOR': '1',
        'LANGUAGE': 'en_US',
        'SHELL': '/bin/bash',
        'LC_NUMERIC': 'pt_BR.UTF-8',
        'LC_PAPER': 'pt_BR.UTF-8',
        'MATE_DESKTOP_SESSION_ID': 'this-is-deprecated',
        'JPY_PARENT_PID': '9099',
        'TERM': 'xterm-color',
        ... [truncado]
        'MPLBACKEND': 'module://ipykernel.pylab.backend_inline'}

» # essa propriedade é um tipo dicionário. por ex.:
» os.environ['USER']
↳ guilherme

Os seguintes testes foram realizados no prompt do python, em sessão do Anaconda.

>>> import os
>>> os.times()
↳ posix.times_result(user=0.03, system=0.0, children_user=0.0, children_system=0.0, elapsed=17180795.37)
>>> os.system('date')
↳ sáb 05 jun 2021 14:19:27 -03

# supondo a existência de um programa (ou atalho) de nome caja
# o aplicativo caja é executado (no caso um gerenciador de arquivos)
>>> os.system('caja')

O método os.walk(top, topdown=True, onerror=None, followlinks=False) retorna tuplas com pasta atual, subpastas e arquivos nelas contidos. Por default ele percorre as pastas iniciando em top e descendo para as substastas nele localizadas.

» import os
» path = '/home/guilherme/Temp'
» for (root, dirs, files) in os.walk(path):
»     print('Pasta atual: %s' % root)
»     print('Com as subpastas:\n', dirs)
»     print('Arquivos presentes:\n', files)
»     print('\n--------------------------------\n')

# saida (truncada)
↳ Pasta atual: /home/guilherme/Temp
↳ Com as subpastas:
↳  ['RevelationSpace', 'pandas']
↳ Arquivos presentes:
↳  ['Livro1', 'modal.html', 'FileZilla.xml']
↳ 
↳ Pasta atual: /home/guilherme/Temp/RevelationSpace
↳ Com as subpastas:
↳  []
↳ Arquivos presentes:
↳  ['01-RevealSpace.mp3', '02-RevealSpace.mp3', '03-RevealSpace.mp3', '04-RevealSpace.mp3', '05-RevealSpace.mp3']
↳ 
↳ Pasta atual: /home/guilherme/Temp/pandas
↳ Com as subpastas:
↳  ['Lambda', 'Pandas']
↳ Arquivos presentes:
↳  ['Lambda.html', 'Pandas.html']

Segue uma lista de alguns dos atributos de os:

Método significado
os.system() executa um comando shell,
os.environ() obtenha o ambiente dos usuários,
os.getcwd() retorna pasta de trabalho atual,
os.getgid() retorna id de grupo real do processo atual,
os.getuid() retorna ID do usuário do processo atual,
os.getpid() retorna ID de processo real do processo atual,
os.umask(mascara) define o umask numérico atual e retorne o umask anterior,
os.uname() retorna informações que identificam o sistema operacional atual,
os.chroot(caminho) altera pasta raiz do processo atual para caminho,
os.listdir(caminho) retorna lista de arquivos e pastas no caminho,
os.mkdir(caminho) cria pasta caminho,
os.makedirs(caminho) criação recursiva de pastas,
os.remove(caminho) remove arquivo em caminho,
os.removedirs(caminho) remove pastas recursivamente,
os.rename(src, dst) renomeia arquivo ou pasta src para dst,
os.rmdir(caminho) remove pasta em caminho,
os.system(comando) executa comando na shell do sistema operacional,
os.walk(caminho) retorna tuplas com pasta, subpastas e arquivos em cada uma.

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
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).

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.
%Z nome do fuso horário. ,
%j dia do ano, número preenchido com zero. 273,
%-j dia do ano, número. (p) 273,
%U número da semana no ano (domingo é dia 1), número preenchido com zero. 39,
%W número da semana do ano (segunda-feira é dia 1), número. 39,
%c representação completa de data e hora (l). Seg, 30 de setembro 07:06:05 2013,
%x representação completa de data apenas (l). 30/09/13,
%X representação completa de tempo (l). 07:06:05,
%% o caracter literal ‘%‘.

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

Leia sobre 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!
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.

Expressões Regulares (regex)


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.

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.
Exemplos de uso são:

  • filtragem de linhas de um texto longo com determinados caracteres no início, meio ou fim da linha;
  • remoção de tags de um texto escrito em html, com possível seleção dentro de uma tag determinada;
  • validação de texto para representar datas e horas;
  • validação de texto digitado pelo usuário para representar CPF, email, URL, CEP, cartão de crédito, etc;
  • extração de dados de um arquivo csv, json, XML, markdown ou outro formato estruturado qualquer.

Usaremos, por concisão, a expressão sensível (insensivel) ao caso significando sensível (insensivel) à minúsculas e maiúsculas.

Tags do HTML são comandos de formatação de texto envolvidos por dois sinais < >. Alguns exemplos são
<p>um parágrafo</p>, <b>letras em negrito</b>, etc.

Dizemos que um determinado padrão regex é procurado no texto até que ocorra um ou mais “casamentos” (match, em inglês). Por exemplo, o padrão ab é encontrado (e, portanto, casa com) os textos abraço, absolvido ou aberração, mas não casa com asbestos. Se for pedida uma busca insensível ao caso, ele também casa com Abraço ou ABROLHOS. Em todo esse artigo marcaremos as partes “casadas” pela essa formatação especial.

Nos exemplos e tabelas desse artigo há uma certa quantidade de repetições, buscando facilitar o aprendizado. Tabelas enxutas para consultas serão listadas em outra parte.

Caracteres simples ou conjuntos de caracteres (strings comuns) são casados literalmente.

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
em 1945 caracteres alfanuméricos em 1945 a segunda guerra mundial terminou em 1945.

Metacaracteres

Claro que não precisaríamos de nenhuma tecnologia especial para encontrar letras ou conjuntos de letras. Para construir padrões mais complexos usamos os metacaracteres (o que significa que têm significados especiais):
. * ^ $ + ? { } [ ] \ | ( ).

O ponto (.) representa (substitui ou casa com) qualquer caracter único.

padrão casa com não casa com
p.ata prata, plata pata pirata
lu.a luta, luma lua lucca
phylos.net phylos.net, phylos-net (1) phylosnet

(1): O ponto (.) casa com todos os caracteres, inclusive o próprio ponto.

Quantificadores

Os quantificadores * ? + {} permitem o controle de quantas repetições de um padrão ocorrem.

O asterisco * representa zero, uma ou várias repetições do caracter que o precede.

padrão casa com não casa com
Jo*e Je, Joe, Jooe Jose
1*23 23, 123, 1123 1021
.* qualquer número de qualquer caracter

A interrogação ? representa zero ou uma repetição do caracter que o precede.

padrão casa com não casa com
Jo?e Je, Joe Jooe
1?23 23, 123 1123 1021
.? nenhum ou uma ocorrência de qualquer caracter

O sinal de mais + representa uma ou mais repetições do caracter que o precede.

padrão casa com não casa com
Jo+e Joe, Jooe Je
1+23 123, 1123 23 1021
.+ 1 ou mais ocorrências de qualquer caracter


As chaves {n} indicam n repetições do caracter precedente.
{n,} significa mínimo de n repetições do caracter precedente.
{n,m} significa mínimo de n, máximo de m repetições do caracter precedente.

padrão casa com não casa com
A{3} AAA, GAAAA AA AAB
Ot{2}o Otto Oto
1{2}9{3} 11999, A:11999 119, 11199
s{2,} passar, asssb casa, sapo
s{2,4} passar, asssb , sssss casa, sapo
.{5} 5 repetições de qualquer caracter

Os marcadores de posição ou âncoras ^ $ \b \B permitem o controle da posição onde o padrão ocorre.

O circunflexo ^ indica que o padrão seguinte está no início da linha.
O cifrão $ indica que o padrão prévio está no final da linha.
A marca \b indica início ou fim de uma palavra (uma sequência contínua de caracteres). \b marca a passagem de um caracter alfanumérico ou sublinhado \w para um não-caracter \W.
A marca \B, a negação de \b, representa início ou fim de uma palavra envolta em um não-caracter \W.

padrão casa com não casa com
^f filho afazia
^3{2} 333-123 133-7654
s$ bois essência
m{3}$ booommm mmmassa
\bPedro PedroPedro .Pedro LPedro _Pedro
Pedro\b Pedro PedroPedro. PedroL Pedro_
\BMaria LMaria _Maria 3Maria Maria -Maria
Maria\b Maria MariaMaria. MariaL Maria_

Observe que o sinal ^ serve como âncora, para marcar início da string, ou como sinal de negação.
A negação ocorre quando ^ é usado como primeiro sinal dentro de uma lista. Nesse caso ele nega todos os caracteres da lista.

padrão casa com não casa com
^[0-9].* 1989, 5G (1) G5
^[^0-9].* G7, I-99 (2) 5G
[0-9]^ 0^, 1^ (3) 55
^^.* uma linha iniciada por ^
^$ uma linha vazia
.{5}$ casa 5 últimos caracteres de uma linha
^.{15,30}$ casa linhas com 15 até 30 caracteres

(1): O padrão casa com textos que possuem um dígito no início.
(2): O padrão casa com textos que não se iniciam com um dígito.
(3): Qualquer dígito seguido do circunflexo literal ^

Classes de caracteres


Classes de caracteres são marcações que funcionam como “shortcuts”, representando um grupo de caracteres ou controles.
\s representa um espaço simples.
\S representa espaço negado (não é um espaço).
\d representa qualquer dígito. O mesmo que [0-9].
\D \d negado. Qualquer não dígito. O mesmo que [^0-9].
\w caracter alfanumérico e sublinhado.
\W \w negado. Qualquer sinal exceto caracter alfanumérico e sublinhado.

padrão casa com não casa com
\s casadamãeJoana (espaços) casa_da_mãe_Joana
123\s456 123 456 99.123 456.90 123.456__123_456
\S casa “   ”
\d{3}/\d{2} 123/56 987/78 22/3 aaa/bb
\D+ basta 123 0000
\D{3}-\D{2} abc-de aAs-fg1 a2c-de 123-00
\w+ atr3v1d0_b3sta phylos.net — @@@ %%%
\W+ --- @@@ %%% phylos.net atr3v1d0_b3sta
[\b] caracter de backspace
\c caracter de controle

Para procurar por um dos metacaracteres como um literal dentro do texto usamos o escape \.

padrão casa com não casa com
\\s No regex \s casa espaço, número \s987 s ss “ ”
R\$45 R$45 R45
(U\$5|R\$25) U$5, R$25 U$ 5
\\d\(\d{2}\) \d(69), \d(00) \d(123), \d12

Agrupamentos

É possível encontrar um dos caracteres dentro de um conjunto de caracteres usando chaves []. Essa notação permite a descrição de intervalos como [a-z], significando todas as letras minúsculas de a até z, ou [0-9], todos os dígitos.

padrão casa com não casa com
pr[ae]to prato, preto prto, proto
[a-e]rm arm, permanente, ermitão frm, rm
[5-7]00 500, 600, 700 800, 00
c[ep]* c, ce, cp, cep, ceep, ceepp bep, ep
</[bi]*> </b>, </i>, </>, </bi>, /b, /i>
</[bi]{1}> </b>, </i> </>, </bi>


Outros agrupamentos: alguns caracteres de controle permitem o agrupamento de texto e padrões.
(abc) permite o agrupamento simples de caracteres (no caso ‘abc’).
[abc] quaisquer dos caracteres (no caso abc).
[a-z]: qualquer caracter no intervalo de a até z.
[^a-z]: negação de [a-z]. Todos os caracteres exceto aqueles entre a e z.
[0-9]: qualquer dígito entre 0 e 9.
[^0-9]: negação de [0-9]. Tudo o que não é dígito.
a|b: opcional, a ou b.
(padrao1|padrao1): busca por um ou outro padrão.

padrão casa com não casa com
(est) muito estudo, sem estado Estimo, set
[est]ato sato, tato mato
[f-h]ato fato, hato, gato mato, rato, feto
[^f-h]ato mato, rato, lato fato, gato
[^a-c]123 r123, M-123, s/123 a123, b123
[1-5]-[6-9] 1-6, 123-456, A5-6 a1-b2, 6-1
[^1-5]6789 06789, 12345678900, R56789 16789
p(l|r)at platão, prata pato, piato
p(at|len)o pato, pleno patleno, po, plo
p(ratic|ublic|enal)idade praticidade, publicidade, penalidade pcidade, pibcidade
(s{2}|r{2})os carros passos nossos erros casos, cappos, cassas
[0-2][0-9]:[0-5][0-9] 04:20, 12:50 34:30, 11:65

Uma observação importante: dentro de um agrupamento por colchetes ([]) o ponto e todos os demais metacaracteres têm valor literal e não precisam ser “escapados”. A única exceção é o hífen, - que serve para representar intervalos. Para inserir um hífen no padrão agrupado devemos colocá-lo como o último do grupo. Para usar o caracter ] ele deve ser posto como o primeiro elemento para não ser confundido com o fechamento do grupo. Não há restrição sobre onde colocar o [.

padrão casa com não casa com
12[:. ]45 12:45, 12.45, 12 45 1245, 12-45, 12/45
[$€¥]137 $137, €137, ¥137 137, R137
876[/_-]543 876/543, 876_543, 876-543 876 543, 876|543
doid[]oa] doid], doido, doida doid[, doide, doid*
doid[[oa] doid[, doido, doida doid], doide, doid*
9[1-5-][a-f] 92b, 9-b 95, 9-, 91g
[*ab]\d *9, a3, b5 64

Observe que [*] = \*.

Correspondência gananciosa ou preguiçosa

Por default as buscas quantificadas por * + {} são gananciosas (greddy), o que significa que englobam a maior porção de texto casada com o padrão.

Em inglês se usa os termos greddy e lazy, que significam literalmente “ganancioso” e “preguiçoso” para se referir à correspondência do maior ou menor intervalo possível de texto. Em português são comuns as traduções ganancioso e prequiçoso ou guloso e não-guloso.


O padrão <.+> significa ‘todos os caracteres entre os sinais < > e casa com o maior texto possível iniciado por < e finalizado por >, mesmo que o caracter finalizador ocorra nesse intervalo casado. Portanto, se quisermos extrair o texto circundado pela tag div devemos ser capazes de escrever um padrão que é interrompido no primeiro encontro do caracter finalizador >. Essas são as chamadas buscas preguiçosas, o que é possível se obter com o acréscimo do sinal ? após o quantificador.

padrão casa com não casa com
<.+> <div class="classe">Todo o texto dentro da TAG</div> texto sem tags
<.+?> <div class="classe">Todo o texto dentro da TAG</div> texto sem tags
.*(\sR) primeiro R segundo R fim (1) primeiro, segundo, fim
.*?(\sR) primeiro R segundo R fim (2) primeiro, segundo, fim
\d+ 1957 2021 258.1258 abcd
\d+? 1957 2021

(1): .*(\sR) significa “qualquer quantidade de caracteres seguido de espaço, depois um R”.
(2): A versão “prequiçosa” pára na primeira ocorrência de ” R”, depois casa um segundo grupo.

Observe que o padrão .*? significa “zero ou qualquer número de qualquer caracter” e portando não casa com coisa alguma.

Resumindo:

Ganancioso casa
ab* abbbb
ab+ abbb
ab? ab
ab{1,3} abbb
Preguiçoso casa
ab*? a
ab+? ab
ab?? a
ab{1,3}? ab

Grupos

Caracteres dentro de chaves ()são tratados como grupos e casados juntos. Todos os quantificadores e âncoras podem ser aplicadas aos grupos e podemos inclusive aninhá-los (usar um grupo dentro de outro).

padrão casa com não casa com
([a-z]+) casa com palavras de minúsculas
(bem|mal)\sfeito bem feito, mal feito
(bem|mal)?\sfeito bem feito, mal feito, feito
(in|con)?certo incerto, concerto, certo
(in|con)+certo incerto, concerto certo
(\.\d){2} .1.2, .4.5, .0.0 12, 1.2
(www\.)?phylos.net www.phylos.net, phylos.net
(hiper|hipo)(trofia|plasia) hipertrofia, hipotrofia, hiperplasia, hipoplasia plasia, hipo
((su|hi)per)?mercado supermercado, hipermercado, mercado plasia, hipo

Retrovisores

O grupos permitem o uso de retrovisores com a sintaxe (grupo)\1...9. Esse uso de \1...9 (\ seguido de um dígito, 1 – 9) não tem relação com escape mas denota um grupo casado. Ele serve para a reutilização do trecho casado para uma nova busca no texto alvo.

Por ex.:
([a-z]+) casa com palavras de uma ou mais letras minúsculas.
([a-z]+)- casa com palavras de uma ou mais letras minúsculas seguidas de hífen.
([a-z]+)-\1 armazena o texto casado e o procura novamente, uma vez.

padrão casa com não casa com
([a-z]+)-\1 quero-quero, mau-mau, asdfg-asdfg quero
([a-z]+)-?\1 quero-quero, queroquero, bombom, bombomzeiro bom
in(co)lo(r) = sem \1\2 incolor = sem cor
\b([a-z]+)-?\1\b quero-quero, queroquero, bombom (1) bombomzeiro
\b(bo(na|to))\1\b bonabona, botoboto (2) rbotoboto, bonabonas
(rapida)(mente) conseguimos uma \2 \1 rapidamente conseguimos uma mente rapida (3)
(su)d(ão) do \1l n\2 sudão do sul não
(AA)(99)(hh) \3 \1 \2 AA99hh hh AA 99 (4) AA99hhhhAA99
(AA)(99)(hh)\3\1\2 AA99hhhhAA99 (5) AA99hh hh AA 99
(\d{2})(\d{3})(\d{2}) \1-\2-\3 1122233 11-222-33, 9988877 99-888-77 998877 99-88-77
((band)eira)nte \1 \2alheira bandeirante bandeira bandalheira (6)

(1): \b inicial e final significa que o padrão circundado é uma “palavra”.
(2): O padrão casado e repetido é bona ou boto no início e no fim da “palavra”.
(3): o 1º grupo é rapida, o 2º é mente.
(4): os grupos 1, 2 e 3 são capturados na ordem, e repetidos em outra ordem, separados com espaços.
(5): o mesmo que (4), exceto que os grupos são repetidos sem espaços.
(6): ilustrado na figura abaixo.

O retrovisor serve para procurar grupos repetidos. Os grupos são numerados de 1 até 9, contando-se da esquerda para a direita, sendo que o primeiro parêntese encontrado define a ordem do grupo.

Outras técnicas de grupos

Algumas técnicas mais sofisticadas foram implementadas nas expressões regulares, nem todas reconhecidas por todos os editores, IDEs e linguagens de programação. Para isso o metacaracter ainda não utilizado, (?..) ganhou significado de operador em regex.

(?#texto de comentário)
(?:regex): grupo casado mas não armazenado nem incluído na contagem dos grupos.

padrão casa com não casa com
(?#nome)(pa)(pi) \2\1 papi pipa papi papi
(Jó) (?:Alto)- (Rui) \1 \2 Jó Alto Rui- Jó Rui
(?:Z)-(\d{2})-(\d{4}):\2:\1 Z-11-2222:2222:11, Z-45-9876:9876:45
(?:\w)-(\d{3}) \1-\1 a-123 123-123, b-456 456-456 a-123 123-121

padrao(?=regex): não é casado mas determina regex que deve existir após padrao.
(?<=regex)padrao: não é casado mas determina regex que deve existir antes de padrao.

padrão casa com não casa com
casa (?=\d{2}) casa 23, casa 899 casa dez, 852 casa
Pedro(?=\sCa) Pedro Cabral, Pedro Camilo Pedro Barata, Pedroca
\d{4}(?=[A-Z]) , 1234A, 0987H, 6666GGG 354W, G5432, 987G
(?<=Albert) Einstein Albert Einstein Alberto Einstein
(?<=\d{3}) [a-r.]{5} 123 roman, 987 coma. 123 ruela

(?!regex): não é casado mas determina regex que não deve existir após outro padrão.
(?<!regex): não é casado mas determina regex que não deve existir antes de outro padrão.

padrão casa com não casa com
casa (?!\d{2}) casa dez, casa verde casa 12, casa 123
Pedro(?!\sCa) Pedro Bernardo, Pedro Bento, Pedroca Pedro Cabral
\d{4}(?![A-Z]) , 1234890, 0987-987 3354W, 5432H, 987G
(?<!\d{2}) casa naquela casa, outra casa 12 casa, 123 casa
(?<!Pedro) Cabral José Cabral Pedro Cabral
(?<![A-Z])-\d{4} 987-1234, 4-0987 A-3354, H-5432

(?P<nome>regex): grupo casado e nomeado com nome, ao invés de numerado com \1…\9.
Obs.: Essa é a sintaxe usado no Python. Ela pode variar em outros ambientes.

» data= '23 de junho de 2021'
» regex= '^(?P\d{1,2})\sde\s(?P\w+)\sde\s(?P\d{4})'
» matches= re.search(regex, data)

» print('Dia: ', matches.group('dia'))
» print('Mês: ', matches.group('mes'))
» print('Ano: ', matches.group('ano'))
↳ Dia:  23
↳ Mês:  junho
↳ Ano:  2021

(?modificador): modificador é uma ou mais letras que ativam uma funcionalidade, sendo:

Modificador Significado
i busca insensível ao caso
m força o metacaracter . a casar com \n
s obriga as âncoras ^ e $ a casarem com \n
x permite a inclusão de espaços e comentários
L força o uso da localização do sistema (só Python)
u considera a tabela Unicode (só Python)
padrão casa com não casa com
(?i)[a-z]* Pedro, aLLana 654-654
(?i)[A-Z]* Pedro, aLLana 654-654
(?i)\d+\.png 1234.png, 1234.PNG foto.png, 987000.jpg

Precedência de metacaracteres

Quando vários metacaracteres aparecem juntos eles obedecem a uma ordem definida de precedência, definida pela ordem na tabela.

Ordem Tipo Exemplo Significado
0 () (grupo) grupos não quebrado
1 quantificador abc+ ab seguidos de c em qualquer quantidade
2 concatenar abc abc simples
3 | ab|c ab ou c
3 | ab|c ab ou c

Alguns exemplos dessas regras de precedência:

padrão casa com significado
abc+ abc abcc abccc “ab” seguido de 1 ou mais “c”
abc abc abcc abccc “abc”, juntos
(abc) abc abcc abccc “abc”, juntos, em grupo
ab|c abc abc “ab” ou “c”
a(b|c) ab ac abc a abcc accc “a” seguido de “b” ou “c”
ab|cd* ab cd cddd abcdddddddd (1) o mesmo que (ab)|(c(d*))
s/ n/|número \d* s/ n/ número 19000 o mesmo que (s/ n/)|(número (\d*))

Para forçar uma união de caracteres em um grupo inquebrável usamos ().
(1): A concatenação em ab tem prioridade sobre a alternância |. d* ocorre antes da concatenação com c. Portanto ab|cd* é o mesmo que (ab)|(c(d*)).

Caracteres acentuados

Em português e outras línguas européias precisamos criar padrões que incluem caracteres com acentos. Uma alterniva é usar as classes POSIX listadas abaixo. Alternativamente podemos extender grupos de acordo com a tabela ASCII, o que é útil quando POSIX não está disponível.

POSIX alternativa significado
[[:lower:]] [a-zà-ü] minúsculas, acentuadas ou não
[[:upper:]] [A-ZÀ-Ü] maiúsculas, acentuadas ou não
[[:alpha:]] [A-Za-zÀ-ü] minúsculas e maiúsculas, acentuadas ou não
[[:alnum:]] [A-Za-zÀ-ü0-9] todas as letras, acentuadas ou não, e dígitos
padrão casa com significado
(ção)|(ções) noção noções
[à-ü] estúpido eqüinócio
[a-zà-ü]* retratação RETRATAÇÃO
[A-Za-zÀ-ü0-9]* retratação RETRATAÇÃO 2001

Classes POSIX

Nem todas as linguagens de programação aceitam as classes POSIX. Java e C dão suporte a POSIX e existem bibliotecas Python para o mesmo resultado.

Classe Descrição
[:digit:] dígito, \d; equivalente a [0-9]
^[:digit:] não dígito, \D; equivalente a [^0-9]
[:alnum] letras e números ; equivalente a [A-Za-z0-9]
[:space:] caracteres brancos ; equivalente a [ \t\n\r\f\v]
^[:space:] não espaço: \S
[:alpha:] letras; equivalente a [A-Za-z]
[:lower:] minúsculas; equivalente a [a-z]
[:upper:] maiúsculas; equivalente a [A-Z]
[:xdigit:] números hexadecimais; equivalente a [0-9A-Fa-f]
[:word:] \w qualquer caractere alfanumérico mais underscore (_); equivalente a [[:alnum:]_]
^[:word:] \W, negação de \w
[:blank:] espaço em branco e TAB; equivalente a [\t]
[:punct:] pontuação; equivalente a [!”\#$%&'()*+,\-./:;<=>?@\[\\\]^_‘{|}~]

Exemplos de validações com Regex

Alguns exemplos de validações com Expressões Regulares

Tipo de Validação regex
Dígitos ^\d+$
Letras ^\w+$
Decimal ^[+-]?((\d+|\d{1,3}(\.\d{3})+)(\,\d*)?|\,\d+)$ ^[-+]?([0-9]*\,[0-9]+|[0-9]+)$
URL ^((http)|(https)|(ftp)):\/\/([\- \w]+\.)+\w{2,3}(\/ [%\-\w]+(\.\w{2,})?)*$
E-mail ^([\w\-]+\.)*[\w\- ]+@([\w\- ]+\.)+([\w\-]{2,3})$
Endereço IP \b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b
Tempo (24 horas) ^([0|1|2]{1}\d):([0|1|2|3|4|5]{1} \d)$
Data (dd/mm/aaaa) ^((0[1-9]|[12]\d)\/(0[1-9]|1[0-2])|30\/(0[13-9]|1[0-2])|31\/(0[13578]|1[02])) \/\d{4}$
Telefone ^\(\d{3}\)-\d{4}-\d{4}$
Senha ^\w{4,10}$ ^[a-zA-Z]\w{3,9}$ ^[a-zA-Z]\w*\d+\w*$
🔺Início do artigo

Bibliografia

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 as 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 %lt;= precoFinal/preco %lt;= 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

Embora sendo uma linguagem multiparadigma Python tem 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 à 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. Um objeto instanciado de uma classe herda suas propriedades e métodos.

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 da classe. Esses objetos possuem as mesmas propriedades e métodos da classe.

A classe mais simples é aquela que contém sua definição, sem 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
» 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

Vemos acima que uma alteração na classe gera alterações nas instâncias. *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.

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 pode 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 propridade __class__ indice 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 que ainda não foram modificados e das novas instâncias. Propriedades nas instâncias alteradas após a inicialização não são afetadas.

# 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, mesmo que não explicitamente definido, possuem um método chamado __init __ (), que é sempre executado durante sua inicialização. O nome __init__, iniciado e terminado por 2 sublinhados (o que em inglês tem sido chamado de dunder, double underscore), é chamado de construtor da classe.

Ele é usada 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 tipo Nota que 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 ex. 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âmtro 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


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 x) 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 *
» import escola
» 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 vará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 apropriados 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()
# usa a função exibir() para 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.

Iteradores

Já vimos que objetos que são sequências e coleções podem ser lidos iterativamente com o uso do operador for.

» lista = ['Aa', 'Bb', 'Cc', 'Dd', 'Ee', 'Ff']
» for t in lista:
»     print(t, end=' < ')
↳ Aa < Bb < Cc < Dd < Ee < Ff <

Laços desse tipo são claros e concisos. Por trás desse resultado simples, a instrução for chama a função iter() na sequência (ou coleção) que retorna um objeto iterador. Dentro do iterador existe o método __next__() que acessa os elementos no contêiner um de cada vez. Ao final da iteração, quando se extinguem os elementos, __next__() levanta uma exceção StopIteration que é reconhecida pelo laço como o fim da iteração. O método __next__() pode ser chamado através da função interna next(), como mostra o exemplo:

» iterador = iter('OMS')
» print(next(iterador))
» print(next(iterador))
» print(next(iterador))
» print(next(iterador))
↳ O
↳ M
↳ S
↳ StopIteration:

Podemos tornar uma classe iterável inserindo nela os métodos __iter__() e next(). No caso abaixo a classe simplesmente retorna a sequência invertida, do último elemento para o primeiro

» class Inverter:
»     def __init__(self, data):
»         self.data = data
»         self.index = len(data)

»     def __iter__(self):
»         return self

»     def __next__(self):
»         if self.index == 0:
»             raise StopIteration
»         self.index -= 1
»         return self.data[self.index]

# inv é Inverter usando uma lista como argumento
» inv = Inverter(['Aa', 'Bb', 'Cc', 'Dd', 'Ee', 'Ff'])

» for t in inv:
»     print(t, end=' < ')
↳ Ff < Ee < Dd < Cc < Bb < Aa <

# s é Inverter usando uma string como argumento
» s = 'Joazeiro'
» seq = Inverter(s)
» for t in seq:
»     print(t, end=' < ')
↳ o < r < i < e < z < a < o < J <     
🔺Início do artigo

Bibliografia

Consulte a bibliografia no final do primeiro artigo dessa série.

Python: Arquivos e pastas

Quase sempre um programa de computador precisa recuperar dados ou informações previamente existentes, processá-los de algum modo e retornar um resultado que deve ficar armazenado para uso futuro. Esses dados podem ser lidos em papel impresso ou digitados pelo usuário. O resultado final também pode ser impresso ou exibido na tela. Mas, como maior frequência a informação inicial e final é lida e gravada em disco, em arquivos de texto ou binários, planilhas e bancos de dados (entre outros muitos formatos).

O objeto arquivo (file)

Abrir e ler um arquivo:

No Python existem diversas funções no módulo básico para abrir, manipular e escrever em arquivos. Um arquivo aberto é um objeto que pode ser atribuído à uma variável e, por meio dessa variável, as propriedades e métodos do objeto podem ser acessadas. A função open() estabelece uma conexão com o arquivo no disco.

obj = open('caminho_para_arquivo', modo)

Os modos são listados abaixo.

Por ex., suponha que existe na pasta de trabalho um arquivo com nome alunos.txt contendo informações sobre alunos de um curso. Seu conteúdo pode ser exibido da seguinte forma:

» objeto_arquivo = open('alunos.txt', 'r')
» texto = objeto_arquivo.read()
» objeto_arquivo.close()
» print(texto)

↳ id, nome     , cidade         , idade , nota
↳ 10, Pedro    , São Paulo      , 34    , 83.0
↳ 11, Maria    , São Paulo      , 23    , 59.0
↳ 12, Janaina  , Rio de Janeiro , 32    , 86.0
↳ 13, Wong     , Brasília       , 43    , 89.0
↳ 14, Roberto  , Salvador       , 38    , 98.0
↳ 15, Marco    , Curitiba       , 31    , 61.0
↳ 16, Paula    , Belo Horizonte , 34    , 44.0


O método read() lê o arquivo inteiro e o retorna para a variável texto que passa a conter um vínculo com o arquivo no disco.

Após o uso o arquivo deve ser fechado com o método close(). O fechamento do objeto libera recursos da máquina, permite que o arquivo seja acessado por outro bloco de código. Em alguns casos alterações feitas ao arquivo no programa podem não ser gravadas em disco até que eles seja fechado. Um problema técnico, como a falta de energia, pode ocasionar em perda de dados.

Estritamente dizendo o mais adequado para lidar com arquivos seria um bloco de código do seguinte tipo.

» try:
»     f = open("data.txt", "w")
»     # código com as operações necessárias usando o arquivo f
» finally:
»     f.close()

Isso evita a possibilidade de que algum erro faça com que o comando de fechamento seja pulado. Uma forma mais compacta de escrever isso, e que evita o problema citado, usa o comando with. No Python a declaração with é usada para manipular exceções e tornar o código mais enxuto e de fácil leitura.

# exemplo de código usando with
» with open('data.txt', 'r') as f:
»     # codigo usando o arquivo f
»     # (pronto!)

O fechamento nesse caso é automático. O comando with facilita a escrita do código que envolve recursos que devem ser finalizados. Essa finalização significa liberação de recursos de memória e garantia de que erros desnecessários sejam introduzidos.

Ler linhas de um arquivo:

Também é possível ler o arquivo com readlines() que retorna uma lista de linhas que podem ser percorridas depois.

# todas as linhas do arquivo são impressas dessa forma    
» with open('alunos.txt', 'r') as f:
»     linhas = f.readlines()
» for linha in linhas:
»     print(linha, end ='')
# as linhas acima são impressas (mas foram aqui omitidas)

# como readlines() retorna uma lista de linhas
# ele pode ser usado para contar quantas linhas existem no arquivo
» with open('alunos.txt', 'r') as f:
»     print(len(f.readlines()))
↳ 8    

Uma forma alternativa consiste em ler o arquivo uma linha de cada vez, processando as linhas na medida em que são lidas.

» with open('alunos.txt', 'r') as f:
»     print('1-', f.readline(), end='')
»     print('2-', f.readline(), end='')

↳ 1- id, nome     , cidade         , idade , nota
↳ 2- 10, Pedro    , São Paulo      , 34    , 83.0

Uma linha é o texto que termina em um sinal de quebra linha.

Quebras de linhas (end of line, EOL) são caracteres especiais inseridos em arquivos para indicar o final de uma linha e o início de outra. Esses caracateres não são vistos nos editores mas marcam os finais de cada linha. Alimentação de linha (line feed, LF) e retorno de carro (carriage return, CR) são usados, dependendo do sistema operacional. LF é representado por \n, 0x0A em hexadecimal ou 10 decimal. CR é representado por \r, 0x0D em hexadecimal ou 13 decimal. O Linux (e outros sistemas derivados do UNIX) usa \n. O Windows usa \r\n e o OSX usa \r.

A cada uso do método .readline() uma nova linha é recuperada, até o fim do arquivo. A operação pode ser repetida até que uma string nula seja retornada. Ao final o iterador fica vazio e deve ser lido novamente caso outra iteração seja necessária. Com essa forma de leitura as linhas são lidas uma de cada vez e a requisição de memória é menor.

» with open('alunos.txt', 'r') as f:
»     while True:
»         linha = f.readline()
»         if not linha:
»             break
»         print(linha, end='')

# Observe que readline() é o método default do objeto arquivo.
# Isso significa que o mesmo resultado pode ser obtido com as linhas

» with open('alunos.txt', 'r') as f:
»     for linha in f:
»         print(linha, end='')


Em ambos os casos o arquivo dos alunos é exibido. O teste de final de arquivo pode ser excluído porque o iterador se esgota ao final e o laço for é abandonado.

Observe que a linha if not linha: tem o mesmo efeito que if linha == '':. Ao final da iteração uma string vazia é retornada e strings vazias são avaliadas como False em testes booleanos.

Gravar em um arquivo:

No código abaixo um arquivo é aberto para gravação.

» texto = (
»     'Texto a ser gravado em disco\n'
»     'pode conter quantas linhas se desejar\n'
»     'novas linhas são inseridas com \'newline\''
» )
» arquivo = open('teste_gravar.txt', 'w')
» arquivo.write(texto)
» arquivo.close()

# a mesma operação usando with
» with open('teste_gravar.txt', 'w') as arquivo:
»     arquivo.write(texto)

Após a execução dessas linhas um arquivo teste_gravar.txt será encontrado na pasta de trabalho. No texto a ser inserido novas linhas são criadas após o sinal de newline (\n).

Gravação incremental em um arquivo:

Para acrescentar linhas ao arquivo, sem apagar as já existentes, usamos o parâmetro ‘a’ (de append):

# para acrescentar um novo aluno ao arquivo alunos.txt:
» novo_aluno = '17, Ana      , Belo Horizonte , 21    , 78.5\n'
» with open('alunos.txt', 'a') as f:
»     f.write(novo_aluno)
# a nova aluna, Ana e seus dados, fica acrescentada ao final do texto.

Observe que write() não insere quebras de linha automaticamente. Por isso terminamos a linha com o sinal de nova linha \n. Dessa forma novo texto inserido posteriormente já se inicia em linha própria.

Outra forma de iterar sobre as linhas de um arquivo consiste em tratar o objeto de arquivo o como um iterador em um laço for. O ex. mostra que readlines() retorna um iterável que pode ser percorrido dentro de laço for:

» with open('alunos.txt', 'r') as f:
»     for linha in f.readlines():
»         print(linha)
# o arquivo inteiro é exibido (mas omitido aqui)

Todo o texto lido em um arquivo dessa forma é uma string, mesmo que composto de dígitos. Se inteiros e números de ponto flutuante estão no texto e devem ser usados como números, por ex. para um cálculo, eles devem ser convertidos usando-se as conversões int() e float(), respectivamente.

Em exemplo disso segue abaixo. Usamos o arquivo dos alunos para ler as idades e as notas, que são convertidas para inteiro e flutuante, respectivamente.

» with open('alunos.txt', 'r') as f:
»     maior, nota_media, n = 0, 0, 0
»     f.readline()
»     for linha in f:
»         palavras = linha.split(',')
»         n +=1
»         nota_media += float(palavras[4])
»         idade = int(palavras[3])
»         if idade > maior:
»             maior = idade
»             nome = palavras[1]
» print('%s é o aluno mais velho, com %d anos.' % (nome.strip(), maior))
» print('A nota média entre %d alunos é %2.1f' % (n, nota_media/n))

↳ Wong é o aluno mais velho, com 43 anos.
↳ A nota média entre 7 alunos é 74.3

O método split(',') quebra a linhas em palavras, partindo cada uma nas vígulas e retornando uma lista. palavra[3] é a idade, palavra[4] a nota. A maior idade e o nome do aluno mais velho são armazenados dentro do teste final.

Propriedades de arquivos e leitura incremental:

Para os exemplos abaixo um novo arquivo com 3 linhas é gravado.

» lista = ['Esta é a linha 1\n',
»          'Esta é a linha 2\n',
»          'Esta é a linha 3\n'
»         ]
» with open('novo.txt', 'w') as f:
»     f.writelines(lista)
» with open('novo.txt', 'r') as f:
» with open('novo.txt', 'r') as f:
»     print(f.read())

↳ Esta é a linha 1
↳ Esta é a linha 2
↳ Esta é a linha 3

Um objeto de arquivo tem diversas propriedades, entre elas file.name, o nome do arquivo, file.closed que é True se o arquivo está fechado (embora a variável continue apontando para o objeto), e file.mode que contém o modo em que ele foi aberto.

» with open('novo.txt', 'r') as f:
»     info = 'O arquivo "'+ f.name + '" está ' + ('fechado' if f.closed else 'aberto')
»     info += ' no modo %s' % f.mode
»     print(info)
» print('Agora o arquivo está ' + ('fechado' if f.closed else 'aberto'))

↳ O arquivo "novo.txt" está aberto no modo r
↳ Agora o arquivo está fechado

Leitura incremental de um arquivo:

A abertura de um arquivo com muitas linhas, ou uma linha muito longa sem quebras pode esgotar os recursos de memória de seu computador. Para evitar esse problema tanto readline ou readlines aceitam um argumento opcional estabelecendo quanto dado deve ser lido de cada vez. É possível ler um arquivo de modo incremental com arquivo.read(n) onde n é o número de bytes lidos (que resulta em um caracter simples). Como a primeira linha contém 16 caracteres (‘Esta é a linha 1’) ela é esgotada em duas iterações.

» with open('novo.txt', 'r') as f:
»     print(f.read(8))
»     print(f.read(8))

↳ Esta é a
↳  linha 1

Após as 2 leituras no código anterior o ponteiro (cursor) está posicionado sobre o byte 17. Essa posição pode ser lida e alterada, usando-se os métodos tell() e seek(). Nas linhas abaixo 17 bytes são lidos e impressos na linha 1. A linha 2 mostra a posição do ponteiro, 18. seek()

» with open('linhas.txt', 'r') as f:
»     print('1 : ', f.read(17), end='')
»     print('2 : ', f.tell())
»     f.seek(0, 0)
»     print('3 : ', f.read(17), end='')
»     f.seek(36, 0)
»     print('4 : ',  f.read(17), end='')

↳ 1 :  Esta é a linha 1
↳ 2 :  18
↳ 3 :  Esta é a linha 1
↳ 4 :  Esta é a linha 3

No exemplo seguinte a primeira linha é lida inteira e apenas 10 caracteres da segunda linha.

» with open('alunos.txt', 'r') as f:
»     txt1 = f.readline()
»     txt2 = f.readline(10)
» print(txt1, end = '')
» print(txt2)

↳ id, nome     , cidade         , idade , nota
↳ 10, Pedro    , São Paulo

Métodos de arquivo

Método Descrição
open(arquivo, mode) Abre o arquivo (descrição dos parâmetros abaixo)

Os seguintes parâmetros são usados com open():

Parâmetro Descrição
arquivo caminho completo e nome do arquivo
mode Uma string que define em que modo o arquivo será aberto

O parâmetro mode pode ser:

Parâmetro Descrição
r para leitura – O ponteiro é colocado no início do arquivo. Default.
r+ para leitura e gravação. O ponteiro fica no início do arquivo.
w apenas para gravação. Substitui arquivo existente, cria novo se o arquivo não existir.
w+ escrita e leitura. Substitui arquivo existente, cria novo se arquivo não existir.
rb para leitura em formato binário. O ponteiro fica no início do arquivo.
rb+ para leitura e escrita em formato binário.
wb+ para escrita e leitura em formato binário. Substitui o arquivo existente. Cria novo se não existir.
a para anexar. Conteúdo existente fica inalterado e o ponteiro fica no final do arquivo. Cria novo se não existir.
ab um arquivo para anexar em formato binário. O ponteiro fica no final do arquivo. Cria novo se não existir.
a+ para anexar e ler. O ponteiro fica no final do arquivo, se arquivo existir. Cria novo se não existir.
ab+ anexar e ler em formato binário. O ponteiro no final do arquivo. Cria novo se não existir.
x cria novo arquivo lançando erro se já existir.

São métodos do objeto file:

Método Descrição
close() fecha o arquivo; sem efeito se arq. já está fechado
detach() retorna o fluxo bruto separado do buffer
fileno() retorna número inteiro diferente para cada arquivo aberto
flush() descarrega no disco as alterações no buffer
isatty() retorna True se o fluxo de arquivo é interativo
next(arquivo) itera sobre arquivo, lançando erro no final
read() retorna o conteúdo do arquivo
readable() retorna True se o fluxo do arquivo pode ser lido
readline() retorna uma única linha do arquivo
readlines() retorna lista com todas as linhas do arquivo
search() localiza item no arquivo e retorna sua posição
searchable() retorna True se o arquivo é pesquisável, por ex. com seek()
tell() retorna a posição atual do arquivo
truncate([tamanho]) redimensiona (truncando) o arquivo para um tamanho especificado
writable() retorna True se o arquivo pode receber gravações
write() grava a string especificada no arquivo
writelines() escreve a sequencia no arquivo. Qualquer objeto iterável composto por strings pode ser usado

Módulo os


Vimos que existem diversas funções e outros objetos no módulo básico que são carregados juntamente com o próprio Python. No entanto muitos outros módulos podem sem anexados no código, inclusive módulos na chamada biblioteca padrão. Módulos podem ser escritos pelo usuário e importados em seu código ou compartilhados. Além disso, outros módulos podem ser instalados, inclusive desenvolvido por terceiros.

De particular interesse para a manipulação de arquivos é o módulo os que contém as funcões básicas de interação com o sistema operacional. Para habilitar o uso de módulos instalados usamos import nome_do_modulo.

Listar pastas e arquivos:

Por exemplo, após a importação de os podemos usar o método os.listdir(pasta) que retorna uma lista com o conteúdo da pasta especificada.

» import os
» pasta ='/home/guilherme/Projetos/Python'
» lista_arquivos = os.listdir(pasta)
» print(lista_arquivos)
↳ ['arquivos', 'wallpaper.py', 'turtle.py', 'numpy_02.py', 'NLP', 'output', 'osDir.py',
↳  'lerShelfFile.py', 'guessNumber.py', ...]

Embora esse método ainda esteja disponível, no Python 3.5 foi inserido uma nova forma de obter esse resultado por meio de os.scandir() que retorna um iterável. Os elementos desse iterável possuem diversas propriedades, entre elas name, usada para recuperar o nome do arquivo ou pasta.

» arquivos = os.scandir(pasta)
» for t in arquivos:
»     print(t.name)
» arquivos
↳ wallpaper.py
↳ turtle.py
↳ numpy_02.py
↳ NLP
↳ output
↳ osDir.py
↳ ...

Observação (Windows): Como o caracter \ tem significado especial (escape) no Python é necessário escrever caminhos no Windows de uma forma especial, de uma das duas formas:

» caminho = r'C:\Windows\Temp'
# ou
» caminho = 'C:\\Windows\\Temp'

No código abaixo usamos os.path.join(local, t) que faz a concatenação correta do caminho com o nome do arquivo, agindo de forma diferente de acordo com o sistema operacional. O método os.path.split age de forma inversa, retornando uma tupla que contém a caminho e o nome do arquivo. Depois de extraído o nome do arquivo podemos obter uma tupla com nome separado da extensão usando os.path.splitext.

# como funciona os.path.join (no linux ou OSX)
» os.path.join('caminho_completo', 'nome_do_arquivo')
↳ 'caminho_completo/nome_do_arquivo'

# separando caminho do arquivo
» os.path.split('/home/usuario/Documentos/Artigo.txt')
↳ ('/home/usuario/Documentos', 'Artigo.txt')

# separando a extensão
» os.path.splitext('image.jpeg')
↳ ('image', '.jpeg')

# no windows uma string diferente seria retornada com join
» os.path.join('caminho_completo', 'nome_do_arquivo')
↳ 'caminho_completo\\nome_do_arquivo'

Filtrar pastas e arquivos:

Para investigar se o elemento retornado por listdir é um arquivo podemos usar os.path.isfile(), e os.path.isdir() para pastas (diretórios).

# para filtrar os arquivos
» local = '.'
» for t in os.listdir(local):
»     if os.path.isfile(os.path.join(local, t)):
»         print(t)
↳ arquivo001.txt
↳ arquivo002.txt
↳ jupyter001.ipynb
↳ teste.csv

# para filtrar as pastas
» for t in os.listdir(local):
»     if os.path.isdir(os.path.join(local, t)):
»         print(t)
↳ .ipynb_checkpoints
↳ dados

# para listar arquivos dentro da pasta 'dados'
» local = './dados'
» for t in os.listdir(local):
»     if os.path.isfile(os.path.join(local, t)):
»         print(t)
↳ nums.csv
↳ alunos.pkl

A mesma operação feita usando os.scandir()

# usando scandir
» local = '.'
» with os.scandir(local) as arqs:
»     for arq in arqs:
»         if arq.is_file():
»             print('arquivo:', arq.name)
»         elif arq.is_dir():
»             print('pasta:  ', arq.name)
↳ arquivo: pandas001.ipynb
↳ arquivo: python001.ipynb
↳ arquivo: teste_gravar
↳ arquivo: alunos.txt
↳ arquivo: teste_novo.csv
↳ pasta:   .ipynb_checkpoints
↳ arquivo: teste.py
↳ arquivo: alunos.csv
↳ pasta:   dados
↳ ...            

Claro que essa mesma lista pode ser obtida através de uma compreensão de lista, o que ilustra mais uma vez o poder de concisão dessa construção.

# para arquivos
» lst_arquivos = [t.name for t in os.scandir(local) if t.is_file()]
» lst_arquivos
↳ [pandas001.ipynb, python001.ipynb, teste_gravar, alunos.txt,
↳  teste_novo.csv, teste.py, alunos.csv]

# para as pastas
» [t.name for t in os.scandir(local) if t.is_dir()]
↳ ['.ipynb_checkpoints', 'dados']

A recuperação dos nomes dos arquivos, associada a testes de string com esses nomes, permite uma filtragem mais específica de arquivos retornados. No exemplo abaixo usamos .endswith para verificar se os arquivos possuem uma extensão determinada (no caso ‘.csv’). No segundo exemplo usamos compreensão de lista e testamos a substring após o último ponto.

# usando .endswith()
» print('Arquivos com extensão csv:')
» with os.scandir(local) as arqs:
»     for arq in arqs:
»         if arq.is_file() and  arq.name.endswith('.csv'):
»             print(arq.name)
Arquivos com extensão csv:
↳ teste_novo.csv
↳ alunos.csv

# alternativamente, usando compreensão de lista e .split('.')
» [arq.name for arq in os.scandir(local) if arq.is_file() if arq.name.split('.')[-1]=='txt']
↳ ['alunos.txt']

O método os.scandir() não recupera apenas os nomes dos arquivos. Cada objeto do ScandirIterator possue o método .stat() que retorna informações sobre o arquivo ou pasta ao qual se refere, como tamanho do arquivo. O atributo st_mtime, por ex., contém a hora da última alteração.

No exemplo seguinte fazemos uma iteração sobre os arquivos e pastas em local e imprimimos a data de sua última alteração.

# atributo de arquivos: tempo desde última alteração
» with os.scandir(local) as arquivos:
»     for arq in arquivos:
»         info = arq.stat()
»         print(info.st_mtime)
↳ 1612654770.8688447
↳ 1615833776.4532673
↳ 1611327215.6848917
↳ ...

O retorno é dado em segundos desde a época, uma medida de tempo também chamada de Unix time que consiste no número de segundos decorridos desde Zero Horas do dia 1 de Janeiro de 1970 (UTC, longitude 0°) e pode ser convertido em uma data mais legível através de diversas funções de tratamento de datas e horas.

No código seguinte lemos os arquivos na pasta local e retornamos seu nome e data de última modificação, convertida em forma mais legível usano uma função do módulo datetime. Veremos mais tarde outras funcionalidades desse módulo.

» from datetime import datetime

» def converter_data(timestamp):
»     d = datetime.utcfromtimestamp(timestamp)
»     return d.strftime('%d/%m/%Y')

» def ler_arquivos():
»     arquivos = os.scandir(local)
»     for arq in arquivos:
»         if arq.is_file():
»             info = arq.stat()
»             print(f'{arq.name}\t\t Modificado em: {converter_data(info.st_mtime)}')

↳ Pandas001.ipynb      Modificado em: 06/02/2021
↳ Python001.ipynb      Modificado em: 15/03/2021
↳ teste_gravar         Modificado em: 15/03/2021
↳ alunos.txt           Modificado em: 16/03/2021
↳ teste_novo.csv       Modificado em: 16/11/2020
↳ ...

Os argumentos passados para strftime() são: %d o dia do mês, %m o número do mês e %Y o ano, com 4 dígitos. Várias outras formatações são possíveis.

O módulo os contém os métodos os.getcwd() para ler a pasta ativa no momento, e os.chdir(‘nova_pasta’) para trocar para uma nova pasta.

» import os
» os.getcwd()
↳ '/home/guilherme/Projetos/Phylos.net'

» os.chdir('/home/guilherme/Music')
» os.getcwd()
↳ '/home/guilherme/Music'

Também podemos criar novas pastas. Para isso usamos os.mkdir() (cria uma pasta) e os.makedirs() (cria várias pastas). Podemos criar, dentro da pasta atual, a pasta exemplo e exemplo/textos.

» import os
» os.mkdir('exemplo')
» os.mkdir('exemplo/textos')

# a tentativa de criar uma pasta existente resulta em erro
» os.mkdir('exemplo')
↳ FileExistsError: [Errno 17] File exists: 'exemplo'
Figura 1

Várias pastas e subpastas podem ser criadas simultaneamente. O comando abaixo cria três pastas.

» os.makedirs('2021/03/22')

A estrutura de pastas criada na pasta atual é ilustrada na figura 1:

Os seguintes comandos ilustram operações feitas com métodos de os:

# armazena pasta de trabalho inicial
» pasta_trabalho = os.getcwd()
# altera pasta atual para outra
» os.chdir('/home/guilherme/Music')
# cria subpastas na pasta atual (resultado na figura)
» os.makedirs('subpasta10/subpasta11/subpasta12')

# retorna para a pasta de trabalho
» os.chdir(pasta_trabalho)

# cria sub pasta
» os.mkdir('teste')

# muda pasta atual ('.' é atalho para pasta atual)
» os.chdir('./teste')
# verifica qual é a pasta atual
» os.getcwd()
↳ '/home/guilherme/Projetos/Artigos/teste'

# volta para pasta no nível acima ('..' é atalho para pasta 'mãe')
» os.chdir('..')

# renomeia pasta
» os.rename('teste', 'novo')

# os.remove só apaga arquivos
» os.remove('novo') # só pode apagar arquivo
↳ IsADirectoryError: [Errno 21] Is a directory: 'novo'

» os.remove('aluno.txt') # só pode apagar arquivo
# o arquivo 'aluno.txt' foi apagado (se existe)

# para apagar uma pasta use rmdir
» os.rmdir('novo')
# a pasta 'novo' foi apagada

O método os.remove(arquivo) lança um erro se o arquivo não existe ou se é uma pasta. Para evitar essa possibilidade usamos um try ou testamos previamente o arquivo. Para apagar pastas e subpastas podemos percorrer cada uma delas (veja método abaixo) ou usar o módulo shutil.

» arquivo_apagar = 'home/data.txt'
# testa a existência da arquivo
» if os.path.isfile(arquivo_apagar):
»     os.remove(arquivo_apagar)
» else:
»     print(f'O arquivo: {data_file} não existe ou é uma pasta')

Para apagar uma pasta podemos usar os.rmdir() ou shutil.rmtree().

» apagar_pasta = 'documentos/pasta'
» try:
»     os.rmdir(apagar_pasta)
» except OSError as e:
»     print(f'Error: {apagar_pasta} : {e.strerror}')

# apagar pastas e subpastas, mesmo que não estejam vazias
» import shutil
» apagar_pasta = 'documentos/pasta'
» try:
»     shutil.rmtree(apagar_pasta)
» except OSError as e:
»     print(f'Error: {apagar_pasta} : {e.strerror}')  
Figura 2

Percorrendo a árvore de pastas:

Não é rara a necessidade de percorrer as pastas e subpastas em uma estrutura de diretórios, eventualmente executando uma tarefa nos arquivos no disco ou fazendo buscas. O método os.walk() pode auxiliar nessa tarefa, percorrendo a árvores de pastas tanto da pasta raiz para as subpastas (de cima para baixo, top down) quanto no sentido inverso (bottom up). Por default os.walk(pasta) faz várias iterações em pasta e subpastas, até esgotar a árvore, de mãe para filhos. Em cada iteração retorna:

  • uma string com o nome da pasta atual
  • uma lista de suas subpastas
  • uma lista dos arquivos da pasta atual

Depois a iteração passa para uma subpasta (no modo de cima para baixo) ou para a pasta mãe. Para percorrer a árvore no sentido de baixo para cima usamos o parâmetro para baixo os.walk(pasta, topdown=False). No código abaixo percorremos a árvore (lembrando que ‘.’ simboliza pasta ativa atual. O teste if not sub_pastas resulta True se a lista está vazia. O resultado exibido supõe uma estrura de pastas como na figura 2.

# usando os.walk() para listas arquivos e pastas
» for pasta, sub_pastas, arquivos in os.walk('.'):
»     if not sub_pastas:
»         print('A pasta:', pasta, 'não possui subpastas')
»     else:    
»         print('A pasta:', pasta, 'possui as subpastas:')
»         for sub in sub_pastas:
»             print('\t\t',sub)
»     if not arquivos:
»         print('\t não possui arquivos')
»     else:    
»         print('\t e os arquivos:')    
»         for nome_arquivo in arquivos:
»             print('\t\t', nome_arquivo)

↳ A pasta: . possui as subpastas:
↳        pasta_1
↳        pasta_2
↳    e os arquivos:
↳        texto.txt
↳ A pasta: ./pasta_1 não possui subpastas.
↳    e os arquivos:
↳        arquivo1.py
↳        arquivo2.py         
↳        arquivo3.py
↳ A pasta: ./pasta_2 não possui subpastas.
↳    e os arquivos:
↳        arquivo4.py
↳        arquivo5.py         
↳        arquivo6.py

Informações do sistema operacional:

O módulo os possui muitos outros métodos. Por exemplo, os.uname() retorna o sistema operacional, nome de usuário e dados da versão do sistema usado.

» for t in os.uname():
»     print(t)
↳ Linux
↳ guilherme-Lenovo
↳ 5.8.0-45-generic
↳ #51-Ubuntu SMP Fri Feb 19 13:24:51 UTC 2021
↳ x86_64

Métodos de OS

Alguns dod métodos de OS.

Método Descrição
chdir(“novaPasta”) mudar a pasta ativa para novaPasta. Se novaPasta = “..” vai para pasta mãe
getcwd() ler diretório (pasta) atual
listdir(“pasta”) lista arquivos e subpastas de “pasta”
makedirs(sequencia) criar pastas com nomes em sequencia
mkdir(“nomeDaPasta”) criar uma pasta
remove(arquivo) apagar arquivo
removedirs(caminho) apagar pastas recursivamente
rmdir(“pastaRemover”) apagar pasta “pastaRemover”. Não pode ser pasta atual ou estar em uso por ouro processo
rename(“nomeAntigo”,”nomeNovo”) renomear pasta “nomeAntigo” para “nomeNovo”
uname() retorna dados sobre o sistema operacional, usuário e máquina usada
🔺Início do artigo

Bibliografia

Consulte a bibliografia no final do primeiro artigo dessa série.

Python: Compreensão de listas


Sobre tuplas e Zips

Algumas propriedades extras sobre tuplas são listadas aqui. Além disso usamos a fução zip() para unir objetos iteráveis de modo a poderem ser percorridos simultaneamante.

# Uma tupla não precisa de parenteses
» t = 1, 2, 3, 4
» t
↳ (1, 2, 3, 4)
» type(t)
↳ tuple
# podemos usar tuplas para declarações múltiplas
» x, y = 45, 78
» print(x,y, x+y)
↳ 45 78 123
# E percorrer uma tupla (ou lista) em um loop for
» t = [('a', 0, True), ('b', 1, False), ('c', 2, True)]
» for letra, num, b in t:
»     if b: print (num, letra)
↳ 0 a
↳ 2 c

# qualquer sequência pode ser usada no construtor de tuplas
» t = tuple('palavras')
» t
↳ ('p', 'a', 'l', 'a', 'v', 'r', 'a', 's')

# uma tupla de 1 elemento deve conter (,)
» t = ('a',)
» t
↳ ('a',)

» type(t)
↳ tuple

# tuplas para 'swap' de variáveis
» a = 'both'
» b = 'ambos'
print(a,b)
↳ both ambos
» a, b = b, a
» print(a,b)
↳ ambos both

Funções retornam uma variável ou None. No entanto essa variável pode ser uma tupla, um dicionário ou outro objeto qualquer, o que tem o efeito de ter uma função retornando mais de um valor. Por exemplo, divmod(a,b) retorna uma tupla com o resultado inteira da divisão e o resto. Funções do usuário podem fazer o mesmo, inclusive retornando outra função, como veremos.

» divmod(10,3)
↳ (3, 1)

» div, resto = divmod(10,3)
» print(div)
↳ 3

» print(resto)
↳ 1

# Um exemplo de uma função que retorna uma sequência
» def min_max(seq):
»     return min(seq), max(seq)
»
» seq = (23,45,23,78,1,23,0,-34)
» min_max(seq)
↳ (-34, 78)

» seq = {23,45,23,78,1,23,0,-34}
» min_max(seq)
↳ (-34, 78)

# funções com n argumentos
» def imprima_tudo(*args):
»     print(args)

» imprima_tudo(1,2, '3', True)
↳ (1, 2, '3', True)

# sem * temos um erro
» def imprima(args):
»     print(args)
» imprima(1,2)
↳
---------------------------------------------------------------------------
TypeError: imprima() takes 1 positional argument but 2 were given

Função zip()

Uma função interna interessante e útil é zip(). Ela recebe como argumentos duas ou mais sequências e as compacta em um objeto zip que é um iterador de tuplas, onde cada tupla contem um elemento de cada sequência usada como parâmetro. Ela retorna um iterador de tuplas, onde a i-ésima tupla contém o i-ésimo elemento de cada uma das sequências ou iteráveis fornecidas em seu argumento. Se as sequências não tem o mesmo comprimento o iterador retornado tem o comprimento da menor. Se nenhum argumento for fornecido ela retorna um iterador vazio. Com um único argumento iterável, ele retorna um iterador de tuplas de um elemento.

» seq1 = 'Blade'
» seq2 = 'Runner'
» zipado = zip(seq1, seq2)
» print(zipado)
↳ <zip object at 0x7f27c9f77500>

# use a função tuple() para visualizar o objeto zip
» print(tuple(zipado))
↳ (('B', 'R'), ('l', 'u'), ('a', 'n'), ('d', 'n'), ('e', 'e'))

# usando a mesma variável já definida
» for letra1, letra2 in zipado:
»     print(letra1, letra2)
↳ B R
↳ l u
↳ a n
↳ d n
↳ e e

# um objeto zip pode ser passado como argumento na construção
# de uma lista gerando uma lista de tuplas
» s1 = [1, 2, 3]
» s2 = ['a', 'b', 'c']
» list(zip(s1, s2))
↳ [(1, 'a'), (2, 'b'), (3, 'c')]

# várias sequências podem ser fornecidas
» s1 = 'Margem'
» s2 = 'direita'
» s3 = 'do rio'
» zip123 = zip(s1, s2, s3)
» for a,b,c in zip123:
»     print(a, b, c)
↳ M d d
↳ a i o
↳ r r
↳ g e r
↳ e i i
↳ m t o

No útimo exemplo a sequência s2 tem maior comprimento que as demais. A letra final ‘a’ foi ignorada.

Suponha que desejamos analisar duas sequências e verificar se elas possuem elementos iguais em uma mesma posição. No exemplo abaixo fazemos este teste usando zip() e definindo uma função que retorna True se houver essa coincidência. Observe que a função é abandonada na ocorrência da primeira coincidência.

Pode ocorrer que precisamos saber qual é a posição desse (ou de vários elementos comuns). Para isso usamos enumerate(). Ela tem a seguinte forma:

enumerate(iteravel, inicio=0)

onde iteravel é uma sequência ou qualquer objeto iterável. Ela retorna pares iteráveis contendo o elemento e sua posição, começando em início (com default 0) e o elemento da sequência.

# função que retorna True se houver coincidência de elementos (ou False se não houver)
» def mesmo_elemento(t1, t2):
» for i, j in zip(t1, t2):
»     if i == j:
»         return True
» return False

» t1 = [1,2,3,4,5,6,7,8,9]
» t2 = [9,8,7,6,5,4,3,2,1]
» mesmo_elemento(t1,t2)
↳ True

# Para informar a posição da coincidência usamos enumerate
» for index, element in enumerate('abc'):
»     print(index, element)
↳ 0 a
↳ 1 b
↳ 2 c

» type(enumerate('abdc'))
↳ enumerate

# Repetindo o exemplo anterior, queremos descobrir se duas sequências
# possuem elementos comuns na mesma posição, e que posição é essa

» def posicao_elemento(t1, t2):
»     ''' retorna dictionary com indice e elemento onde elemento de t1 é igual ao de t2 '''
»     dict = {}
»     if len(t2) < len(t1):
»         (t1, t2) = (t2, t1)
»     for index, i in enumerate(t1):
»         if i == t2[index]:
»             dict[index] = i
»     return dict

» t2 = [1,2,3,4,5,6,7,6,9]
» t1 = [9,8,7,4,3,4,5,6,7,8,9,0,23,45]
» posicao_elemento(t1,t2)
↳ {3: 4, 7: 6}

» t1 = 'josefina'
» t2 = 'gasolina cara'
» posicao_elemento(t1,t2)
↳ {2: 's', 5: 'i', 6: 'n', 7: 'a'}

A linha if len(t2) < len(t1): (t1, t2) = (t2, t1) garante que t2 tenha o maior comprimento. Caso contrário o teste i == t2[index] poderia poderia dar erro ao tentar obter elemento inexistente de t2.

Compreensão de Listas (List Comprehensions)


Suponha que queremos uma lista com os quadrados de todos os números de 0 até 10. Podemos conseguir isso usando um laço for:

» quadrados = []
» for i in range(11):
»     quadrados.append(i**2)
»
» quadrados
↳ [0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

Há uma forma alternativa no Python para conseguir o mesmo resultado chamada compreensão de listas (list comprehensions).
Sua sintaxe básica é

[expressão(item) for item in lista if condição].

» quads = [i**2 for i in range(1, 11)]
» quads
↳ [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

Essa forma permite que se escreva um código mais compacto, legível e eficiente (portanto de mais rápida execução).
A expressão pode ser uma função do usuário. No exemplo usamos a função já definida anteriormente para o cálculo de fatoriais:

» def fatorial(n):
»     if n <= 1: return 1
»     else: return n*fatorial(n-1)
»
# exibindo os fatoriais de ímpares de 1 a 9
» fats = [fatorial(i) for i in [1, 3, 5, 7, 9]]
» fats
↳ [1, 6, 120, 5040, 362880]

# atuando sobre strings
» palavras = ['Organização', 'Nações', 'Unidas']
» letras = [l[0] for l in palavras]
» letras
↳ ['O', 'N', 'U']

» tamanho = [len(p) for p in palavras]
» tamanho
↳ [11, 6, 6]

Uma ou mais condições pode ser incluída na lista a ser iterada.

[expressao(item) for item in lista if condição1 if condição2 ...]

No exemplo abaixo usamos i%2, que é o resto da divisão de i por 2. Se esse resto for 0 o número é par.

# ex.: lista dos números pares de 0 até 10, exclusive
# i%2 == 0 significa que o número é par
» num = [i for i in range(10) if i%2==0 ]
» print(num)
↳ [0, 2, 4, 6, 8]

# um exemplo com strings: apenas as palavras com comprimento maior que 5
» palavras = ['casa', 'aleatório', 'rudimentar', 'longo']
» [p for p in palavras if len(p) > 5 ]
↳ ['aleatório', 'rudimentar']

# Várias condições podem ser impostas simultaneamente
# São retornados os múltiplos de 2 e 3 simultaneamente, i.e. os múltiplos de 6.
» print([i for i in range(50) if i%2==0 if i%3==0])
↳ [0, 6, 12, 18, 24, 30, 36, 42, 48]

# a expressão na lista pode conter 'if else':
# a abaixo expressão retorna 'abacaxi' para números pares,
# 'laranja' para múltiplos de 3 e 'caqui' para todos os demais
» fruits = ['abacaxi' if i%2==0 else 'laranja' if i%3==0 else 'caqui' for i in range(10)]
» print(fruits)
↳ ['abacaxi', 'caqui', 'abacaxi', 'laranja', 'abacaxi', 'caqui', 'abacaxi', 'caqui', 'abacaxi', 'laranja']

# As listas podem ser aninhadas. Abaixo a lista externa percorre letras,
# a lista interna percorre números
» matriz = [[i+j for i in 'abcd'] for j in '1234']
» matriz
↳ [['a1', 'b1', 'c1', 'd1'],
   ['a2', 'b2', 'c2', 'd2'],
   ['a3', 'b3', 'c3', 'd3'],
   ['a4', 'b4', 'c4', 'd4']]

Um teste pode ser negado com o operador lógico not (que inverte o booleano). Usamos também o método string.isalpha() que retorna True se string for formado apenas de caracteres (sem dígitos ou pontuações).

# teste not in
# isalpha() retorna True se a string é composta de caracteres alfabéticos
» vogais = {'a', 'e', 'i', 'o', 'u'}
» texto = 'Aprenda todas as regras e transgrida algumas 1234.'
» letras = set(texto.lower())
» consoantes = {letra for letra in letras if letra not in vogais if letra.isalpha()}
» consoantes
↳ {'d', 'g', 'l', 'm', 'n', 'p', 'r', 's', 't'}

# na expressão seguinte um set de tuplas é gerado
{(i, j) for i in range(3, 5) for j in range(2)}
↳ {(3, 0), (3, 1), (4, 0), (4, 1)}

» print([i+j for i in 'abcd' for j in 'efgh'])
↳ ['ae', 'af', 'ag', 'ah', 'be', 'bf', 'bg', 'bh', 'ce', 'cf', 'cg', 'ch', 'de', 'df', 'dg', 'dh']

» substantivo = ['pão', 'pé', 'carro', 'bolo', 'mato']
» adjetivo = ['pequeno', 'bonito', 'bom', 'caro']
» ['%s %s' % (s, a) for s in substantivo for a in adjetivo if s[0] == a[0]]
↳ ['pão pequeno', 'pé pequeno', 'carro caro', 'bolo bonito', 'bolo bom']

# Podemos calcular o produto escalar de 2 vetores
» u = [1,3,4,5,6]
» v = [-1,0,9,-3,2]
» sum([a+b for a,b in zip(u,v)])
↳ 26

No cálculo do produto escalar zip(u,v) contém 5 pares que são somados formando uma lista de 5 elementos. A função sum(lista) soma esses 5 elementos.

Compreensão com dicionários

Compreensão de listas podem ser aplicadas a dicionários, alterando tanto suas chaves como valores. Lembrando dic.items() retorna uma tupla chave:valor do dicionário.

# uma compreensão para duplicar os valores em um dicionário
» dic1 = {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5}
» duplo_dic1 = {k:v*2 for (k,v) in dic1.items()}
» duplo_dic1
↳ {'a': 2, 'b': 4, 'c': 6, 'd': 8, 'e': 10}

# altera texto nas chaves
» R_dic1 = {'R'+k : v*2 for (k,v) in dic1.items()}
» R_dic1
↳ {'Ra': 2, 'Rb': 4, 'Rc': 6, 'Rd': 8, 'Re': 10}

» s_dic = {l.upper(): l*3 for l in 'dna'}
» print (s_dic)
↳ {'D': 'ddd', 'N': 'nnn', 'A': 'aaa'}

# queremos construir um dicionário onde a chave é um
# par entre 1 e 10, inclusive, e o valor é seu quadrado.
# Obs. percorrer i = 1, ..., 5 e tomar seu dobro garante que usamos apenas os pares
» dic_2 = {}
» for i in range(1,6):
»     dic_2[i*2] = (i*2)**2
» dic_2
↳ {2: 4, 4: 16, 6: 36, 8: 64, 10: 100}

# alternativamente, com compreensão
» dic_3 = {n:n**2 for n in range(1,11) if n%2 == 0}
» dic_3
↳ {2: 4, 4: 16, 6: 36, 8: 64, 10: 100}

# o mesmo resultado será obtido com
» dic_4 = {2*n:(2*n)**2 for n in range(1,6)}
» dic_4
↳ {2: 4, 4: 16, 6: 36, 8: 64, 10: 100}

# Podemos usar compreensões para filtrar elementos de um dicionário
» dic_4a = {k:v for (k,v) in dic_4.items() if v>36}
» dic_4a
↳ {8: 64, 10: 100}

» dic_4b = {k:v for (k,v) in dic_4.items() if v>16 if v<100}
» dic_4b
↳ {6: 36, 8: 64}

# duas condições podem ser usadas
» dic_4b = {k:v for (k,v) in dic_4.items() if v>16 if v<100}
» dic_4b
↳ {6: 36, 8: 64}

# duas condições, sobre chave e valor
» dic_4c = {k:v for (k,v) in dic_4.items() if v>15 if v<90}
» dic_4c
↳ {4: 16, 6: 36, 8: 64}

# o valor é (im)par para chave (im)par
» dic_par_impar = {i:('par' if i%2==0 else 'impar') for i in range(5)}
» dic_par_impar
↳ {0: 'par', 1: 'impar', 2: 'par', 3: 'impar', 4: 'par'}

Algumas vezes pode ser útil usar zip() para construir dicionários através de uma compreensão.

# Lists to represent keys and values
» keys = [101, 201, 301, 401, 501]
» values = ['inglês', 'francês', 'alemão', 'russo', 'espanhol']

# keys e values são colocados em um único iterável (um objeto zip)
» dic_5 = { k:v for (k,v) in zip(keys, values)}
» print(dic_5)
↳ {101: 'inglês', 201: 'francês', 301: 'alemão', 401: 'russo', 501: 'espanhol'}

# Claro que o iterável poderia ser dado diretamente como parâmetro do construtor
» dic_6 = dict(zip(keys, values))
» print(dic_6)
↳ {101: 'inglês', 201: 'francês', 301: 'alemão', 401: 'russo', 501: 'espanhol'}

# dicionários aninhados
» dicionario = {i: {j: i*j for j in range(1, 6)} for i in range(2, 5)}
» print(dicionario)
↳ {2: {1: 2, 2: 4, 3: 6, 4: 8, 5: 10}, 3: {1: 3, 2: 6, 3: 9, 4: 12, 5: 15}, 4: {1: 4, 2: 8, 3: 12, 4: 16, 5: 20}}

Nesse dicionário as chaves vão de 2 até 4 e seus valores são outros dicionários.

Compreensões de listas e de dicionários são formas elegantes e de fácil leitura. No entanto, se forem muito complexas elas podem tornar o código difícil de ler e de debugar (encontrar eventuais erros). Devemos nos lembrar que outras pessoas podem necessitar ler o nosso código, ou você mesmo, daqui a um tempo, quando dificilmente se lembrará do raciocínio que te levou à construção de bloco sofisticados e elegantes.

Função filter()


A função tem a forma de

filter(funcao, sequencia)

e constroi um iterador usando uma função booleana (que retorna True ou False) e uma sequência ou um iterável qualquer. Ela testa cada elemento da sequência usando a função e retorna apenas os elementos avaliados como True.

» idades = [5, 12, 17, 18, 24, 32]
» def maior(x):
»     if x < 18:
»         return False
»     else:
»         return True

» adultos = filter(maior, idades)
» for x in adultos:
»     print(x)
↳ 18
↳ 24
↳ 32

# observe que a função acima poderia ser escrita simplesmente como
» def maior(x):
»     return x ≥ 18

No próximo exemplo temos uma lista de dicionários. Cada entrada dessa lista é um dicionário contendo dados sobre um animal. O código define uma função pesquisa que recebe essa lista e procura quais de suas entradas contém algum valor com o substring busca.

» bicharada = [
»   {'nome': 'Asdrubal', 'especie': 'tubarão', 'peso': '290', 'habitat': 'oceano'},
»   {'nome': 'Ana Lee', 'especie': 'caranquejo', 'peso': '1.2', 'habitat': 'oceano'},
»   {'nome': 'Raul', 'especie': 'urso', 'peso': '180', 'habitat': 'floresta'},
»   {'nome': 'Joana', 'especie': 'raposa', 'peso': '21', 'habitat': 'floresta'},
»   {'nome': 'Omar Lee', 'especie': 'golfinho', 'peso': '120', 'habitat': 'oceano'},
»   {'nome': 'Tulio', 'especie': 'rato', 'peso': '1.4', 'habitat': 'doméstico'}
» ]

» def pesquisa(lista, busca):
»     def meu_iterador(x):
»         for v in x.values():
»             if busca in v:
»                 return True
»         return False
»     return filter(meu_iterador, lista)

» lista_filtrada = pesquisa(bicharada, 'tuba')
» for t in lista_filtrada:
»     print(t)
↳ {'nome': 'Asdrubal', 'especie': 'tubarão', 'peso': '290', 'habitat': 'oceano'}

» lista_filtrada = pesquisa(bicharada, 'Lee')
» for t in lista_filtrada:
»     print(t)
↳ {'nome': 'Ana Lee', 'especie': 'caranquejo', 'peso': '1.2', 'habitat': 'oceano'}
↳ {'nome': 'Omar Lee', 'especie': 'golfinho', 'peso': '120', 'habitat': 'oceano'}

Sugestão: altere o código acima para encontrar parte de um valor dentro de um campo especificado. Encontre o animal cujo habitat contém a substring ‘ocea’.

Iteradores (iterators)

No Python um iterador é um objeto que contém um número contável de elementos que podem ser iterados, ou seja, que podem ser acessados e lidos um de cada vez. Além dos laços for e das compreensões de listas e sequências no Python podemos utilizar iteradores para percorrer seuências. Eles pode ser construídos com a função

nome_iterador = iter(sequencia, marcador).

O parâmetro marcador é opcional e …..

O iterador tem o método _next()_ que fornece o próximo elemento. Uma exceção StopIteration é lançada no final, quando todos os elementos foram percorridos. O iterador é esgotado e, portanto, só pode ser percorrido uma vez.

Exemplos de uso estão no código abaixo. Lista são convertidas em iteradores, que são percorridos com next().
Se a iteração não é interrompida antes do fim um erro é lançado. Um erro é gerado no final e sua captura é usada para interromper o laço.

# Uma lista é convertida em um iterador
lista = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
it = iter(lista)
for k in range(len(lista)):
    print(next(it), end=' ')
a b c d e f g h

# queremos separar números positivos e negativos de uma lista
# o erro no final da iteração é usado para interromper o laço
» original = [1,6,-8,-3,0, -2, 9, 76, -45]
» positivos = []
» negativos = []
» t = iter(original)
» try:
»     while True:
»         x = next(t)
»         if x >= 0:
»             positivos.append(x)
»         else:
»             negativos.append(x)
» except StopIteration:
»     print('São positivos:' , positivos, '\nSão negativos:' , negativos)

↳ São positivos: [1, 6, 0, 9, 76]
↳ São negativos: [-8, -3, -2, -45]

Embora a mesma funcionalidade possa ser conseguida de formas diferentes a transformação de um objeto iterável em um iterador é bastante útil para se escrever classes e métodos definidos pelo programador, como veremos.

Funções Lambda

Funções são boas formas de escrever código de fácil reutilização. No entanto, algumas vezes precisamos de definir um operador de forma compacta e que será utilizado apenas naquele ponto do código. As funções lambda (ou funções anônimas) permitem a criação de funções (que sequer recebem um nome) mas agem da forma desejada sobre seus argumentos.

Funções lambda tem uma sintaxe concisa:

lambda <arg1, ..., argn> : < expressão >

Elas só podem conter uma única expressão, nenhuma declaração, deve ser escrita em linha única e não comportam anotações. São exemplos simples de funções lambda:

# uma função bem simples é a identidade.
# a definição usual seria
» def id(x):
»     return x

# usando função lambda
» i = lambda x: x
» i(9)
↳ 9

# a função não precisa ser associada a uma variável
» (lambda x: abs(x))(-9)
↳ 9

# outros ex. Soma e multiplicação
» x = lambda a, b : a + b
» x(5,7)
↳ 12

# múltiplos argumentos não são envoltos em parênteses
» exp = lambda x,y : x**y
» exp(2,3)
↳ 8

» mix = lambda x,y,z : (x**y)/z
» mix(3,4,5)
↳ 16.2

# qualquer tipo de objeto pode ser passado como argumento
# no exemplo uma tupla é passada
» u = (4,7)
» (lambda x: x[0]*x[1])(u)
↳ 28

# uma função lamba que age sobre strings
# .title torna o primeiro caracter maiúsculo
» nome_autor = lambda nome, sobrenome: f'Autor: {nome.title()} {sobrenome.title()}'
» nome_autor('richard','dawkins')
↳ 'Autor: Richard Dawkins'

Um exemplo de uso frequente consiste no uso de lambdas para fornecer um critério de ordenamento.
Vimos que a função sorted(sequencia, key) pode ordenar uma sequência de acordo com um critério dado em key. No caso abaixo a cada tupla é associado o produto de seus pares, e a ordenação é feita nessa métrica.

» p = [(3, 3), (4, 2), (2, 2), (5, 2), (1, 7)] 
» sorted(p, key=lambda x: x[0]*x[1])
↳ [(2, 2), (1, 7), (4, 2), (3, 3), (5, 2)]

# dados vetores no espaço, ordená-los em ordem crescente de módulo
# a função lambda calcula o quadrado do módulo
» v =[(1,5,3), (1,8,2), (7,8,9), (2,4,3), (2,2,-1)]
» sorted(v, key=lambda x: x[0]**2+x[1]**2+x[2]**2)
↳ [(2, 2, -1), (2, 4, 3), (1, 5, 3), (1, 8, 2), (7, 8, 9)]

No exemplo seguinte definimos uma função que recebe um argumento e opera nele com uma função lambda. Essa última, por sua vez, recebe outro argumento. O exemplo também ilustra o fato de que uma função é um objeto e pode ser atribuída à uma variável.

# queremos uma função que multiplica seu argumento por um número especificado
# n pode ser visto como um parâmetro
» def vezes_n(n):
»     return lambda a : a * n

» funcao_duplicar = vezes_n(2)
» funcao_triplicar = vezes_n(3)
» funcao_x1000 = vezes_n(1000)

» funcao_duplicar(23)
↳ 46

» funcao_triplicar(15)
↳ 45

» funcao_x1000(12.345)
↳ 12345.0

A minha_funcao abaixo é uma função lambda com dois argumentos sendo um deles outra função. No primeiro caso 2 + 2*10 = 22. No segundo caso 4 + (4)**3 -6 = 62.

» minha_funcao = lambda x, f: x + f(x)
» minha_funcao(2, lambda x: x*10 )
↳ 22
» minha_funcao(4, lambda x: x**3-6)
↳ 62

Função map()

Lambdas são muito usadas junto com a função map() que tem a seguinte assinatura:

map(funcao, iteravel1, ..., iteraveln).

Os dois argumentos são obrigatórios. A função percorre os iteráveis fornecidas usando a função especificada. Essa função deve usar um elemento de cada iterável. Ela usa um algoritmo otimizado para realizar sua transformação, o que a torna mais eficiente e rápida do que laços usuais do Python.

Ela retorna um objeto iterável map que pode ser convertido em lista com list(). Alguns exemplos:

# a operação abaixo concatena duas strings
» def concatena(a, b):
»     return (a + ' ' + b)
» iter1 = ('banana', 'laranja', 'uva')
» iter2 = ('nanica', 'bahia', 'syrah')
» x = map(concatena, iter1, iter2)

# para ver o resultado podemos percorrer a iterável gerada
# cada i no laço é uma única string (como 'banana nanica')
» for i in x:
»     print(i)
↳ banana nanica
↳ laranja bahia
↳ uva syrah

# a funcão pow(x,y) retorna x^y (x elevado a y)
# duas sequências devem ser dadas em map
» y = map(pow, [6,7,8],[3,2,1])
» for i in y:
»     print(i, end=' - ')
↳ 216 - 49 - 8 -

# a função pode ser uma lambda
# duplicar cada elemento da lista
» lista = [10, 25, 17, 9, 30, -5]
» lista_2 = map(lambda n : n*2, lista)
# para exibir a sequência retornada podemos transformá-la em uma lista
» list(lista_2)
↳ [20, 50, 34, 18, 60, -10]

O uso de funções lambda pode tornar mais compacto o uso de map.

# para retornar uma lista de quadrados
» def quadrado(n):
»     return n**2
» numeros = [1, 2, 3, 4, 5]
» quadrados = map(quadrado, numeros)
» list(quadrados)
↳ [1, 4, 9, 16, 25]

# o mesmo resultado pode ser conseguido usando lambdas
» list(map(lambda x:x**2, range(1,6)))
↳ [1, 4, 9, 16, 25]

# abs() é função interna, que retorna valor absoluto
» nums = [-5, -3, -1, 0, 1, 3, 5]
» absolutos = list(map(abs, nums))
» absolutos
↳ [5, 3, 1, 0, 1, 3, 5]

# somar e multiplicar com 3 listas
» list(map(lambda i, j, k: i+j*k, [2, 4], [1, 3], [7, 8]))
↳ [9, 28]

# strip() remove string dados nas extremidades da string
» traco = ['---12356.876-', '-casa-da-vó---', '-mil-', '---']
» list(map(lambda s: s.strip("-"), traco))
↳ ['12356.876', 'casa-da-vó', 'mil', '']

# função recebe um número e retorna tuplas de 3 elementos
» def pot_1_2_3(x):
»     return x, x ** 2, x ** 3
» numeros = [1, 2, 3, 4]
» list(map(pot_1_2_3, numeros))
↳ [(1, 1, 1), (2, 4, 8), (3, 9, 27), (4, 16, 64)]

# a mesma coisa usando lambda
» list(map(lambda x: (x, x**2, x**3), numeros))
↳ [(1, 1, 1), (2, 4, 8), (3, 9, 27), (4, 16, 64)]

Em muitos casos uma compreensão de lista faz o mesmo que uma iteração via map(). No entanto é útil conhecer a função não apenas para poder ler e compreender código escrito por outras pessoas mas também para eventuais situações onde ela pode ser mais simples ou mais eficaz.

🔺Início do artigo

Bibliografia

Consulte a bibliografia no final do primeiro artigo dessa série.