Python a partir do R I: importando pacotes (e por que aprender novas linguagens é difícil)

python
Autor

Alberson Miranda

Data de Publicação

12 de junho de 2021

Resumo

Ao aprender uma nova linguagem de programação, simplesmente buscar código equivalente para as práticas que você já tem pode ser enganoso. Aqui vemos que um equivalente à chamada library() do R é, na verdade, considerada uma má prática em Python e, se você fizer isso em uma entrevista de emprego, não espere ser chamado de volta.

1 Motivação

Me ocorreu uma analogia sobre aprender um idioma estrangeiro: é impossível aprender uma nova língua traduzindo palavra por palavra. Não é apenas uma questão de vocabulário. Cada idioma tem sua própria gramática, verbos frasais, dicção, expressões, ritmo etc. Esse tipo de questão também aparece ao aprender uma nova linguagem de programação e acho que importar pacotes é um bom, ainda que simples, exemplo disso.

2 Chamando Uma Função De Um Pacote

2.1 Experiência Em R

No R, todo pacote instalado nas árvores de bibliotecas é listado sempre que um terminal é aberto. Esses pacotes listados estão disponíveis para os usuários o tempo todo e podem ser chamados explicitamente. Por exemplo:

2.1.1 Caso 1: Chamada Explícita

# procurar por medidas de machine learning que contenham "AUC" no pacote {mlr3}
mlr3::mlr_measures$keys("auc")
[1] "classif.auc"       "classif.mauc_au1p" "classif.mauc_au1u"
[4] "classif.mauc_aunp" "classif.mauc_aunu" "classif.mauc_mu"  
[7] "classif.prauc"    

Mas esse modo de chamar funções geralmente só é usado se aquele pacote não será necessário com frequência. Caso contrário, é cultural para usuários de R carregar e anexar todo o namespace do pacote ao search path1 com uma chamada library().

2.1.2 Caso 2: Anexando

# cansado: chamada explícita do {ggplot2}
t1 = mtcars |>
  dplyr::mutate(hp_by_cyl = hp / cyl)

# animado: anexando o namespace do {ggplot2}
library(dplyr)

Attaching package: 'dplyr'
The following objects are masked from 'package:stats':

    filter, lag
The following objects are masked from 'package:base':

    intersect, setdiff, setequal, union
t2 = mtcars |>
  mutate(hp_by_cyl = hp / cyl)

# são equivalentes?
all.equal(t1, t2)
[1] TRUE

O problema aparece quando há conflitos de namespace. Você notou o aviso sobre objetos sendo mascarados de {stats} e {base}? Normalmente, os usuários simplesmente ignoram avisos de inicialização 😨 e isso pode eventualmente levar a resultados inconsistentes ou erros difíceis de depurar.

Isso pode ser evitado anexando apenas as funções específicas que você realmente vai usar:

2.1.3 Caso 3: Anexando Funções Específicas

# desanexando dplyr
detach("package:dplyr")

# anexando apenas mutate():
library(dplyr, include.only = "mutate")

E nenhum aviso de conflito será disparado. Infelizmente, não ouço muito falar do argumento include.only na comunidade R 🤷‍♂. Pelo contrário, meta pacotes como o {tidyverse}, que carregam e anexam MUITAS coisas ao namespace — muitas vezes desnecessárias para o que você vai fazer —, são bastante comuns.

2.2 Experiência Em Python

Todos os 3 casos citados antes são possíveis em Python, mas os padrões da comunidade são bem diferentes. Especialmente em relação à consciência do que está carregado no namespace — ou symbol table, como é chamado em Python2.

Primeiro, pacotes instalados não ficam imediatamente disponíveis. Então, se eu tentar, por exemplo, listar funções/métodos/atributos do {pandas}, resultará em erro:

# inspecionando módulos em {pandas}
import pandas
dir(pandas)
['ArrowDtype', 'BooleanDtype', 'Categorical', 'CategoricalDtype', 'CategoricalIndex', 'DataFrame', 'DateOffset', 'DatetimeIndex', 'DatetimeTZDtype', 'ExcelFile', 'ExcelWriter', 'Flags', 'Float32Dtype', 'Float64Dtype', 'Grouper', 'HDFStore', 'Index', 'IndexSlice', 'Int16Dtype', 'Int32Dtype', 'Int64Dtype', 'Int8Dtype', 'Interval', 'IntervalDtype', 'IntervalIndex', 'MultiIndex', 'NA', 'NaT', 'NamedAgg', 'Period', 'PeriodDtype', 'PeriodIndex', 'RangeIndex', 'Series', 'SparseDtype', 'StringDtype', 'Timedelta', 'TimedeltaIndex', 'Timestamp', 'UInt16Dtype', 'UInt32Dtype', 'UInt64Dtype', 'UInt8Dtype', '__all__', '__builtins__', '__cached__', '__doc__', '__docformat__', '__file__', '__git_version__', '__loader__', '__name__', '__package__', '__path__', '__spec__', '__version__', '_built_with_meson', '_config', '_is_numpy_dev', '_libs', '_pandas_datetime_CAPI', '_pandas_parser_CAPI', '_testing', '_typing', '_version_meson', 'annotations', 'api', 'array', 'arrays', 'bdate_range', 'compat', 'concat', 'core', 'crosstab', 'cut', 'date_range', 'describe_option', 'errors', 'eval', 'factorize', 'from_dummies', 'get_dummies', 'get_option', 'infer_freq', 'interval_range', 'io', 'isna', 'isnull', 'json_normalize', 'lreshape', 'melt', 'merge', 'merge_asof', 'merge_ordered', 'notna', 'notnull', 'offsets', 'option_context', 'options', 'pandas', 'period_range', 'pivot', 'pivot_table', 'plotting', 'qcut', 'read_clipboard', 'read_csv', 'read_excel', 'read_feather', 'read_fwf', 'read_gbq', 'read_hdf', 'read_html', 'read_json', 'read_orc', 'read_parquet', 'read_pickle', 'read_sas', 'read_spss', 'read_sql', 'read_sql_query', 'read_sql_table', 'read_stata', 'read_table', 'read_xml', 'reset_option', 'set_eng_float_format', 'set_option', 'show_versions', 'test', 'testing', 'timedelta_range', 'to_datetime', 'to_numeric', 'to_pickle', 'to_timedelta', 'tseries', 'unique', 'util', 'value_counts', 'wide_to_long']

Você pode checar a symbol table com o seguinte comando.

# o que está anexado à symbol table?
print(*globals(), sep = "\n")
__name__
__doc__
__package__
__loader__
__spec__
__annotations__
__builtins__
r
pandas

Dependendo do sistema/ferramentas que você está usando, o interpretador Python vai carregar alguns módulos ou não. Se você iniciar um REPL — um terminal interativo Python —, nenhum módulo será carregado. Se iniciar um Jupyter notebook, alguns módulos necessários para ele rodar serão carregados. Neste caso, como estou rodando Python a partir do R via {reticulate}, alguns módulos foram carregados:

  • sys: para acesso a variáveis e funções usadas pelo interpretador
  • os: para rotinas do sistema operacional NT ou Posix

Então, se quero trabalhar com {pandas}, preciso anexá-lo à symbol table com um equivalente ao library() do R. E assim como sua função prima, o import do Python também tem diferentes formas.

Primeiro, import pandas torna o pacote disponível para chamadas explícitas.

# importar pandas
import pandas

# o que está anexado à symbol table?
print(*globals(), sep = "\n")
__name__
__doc__
__package__
__loader__
__spec__
__annotations__
__builtins__
r
pandas

Note que apenas {pandas} está anexado à symbol table, não suas funções/métodos/atributos. Portanto, essa instrução não é equivalente ao library(). Para criar um dataframe simples com {pandas}:

2.2.1 Caso 1: Chamada Explícita

# isso resultará em NameError: name 'DataFrame' is not defined
DataFrame(
  {
    "capital": ["Vitoria", "São Paulo", "Rio de Janeiro"],
    "state": ["Espírito Santo", "São Paulo", "Rio de Janeiro"]
  }
)
NameError: name 'DataFrame' is not defined
# isso funciona
pandas.DataFrame(
  {
    "capital": ["Vitoria", "São Paulo", "Rio de Janeiro"],
    "state": ["Espírito Santo", "São Paulo", "Rio de Janeiro"]
  }
)
          capital           state
0         Vitoria  Espírito Santo
1       São Paulo       São Paulo
2  Rio de Janeiro  Rio de Janeiro

Se quisermos replicar o comportamento do library() (ou seja, carregar e anexar todas as funções/métodos/atributos do {pandas} à symbol table), então:

2.2.2 Caso 2: Anexando

# importando todo o {pandas} para a symbol table
from pandas import *

# symbol table atualizada
print(*globals(), sep = "\n")
__name__
__doc__
__package__
__loader__
__spec__
__annotations__
__builtins__
r
pandas
ArrowDtype
BooleanDtype
Categorical
CategoricalDtype
CategoricalIndex
DataFrame
DateOffset
DatetimeIndex
DatetimeTZDtype
ExcelFile
ExcelWriter
Flags
Float32Dtype
Float64Dtype
Grouper
HDFStore
Index
IndexSlice
Int16Dtype
Int32Dtype
Int64Dtype
Int8Dtype
Interval
IntervalDtype
IntervalIndex
MultiIndex
NA
NaT
NamedAgg
Period
PeriodDtype
PeriodIndex
RangeIndex
Series
SparseDtype
StringDtype
Timedelta
TimedeltaIndex
Timestamp
UInt16Dtype
UInt32Dtype
UInt64Dtype
UInt8Dtype
api
array
arrays
bdate_range
concat
crosstab
cut
date_range
describe_option
errors
eval
factorize
get_dummies
from_dummies
get_option
infer_freq
interval_range
io
isna
isnull
json_normalize
lreshape
melt
merge
merge_asof
merge_ordered
notna
notnull
offsets
option_context
options
period_range
pivot
pivot_table
plotting
qcut
read_clipboard
read_csv
read_excel
read_feather
read_fwf
read_gbq
read_hdf
read_html
read_json
read_orc
read_parquet
read_pickle
read_sas
read_spss
read_sql
read_sql_query
read_sql_table
read_stata
read_table
read_xml
reset_option
set_eng_float_format
set_option
show_versions
test
testing
timedelta_range
to_datetime
to_numeric
to_pickle
to_timedelta
tseries
unique
value_counts
wide_to_long
# agora isso funciona
DataFrame(
  {
    "capital": ["Vitoria", "São Paulo", "Rio de Janeiro"],
    "state": ["Espírito Santo", "São Paulo", "Rio de Janeiro"]
  }
)
          capital           state
0         Vitoria  Espírito Santo
1       São Paulo       São Paulo
2  Rio de Janeiro  Rio de Janeiro

Mas você não verá nenhum usuário experiente de Python fazendo isso, pois eles se preocupam em carregar muitos nomes na symbol table e os possíveis conflitos que isso pode causar. Uma abordagem aceitável seria anexar apenas alguns nomes frequentes, como em:

2.2.3 Caso 3: Anexando Funções Específicas

# desanexando {pandas}
for name in vars(pandas):
    if not name.startswith('_'):
        del globals()[name]
KeyError: 'annotations'
# anexando apenas DataFrame()
from pandas import DataFrame

# symbol table atualizada
print(*globals(), sep = "\n")
__name__
__doc__
__package__
__loader__
__spec__
__annotations__
__builtins__
r
pandas
ArrowDtype
BooleanDtype
Categorical
CategoricalDtype
CategoricalIndex
DataFrame
DateOffset
DatetimeIndex
DatetimeTZDtype
ExcelFile
ExcelWriter
Flags
Float32Dtype
Float64Dtype
Grouper
HDFStore
Index
IndexSlice
Int16Dtype
Int32Dtype
Int64Dtype
Int8Dtype
Interval
IntervalDtype
IntervalIndex
MultiIndex
NA
NaT
NamedAgg
Period
PeriodDtype
PeriodIndex
RangeIndex
Series
SparseDtype
StringDtype
Timedelta
TimedeltaIndex
Timestamp
UInt16Dtype
UInt32Dtype
UInt64Dtype
UInt8Dtype
api
array
arrays
bdate_range
concat
crosstab
cut
date_range
describe_option
errors
eval
factorize
get_dummies
from_dummies
get_option
infer_freq
interval_range
io
isna
isnull
json_normalize
lreshape
melt
merge
merge_asof
merge_ordered
notna
notnull
offsets
option_context
options
period_range
pivot
pivot_table
plotting
qcut
read_clipboard
read_csv
read_excel
read_feather
read_fwf
read_gbq
read_hdf
read_html
read_json
read_orc
read_parquet
read_pickle
read_sas
read_spss
read_sql
read_sql_query
read_sql_table
read_stata
read_table
read_xml
reset_option
set_eng_float_format
set_option
show_versions
test
testing
timedelta_range
to_datetime
to_numeric
to_pickle
to_timedelta
tseries
unique
value_counts
wide_to_long
name

Segundo o The Hitchhiker’s Guide to Python [@pythonguide], caso 2 é o pior cenário possível e geralmente é considerado má prática, pois “torna o código mais difícil de ler e as dependências menos compartimentalizadas”. Essa afirmação é endossada pela documentação oficial do Python [@pythontutorial]:

Embora certos módulos sejam projetados para exportar apenas nomes que seguem certos padrões quando você usa import *, ainda é considerado má prática em código de produção.

Na opinião dos autores do guia, caso 3 seria uma opção melhor porque destaca nomes específicos3, enquanto caso 1 seria a melhor prática, pois “Ser capaz de dizer imediatamente de onde uma classe ou função vem melhora muito a legibilidade e compreensão do código, exceto nos projetos mais simples de arquivo único.”

Notas de rodapé

  1. Uma lista ordenada onde o R procura por uma função. Pode ser acessada com search().↩︎

  2. Acho? Não sei, ainda estou aprendendo lol 😂↩︎

  3. Python Foundation diz “Não há nada de errado em usar from package import specific_submodule! Na verdade, essa é a notação recomendada, a menos que o módulo importador precise usar submódulos com o mesmo nome de pacotes diferentes.”↩︎