Введение
В этой лабораторной работе вы узнаете, как инкапсулировать внутренние компоненты объекта, используя приватные атрибуты, и как реализовать декораторы свойств (property decorators) для контроля доступа к атрибутам. Эти методы необходимы для поддержания целостности ваших объектов и обеспечения правильной обработки данных.
Вы также поймете, как ограничить создание атрибутов с помощью __slots__. Мы будем изменять файл stock.py на протяжении всей этой лабораторной работы, чтобы применить эти концепции.
Реализация приватных атрибутов
В Python мы используем соглашение об именовании, чтобы указать, что атрибут предназначен для внутреннего использования внутри класса. Мы добавляем префикс в виде символа подчеркивания (_) к таким атрибутам. Это сигнализирует другим разработчикам, что эти атрибуты не являются частью публичного API (public API) и не должны быть доступны напрямую извне класса.
Давайте посмотрим на текущий класс Stock в файле stock.py. У него есть переменная класса (class variable) с именем types.
class Stock:
## Class variable for type conversions
types = (str, int, float)
## Rest of the class...
Переменная класса types используется внутри класса для преобразования данных строки. Чтобы указать, что это деталь реализации (implementation detail), мы отметим ее как приватную.
Инструкции:
Откройте файл
stock.pyв редакторе.Измените переменную класса
types, добавив в начало символ подчеркивания, изменив ее на_types.class Stock: ## Class variable for type conversions _types = (str, int, float) ## Rest of the class...Обновите метод
from_row, чтобы он использовал переименованную переменную_types.@classmethod def from_row(cls, row): values = [func(val) for func, val in zip(cls._types, row)] return cls(*values)Сохраните файл
stock.py.Создайте Python-скрипт с именем
test_stock.pyдля проверки ваших изменений. Вы можете создать файл в редакторе, используя следующую команду:touch /home/labex/project/test_stock.pyДобавьте следующий код в файл
test_stock.py. Этот код создает экземпляры классаStockи выводит информацию о них.from stock import Stock ## Create a stock instance s = Stock('GOOG', 100, 490.10) print(f"Name: {s.name}, Shares: {s.shares}, Price: {s.price}") print(f"Cost: {s.cost()}") ## Create from row row = ['AAPL', '50', '142.5'] apple = Stock.from_row(row) print(f"Name: {apple.name}, Shares: {apple.shares}, Price: {apple.price}") print(f"Cost: {apple.cost()}")Запустите тестовый скрипт, используя следующую команду в терминале:
python /home/labex/project/test_stock.pyВы должны увидеть вывод, похожий на:
Name: GOOG, Shares: 100, Price: 490.1 Cost: 49010.0 Name: AAPL, Shares: 50, Price: 142.5 Cost: 7125.0
Преобразование методов в свойства (Properties)
Свойства (properties) в Python позволяют вам получать доступ к вычисляемым значениям, как к атрибутам. Это устраняет необходимость в круглых скобках при вызове метода, делая ваш код чище и более последовательным.
В настоящее время в нашем классе Stock есть метод cost(), который вычисляет общую стоимость акций.
def cost(self):
return self.shares * self.price
Чтобы получить значение стоимости, мы должны вызвать его с круглыми скобками:
s = Stock('GOOG', 100, 490.10)
print(s.cost()) ## Calls the method
Мы можем улучшить это, преобразовав метод cost() в свойство (property), что позволит нам получать доступ к значению стоимости без круглых скобок:
s = Stock('GOOG', 100, 490.10)
print(s.cost) ## Accesses the property
Инструкции:
Откройте файл
stock.pyв редакторе.Замените метод
cost()свойством (property), используя декоратор@property:@property def cost(self): return self.shares * self.priceСохраните файл
stock.py.Создайте новый файл с именем
test_property.pyв редакторе:touch /home/labex/project/test_property.pyДобавьте следующий код в файл
test_property.py, чтобы создать экземплярStockи получить доступ к свойствуcost:from stock import Stock ## Create a stock instance s = Stock('GOOG', 100, 490.10) ## Access cost as a property (no parentheses) print(f"Stock: {s.name}") print(f"Shares: {s.shares}") print(f"Price: {s.price}") print(f"Cost: {s.cost}") ## Using the propertyЗапустите тестовый скрипт:
python /home/labex/project/test_property.pyВы должны увидеть вывод, похожий на:
Stock: GOOG Shares: 100 Price: 490.1 Cost: 49010.0
Реализация валидации свойств (Property Validation)
Свойства (properties) также позволяют вам контролировать, как значения атрибутов извлекаются, устанавливаются и удаляются. Это полезно для добавления валидации (validation) к вашим атрибутам, гарантируя, что значения соответствуют определенным критериям.
В нашем классе Stock мы хотим убедиться, что shares является неотрицательным целым числом, а price - неотрицательным числом с плавающей точкой. Мы будем использовать декораторы свойств (property decorators) вместе с геттерами (getters) и сеттерами (setters) для достижения этой цели.
Инструкции:
Откройте файл
stock.pyв редакторе.Добавьте приватные атрибуты
_sharesи_priceв классStockи измените конструктор, чтобы использовать их:def __init__(self, name, shares, price): self.name = name self._shares = shares ## Using private attribute self._price = price ## Using private attributeОпределите свойства (properties) для
sharesиpriceс валидацией (validation):@property def shares(self): return self._shares @shares.setter def shares(self, value): if not isinstance(value, int): raise TypeError("Expected integer") if value < 0: raise ValueError("shares must be >= 0") self._shares = value @property def price(self): return self._price @price.setter def price(self, value): if not isinstance(value, float): raise TypeError("Expected float") if value < 0: raise ValueError("price must be >= 0") self._price = valueОбновите конструктор, чтобы использовать сеттеры свойств (property setters) для валидации (validation):
def __init__(self, name, shares, price): self.name = name self.shares = shares ## Using property setter self.price = price ## Using property setterСохраните файл
stock.py.Создайте тестовый скрипт с именем
test_validation.py:touch /home/labex/project/test_validation.pyДобавьте следующий код в файл
test_validation.py:from stock import Stock ## Create a valid stock instance s = Stock('GOOG', 100, 490.10) print(f"Initial: Name={s.name}, Shares={s.shares}, Price={s.price}, Cost={s.cost}") ## Test valid updates try: s.shares = 50 ## Valid update print(f"After setting shares=50: Shares={s.shares}, Cost={s.cost}") except Exception as e: print(f"Error setting shares=50: {e}") try: s.price = 123.45 ## Valid update print(f"After setting price=123.45: Price={s.price}, Cost={s.cost}") except Exception as e: print(f"Error setting price=123.45: {e}") ## Test invalid updates try: s.shares = "50" ## Invalid type (string) print("This line should not execute") except Exception as e: print(f"Error setting shares='50': {e}") try: s.shares = -10 ## Invalid value (negative) print("This line should not execute") except Exception as e: print(f"Error setting shares=-10: {e}") try: s.price = "123.45" ## Invalid type (string) print("This line should not execute") except Exception as e: print(f"Error setting price='123.45': {e}") try: s.price = -10.0 ## Invalid value (negative) print("This line should not execute") except Exception as e: print(f"Error setting price=-10.0: {e}")Запустите тестовый скрипт:
python /home/labex/project/test_validation.pyВы должны увидеть вывод, показывающий успешные допустимые обновления и соответствующие сообщения об ошибках для недопустимых обновлений.
Initial: Name=GOOG, Shares=100, Price=490.1, Cost=49010.0 After setting shares=50: Shares=50, Cost=24505.0 After setting price=123.45: Price=123.45, Cost=6172.5 Error setting shares='50': Expected integer Error setting shares=-10: shares must be >= 0 Error setting price='123.45': Expected float Error setting price=-10.0: price must be >= 0
Использование __slots__ для оптимизации памяти (Memory Optimization)
Атрибут __slots__ ограничивает атрибуты, которые может иметь класс. Он предотвращает добавление новых атрибутов к экземплярам и снижает использование памяти.
В нашем классе Stock мы будем использовать __slots__, чтобы:
- Ограничить создание атрибутов только атрибутами, которые мы определили.
- Повысить эффективность использования памяти, особенно при создании большого количества экземпляров.
Инструкции:
Откройте файл
stock.pyв редакторе.Добавьте переменную класса
__slots__, перечислив все имена приватных атрибутов, используемых классом:class Stock: ## Class variable for type conversions _types = (str, int, float) ## Define slots to restrict attribute creation __slots__ = ('name', '_shares', '_price') ## Rest of the class...Сохраните файл.
Создайте тестовый скрипт с именем
test_slots.py:touch /home/labex/project/test_slots.pyДобавьте следующий код в файл
test_slots.py:from stock import Stock ## Create a stock instance s = Stock('GOOG', 100, 490.10) ## Access existing attributes print(f"Name: {s.name}") print(f"Shares: {s.shares}") print(f"Price: {s.price}") print(f"Cost: {s.cost}") ## Try to add a new attribute try: s.extra = "This will fail" print(f"Extra: {s.extra}") except AttributeError as e: print(f"Error: {e}")Запустите тестовый скрипт:
python /home/labex/project/test_slots.pyВы должны увидеть вывод, показывающий, что вы можете получить доступ к определенным атрибутам, но попытка добавить новый атрибут вызывает
AttributeError.Name: GOOG Shares: 100 Price: 490.1 Cost: 49010.0 Error: 'Stock' object has no attribute 'extra'
Согласование валидации типов (Type Validation) с переменными класса (Class Variables)
В настоящее время наш класс Stock использует как переменную класса _types, так и сеттеры свойств (property setters) для обработки типов. Чтобы повысить согласованность и удобство сопровождения, мы согласуем эти механизмы, чтобы они использовали одну и ту же информацию о типах.
Инструкции:
Откройте файл
stock.pyв редакторе.Измените сеттеры свойств (property setters), чтобы использовать типы, определенные в переменной класса
_types:@property def shares(self): return self._shares @shares.setter def shares(self, value): if not isinstance(value, self._types[1]): raise TypeError(f"Expected {self._types[1].__name__}") if value < 0: raise ValueError("shares must be >= 0") self._shares = value @property def price(self): return self._price @price.setter def price(self, value): if not isinstance(value, self._types[2]): raise TypeError(f"Expected {self._types[2].__name__}") if value < 0: raise ValueError("price must be >= 0") self._price = valueСохраните файл
stock.py.Создайте тестовый скрипт с именем
test_subclass.py:touch /home/labex/project/test_subclass.pyДобавьте следующий код в файл
test_subclass.py:from stock import Stock from decimal import Decimal ## Create a subclass with different types class DStock(Stock): _types = (str, int, Decimal) ## Test the base class s = Stock('GOOG', 100, 490.10) print(f"Stock: {s.name}, Shares: {s.shares}, Price: {s.price}, Cost: {s.cost}") ## Test valid update with float try: s.price = 500.25 print(f"Updated Stock price: {s.price}, Cost: {s.cost}") except Exception as e: print(f"Error updating Stock price: {e}") ## Test the subclass with Decimal ds = DStock('AAPL', 50, Decimal('142.50')) print(f"DStock: {ds.name}, Shares: {ds.shares}, Price: {ds.price}, Cost: {ds.cost}") ## Test invalid update with float (should require Decimal) try: ds.price = 150.75 print(f"Updated DStock price: {ds.price}") except Exception as e: print(f"Error updating DStock price: {e}") ## Test valid update with Decimal try: ds.price = Decimal('155.25') print(f"Updated DStock price: {ds.price}, Cost: {ds.cost}") except Exception as e: print(f"Error updating DStock price: {e}")Запустите тестовый скрипт:
python /home/labex/project/test_subclass.pyВы должны увидеть, что базовый класс
Stockпринимает значенияfloatдля цены, в то время как подклассDStockтребует значенияDecimal.Stock: GOOG, Shares: 100, Price: 490.1, Cost: 49010.0 Updated Stock price: 500.25, Cost: 50025.0 DStock: AAPL, Shares: 50, Price: 142.50, Cost: 7125.00 Error updating DStock price: Expected Decimal Updated DStock price: 155.25, Cost: 7762.50
Итог (Summary)
В этой лабораторной работе вы узнали, как использовать приватные атрибуты, преобразовывать методы в свойства (properties), реализовывать валидацию свойств (property validation), использовать __slots__ для оптимизации памяти (memory optimization) и согласовывать валидацию типов (type validation) с переменными класса (class variables). Эти методы повышают надежность, эффективность и удобство сопровождения ваших классов, обеспечивая инкапсуляцию и предоставляя четкие интерфейсы.