segunda-feira, 5 de abril de 2021

O operador de controle de fluxo match

O Rust tem um operador de fluxo de controle extremamente poderoso, chamado, match que permite comparar um valor com uma série de padrões e, em seguida, executar o código com base nos padrões correspondentes. Os padrões podem ser compostos de valores literais, nomes de variáveis, curingas e muitas outras coisas; O Capítulo 18 cobre todos os diferentes tipos de padrões e o que eles fazem. O poder de match vem da expressividade dos padrões e do fato de que o compilador confirma que todos os casos possíveis foram tratados.

Pense em uma expressão match como uma máquina de separar moedas: as moedas deslizam por uma trilha com orifícios de vários tamanhos ao longo dela, e cada moeda cai pelo primeiro orifício que encontra em que se encaixa. Da mesma forma, os valores passam por cada padrão em match, e no primeiro padrão o valor “se ajusta”, o valor cai no bloco de código associado a ser usado durante a execução.

Como acabamos de mencionar as moedas, vamos usá-las como exemplo usando match! Podemos escrever uma função que pode pegar uma moeda desconhecida dos Estados Unidos e, de maneira semelhante à máquina de contagem, determinar qual moeda ela é e retornar seu valor em centavos, conforme mostrado aqui na Listagem 6-3.

enum Moeda {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn valor_em_centavos(moeda: Moeda) -> u8 {
    match moeda {
        Moeda::Penny => 1,
        Moeda::Nickel => 5,
        Moeda::Dime => 10,
        Moeda::Quarter => 25,
    }
}

fn main() {}

Listagem 6-3: um enum e uma expressão match que tem as variantes do enum como seus padrões.

Vamos decompôr match na função valor_em_centavos. Primeiro, listamos a palavra-chave match seguida por uma expressão, que neste caso é o valor moeda. Isso parece muito semelhante a uma expressão usada com if, mas há uma grande diferença: com if, a expressão precisa retornar um valor booleano, mas aqui pode ser de qualquer tipo. O tipo de moeda neste exemplo é o enum Moeda que definimos na linha 1.

Em seguida estão os braços de match. Um braço tem duas partes: um padrão e algum código. O primeiro braço aqui tem um padrão que é o valor Moeda::Penny e, em seguida, o operador => que separa o padrão e o código a ser executado. O código, neste caso, é apenas o valor 1. Cada braço é separado do próximo por uma vírgula.

Quando a expressão match é executada, ela compara o valor resultante com o padrão de cada braço, em ordem. Se um padrão corresponder ao valor, o código associado a esse padrão será executado. Se esse padrão não corresponder ao valor, a execução continua para o próximo braço, da mesma forma que em uma máquina de classificação de moedas. Podemos ter quantos braços precisarmos: na Listagem 6-3, nossa expressão match tem quatro braços.

O código associado a cada braço é uma expressão e o valor resultante da expressão no braço correspondente é o valor que é retornado para a expressão match inteira.

Os colchetes normalmente não são usados ​​se o código do braço de correspondência for curto, como na Listagem 6-3, onde cada braço apenas retorna um valor. Se você deseja executar várias linhas de código em um braço de correspondência, pode usar chaves. Por exemplo, o código a seguir imprimiria “Lucky penny!” toda vez que o método foi chamado com um, Moeda::Penny mas ainda assim retornaria o último valor do bloco 1:

enum Moeda {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn valor_em_centavos(moeda: Moeda) -> u8 {
    match moeda {
        Moeda::Penny => {
            println!("Moeda da sorte!");
            1
        }
        Moeda::Nickel => 5,
        Moeda::Dime => 10,
        Moeda::Quarter => 25,
    }
}

fn main() {}

Padrões que se ligam a valores

Outro recurso útil dos braços de combinação é que eles podem se vincular às partes dos valores que correspondem ao padrão. É assim que podemos extrair valores de variantes enum.

Como exemplo, vamos alterar uma de nossas variantes de enum para conter os dados dentro dela. De 1999 a 2008, os Estados Unidos cunharam quartos com designs diferentes para cada um dos 50 estados de um lado. Nenhuma outra moeda tem design de estado, então apenas quartos têm esse valor extra. Podemos adicionar essas informações ao nosso enum alterando a variante Quarter para incluir um valor UsState armazenado dentro dela, o que fizemos aqui na Listagem 6-4.

#[derive(Debug)] // para que possamos inspecionar o estado em um minuto
enum UsState {
    Alabama,
    Alaska,
    // --recorte--
}

enum Moeda {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn main() {}

Listagem 6-4: um enum Moeda em que a variante Quarter também possui um valor UsState.

Vamos imaginar que um amigo nosso está tentando coletar todos os 50 bairros estaduais. Enquanto classificamos nosso troco por tipo de moeda, também chamaremos o nome do estado associado a cada trimestre para que, se for um que nosso amigo não tenha, eles possam adicioná-lo à sua coleção.

Na expressão de correspondência para este código, adicionamos uma variável chamada state ao padrão que corresponde aos valores da variante Moeda::Quarter. Quando um Moeda::Quarter corresponde, a variável state se vincula ao valor do estado daquele trimestre. Então, podemos usar state no código para esse braço, assim:

#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
    // --recorte--
}

enum Moeda {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn valor_em_centavos(moeda: Moeda) -> u8 {
    match moeda {
        Moeda::Penny => 1,
        Moeda::Nickel => 5,
        Moeda::Dime => 10,
        Moeda::Quarter(state) => {
            println!("quarter do Estado {:?}!", state);
            25
        }
    }
}

fn main() {
    valor_em_centavos(Moeda::Quarter(UsState::Alaska));
}

Se tivéssemos que chamar valor_em_centavos(Moeda::Quarter(UsState::Alaska)), moeda seria Moeda::Quarter(UsState::Alaska). Quando comparamos esse valor com cada um dos braços do jogo, nenhum deles combina até alcançarmos Moeda::Quarter(state). Nesse ponto, a vinculação para state será o valor UsState::Alaska. Podemos então usar essa ligação na expressão println!, obtendo assim o valor do estado interno da variante enum Moeda para Quarter.

Combinando com Option<T>

Na seção anterior, queríamos obter o valor T interno de Some ao usar Option<T>; também podemos lidar com Option<T> usando match como fizemos com o enum Moeda! Em vez de comparar moedas, compararemos as variantes de Option<T>, mas a forma como a expressão match funciona permanece a mesma.

Digamos que queremos escrever uma função que recebe um Option<i32> e, se houver um valor dentro, adicione 1 a esse valor. Se não houver um valor dentro, a função deve retornar o valor None e não tentar realizar nenhuma operação.

Esta função é muito fácil de escrever, graças a match, e será semelhante à Listagem 6-5.

fn main() {
    fn mais_um(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i + 1),
        }
    }

    let cinco = Some(5);
    let seis = mais_um(cinco);
    let nenhum = mais_um(None);
}

Listagem 6-5: Uma função que usa uma expressão match num Option<i32>.

Vamos examinar a primeira execução de mais_um com mais detalhes. Quando chamamos mais_um(cinco), a variável x no corpo de mais_um terá o valor Some(5). Em seguida, comparamos isso com cada braço de jogo.

fn main() {
    fn mais_um(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i + 1),
        }
    }

    let cinco = Some(5);
    let seis = mais_um(cinco);
    let nenhum = mais_um(None);
}

O valor Some(5) não corresponde ao padrão None, então continuamos para o próximo braço.

fn main() {
    fn mais_um(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i + 1),
        }
    }

    let cinco = Some(5);
    let seis = mais_um(cinco);
    let nenhum = mais_um(None);
}

Some(5) corresponder a Some(i)? Sim, é verdade! Temos a mesma variante. O i vincula-se ao valor contido em Some, portanto, i assume o valor 5. O código no braço de correspondência é então executado, então adicionamos 1 ao valor de i e criamos um novo valor Some com nosso total interno 6.

Agora vamos considerar a segunda chamada de mais_um na Listagem 6-5, onde x é None. Entramos no match e comparamos com o primeiro braço.

fn main() {
    fn mais_um(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i + 1),
        }
    }

    let cinco = Some(5);
    let seis = mais_um(cinco);
    let nenhum = mais_um(None);
}

Corresponde! Não há valor a ser adicionado, então o programa para e retorna o valor None no lado direito de =>. Como o primeiro braço combinou, nenhum outro braço é comparado.

Combinar match e enums é útil em muitas situações. Você verá muito esse padrão no código Rust: match em um enum, vincule uma variável aos dados internos e execute o código com base nele. É um pouco complicado no início, mas quando você se acostumar, vai desejar tê-lo em todas linguagens. É sempre um favorito do usuário.

As partidas são exaustivas

Há um outro aspecto de match que precisamos discutir. Considere esta versão de nossa função mais_um que tem um bug e não compila:

Esse código não compila Esse código não compila.

fn main() {
    fn mais_um(x: Option<i32>) -> Option<i32> {
        match x {
            Some(i) => Some(i + 1),
        }
    }

    let cinco = Some(5);
    let seis = mais_um(cinco);
    let nenhum = mais_um(None);
}

Não tratamos do caso None, então este código causará um bug. Felizmente, é um bug que Rust sabe como detectar. Se tentarmos compilar este código, obteremos este erro:

$ cargo run
   Compiling enums v0.1.0 (file:///projects/enums)
error[E0004]: non-exhaustive patterns: `None` not covered
   --> src/main.rs:3:15
    |
3   |         match x {
    |               ^ pattern `None` not covered
    |
    = help: ensure that all possible cases are being handled, possibly by adding wildcards or more match arms
    = note: the matched value is of type `Option<i32>`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0004`.
error: could not compile `enums`

To learn more, run the command again with --verbose.

Rust sabe que não cobrimos todos os casos possíveis e até sabe qual padrão esquecemos! As correspondências em Rust são exaustivas: devemos esgotar todas as possibilidades para que o código seja válido. Especialmente no caso de Option<T>, quando Rust nos impede de esquecer de lidar explicitamente com o caso None, ele nos protege de assumir que temos um valor quando poderíamos ter nulo, tornando assim impossível o erro de bilhões de dólares discutido anteriormente.

O espaço reservado _

Rust também tem um padrão que podemos usar quando não queremos listar todos os valores possíveis. Por exemplo, a u8 pode ter valores válidos de 0 a 255. Se nos preocupamos apenas com os valores 1, 3, 5 e 7, não queremos ter que listar 0, 2, 4, 6, 8, 9 todo o caminho até 255. Felizmente, não precisamos: podemos usar o padrão especial _ em vez disso:

fn main() {
    let algum_valor_u8 = 0u8;
    match algum_valor_u8 {
        1 => println!("um"),
        3 => println!("tres"),
        5 => println!("cinco"),
        7 => println!("sete"),
        _ => (),
    }
}

O padrão _ corresponderá a qualquer valor. Colocando-o após nossos outros braços, o _irá corresponder a todos os casos possíveis que não foram especificados antes dele. O () é apenas o valor da unidade, portanto, nada acontecerá no caso _. Como resultado, podemos dizer que não queremos fazer nada para todos os valores possíveis que não listamos antes do espaço reservado _.

No entanto, a expressão match pode ser um pouco prolixa em uma situação na qual nos importamos com apenas um dos casos. Para esta situação, a Rust fornece if let.

Mais sobre padrões e correspondência podem ser encontrados no capítulo 18.

Traduzido por Acervo Lima. O original pode ser acessado aqui.

Licença

Como criar gráficos com pandas?

In [1]: import pandas as pd

In [2]: import matplotlib.pyplot as plt
  • Dados de qualidade do ar

    Para este tutorial, são usados dados de qualidade do ar sobre o NO2, disponibilizados pela openaq e usando o pacote py-openaq. O conjunto de dados air_quality_no2.csv fornece valores de NO2 para as estações de medição FR04014, BETR801 e London Westminster em Paris, Antuérpia e Londres, respectivamente.

    O arquivo csv usado nesse post pode ser baixado aqui.

    In [3]: air_quality = pd.read_csv("data/air_quality_no2.csv", index_col=0, parse_dates=True)
    
    In [4]: air_quality.head()
    Out[4]: 
                         station_antwerp  station_paris  station_london
    datetime                                                           
    2019-05-07 02:00:00              NaN            NaN            23.0
    2019-05-07 03:00:00             50.5           25.0            19.0
    2019-05-07 04:00:00             45.0           27.7            19.0
    2019-05-07 05:00:00              NaN           50.4            16.0
    2019-05-07 06:00:00              NaN           61.9             NaN
    

    Observação

    Usamos os parâmetros index_col e parse_dates da função read_csv para definir a primeira (0ª) coluna como índice do DataFrame resultante e converter as datas na coluna em objetos Timestamp, respectivamente.

  • Quero uma verificação visual rápida dos dados.

    In [5]: air_quality.plot()
    Out[5]: <AxesSubplot:xlabel='datetime'>
    

    Com o DataFrame, o pandas cria por padrão um gráfico de linha para cada uma das colunas com dados numéricos.

  • Quero mostrar apenas as colunas da tabela de dados com os dados de Paris.

    In [6]: air_quality["station_paris"].plot()
    Out[6]: <AxesSubplot:xlabel='datetime'>
    

    Para mostrar uma coluna específica, use o método de seleção do tutorial de dados de subconjunto em combinação com método plot(). Portanto, o método plot() funciona em ambos Series e DataFrame.

  • Eu quero comparar visualmente os valores NO2 medidos em Londres versus Paris.

    In [7]: air_quality.plot.scatter(x="station_london", y="station_paris", alpha=0.5)
    Out[7]: <AxesSubplot:xlabel='station_london', ylabel='station_paris'>
    

Além do gráfico de linha padrão ao usar a função plot(), várias alternativas estão disponíveis para mostrar os dados. Vamos usar algum Python padrão para obter uma visão geral dos métodos de plot disponíveis:

In [8]: [
   ...:     method_name
   ...:     for method_name in dir(air_quality.plot)
   ...:     if not method_name.startswith("_")
   ...: ]
   ...: 
Out[8]: 
['area',
 'bar',
 'barh',
 'box',
 'density',
 'hexbin',
 'hist',
 'kde',
 'line',
 'pie',
 'scatter']

Observação

Em muitos ambientes de desenvolvimento, bem como IPython e Jupyter Notebook, use o botão TAB para obter uma visão geral dos métodos disponíveis, por exemplo air_quality.plot.+ TAB.

Uma das opções é DataFrame.plot.box(), que se refere a um boxplot. O método box é aplicável nos dados de exemplo de qualidade do ar:

In [9]: air_quality.plot.box()
Out[9]: <AxesSubplot:>
  • Eu quero cada uma das colunas em uma subtrama separada.

    In [10]: axs = air_quality.plot.area(figsize=(12, 4), subplots=True)
    

    Subplots separados para cada uma das colunas de dados são suportados pelo argumento subplots das funções plot. As opções integradas disponíveis em cada uma das funções do pandas representam que vale a pena dar uma olhada.

  • Eu quero personalizar ainda mais, estender ou salvar o enredo resultante.

    In [11]: fig, axs = plt.subplots(figsize=(12, 4))
    
    In [12]: air_quality.plot.area(ax=axs)
    Out[12]: <AxesSubplot:xlabel='datetime'>
    
    In [13]: axs.set_ylabel("NO$_2$ concentration")
    Out[13]: Text(0, 0.5, 'NO$_2$ concentration')
    
    In [14]: fig.savefig("no2_concentrations.png")
    

Cada um dos objetos de trama criados por pandas é um objeto matplotlib. Como o Matplotlib oferece muitas opções para personalizar os gráficos, tornar explícito o link entre o pandas e o Matplotlib permite todo o poder do matplotlib no gráfico. Essa estratégia é aplicada no exemplo anterior:

fig, axs = plt.subplots(figsize=(12, 4))        # Create an empty matplotlib Figure and Axes
air_quality.plot.area(ax=axs)                   # Use pandas to put the area plot on the prepared Figure/Axes
axs.set_ylabel("NO$_2$ concentration")          # Do any matplotlib customization you like
fig.savefig("no2_concentrations.png")           # Save the Figure/Axes using the existing matplotlib method.

LEMBRAR

  • Os métodos .plot.* são aplicáveis em Series e DataFrames

  • Por padrão, cada uma das colunas é plotada como um elemento diferente (linha, boxplot, ...)

  • Qualquer plotagem criada por pandas é um objeto Matplotlib.

Traduzido por Acervo Lima. O original pode ser acessado aqui.

domingo, 4 de abril de 2021

Como seleciono um subconjunto de um DataFrame?

Este tutorial usa o conjunto de dados do Titanic, armazenado como CSV. Os dados consistem nas seguintes colunas de dados:

  • PassengerId: Id de cada passageiro.
  • Survived: Este recurso tem valores 0 e 1. 0 para não sobreviveu e 1 para sobreviveu.
  • Pclass: Existem 3 classes: Classe 1, Classe 2 e Classe 3.
  • Name: Nome do passageiro.
  • Sex: gênero do passageiro.
  • Age: Idade do passageiro.
  • SibSp: Indicação de que o passageiro tem irmãos e cônjuge.
  • Parch: se o passageiro está sozinho ou tem família.
  • Ticket: Número do bilhete do passageiro.
  • Fare: Indicando a tarifa.
  • Cabin: A cabine do passageiro.
  • Embarked: a categoria embarcada.

O arquivo csv pode ser baixado aqui.

Como seleciono colunas específicas de um DataFrame?

  • Estou interessado na idade dos passageiros do Titanic.

    In [4]: ages = titanic["Age"]
    
    In [5]: ages.head()
    Out[5]: 
    0    22.0
    1    38.0
    2    26.0
    3    35.0
    4    35.0
    Name: Age, dtype: float64
    

    Para selecionar uma única coluna, use colchetes [] com o nome da coluna de interesse.

Cada coluna em um DataFrame é uma Series. Como uma única coluna é selecionada, o objeto retornado é uma Series pandas. Podemos verificar isso verificando o tipo de saída:

In [6]: type(titanic["Age"])
Out[6]: pandas.core.series.Series

E dê uma olhada no shape (forma) da saída:

In [7]: titanic["Age"].shape
Out[7]: (891,)

DataFrame.shape é um atributo (lembre-se do tutorial de leitura e escrita, não use parênteses para atributos) de uma Series pandas e que DataFrame contém o número de linhas e colunas: (nrows, ncolumns). Uma série de pandas é unidimensional e apenas o número de linhas é retornado.

  • Estou interessado na idade e no sexo dos passageiros do Titanic.

    In [8]: age_sex = titanic[["Age", "Sex"]]
    
    In [9]: age_sex.head()
    Out[9]: 
        Age     Sex
    0  22.0    male
    1  38.0  female
    2  26.0  female
    3  35.0  female
    4  35.0    male
    

    Para selecionar várias colunas, use uma lista de nomes de colunas dentro dos colchetes de seleção [].

Observação

Os colchetes internos definem uma lista Python com nomes de colunas, enquanto os colchetes externos são usados para selecionar os dados de um DataFrame pandas, como visto no exemplo anterior.

O tipo de dados retornado é um DataFrame pandas:

In [10]: type(titanic[["Age", "Sex"]])
Out[10]: pandas.core.frame.DataFrame
In [11]: titanic[["Age", "Sex"]].shape
Out[11]: (891, 2)

A seleção retornou um DataFrame com 891 linhas e 2 colunas. Lembre-se de que o DataFrame é bidimensional com dimensão de linha e coluna.

Como posso filtrar linhas específicas de um DataFrame?

  • Estou interessado nos passageiros com mais de 35 anos.

    In [12]: above_35 = titanic[titanic["Age"] > 35]
    
    In [13]: above_35.head()
    Out[13]: 
        PassengerId  Survived  Pclass                                               Name  ...    Ticket     Fare  Cabin  Embarked
    1             2         1       1  Cumings, Mrs. John Bradley (Florence Briggs Th...  ...  PC 17599  71.2833    C85         C
    6             7         0       1                            McCarthy, Mr. Timothy J  ...     17463  51.8625    E46         S
    11           12         1       1                           Bonnell, Miss. Elizabeth  ...    113783  26.5500   C103         S
    13           14         0       3                        Andersson, Mr. Anders Johan  ...    347082  31.2750    NaN         S
    15           16         1       2                   Hewlett, Mrs. (Mary D Kingcome)   ...    248706  16.0000    NaN         S
    
    [5 rows x 12 columns]
    

    Para selecionar linhas com base em uma expressão condicional, use uma condição dentro dos colchetes de seleção [].

A condição dentro dos colchetes de seleção titanic["Age"] > 35 verifica para quais linhas a coluna Age tem um valor maior que 35:

In [14]: titanic["Age"] > 35
Out[14]: 
0      False
1       True
2      False
3      False
4      False
       ...  
886    False
887    False
888    False
889    False
890    False
Name: Age, Length: 891, dtype: bool

A saída da expressão condicional ( >, mas também ==, !=, <, <=, ... iria funcionar) é realmente uma Series pandas de valores booleanos (quer True ou False) com o mesmo número de linhas que o DataFrame original. Essa Series de valores booleanos podem ser usados para filtrar o DataFrame colocando-o entre os colchetes de seleção []. Apenas as linhas para as quais o valor é True serão selecionadas.

Já sabemos que o DataFrame original Titanic consiste em 891 linhas. Vamos dar uma olhada no número de linhas que satisfazem a condição, verificando o atributo shape do DataFrame above_35:

In [15]: above_35.shape
Out[15]: (217, 12)
  • Estou interessado nos passageiros do Titanic da classe 2 e 3 da cabine.

    In [16]: class_23 = titanic[titanic["Pclass"].isin([2, 3])]
    
    In [17]: class_23.head()
    Out[17]: 
       PassengerId  Survived  Pclass                            Name     Sex  ...  Parch            Ticket     Fare Cabin  Embarked
    0            1         0       3         Braund, Mr. Owen Harris    male  ...      0         A/5 21171   7.2500   NaN         S
    2            3         1       3          Heikkinen, Miss. Laina  female  ...      0  STON/O2. 3101282   7.9250   NaN         S
    4            5         0       3        Allen, Mr. William Henry    male  ...      0            373450   8.0500   NaN         S
    5            6         0       3                Moran, Mr. James    male  ...      0            330877   8.4583   NaN         Q
    7            8         0       3  Palsson, Master. Gosta Leonard    male  ...      1            349909  21.0750   NaN         S
    
    [5 rows x 12 columns]
    

    Semelhante à expressão condicional, a função condicional isin() retorna True para cada linha em que os valores estão na lista fornecida. Para filtrar as linhas com base em tal função, use a função condicional dentro dos colchetes de seleção []. Nesse caso, a condição dentro dos colchetes de seleção titanic["Pclass"].isin([2, 3]) verifica quais linhas da coluna Pclass é 2 ou 3.

O acima é equivalente a filtrar por linhas para as quais a classe é 2 ou 3 e combinar as duas instruções com um operador (ou) |:

In [18]: class_23 = titanic[(titanic["Pclass"] == 2) | (titanic["Pclass"] == 3)]

In [19]: class_23.head()
Out[19]: 
   PassengerId  Survived  Pclass                            Name     Sex  ...  Parch            Ticket     Fare Cabin  Embarked
0            1         0       3         Braund, Mr. Owen Harris    male  ...      0         A/5 21171   7.2500   NaN         S
2            3         1       3          Heikkinen, Miss. Laina  female  ...      0  STON/O2. 3101282   7.9250   NaN         S
4            5         0       3        Allen, Mr. William Henry    male  ...      0            373450   8.0500   NaN         S
5            6         0       3                Moran, Mr. James    male  ...      0            330877   8.4583   NaN         Q
7            8         0       3  Palsson, Master. Gosta Leonard    male  ...      1            349909  21.0750   NaN         S

[5 rows x 12 columns]

Observação

Ao combinar várias instruções condicionais, cada condição deve estar entre parênteses (). Além disso, você não pode usar or / and você precisa usar o operador or (|) e o operador and (&).

  • Quero trabalhar com dados de passageiros cuja idade é conhecida.

    In [20]: age_no_na = titanic[titanic["Age"].notna()]
    
    In [21]: age_no_na.head()
    Out[21]: 
       PassengerId  Survived  Pclass                                               Name  ...            Ticket     Fare  Cabin  Embarked
    0            1         0       3                            Braund, Mr. Owen Harris  ...         A/5 21171   7.2500    NaN         S
    1            2         1       1  Cumings, Mrs. John Bradley (Florence Briggs Th...  ...          PC 17599  71.2833    C85         C
    2            3         1       3                             Heikkinen, Miss. Laina  ...  STON/O2. 3101282   7.9250    NaN         S
    3            4         1       1       Futrelle, Mrs. Jacques Heath (Lily May Peel)  ...            113803  53.1000   C123         S
    4            5         0       3                           Allen, Mr. William Henry  ...            373450   8.0500    NaN         S
    
    [5 rows x 12 columns]
    

    A função condicional notna() retorna True para cada linha, os valores não são um Nullvalor. Como tal, isso pode ser combinado com os colchetes de seleção []para filtrar a tabela de dados.

Você pode se perguntar o que realmente mudou, já que as 5 primeiras linhas ainda são os mesmos valores. Uma maneira de verificar é verificar se a forma mudou:

In [22]: age_no_na.shape
Out[22]: (714, 12)

Como seleciono linhas e colunas específicas de um DataFrame?

  • Estou interessado nos nomes dos passageiros com mais de 35 anos.

    In [23]: adult_names = titanic.loc[titanic["Age"] > 35, "Name"]
    
    In [24]: adult_names.head()
    Out[24]: 
    1     Cumings, Mrs. John Bradley (Florence Briggs Th...
    6                               McCarthy, Mr. Timothy J
    11                             Bonnell, Miss. Elizabeth
    13                          Andersson, Mr. Anders Johan
    15                     Hewlett, Mrs. (Mary D Kingcome) 
    Name: Name, dtype: object
    

    Neste caso, um subconjunto de linhas e colunas é feito de uma vez e apenas usar colchetes de seleção [] não é mais suficiente. Os operadores loc / iloc são necessários na frente dos colchetes de seleção []. Ao usar loc / iloc, a parte antes da vírgula são as linhas que você deseja e a parte depois da vírgula são as colunas que você deseja selecionar.

Ao usar os nomes das colunas, rótulos de linha ou uma expressão de condição, use o operador loc na frente dos colchetes de seleção []. Para a parte antes e depois da vírgula, você pode usar um único rótulo, uma lista de rótulos, uma fatia de rótulos, uma expressão condicional ou dois pontos. Usar dois pontos especifica que você deseja selecionar todas as linhas ou colunas.

  • Estou interessado nas linhas 10 a 25 e nas colunas 3 a 5.

    In [25]: titanic.iloc[9:25, 2:5]
    Out[25]: 
        Pclass                                 Name     Sex
    9        2  Nasser, Mrs. Nicholas (Adele Achem)  female
    10       3      Sandstrom, Miss. Marguerite Rut  female
    11       1             Bonnell, Miss. Elizabeth  female
    12       3       Saundercock, Mr. William Henry    male
    13       3          Andersson, Mr. Anders Johan    male
    ..     ...                                  ...     ...
    20       2                 Fynney, Mr. Joseph J    male
    21       2                Beesley, Mr. Lawrence    male
    22       3          McGowan, Miss. Anna "Annie"  female
    23       1         Sloper, Mr. William Thompson    male
    24       3        Palsson, Miss. Torborg Danira  female
    
    [16 rows x 3 columns]
    

    Novamente, um subconjunto de linhas e colunas é feito de uma vez e apenas usar colchetes de seleção [] não é mais suficiente. Quando estiver especificamente interessado em certas linhas e / ou colunas com base em sua posição na tabela, use o operador iloc na frente dos colchetes de seleção [].

Ao selecionar linhas e / ou colunas específicas com loc ou iloc, novos valores podem ser atribuídos aos dados selecionados. Por exemplo, para atribuir o nome anonymous aos 3 primeiros elementos da terceira coluna:

In [26]: titanic.iloc[0:3, 3] = "anonymous"

In [27]: titanic.head()
Out[27]: 
   PassengerId  Survived  Pclass                                          Name  ...            Ticket     Fare  Cabin  Embarked
0            1         0       3                                     anonymous  ...         A/5 21171   7.2500    NaN         S
1            2         1       1                                     anonymous  ...          PC 17599  71.2833    C85         C
2            3         1       3                                     anonymous  ...  STON/O2. 3101282   7.9250    NaN         S
3            4         1       1  Futrelle, Mrs. Jacques Heath (Lily May Peel)  ...            113803  53.1000   C123         S
4            5         0       3                      Allen, Mr. William Henry  ...            373450   8.0500    NaN         S

[5 rows x 12 columns]

LEMBRAR

  • Ao selecionar subconjuntos de dados, colchetes [] são usados.

  • Dentro desses colchetes, você pode usar um único rótulo de coluna / linha, uma lista de rótulos de coluna / linha, uma fatia de rótulos, uma expressão condicional ou dois pontos.

  • Selecione linhas e / ou colunas específicas usando loc ao usar os nomes de linha e coluna.

  • Selecione linhas e / ou colunas específicas usando iloc ao usar as posições na tabela.

  • Você pode atribuir novos valores a uma seleção com base em loc / iloc.

Traduzido por Acervo Lima. O original pode ser acessado aqui.

Enums e correspondência de padrões em Rust

Neste capítulo, examinaremos as enumerações, também conhecidas como enums. Enums permitem definir um tipo enumerando suas variantes possíveis. Primeiro, vamos definir e usar um enum para mostrar como um enum pode codificar o significado junto com os dados. A seguir, exploraremos um enum particularmente útil, chamado Option, que expressa que um valor pode ser algo ou nada. Em seguida, veremos como a correspondência de padrões na expressão match facilita a execução de diferentes códigos para diferentes valores de um enum. Finalmente, vamos cobrir como a construção if let é outro conveniencia e disponível para você lidar com enums em seu código.

Enums são um recurso em muitas linguagens, mas seus recursos diferem em cada linguagem. Os enums de Rust são mais semelhantes aos tipos de dados algébricos em linguagens funcionais, como F#, OCaml e Haskell.

Definindo um Enum

Vamos examinar uma situação que podemos querer expressar em código e ver por que enums são úteis e mais apropriados do que structs neste caso. Digamos que precisamos trabalhar com endereços IP. Atualmente, dois padrões principais são usados para endereços IP: versão quatro e versão seis. Estas são as únicas possibilidades para um endereço IP que nosso programa encontrará: podemos enumerar todas as variantes possíveis, que é de onde a enumeração obtém seu nome.

Qualquer endereço IP pode ser um endereço de versão quatro ou seis, mas não os dois ao mesmo tempo. Essa propriedade dos endereços IP torna a estrutura de dados enum apropriada, porque os valores enum podem ser apenas uma de suas variantes. Os endereços da versão quatro e da versão seis ainda são fundamentalmente endereços IP, portanto, devem ser tratados como o mesmo tipo quando o código está lidando com situações que se aplicam a qualquer tipo de endereço IP.

Podemos expressar esse conceito em código, definindo uma enumeração IpAddrKind e listando os tipos possíveis que um endereço IP pode ser, V4 ou V6. Estas são as variantes do enum:

enum IpAddrKind {
    V4,
    V6,
}

fn main() {
    let four = IpAddrKind::V4;
    let six = IpAddrKind::V6;

    route(IpAddrKind::V4);
    route(IpAddrKind::V6);
}

fn route(ip_kind: IpAddrKind) {}

IpAddrKind agora é um tipo de dados personalizado que podemos usar em outro lugar em nosso código.

Valores Enum

Podemos criar instâncias de cada uma das duas variantes IpAddrKind assim:

enum IpAddrKind {
    V4,
    V6,
}

fn main() {
    let four = IpAddrKind::V4;
    let six = IpAddrKind::V6;

    route(IpAddrKind::V4);
    route(IpAddrKind::V6);
}

fn route(ip_kind: IpAddrKind) {}

Observe que as variantes do enum têm namespaces sob seu identificador e usamos dois-pontos duplos para separar os dois. A razão pela qual isso é útil é que agora ambos os valores IpAddrKind::V4 e IpAddrKind::V6 são do mesmo tipo: IpAddrKind. Podemos então, por exemplo, definir uma função que leva qualquer IpAddrKind:

enum IpAddrKind {
    V4,
    V6,
}

fn main() {
    let four = IpAddrKind::V4;
    let six = IpAddrKind::V6;

    route(IpAddrKind::V4);
    route(IpAddrKind::V6);
}

fn route(ip_kind: IpAddrKind) {}

E podemos chamar essa função com qualquer uma das variantes:

enum IpAddrKind {
    V4,
    V6,
}

fn main() {
    let four = IpAddrKind::V4;
    let six = IpAddrKind::V6;

    route(IpAddrKind::V4);
    route(IpAddrKind::V6);
}

fn route(ip_kind: IpAddrKind) {}

Usar enums tem ainda mais vantagens. Pensando mais no nosso tipo de endereço IP, no momento não temos como armazenar os dados reais do endereço IP; só sabemos de que tipo é. Dado que você acabou de aprender sobre structs no Capítulo 5, você pode resolver esse problema conforme mostrado na Listagem 6-1.

fn main() {
    enum IpAddrKind {
        V4,
        V6,
    }

    struct IpAddr {
        kind: IpAddrKind,
        address: String,
    }

    let home = IpAddr {
        kind: IpAddrKind::V4,
        address: String::from("127.0.0.1"),
    };

    let loopback = IpAddr {
        kind: IpAddrKind::V6,
        address: String::from("::1"),
    };
}

Listagem 6-1: Armazenando os dados e a variante IpAddrKind de um endereço IP usando uma struct.

Aqui, definimos uma estrutura IpAddr que possui dois campos: um campo kind do tipo IpAddrKind (o enum que definimos anteriormente) e um campo address do tipo String. Temos duas instâncias desta estrutura. O primeiro, home, tem um valor IpAddrKind::V4 como kind com dados de endereço associado 127.0.0.1. A segunda instância, loopback, tem a outra variante de IpAddrKind como seu valor de kind sendo V6, e tem o endereço ::1 associado a ela. Nós usamos uma estrutura para agrupar os valores kind e address juntos, então agora a variante está associada com o valor.

Podemos representar o mesmo conceito de uma maneira mais concisa usando apenas um enum, em vez de um enum dentro de uma estrutura, colocando dados diretamente em cada variante de enum. Esta nova definição do enum IpAddr diz que ambas variantes, V4 e V6, terão valores String associados:

fn main() {
    enum IpAddr {
        V4(String),
        V6(String),
    }

    let home = IpAddr::V4(String::from("127.0.0.1"));

    let loopback = IpAddr::V6(String::from("::1"));
}

Anexamos dados a cada variante do enum diretamente, portanto, não há necessidade de uma estrutura extra.

Há outra vantagem em usar um enum em vez de uma estrutura: cada variante pode ter diferentes tipos e quantidades de dados associados. Os endereços IP do tipo da versão quatro sempre terão quatro componentes numéricos que terão valores entre 0 e 255. Se quiséssemos armazenar endereços V4 como quatro valores u8, mas ainda expressar endereços V6 como um valor String, não poderíamos com uma estrutura. Enums lidam com este caso com facilidade:

fn main() {
    enum IpAddr {
        V4(u8, u8, u8, u8),
        V6(String),
    }

    let home = IpAddr::V4(127, 0, 0, 1);

    let loopback = IpAddr::V6(String::from("::1"));
}

Mostramos várias maneiras diferentes de definir estruturas de dados para armazenar endereços IP da versão quatro e da versão seis. No entanto, ao que parece, querer armazenar endereços IP e codificar que tipo eles são é tão comum que a biblioteca padrão tem uma definição que podemos usar! Vejamos como a biblioteca padrão define IpAddr: ela tem o enum exato e as variantes que definimos e usamos, mas incorpora os dados de endereço dentro das variantes na forma de duas estruturas diferentes, que são definidas de forma diferente para cada variante:


#![allow(unused)]
fn main() {
struct Ipv4Addr {
    // --snip--
}

struct Ipv6Addr {
    // --snip--
}

enum IpAddr {
    V4(Ipv4Addr),
    V6(Ipv6Addr),
}
}

Este código ilustra que você pode colocar qualquer tipo de dado dentro de uma variante enum: strings, tipos numéricos ou structs, por exemplo. Você pode até incluir outro enum! Além disso, os tipos de biblioteca padrão geralmente não são muito mais complicados do que o que você pode imaginar.

Observe que, embora a biblioteca padrão contenha uma definição para IpAddr, ainda podemos criar e usar nossa própria definição sem conflito porque não trouxemos a definição da biblioteca padrão para o nosso escopo. Falaremos mais sobre como trazer os tipos no escopo no Capítulo 7.

Vejamos outro exemplo de enum na Listagem 6-2: este possui uma grande variedade de tipos embutidos em suas variantes.

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

fn main() {}

Listagem 6-2: um Messageenum cujas variantes cada uma armazena diferentes quantidades e tipos de valores.

Este enum tem quatro variantes com tipos diferentes:

  • Quit não tem nenhum dado associado a ele.
  • Move inclui uma estrutura anônima dentro dela.
  • Write inclui uma única String.
  • ChangeColor inclui três valores i32.

Definir um enum com variantes como as da Listagem 6-2 é semelhante a definir diferentes tipos de definições de estrutura, exceto que o enum não usa a structpalavra - chave e todas as variantes são agrupadas sob o Message tipo. Os seguintes structs podem conter os mesmos dados que as variantes enum anteriores contêm:

struct QuitMessage; // unit struct
struct MoveMessage {
    x: i32,
    y: i32,
}
struct WriteMessage(String); // tuple struct
struct ChangeColorMessage(i32, i32, i32); // tuple struct

fn main() {}

Mas se usássemos as diferentes estruturas, cada uma com seu próprio tipo, não poderíamos definir tão facilmente uma função para receber qualquer um desses tipos de mensagens como poderíamos com o Messageenum definido na Listagem 6-2, que é um tipo único.

Há mais uma semelhança entre enums e structs: assim como podemos definir métodos em structs usando impl, também podemos definir métodos em enums. Aqui está um método chamado call que podemos definir em nosso enum Message:

fn main() {
    enum Message {
        Quit,
        Move { x: i32, y: i32 },
        Write(String),
        ChangeColor(i32, i32, i32),
    }

    impl Message {
        fn call(&self) {
            // method body would be defined here
        }
    }

    let m = Message::Write(String::from("hello"));
    m.call();
}

O corpo do método usaria self para obter o valor com o qual chamamos o método. Neste exemplo, criamos uma variável m que tem o valor Message::Write(String::from("hello")) e é isso que self será no corpo do método call quando m.call() for executado.

Vamos olhar um outro enum na biblioteca padrão que é muito comum e útil: Option.

O Enum Option e suas vantagens sobre os valores nulos

Na seção anterior, vimos como o enum IpAddr nos permite usar o sistema de tipos de Rust para codificar mais informações do que apenas os dados em nosso programa. Esta seção explora um estudo de caso de Option, que é outro enum definido pela biblioteca padrão. O tipo Option é usado em muitos lugares porque codifica o cenário muito comum em que um valor pode ser algo ou nada. Expressar esse conceito em termos do sistema de tipos significa que o compilador pode verificar se você tratou de todos os casos que deveria tratar; essa funcionalidade pode evitar bugs que são extremamente comuns em outras linguagens de programação.

O design da linguagem de programação geralmente é pensado em termos de quais recursos você inclui, mas os recursos que você exclui também são importantes. Rust não possui o recurso nulo que muitas outras linguagens possuem. Nulo é um valor que significa que não há valor lá. Em linguagens com nulo, as variáveis ​​podem sempre estar em um de dois estados: nulo ou não nulo.

Em sua apresentação de 2009, "Referências nulas: o erro de um bilhão de dólares", Tony Hoare, o inventor do nulo, disse:

Eu chamo isso de meu erro de um bilhão de dólares. Naquela época, eu estava projetando o primeiro sistema de tipos abrangente para referências em uma linguagem orientada a objetos. Meu objetivo era garantir que todo uso de referências fosse absolutamente seguro, com checagem realizada automaticamente pelo compilador. Mas não pude resistir à tentação de colocar uma referência nula, simplesmente porque era muito fácil de implementar. Isso levou a inúmeros erros, vulnerabilidades e falhas no sistema, que provavelmente causaram um bilhão de dólares em dores e danos nos últimos quarenta anos.

O problema com valores nulos é que se você tentar usar um valor nulo como um valor não nulo, obterá algum tipo de erro. Como essa propriedade nula ou não nula é difundida, é extremamente fácil cometer esse tipo de erro.

No entanto, o conceito que nulo está tentando expressar ainda é útil: um nulo é um valor que atualmente é inválido ou ausente por algum motivo.

O problema não é realmente com o conceito, mas com a implementação particular. Como tal, Rust não tem nulos, mas tem um enum que pode codificar o conceito de um valor presente ou ausente. Este enum é Option<T> e é definido pela biblioteca padrão da seguinte forma:


#![allow(unused)]
fn main() {
enum Option<T> {
    Some(T),
    None,
}
}

O enum Option<T> é tão útil que até está incluído no prelúdio; você não precisa colocá-lo no escopo explicitamente. Além disso, são suas variantes: você pode usar Some e None diretamente sem o prefixo Option::. O enum Option<T> ainda é apenas um enum regular Some(T) e None ainda são variantes do tipo Option<T>.

A sintaxe <T> é um recurso do Rust sobre o qual ainda não falamos. É um parâmetro de tipo genérico e abordaremos os genéricos com mais detalhes no Capítulo 10. Por enquanto, tudo o que você precisa saber é que <T> a variante Some do enum Option pode conter um dado de qualquer tipo. Aqui estão alguns exemplos de uso dos valores Option para conter tipos de número e tipos de string:

fn main() {
    let some_number = Some(5);
    let some_string = Some("a string");

    let absent_number: Option<i32> = None;
}

Se usarmos None em vez de Some, precisamos dizer a Rust que tipo Option<T> temos, porque o compilador não pode inferir o tipo que a variante Some manterá olhando apenas para um valor None.

Quando temos um valor Some, sabemos que ele está presente e o valor está dentro de Some. Quando temos um valor None, em certo sentido, significa a mesma coisa que nulo: não temos um valor válido. Então, por que ter algum Option<T> é melhor do que ter null?

Resumindo, como Option<T> e T(onde T pode ser qualquer tipo) são tipos diferentes, o compilador não nos permite usar um valor Option<T> como se fosse definitivamente um valor válido. Por exemplo, este código não compila porque está tentando adicionar um valor i8 a um Option<i8>:

Esse código não compila Esse código não compila.

fn main() {
    let x: i8 = 5;
    let y: Option<i8> = Some(5);

    let sum = x + y;
}

Se executarmos esse código, receberemos uma mensagem de erro como esta:

$ cargo run
   Compiling enums v0.1.0 (file:///projects/enums)
error[E0277]: cannot add `Option<i8>` to `i8`
 --> src/main.rs:5:17
  |
5 |     let sum = x + y;
  |                 ^ no implementation for `i8 + Option<i8>`
  |
  = help: the trait `Add<Option<i8>>` is not implemented for `i8`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0277`.
error: could not compile `enums`

To learn more, run the command again with --verbose.

Intenso! Na verdade, essa mensagem de erro significa que Rust não entende como adicionar um i8 e um Option<i8>, porque são tipos diferentes. Quando temos um valor de um tipo como i8 em Rust, o compilador garantirá que sempre tenhamos um valor válido. Podemos prosseguir com segurança sem precisar verificar se há nulo antes de usar esse valor. Somente quando temos um Option<i8> (ou qualquer tipo de valor com o qual estamos trabalhando) é que temos que nos preocupar com a possibilidade de não ter um valor, e o compilador se certificará de que trataremos desse caso antes de usar o valor.

Em outras palavras, você deve converter um Option<T> em a T antes de poder realizar operações T com ele. Geralmente, isso ajuda a detectar um dos problemas mais comuns com nulo: assumir que algo não é nulo quando na verdade é.

Não ter que se preocupar em assumir incorretamente um valor não nulo ajuda você a ter mais confiança em seu código. Para ter um valor que possivelmente pode ser nulo, você deve ativá-lo explicitamente definindo o tipo desse valor Option<T>. Então, ao usar esse valor, você deve lidar explicitamente com o caso em que o valor é nulo. Onde quer que um valor tenha um tipo que não seja um Option<T>, você pode assumir com segurança que o valor não é nulo. Esta foi uma decisão de design deliberada do Rust para limitar a difusão do null e aumentar a segurança do código do Rust.

Então, como você obtém o valor T de uma variante Some quando você tem um valor do tipo Option<T> para que possa usar esse valor? O enum Option<T> possui um grande número de métodos que são úteis em uma variedade de situações; você pode verificá-los em sua documentação. Familiarizar-se com os métodos do Option<T> será extremamente útil em sua jornada com o Rust.

Em geral, para usar um valor Option<T>, você deseja ter um código que trate de cada variante. Você quer algum código que será executado apenas quando você tiver um valor Some(T), e esse código tem permissão para usar o interno T. Você deseja que algum outro código seja executado se tiver um valor None e esse código não tiver um valor T disponível. A expressão match é uma construção de fluxo de controle que faz exatamente isso quando usada com enums: ela executará um código diferente dependendo de qual variante da enum ela possui, e esse código pode usar os dados dentro do valor correspondente.

Traduzido por Acervo Lima. O original pode ser acessado aqui.

Licença