Введение
В этом практическом занятии (лабораторной работе) вы узнаете о переменных класса и методах класса в Python. Вы поймете их назначение и способы использования, а также научитесь эффективно определять и использовать методы класса.
Кроме того, вы реализуете альтернативные конструкторы с использованием методов класса, исследуете взаимосвязь между переменными класса и наследованием, а также создадите гибкие утилиты для чтения данных. Во время этого практического занятия будут модифицированы файлы stock.py и reader.py.
Понимание переменных класса и методов класса
В этом первом шаге мы углубимся в концепции переменных класса и методов класса в Python. Это важные концепции, которые помогут вам писать более эффективный и организованный код. Прежде чем мы начнем работать с переменными класса и методами класса, давайте сначала посмотрим, как в настоящее время создаются экземпляры нашего класса Stock. Это даст нам базовое понимание и покажет, где мы можем внести улучшения.
Что такое переменные класса?
Переменные класса - это особый тип переменных в Python. Они общие для всех экземпляров класса. Чтобы лучше понять это, давайте сравним их с переменными экземпляра. Переменные экземпляра уникальны для каждого экземпляра класса. Например, если у вас есть несколько экземпляров класса, каждый экземпляр может иметь свое собственное значение для переменной экземпляра. С другой стороны, переменные класса определяются на уровне класса. Это означает, что все экземпляры этого класса могут получить доступ к и разделять одно и то же значение переменной класса.
Что такое методы класса?
Методы класса - это методы, которые работают с самим классом, а не с отдельными экземплярами класса. Они привязаны к классу, что означает, что их можно вызывать напрямую на классе без создания экземпляра. Чтобы определить метод класса в Python, мы используем декоратор @classmethod. И вместо того, чтобы принимать экземпляр (self) в качестве первого параметра, методы класса принимают класс (cls) в качестве своего первого параметра. Это позволяет им работать с данными на уровне класса и выполнять действия, связанные с классом в целом.
Текущий подход к созданию экземпляров Stock
Давайте сначала посмотрим, как мы в настоящее время создаем экземпляры класса Stock. Откройте файл stock.py в редакторе, чтобы посмотреть текущую реализацию:
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
Экземпляры этого класса обычно создаются одним из следующих способов:
Прямая инициализация значениями:
s = Stock('GOOG', 100, 490.1)Здесь мы напрямую создаем экземпляр класса
Stock, предоставляя значения для атрибутовname,sharesиprice. Это простой способ создания экземпляра, когда вы заранее знаете значения.Создание из данных, прочитанных из CSV-файла:
import csv with open('portfolio.csv') as f: rows = csv.reader(f) headers = next(rows) ## Пропустить заголовок row = next(rows) ## Получить первую строку данных s = Stock(row[0], int(row[1]), float(row[2]))Когда мы читаем данные из CSV-файла, значения изначально имеют строковый формат. Поэтому при создании экземпляра
Stockиз данных CSV нам нужно вручную преобразовать строковые значения в соответствующие типы. Например, значениеsharesнужно преобразовать в целое число, а значениеprice- в число с плавающей точкой.
Давайте попробуем это. Создайте новый Python-файл с именем test_stock.py в директории ~/project со следующим содержимым:
## test_stock.py
from stock import Stock
import csv
## Метод 1: Прямое создание
s1 = Stock('GOOG', 100, 490.1)
print(f"Stock: {s1.name}, Shares: {s1.shares}, Price: {s1.price}")
print(f"Cost: {s1.cost()}")
## Метод 2: Создание из строки CSV
with open('portfolio.csv') as f:
rows = csv.reader(f)
headers = next(rows) ## Пропустить заголовок
row = next(rows) ## Получить первую строку данных
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()}")
Запустите этот файл, чтобы увидеть результаты:
cd ~/project
python test_stock.py
Вы должны увидеть вывод, похожий на следующий:
Stock: GOOG, Shares: 100, Price: 490.1
Cost: 49010.0
Stock from CSV: AA, Shares: 100, Price: 32.2
Cost: 3220.0
Эта ручная конвертация работает, но имеет некоторые недостатки. Мы должны знать точный формат данных, и мы должны выполнять конвертации каждый раз, когда создаем экземпляр из данных CSV. Это может быть ошибочным и затратным по времени. В следующем шаге мы создадим более элегантное решение с использованием методов класса.
Реализация альтернативных конструкторов с использованием методов класса
В этом шаге мы научимся реализовывать альтернативный конструктор с использованием метода класса. Это позволит нам создавать объекты Stock из данных строки CSV более элегантным способом.
Что такое альтернативный конструктор?
В Python альтернативный конструктор представляет собой полезный шаблон. Обычно мы создаем объекты с использованием стандартного метода __init__. Однако альтернативный конструктор дает нам дополнительный способ создания объектов. Методы класса очень подходят для реализации альтернативных конструкторов, так как они могут обращаться к самому классу.
Реализация метода класса from_row()
Мы добавим переменную класса types и метод класса from_row() в наш класс Stock. Это упростит процесс создания экземпляров Stock из данных CSV.
Давайте модифицируем файл stock.py, добавив выделенный код:
## stock.py
class Stock:
types = (str, int, float) ## Преобразования типов для применения к данным CSV
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):
"""
Создать экземпляр Stock из строки данных CSV.
Аргументы:
row: Список строк [name, shares, price]
Возвращает:
Новый экземпляр Stock
"""
values = [func(val) for func, val in zip(cls.types, row)]
return cls(*values)
## Остальная часть файла остается без изменений
Теперь давайте пошагово разберем, что происходит в этом коде:
- Мы определили переменную класса
types. Это кортеж, который содержит функции преобразования типов(str, int, float). Эти функции будут использоваться для преобразования данных из строки CSV в соответствующие типы. - Мы добавили метод класса
from_row(). Декоратор@classmethodпомечает этот метод как метод класса. - Первым параметром этого метода является
cls, который является ссылкой на сам класс. В обычных методах мы используемselfдля обращения к экземпляру класса, но здесь мы используемcls, так как это метод класса. - Функция
zip()используется для сопоставления каждой функции преобразования типов вtypesс соответствующим значением в спискеrow. - Мы используем генератор списка для применения каждой функции преобразования к соответствующему значению в списке
row. Таким образом, мы преобразуем строковые данные из строки CSV в соответствующие типы. - Наконец, мы создаем новый экземпляр класса
Stockс использованием преобразованных значений и возвращаем его.
Тестирование альтернативного конструктора
Теперь мы создадим новый файл с именем test_class_method.py для тестирования нашего нового метода класса. Это поможет нам убедиться, что альтернативный конструктор работает как ожидается.
## test_class_method.py
from stock import Stock
## Протестировать метод класса from_row()
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()}")
## Попробовать с другой строкой
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()}")
Чтобы увидеть результаты, выполните следующие команды в терминале:
cd ~/project
python test_class_method.py
Вы должны увидеть вывод, похожий на следующий:
Stock: AA
Shares: 100
Price: 32.2
Cost: 3220.0
Stock: GOOG
Shares: 50
Price: 1120.5
Cost: 56025.0
Обратите внимание, что теперь мы можем создавать экземпляры Stock непосредственно из строковых данных без необходимости вручную выполнять преобразования типов вне класса. Это делает наш код более чистым и обеспечивает, что ответственность за преобразование данных обрабатывается внутри самого класса.
Переменные класса и наследование
В этом шаге мы рассмотрим, как переменные класса взаимодействуют с наследованием и как они могут служить механизмом для настройки. В Python наследование позволяет подклассу наследовать атрибуты и методы от базового класса. Переменные класса - это переменные, которые принадлежат самому классу, а не какому-либо конкретному экземпляру класса. Понимание того, как они работают вместе, является важным для создания гибкого и поддерживаемого кода.
Переменные класса в наследовании
Когда подкласс наследует от базового класса, он автоматически получает доступ к переменным класса базового класса. Однако подкласс может переопределить эти переменные класса. Таким образом, подкласс может изменить свое поведение, не влияя на базовый класс. Это очень мощная возможность, так как позволяет настроить поведение подкласса в соответствии с вашими конкретными потребностями.
Создание специализированного класса Stock
Давайте создадим подкласс класса Stock. Мы назовем его DStock, что означает Decimal Stock (акции с использованием десятичных чисел). Основное отличие между DStock и обычным классом Stock заключается в том, что DStock будет использовать тип Decimal для значений цены вместо float. В финансовых расчетах точность имеет огромное значение, и тип Decimal обеспечивает более точную арифметику с десятичными числами по сравнению с float.
Для создания этого подкласса мы создадим новый файл с именем decimal_stock.py. Вот код, который нужно поместить в этот файл:
## decimal_stock.py
from decimal import Decimal
from stock import Stock
class DStock(Stock):
"""
Специализированная версия Stock, которая использует Decimal для цен
"""
types = (str, int, Decimal) ## Переопределить переменную класса types
## Протестировать подкласс
if __name__ == "__main__":
## Создать DStock из данных строки
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__})")
## В сравнении, создать обычный Stock из тех же данных
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__})")
После того, как вы создадите файл decimal_stock.py с приведенным выше кодом, вам нужно запустить его, чтобы увидеть результаты. Откройте терминал и следуйте этим шагам:
cd ~/project
python decimal_stock.py
Вы должны увидеть вывод, похожий на следующий:
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)
Основные моменты о переменных класса и наследовании
Из этого примера мы можем сделать несколько важных выводов:
- Класс
DStockнаследует все методы от классаStock, например методcost(), не требуя их переопределения. Это одно из основных преимуществ наследования, так как оно позволяет избежать написания избыточного кода. - Просто переопределив переменную класса
types, мы изменили способ преобразования данных при создании новых экземпляровDStock. Это показывает, как переменные класса могут быть использованы для настройки поведения подкласса. - Базовый класс
Stockостается неизменным и по-прежнему работает с значениями типаfloat. Это означает, что изменения, которые мы внесли в подкласс, не влияют на базовый класс, что является хорошим принципом дизайна. - Метод класса
from_row()корректно работает как с классомStock, так и с классомDStock. Это демонстрирует мощь наследования, так как один и тот же метод может быть использован с разными подклассами.
Этот пример ясно показывает, как переменные класса могут быть использованы как механизм настройки. Подклассы могут переопределить эти переменные, чтобы настроить свое поведение, не переписывая методы.
Обсуждение дизайна
Рассмотрим альтернативный подход, при котором мы помещаем преобразования типов в метод __init__:
class Stock:
def __init__(self, name, shares, price):
self.name = str(name)
self.shares = int(shares)
self.price = float(price)
С этим подходом мы можем создать объект Stock из строки данных следующим образом:
row = ['AA', '100', '32.20']
s = Stock(*row)
Хотя этот подход может показаться проще на первый взгляд, он имеет несколько недостатков:
- Он объединяет две разные задачи: инициализацию объекта и преобразование данных. Это делает код более сложным для понимания и поддержки.
- Метод
__init__становится менее гибким, так как он всегда преобразует входные данные, даже если они уже имеют правильный тип. - Он ограничивает способ, которым подклассы могут настроить процесс преобразования. Подклассам будет сложнее изменить логику преобразования, если она встроена в метод
__init__. - Код становится более хрупким. Если какое-либо из преобразований завершается неудачей, объект не может быть создан, что может привести к ошибкам в вашей программе.
С другой стороны, подход с использованием методов класса разделяет эти задачи. Это делает код более поддерживаемым и гибким, так как каждая часть кода имеет единственную ответственность.
Создание универсального читателя CSV
В этом последнем шаге мы создадим универсальную функцию. Эта функция сможет читать CSV-файлы и создавать объекты любого класса, который реализует метод класса from_row(). Это демонстрирует силу использования методов класса в качестве унифицированного интерфейса. Унифицированный интерфейс означает, что разные классы могут использоваться одинаково, что делает наш код более гибким и легким в управлении.
Изменение функции read_portfolio()
Сначала мы обновим функцию read_portfolio() в файле stock.py. Мы будем использовать наш новый метод класса from_row(). Откройте файл stock.py и измените функцию read_portfolio() следующим образом:
def read_portfolio(filename):
'''
Прочитать файл с портфелем акций в список экземпляров Stock
'''
import csv
portfolio = []
with open(filename) as f:
rows = csv.reader(f)
headers = next(rows) ## Пропустить заголовок
for row in rows:
portfolio.append(Stock.from_row(row))
return portfolio
Эта новая версия функции проще. Она передает ответственность за преобразование типов классу Stock, где это действительно должно быть. Преобразование типов означает изменение данных из одного типа в другой, например, преобразование строки в целое число. Таким образом, мы делаем наш код более организованным и легким для понимания.
Создание универсального читателя CSV
Теперь мы создадим более универсальную функцию в файле reader.py. Эта функция может читать данные CSV и создавать экземпляры любого класса, который имеет метод класса from_row().
Откройте файл reader.py и добавьте следующую функцию:
def read_csv_as_instances(filename, cls):
'''
Прочитать CSV-файл в список экземпляров заданного класса.
Аргументы:
filename: Имя CSV-файла
cls: Класс для создания экземпляров (должен иметь метод класса from_row)
Возвращает:
Список экземпляров класса
'''
records = []
with open(filename) as f:
rows = csv.reader(f)
headers = next(rows) ## Пропустить заголовок
for row in rows:
records.append(cls.from_row(row))
return records
Эта функция принимает два входных параметра: имя файла и класс. Затем она возвращает список экземпляров этого класса, созданных из данных в CSV-файле. Это очень полезно, так как мы можем использовать ее с разными классами, если они имеют метод from_row().
Тестирование универсального читателя CSV
Давайте создадим тестовый файл, чтобы посмотреть, как работает наш универсальный читатель. Создайте файл с именем test_csv_reader.py со следующим содержимым:
## test_csv_reader.py
from reader import read_csv_as_instances
from stock import Stock
from decimal_stock import DStock
## Прочитать портфель как экземпляры Stock
portfolio = read_csv_as_instances('portfolio.csv', Stock)
print(f"Портфель содержит {len(portfolio)} акций")
print(f"Первая акция: {portfolio[0].name}, {portfolio[0].shares} акций по цене ${portfolio[0].price}")
## Прочитать портфель как экземпляры DStock (с ценами Decimal)
decimal_portfolio = read_csv_as_instances('portfolio.csv', DStock)
print(f"\nДесятичный портфель содержит {len(decimal_portfolio)} акций")
print(f"Первая акция: {decimal_portfolio[0].name}, {decimal_portfolio[0].shares} акций по цене ${decimal_portfolio[0].price}")
## Определить новый класс для чтения данных о маршрутах автобусов
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]))
## Прочитать некоторые данные о маршрутах автобусов (только первые 5 записей для краткости)
print("\nЧтение данных о маршрутах автобусов...")
import csv
with open('ctabus.csv') as f:
rows = csv.reader(f)
headers = next(rows) ## Пропустить заголовок
bus_rides = []
for i, row in enumerate(rows):
if i >= 5: ## Читать только 5 записей для примера
break
bus_rides.append(BusRide.from_row(row))
## Вывести данные о маршрутах автобусов
for ride in bus_rides:
print(f"Маршрут: {ride.route}, Дата: {ride.date}, Тип дня: {ride.daytype}, Количество поездок: {ride.rides}")
Запустите этот файл, чтобы увидеть результаты. Откройте терминал и используйте следующие команды:
cd ~/project
python test_csv_reader.py
Вы должны увидеть вывод, показывающий данные о портфеле, загруженные как экземпляры Stock и DStock, а также данные о маршрутах автобусов, загруженные как экземпляры BusRide. Это доказывает, что наш универсальный читатель работает с разными классами.
Основные преимущества этого подхода
Этот подход демонстрирует несколько мощных концепций:
- Разделение ответственностей: Чтение данных отделено от создания объектов. Это означает, что код для чтения CSV-файла не перемешан с кодом для создания объектов. Это делает код легче для понимания и поддержки.
- Полиморфизм: Один и тот же код может работать с разными классами, которые следуют одному и тому же интерфейсу. В нашем случае, если класс имеет метод
from_row(), наш универсальный читатель может его использовать. - Гибкость: Мы можем легко изменить способ преобразования данных, используя разные классы. Например, мы можем использовать
StockилиDStockдля разной обработки данных о портфеле. - Расширяемость: Мы можем добавлять новые классы, которые работают с нашим читателем, не изменяя код читателя. Это делает наш код более устойчивым к изменениям в будущем.
Это распространенный шаблон в Python, который делает код более модульным, повторно используемым и поддерживаемым.
Финальные замечания о методах класса
Методы класса часто используются в Python в качестве альтернативных конструкторов. Обычно их можно отличить, так как их имена часто содержат слово "from". Например:
## Некоторые примеры из встроенных типов Python
dict.fromkeys(['a', 'b', 'c'], 0) ## Создать словарь с начальными значениями
datetime.datetime.fromtimestamp(1627776000) ## Создать объект datetime из временной метки
int.from_bytes(b'\x00\x01', byteorder='big') ## Создать целое число из байтов
Следуя этой конвенции, вы делаете свой код более читаемым и совместимым с встроенными библиотеками Python. Это помогает другим разработчикам легче понять ваш код.
Резюме
В этом практическом занятии вы узнали о двух важных функциях Python: переменных класса и методах класса. Переменные класса являются общими для всех экземпляров класса и могут использоваться для настройки. Методы класса работают с самим классом и помечаются декоратором @classmethod. Альтернативные конструкторы, распространенное применение методов класса, предлагают разные способы создания объектов. Наследование с использованием переменных класса позволяет подклассам настраивать свое поведение, переопределяя их, а использование методов класса позволяет достичь гибкого дизайна кода.
Эти концепции очень мощны для создания хорошо организованного и гибкого кода на Python. Поместив преобразования типов внутри класса и предоставив унифицированный интерфейс с помощью методов класса, вы можете написать более универсальные утилиты. Чтобы расширить свои знания, вы можете изучить больше вариантов использования, создать иерархии классов и построить сложные конвейеры обработки данных с использованием методов класса.