Variáveis de Classe e Métodos de Classe

Beginner

This tutorial is from open-source community. Access the source code

Introdução

Neste laboratório, você aprenderá sobre variáveis de classe e métodos de classe em Python. Você entenderá seu propósito e uso, e aprenderá como definir e usar métodos de classe de forma eficaz.

Além disso, você implementará construtores alternativos usando métodos de classe, explorará a relação entre variáveis de classe e herança, e criará utilitários flexíveis de leitura de dados. Os arquivos stock.py e reader.py serão modificados durante este laboratório.

Este é um Lab Guiado, que fornece instruções passo a passo para ajudá-lo a aprender e praticar. Siga as instruções cuidadosamente para completar cada etapa e ganhar experiência prática. Dados históricos mostram que este é um laboratório de nível iniciante com uma taxa de conclusão de 100%. Recebeu uma taxa de avaliações positivas de 100% dos estudantes.

Compreendendo Variáveis de Classe e Métodos de Classe

Neste primeiro passo, vamos mergulhar nos conceitos de variáveis de classe e métodos de classe em Python. Estes são conceitos importantes que o ajudarão a escrever um código mais eficiente e organizado. Antes de começarmos a trabalhar com variáveis de classe e métodos de classe, vamos primeiro dar uma olhada em como as instâncias da nossa classe Stock são criadas atualmente. Isso nos dará uma compreensão básica e nos mostrará onde podemos fazer melhorias.

O que são Variáveis de Classe?

Variáveis de classe são um tipo especial de variáveis em Python. Elas são compartilhadas entre todas as instâncias de uma classe. Para entender melhor isso, vamos compará-las com variáveis de instância. Variáveis de instância são exclusivas para cada instância de uma classe. Por exemplo, se você tiver várias instâncias de uma classe, cada instância pode ter seu próprio valor para uma variável de instância. Por outro lado, as variáveis de classe são definidas no nível da classe. Isso significa que todas as instâncias dessa classe podem acessar e compartilhar o mesmo valor da variável de classe.

O que são Métodos de Classe?

Métodos de classe são métodos que funcionam na própria classe, não em instâncias individuais da classe. Eles estão vinculados à classe, o que significa que podem ser chamados diretamente na classe sem criar uma instância. Para definir um método de classe em Python, usamos o decorador @classmethod. E, em vez de receber a instância (self) como o primeiro parâmetro, os métodos de classe recebem a classe (cls) como seu primeiro parâmetro. Isso permite que eles operem em dados no nível da classe e executem ações relacionadas à classe como um todo.

Abordagem Atual para Criar Instâncias de Stock

Vamos primeiro ver como criamos atualmente instâncias da classe Stock. Abra o arquivo stock.py no editor para observar a implementação atual:

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    def cost(self):
        return self.shares * self.price

As instâncias desta classe são normalmente criadas de uma destas maneiras:

  1. Inicialização direta com valores:

    s = Stock('GOOG', 100, 490.1)

    Aqui, estamos criando diretamente uma instância da classe Stock fornecendo os valores para os atributos name, shares e price. Esta é uma maneira direta de criar uma instância quando você conhece os valores antecipadamente.

  2. Criação a partir de dados lidos de um arquivo CSV:

    import csv
    with open('portfolio.csv') as f:
        rows = csv.reader(f)
        headers = next(rows)  ## Skip the header
        row = next(rows)      ## Get the first data row
        s = Stock(row[0], int(row[1]), float(row[2]))

    Quando lemos dados de um arquivo CSV, os valores estão inicialmente em formato de string. Portanto, ao criar uma instância de Stock a partir de dados CSV, precisamos converter manualmente os valores de string para os tipos apropriados. Por exemplo, o valor shares precisa ser convertido em um inteiro, e o valor price precisa ser convertido em um float.

Vamos experimentar isso. Crie um novo arquivo Python chamado test_stock.py no diretório ~/project com o seguinte conteúdo:

## test_stock.py
from stock import Stock
import csv

## Method 1: Direct creation
s1 = Stock('GOOG', 100, 490.1)
print(f"Stock: {s1.name}, Shares: {s1.shares}, Price: {s1.price}")
print(f"Cost: {s1.cost()}")

## Method 2: Creation from CSV row
with open('portfolio.csv') as f:
    rows = csv.reader(f)
    headers = next(rows)  ## Skip the header
    row = next(rows)      ## Get the first data row
    s2 = Stock(row[0], int(row[1]), float(row[2]))
    print(f"\nStock from CSV: {s2.name}, Shares: {s2.shares}, Price: {s2.price}")
    print(f"Cost: {s2.cost()}")

Execute este arquivo para ver os resultados:

cd ~/project
python test_stock.py

Você deve ver uma saída semelhante a:

Stock: GOOG, Shares: 100, Price: 490.1
Cost: 49010.0

Stock from CSV: AA, Shares: 100, Price: 32.2
Cost: 3220.0

Esta conversão manual funciona, mas tem algumas desvantagens. Precisamos saber o formato exato dos dados e temos que realizar as conversões toda vez que criamos uma instância a partir de dados CSV. Isso pode ser propenso a erros e demorado. No próximo passo, criaremos uma solução mais elegante usando métodos de classe.

Implementando Construtores Alternativos com Métodos de Classe

Neste passo, vamos aprender como implementar um construtor alternativo usando um método de classe. Isso nos permitirá criar objetos Stock a partir de dados de linha CSV de uma forma mais elegante.

O que é um Construtor Alternativo?

Em Python, um construtor alternativo é um padrão útil. Normalmente, criamos objetos usando o método padrão __init__. No entanto, um construtor alternativo nos dá uma maneira adicional de criar objetos. Métodos de classe são muito adequados para implementar construtores alternativos porque podem acessar a própria classe.

Implementando o Método de Classe from_row()

Adicionaremos uma variável de classe types e um método de classe from_row() à nossa classe Stock. Isso simplificará o processo de criação de instâncias Stock a partir de dados CSV.

Vamos modificar o arquivo stock.py adicionando o código destacado:

## stock.py

class Stock:
    types = (str, int, float)  ## Type conversions to apply to CSV data

    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    def cost(self):
        return self.shares * self.price

    @classmethod
    def from_row(cls, row):
        """
        Create a Stock instance from a row of CSV data.

        Args:
            row: A list of strings [name, shares, price]

        Returns:
            A new Stock instance
        """
        values = [func(val) for func, val in zip(cls.types, row)]
        return cls(*values)

## The rest of the file remains unchanged

Agora, vamos entender o que está acontecendo neste código passo a passo:

  1. Definimos uma variável de classe types. É uma tupla que contém funções de conversão de tipo (str, int, float). Essas funções serão usadas para converter os dados da linha CSV para os tipos apropriados.
  2. Adicionamos um método de classe from_row(). O decorador @classmethod marca este método como um método de classe.
  3. O primeiro parâmetro deste método é cls, que é uma referência à própria classe. Em métodos normais, usamos self para nos referir a uma instância da classe, mas aqui usamos cls porque é um método de classe.
  4. A função zip() é usada para emparelhar cada função de conversão de tipo em types com o valor correspondente na lista row.
  5. Usamos uma compreensão de lista para aplicar cada função de conversão ao valor correspondente na lista row. Desta forma, convertemos os dados de string da linha CSV para os tipos apropriados.
  6. Finalmente, criamos uma nova instância da classe Stock usando os valores convertidos e a retornamos.

Testando o Construtor Alternativo

Agora, criaremos um novo arquivo chamado test_class_method.py para testar nosso novo método de classe. Isso nos ajudará a verificar se o construtor alternativo funciona como esperado.

## test_class_method.py
from stock import Stock

## Test the from_row() class method
row = ['AA', '100', '32.20']
s = Stock.from_row(row)

print(f"Stock: {s.name}")
print(f"Shares: {s.shares}")
print(f"Price: {s.price}")
print(f"Cost: {s.cost()}")

## Try with a different row
row2 = ['GOOG', '50', '1120.50']
s2 = Stock.from_row(row2)

print(f"\nStock: {s2.name}")
print(f"Shares: {s2.shares}")
print(f"Price: {s2.price}")
print(f"Cost: {s2.cost()}")

Para ver os resultados, execute os seguintes comandos no seu terminal:

cd ~/project
python test_class_method.py

Você deve ver uma saída semelhante a esta:

Stock: AA
Shares: 100
Price: 32.2
Cost: 3220.0

Stock: GOOG
Shares: 50
Price: 1120.5
Cost: 56025.0

Observe que agora podemos criar instâncias Stock diretamente a partir de dados de string sem ter que realizar manualmente as conversões de tipo fora da classe. Isso torna nosso código mais limpo e garante que a responsabilidade pela conversão de dados seja tratada dentro da própria classe.

Variáveis de Classe e Herança

Neste passo, vamos explorar como as variáveis de classe interagem com a herança e como elas podem servir como um mecanismo de personalização. Em Python, a herança permite que uma subclasse herde atributos e métodos de uma classe base. Variáveis de classe são variáveis que pertencem à própria classe, não a nenhuma instância específica da classe. Compreender como elas funcionam juntas é crucial para criar um código flexível e sustentável.

Variáveis de Classe na Herança

Quando uma subclasse herda de uma classe base, ela automaticamente obtém acesso às variáveis de classe da classe base. No entanto, uma subclasse tem a capacidade de substituir essas variáveis de classe. Ao fazer isso, a subclasse pode alterar seu comportamento sem afetar a classe base. Este é um recurso muito poderoso, pois permite que você personalize o comportamento de uma subclasse de acordo com suas necessidades específicas.

Criando uma Classe Stock Especializada

Vamos criar uma subclasse da classe Stock. Vamos chamá-la de DStock, que significa Decimal Stock. A principal diferença entre DStock e a classe Stock regular é que DStock usará o tipo Decimal para valores de preço em vez de float. Em cálculos financeiros, a precisão é extremamente importante, e o tipo Decimal fornece uma aritmética decimal mais precisa em comparação com float.

Para criar esta subclasse, criaremos um novo arquivo chamado decimal_stock.py. Aqui está o código que você precisa colocar neste arquivo:

## decimal_stock.py
from decimal import Decimal
from stock import Stock

class DStock(Stock):
    """
    A specialized version of Stock that uses Decimal for prices
    """
    types = (str, int, Decimal)  ## Override the types class variable

## Test the subclass
if __name__ == "__main__":
    ## Create a DStock from row data
    row = ['AA', '100', '32.20']
    ds = DStock.from_row(row)

    print(f"DStock: {ds.name}")
    print(f"Shares: {ds.shares}")
    print(f"Price: {ds.price} (type: {type(ds.price).__name__})")
    print(f"Cost: {ds.cost()} (type: {type(ds.cost()).__name__})")

    ## For comparison, create a regular Stock from the same data
    s = Stock.from_row(row)
    print(f"\nRegular Stock: {s.name}")
    print(f"Shares: {s.shares}")
    print(f"Price: {s.price} (type: {type(s.price).__name__})")
    print(f"Cost: {s.cost()} (type: {type(s.cost()).__name__})")

Depois de criar o arquivo decimal_stock.py com o código acima, você precisa executá-lo para ver os resultados. Abra seu terminal e siga estas etapas:

cd ~/project
python decimal_stock.py

Você deve ver uma saída semelhante a esta:

DStock: AA
Shares: 100
Price: 32.20 (type: Decimal)
Cost: 3220.0 (type: Decimal)

Regular Stock: AA
Shares: 100
Price: 32.2 (type: float)
Cost: 3220.0 (type: float)

Pontos-chave sobre Variáveis de Classe e Herança

Deste exemplo, podemos tirar várias conclusões importantes:

  1. A classe DStock herda todos os métodos da classe Stock, como o método cost(), sem ter que redefini-los. Esta é uma das principais vantagens da herança, pois evita que você escreva código redundante.
  2. Simplesmente substituindo a variável de classe types, alteramos como os dados são convertidos ao criar novas instâncias de DStock. Isso mostra como as variáveis de classe podem ser usadas para personalizar o comportamento de uma subclasse.
  3. A classe base, Stock, permanece inalterada e ainda funciona com valores float. Isso significa que as alterações que fizemos na subclasse não afetam a classe base, o que é um bom princípio de design.
  4. O método de classe from_row() funciona corretamente com as classes Stock e DStock. Isso demonstra o poder da herança, pois o mesmo método pode ser usado com diferentes subclasses.

Este exemplo mostra claramente como as variáveis de classe podem ser usadas como um mecanismo de configuração. As subclasses podem substituir essas variáveis para personalizar seu comportamento sem ter que reescrever os métodos.

Discussão de Design

Vamos considerar uma abordagem alternativa em que colocamos as conversões de tipo no método __init__:

class Stock:
    def __init__(self, name, shares, price):
        self.name = str(name)
        self.shares = int(shares)
        self.price = float(price)

Com esta abordagem, podemos criar um objeto Stock a partir de uma linha de dados assim:

row = ['AA', '100', '32.20']
s = Stock(*row)

Embora esta abordagem possa parecer mais simples à primeira vista, ela tem várias desvantagens:

  1. Ela combina duas preocupações diferentes: inicialização de objeto e conversão de dados. Isso torna o código mais difícil de entender e manter.
  2. O método __init__ torna-se menos flexível porque sempre converte as entradas, mesmo que já estejam no tipo correto.
  3. Restringe como as subclasses podem personalizar o processo de conversão. As subclasses teriam mais dificuldade em alterar a lógica de conversão se ela estivesse incorporada no método __init__.
  4. O código torna-se mais frágil. Se alguma das conversões falhar, o objeto não poderá ser criado, o que pode levar a erros em seu programa.

Por outro lado, a abordagem do método de classe separa essas preocupações. Isso torna o código mais sustentável e flexível, pois cada parte do código tem uma única responsabilidade.

Criando um Leitor CSV de Propósito Geral

Neste passo final, vamos criar uma função de propósito geral. Esta função será capaz de ler arquivos CSV e criar objetos de qualquer classe que tenha implementado o método de classe from_row(). Isso nos mostra o poder de usar métodos de classe como uma interface uniforme. Uma interface uniforme significa que diferentes classes podem ser usadas da mesma maneira, o que torna nosso código mais flexível e fácil de gerenciar.

Modificando a Função read_portfolio()

Primeiro, atualizaremos a função read_portfolio() no arquivo stock.py. Usaremos nosso novo método de classe from_row(). Abra o arquivo stock.py e altere a função read_portfolio() assim:

def read_portfolio(filename):
    '''
    Read a stock portfolio file into a list of Stock instances
    '''
    import csv
    portfolio = []
    with open(filename) as f:
        rows = csv.reader(f)
        headers = next(rows)  ## Skip header
        for row in rows:
            portfolio.append(Stock.from_row(row))
    return portfolio

Esta nova versão da função é mais simples. Ela dá a responsabilidade da conversão de tipo à classe Stock, onde ela realmente pertence. Conversão de tipo significa mudar os dados de um tipo para outro, como transformar uma string em um inteiro. Ao fazer isso, tornamos nosso código mais organizado e fácil de entender.

Criando um Leitor CSV de Propósito Geral

Agora, criaremos uma função de propósito mais geral no arquivo reader.py. Esta função pode ler dados CSV e criar instâncias de qualquer classe que tenha um método de classe from_row().

Abra o arquivo reader.py e adicione a seguinte função:

def read_csv_as_instances(filename, cls):
    '''
    Read a CSV file into a list of instances of the given class.

    Args:
        filename: Name of the CSV file
        cls: Class to instantiate (must have from_row class method)

    Returns:
        List of class instances
    '''
    records = []
    with open(filename) as f:
        rows = csv.reader(f)
        headers = next(rows)  ## Skip header
        for row in rows:
            records.append(cls.from_row(row))
    return records

Esta função recebe duas entradas: um nome de arquivo e uma classe. Em seguida, retorna uma lista de instâncias dessa classe, criadas a partir dos dados no arquivo CSV. Isso é muito útil porque podemos usá-lo com diferentes classes, desde que elas tenham o método from_row().

Testando o Leitor CSV de Propósito Geral

Vamos criar um arquivo de teste para ver como nosso leitor de propósito geral funciona. Crie um arquivo chamado test_csv_reader.py com o seguinte conteúdo:

## test_csv_reader.py
from reader import read_csv_as_instances
from stock import Stock
from decimal_stock import DStock

## Read portfolio as Stock instances
portfolio = read_csv_as_instances('portfolio.csv', Stock)
print(f"Portfolio contains {len(portfolio)} stocks")
print(f"First stock: {portfolio[0].name}, {portfolio[0].shares} shares at ${portfolio[0].price}")

## Read portfolio as DStock instances (with Decimal prices)
decimal_portfolio = read_csv_as_instances('portfolio.csv', DStock)
print(f"\nDecimal portfolio contains {len(decimal_portfolio)} stocks")
print(f"First stock: {decimal_portfolio[0].name}, {decimal_portfolio[0].shares} shares at ${decimal_portfolio[0].price}")

## Define a new class for reading the bus data
class BusRide:
    def __init__(self, route, date, daytype, rides):
        self.route = route
        self.date = date
        self.daytype = daytype
        self.rides = rides

    @classmethod
    def from_row(cls, row):
        return cls(row[0], row[1], row[2], int(row[3]))

## Read some bus data (just the first 5 records for brevity)
print("\nReading bus data...")
import csv
with open('ctabus.csv') as f:
    rows = csv.reader(f)
    headers = next(rows)  ## Skip header
    bus_rides = []
    for i, row in enumerate(rows):
        if i >= 5:  ## Only read 5 records for the example
            break
        bus_rides.append(BusRide.from_row(row))

## Display the bus data
for ride in bus_rides:
    print(f"Route: {ride.route}, Date: {ride.date}, Type: {ride.daytype}, Rides: {ride.rides}")

Execute este arquivo para ver os resultados. Abra seu terminal e use os seguintes comandos:

cd ~/project
python test_csv_reader.py

Você deve ver uma saída que mostra os dados do portfólio carregados como instâncias Stock e DStock, e os dados da rota do ônibus carregados como instâncias BusRide. Isso prova que nosso leitor de propósito geral funciona com diferentes classes.

Principais Benefícios desta Abordagem

Esta abordagem mostra vários conceitos poderosos:

  1. Separação de preocupações: Ler dados é separado da criação de objetos. Isso significa que o código para ler o arquivo CSV não é misturado com o código para criar objetos. Isso torna o código mais fácil de entender e manter.
  2. Polimorfismo: O mesmo código pode funcionar com diferentes classes que seguem a mesma interface. Em nosso caso, desde que uma classe tenha o método from_row(), nosso leitor de propósito geral pode usá-lo.
  3. Flexibilidade: Podemos facilmente alterar como os dados são convertidos usando classes diferentes. Por exemplo, podemos usar Stock ou DStock para lidar com os dados do portfólio de maneira diferente.
  4. Extensibilidade: Podemos adicionar novas classes que funcionam com nosso leitor sem alterar o código do leitor. Isso torna nosso código mais preparado para o futuro.

Este é um padrão comum em Python que torna o código mais modular, reutilizável e sustentável.

Notas Finais sobre Métodos de Classe

Métodos de classe são frequentemente usados como construtores alternativos em Python. Você geralmente pode diferenciá-los porque seus nomes geralmente têm a palavra "from" neles. Por exemplo:

## Some examples from Python's built-in types
dict.fromkeys(['a', 'b', 'c'], 0)  ## Create a dict with default values
datetime.datetime.fromtimestamp(1627776000)  ## Create datetime from timestamp
int.from_bytes(b'\x00\x01', byteorder='big')  ## Create int from bytes

Ao seguir esta convenção, você torna seu código mais legível e consistente com as bibliotecas integradas do Python. Isso ajuda outros desenvolvedores a entender seu código mais facilmente.

Resumo

Neste laboratório, você aprendeu sobre dois recursos cruciais do Python: variáveis de classe e métodos de classe. Variáveis de classe são compartilhadas entre todas as instâncias da classe e podem ser usadas para configuração. Métodos de classe operam na própria classe, marcados com o decorador @classmethod. Construtores alternativos, um uso comum de métodos de classe, oferecem diferentes maneiras de criar objetos. A herança com variáveis de classe permite que subclasses personalizem o comportamento substituindo-as, e o uso de métodos de classe pode alcançar um design de código flexível.

Esses conceitos são poderosos para criar código Python bem organizado e flexível. Ao colocar as conversões de tipo dentro da classe e fornecer uma interface uniforme via métodos de classe, você pode escrever utilitários de propósito mais geral. Para estender seu aprendizado, você pode explorar mais casos de uso, criar hierarquias de classe e construir pipelines complexos de processamento de dados usando métodos de classe.