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.
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:
Inicialização direta com valores:
s = Stock('GOOG', 100, 490.1)Aqui, estamos criando diretamente uma instância da classe
Stockfornecendo os valores para os atributosname,shareseprice. Esta é uma maneira direta de criar uma instância quando você conhece os valores antecipadamente.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
Stocka partir de dados CSV, precisamos converter manualmente os valores de string para os tipos apropriados. Por exemplo, o valorsharesprecisa ser convertido em um inteiro, e o valorpriceprecisa 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:
- 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. - Adicionamos um método de classe
from_row(). O decorador@classmethodmarca este método como um método de classe. - O primeiro parâmetro deste método é
cls, que é uma referência à própria classe. Em métodos normais, usamosselfpara nos referir a uma instância da classe, mas aqui usamosclsporque é um método de classe. - A função
zip()é usada para emparelhar cada função de conversão de tipo emtypescom o valor correspondente na listarow. - 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. - Finalmente, criamos uma nova instância da classe
Stockusando 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:
- A classe
DStockherda todos os métodos da classeStock, como o métodocost(), sem ter que redefini-los. Esta é uma das principais vantagens da herança, pois evita que você escreva código redundante. - Simplesmente substituindo a variável de classe
types, alteramos como os dados são convertidos ao criar novas instâncias deDStock. Isso mostra como as variáveis de classe podem ser usadas para personalizar o comportamento de uma subclasse. - A classe base,
Stock, permanece inalterada e ainda funciona com valoresfloat. Isso significa que as alterações que fizemos na subclasse não afetam a classe base, o que é um bom princípio de design. - O método de classe
from_row()funciona corretamente com as classesStockeDStock. 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:
- 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.
- O método
__init__torna-se menos flexível porque sempre converte as entradas, mesmo que já estejam no tipo correto. - 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__. - 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:
- 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.
- 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. - Flexibilidade: Podemos facilmente alterar como os dados são convertidos usando classes diferentes. Por exemplo, podemos usar
StockouDStockpara lidar com os dados do portfólio de maneira diferente. - 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.