Введение
В этой лабораторной работе вы узнаете о декораторах классов в Python, а также повторите и расширите концепцию дескрипторов Python. Комбинируя эти концепции, вы сможете создавать мощные и лаконичные структуры кода.
В этой лабораторной работе вы будете опираться на предыдущие концепции дескрипторов и расширять их с помощью декораторов классов. Эта комбинация позволит вам создавать более чистый, более поддерживаемый код с расширенными возможностями валидации. Файлы, которые необходимо будет изменить, это validate.py и structure.py.
Реализация проверки типов с помощью дескрипторов
На этом этапе мы создадим класс Stock, который использует дескрипторы для проверки типов. Но сначала давайте разберемся, что такое дескрипторы. Дескрипторы — это очень мощная функция в Python. Они дают вам контроль над тем, как атрибуты доступны в классах.
Дескрипторы — это объекты, которые определяют, как атрибуты доступны в других объектах. Они делают это, реализуя специальные методы, такие как __get__, __set__ и __delete__. Эти методы позволяют дескрипторам управлять тем, как атрибуты извлекаются, устанавливаются и удаляются. Дескрипторы очень полезны для реализации валидации, проверки типов и вычисляемых свойств. Например, вы можете использовать дескриптор, чтобы убедиться, что атрибут всегда является положительным числом или строкой определенного формата.
Файл validate.py уже содержит классы валидаторов (String, PositiveInteger, PositiveFloat). Мы можем использовать эти классы для валидации атрибутов нашего класса Stock.
Теперь давайте создадим наш класс Stock с дескрипторами.
Сначала откройте файл
stock.pyв вашем редакторе.После открытия файла замените содержимое-заполнитель следующим кодом:
## stock.py
from structure import Structure
from validate import String, PositiveInteger, PositiveFloat
class Stock(Structure):
_fields = ('name', 'shares', 'price')
name = String()
shares = PositiveInteger()
price = PositiveFloat()
@property
def cost(self):
return self.shares * self.price
def sell(self, nshares):
self.shares -= nshares
## Create an __init__ method based on _fields
Stock.create_init()
Давайте разберем, что делает этот код. Кортеж _fields определяет атрибуты класса Stock. Это имена атрибутов, которые будут у объектов Stock.
Атрибуты name, shares и price определены как объекты дескрипторов. Дескриптор String() гарантирует, что атрибут name является строкой. Дескриптор PositiveInteger() гарантирует, что атрибут shares является положительным целым числом. А дескриптор PositiveFloat() гарантирует, что атрибут price является положительным числом с плавающей запятой.
Свойство cost является вычисляемым свойством. Оно рассчитывает общую стоимость акции на основе количества акций и цены за акцию.
Метод sell используется для уменьшения количества акций. Когда вы вызываете этот метод с количеством продаваемых акций, он вычитает это количество из атрибута shares.
Строка Stock.create_init() динамически создает метод __init__ для нашего класса. Этот метод позволяет нам создавать объекты Stock, передавая значения для атрибутов name, shares и price.
После добавления кода сохраните файл. Это гарантирует, что ваши изменения будут сохранены и могут быть использованы при запуске тестов.
Теперь давайте запустим тесты, чтобы проверить вашу реализацию. Сначала перейдите в каталог
~/project, выполнив следующую команду:
cd ~/project
Затем запустите тесты, используя следующую команду:
python3 teststock.py
Если ваша реализация верна, вы увидите вывод, похожий на этот:
.........
----------------------------------------------------------------------
Ran 9 tests in 0.001s
OK
Этот вывод означает, что все тесты пройдены. Дескрипторы успешно проверяют типы каждого атрибута!
Давайте попробуем создать объект Stock в интерпретаторе Python. Сначала убедитесь, что вы находитесь в каталоге ~/project. Затем выполните следующую команду:
cd ~/project
python3 -c "from stock import Stock; s = Stock('GOOG', 100, 490.1); print(s); print(f'Cost: {s.cost}')"
Вы должны увидеть следующий вывод:
Stock('GOOG', 100, 490.1)
Cost: 49010.0
Вы успешно реализовали дескрипторы для проверки типов! Теперь давайте улучшим этот код дальше.
Создание декоратора класса для валидации
В предыдущем шаге наша реализация работала, но была избыточность. Нам приходилось указывать как кортеж _fields, так и атрибуты дескрипторов. Это не очень эффективно, и мы можем это улучшить. В Python декораторы классов — это мощный инструмент, который может помочь нам упростить этот процесс. Декоратор класса — это функция, которая принимает класс в качестве аргумента, как-то его изменяет, а затем возвращает измененный класс. Используя декоратор класса, мы можем автоматически извлекать информацию о полях из дескрипторов, что сделает наш код более чистым и поддерживаемым.
Давайте создадим декоратор класса для упрощения нашего кода. Вот шаги, которым вам нужно следовать:
Сначала откройте файл
structure.pyв вашем редакторе.Затем добавьте следующий код в начало файла
structure.py, сразу после любых операторов импорта. Этот код определяет наш декоратор класса:
from validate import Validator
def validate_attributes(cls):
"""
Class decorator that extracts Validator instances
and builds the _fields list automatically
"""
validators = []
for name, val in vars(cls).items():
if isinstance(val, Validator):
validators.append(val)
## Set _fields based on validator names
cls._fields = [val.name for val in validators]
## Create initialization method
cls.create_init()
return cls
Давайте разберем, что делает этот декоратор:
- Сначала он создает пустой список
validators. Затем он перебирает все атрибуты класса с помощьюvars(cls).items(). Если атрибут является экземпляром классаValidator, он добавляет этот атрибут в списокvalidators. - После этого он устанавливает атрибут
_fieldsкласса. Он создает список имен из валидаторов в спискеvalidatorsи присваивает егоcls._fields. - Наконец, он вызывает метод
create_init()класса для генерации метода__init__, а затем возвращает измененный класс.
После добавления кода сохраните файл
structure.py. Сохранение файла гарантирует, что ваши изменения будут сохранены.Теперь нам нужно изменить наш файл
stock.py, чтобы использовать этот новый декоратор. Откройте файлstock.pyв вашем редакторе.Обновите файл
stock.py, чтобы использовать декораторvalidate_attributes. Замените существующий код следующим:
## stock.py
from structure import Structure, validate_attributes
from validate import String, PositiveInteger, PositiveFloat
@validate_attributes
class Stock(Structure):
name = String()
shares = PositiveInteger()
price = PositiveFloat()
@property
def cost(self):
return self.shares * self.price
def sell(self, nshares):
self.shares -= nshares
Обратите внимание на внесенные нами изменения:
- Мы добавили декоратор
@validate_attributesпрямо над определением классаStock. Это указывает Python применить декораторvalidate_attributesк классуStock. - Мы удалили явное объявление
_fields, поскольку декоратор будет обрабатывать его автоматически. - Мы также удалили вызов
Stock.create_init(), поскольку декоратор берет на себя создание метода__init__.
В результате класс стал проще и чище. Декоратор берет на себя все детали, которые мы раньше обрабатывали вручную.
- После внесения этих изменений нам нужно убедиться, что все по-прежнему работает должным образом. Снова запустите тесты, используя следующие команды:
cd ~/project
python3 teststock.py
Если все работает правильно, вы должны увидеть следующий вывод:
.........
----------------------------------------------------------------------
Ran 9 tests in 0.001s
OK
Этот вывод указывает на то, что все тесты успешно пройдены.
Давайте также протестируем наш класс Stock в интерактивном режиме. Выполните следующую команду в терминале:
cd ~/project
python3 -c "from stock import Stock; s = Stock('GOOG', 100, 490.1); print(s); print(f'Cost: {s.cost}')"
Вы должны увидеть следующий вывод:
Stock('GOOG', 100, 490.1)
Cost: 49010.0
Отлично! Вы успешно реализовали декоратор класса, который упрощает наш код, автоматически обрабатывая объявления полей и инициализацию. Это делает наш код более эффективным и легким в поддержке.
Применение декораторов через наследование
Во втором шаге мы создали декоратор класса, который упрощает наш код. Декоратор класса — это особый тип функции, которая принимает класс в качестве аргумента и возвращает измененный класс. Это полезный инструмент в Python для добавления функциональности классам без изменения их исходного кода. Однако нам все еще приходится явно применять декоратор @validate_attributes к каждому классу. Это означает, что каждый раз, когда мы создаем новый класс, требующий валидации, нам нужно помнить о добавлении этого декоратора, что может быть несколько обременительно.
Мы можем улучшить это, автоматически применяя декоратор через наследование. Наследование — это фундаментальная концепция в объектно-ориентированном программировании, где подкласс может наследовать атрибуты и методы от родительского класса. Метод __init_subclass__ в Python 3.6 был введен для того, чтобы родительские классы могли настраивать инициализацию подклассов. Это означает, что при создании подкласса родительский класс может выполнять над ним некоторые действия. Мы можем использовать эту функцию для автоматического применения нашего декоратора к любому классу, который наследуется от Structure.
Давайте реализуем это:
Откройте файл
structure.pyв вашем редакторе. Этот файл содержит определение классаStructure, и мы собираемся изменить его, чтобы использовать метод__init_subclass__.Добавьте метод
__init_subclass__в классStructure:
class Structure:
_fields = ()
_types = ()
def __init__(self, *args):
if len(args) != len(self._fields):
raise TypeError(f'Expected {len(self._fields)} arguments')
for name, val in zip(self._fields, args):
setattr(self, name, val)
def __repr__(self):
values = ', '.join(repr(getattr(self, name)) for name in self._fields)
return f'{type(self).__name__}({values})'
@classmethod
def create_init(cls):
'''
Create an __init__ method from _fields
'''
body = 'def __init__(self, %s):\n' % ', '.join(cls._fields)
for name in cls._fields:
body += f' self.{name} = {name}\n'
## Execute the function creation code
namespace = {}
exec(body, namespace)
setattr(cls, '__init__', namespace['__init__'])
@classmethod
def __init_subclass__(cls):
validate_attributes(cls)
Метод __init_subclass__ является классовым методом, что означает, что его можно вызывать для самого класса, а не для экземпляра класса. Когда создается подкласс Structure, этот метод будет вызван автоматически. Внутри этого метода мы вызываем декоратор validate_attributes для подкласса cls. Таким образом, каждый подкласс Structure автоматически получит поведение валидации.
- Сохраните файл.
После внесения изменений в файл structure.py нам нужно сохранить его, чтобы изменения были применены.
Теперь давайте обновим наш файл
stock.py, чтобы воспользоваться этой новой функцией. Откройте файлstock.pyв вашем редакторе, чтобы изменить его. Этот файл содержит определение классаStock, и мы собираемся сделать его наследуемым от классаStructureдля использования автоматического применения декоратора.Измените файл
stock.py, чтобы удалить явный декоратор:
## stock.py
from structure import Structure
from validate import String, PositiveInteger, PositiveFloat
class Stock(Structure):
name = String()
shares = PositiveInteger()
price = PositiveFloat()
@property
def cost(self):
return self.shares * self.price
def sell(self, nshares):
self.shares -= nshares
Обратите внимание, что мы:
- Удален импорт
validate_attributes, поскольку нам больше не нужно импортировать его явно, так как декоратор применяется автоматически через наследование. - Удален декоратор
@validate_attributes, поскольку метод__init_subclass__в классеStructureпозаботится о его применении. - Код теперь полагается исключительно на наследование от
Structureдля получения поведения валидации.
- Снова запустите тесты, чтобы убедиться, что все по-прежнему работает:
cd ~/project
python3 teststock.py
Запуск тестов важен, чтобы убедиться, что наши изменения ничего не сломали. Если все тесты пройдены, это означает, что автоматическое применение декоратора через наследование работает правильно.
Вы должны увидеть, что все тесты пройдены:
.........
----------------------------------------------------------------------
Ran 9 tests in 0.001s
OK
Давайте снова протестируем наш класс Stock, чтобы убедиться, что он работает должным образом:
cd ~/project
python3 -c "from stock import Stock; s = Stock('GOOG', 100, 490.1); print(s); print(f'Cost: {s.cost}')"
Эта команда создает экземпляр класса Stock и выводит его представление и стоимость. Если вывод соответствует ожиданиям, это означает, что класс Stock работает правильно с автоматическим применением декоратора.
Вывод:
Stock('GOOG', 100, 490.1)
Cost: 49010.0
Эта реализация еще чище! Используя __init_subclass__, мы устранили необходимость явного применения декораторов. Любой класс, наследуемый от Structure, автоматически получает поведение валидации.
Добавление функциональности преобразования строк
В программировании часто бывает полезно создавать экземпляры класса из строк данных, особенно при работе с данными из таких источников, как файлы CSV. В этом разделе мы добавим возможность создавать экземпляры класса Structure из строк данных. Мы сделаем это, реализовав классовый метод from_row в классе Structure.
Сначала откройте файл
structure.pyв вашем редакторе. Здесь мы внесем изменения в код.Далее мы модифицируем функцию
validate_attributes. Эта функция является декоратором класса, который извлекает экземплярыValidatorи автоматически создает списки_fieldsи_types. Мы обновим ее, чтобы она также собирала информацию о типах.
def validate_attributes(cls):
"""
Class decorator that extracts Validator instances
and builds the _fields and _types lists automatically
"""
validators = []
for name, val in vars(cls).items():
if isinstance(val, Validator):
validators.append(val)
## Set _fields based on validator names
cls._fields = [val.name for val in validators]
## Set _types based on validator expected_types
cls._types = [getattr(val, 'expected_type', lambda x: x) for val in validators]
## Create initialization method
cls.create_init()
return cls
В этой обновленной функции мы собираем атрибут expected_type из каждого валидатора и сохраняем его в классовой переменной _types. Это будет полезно позже, когда мы будем преобразовывать данные из строк в правильные типы.
- Теперь мы добавим классовый метод
from_rowв классStructure. Этот метод позволит нам создавать экземпляр класса из строки данных, которая может быть списком или кортежем.
@classmethod
def from_row(cls, row):
"""
Create an instance from a data row (list or tuple)
"""
rowdata = [func(val) for func, val in zip(cls._types, row)]
return cls(*rowdata)
Вот как работает этот метод:
- Он принимает строку данных, которая может быть в виде списка или кортежа.
- Он преобразует каждое значение в строке в ожидаемый тип, используя соответствующую функцию из списка
_types. - Затем он создает и возвращает новый экземпляр класса, используя преобразованные значения.
После внесения этих изменений сохраните файл
structure.py. Это гарантирует, что изменения в вашем коде будут сохранены.Давайте протестируем наш метод
from_row, чтобы убедиться, что он работает должным образом. Мы создадим простой тест с использованием классаStock. Выполните следующую команду в терминале:
cd ~/project
python3 -c "from stock import Stock; s = Stock.from_row(['GOOG', '100', '490.1']); print(s); print(f'Cost: {s.cost}')"
Вы должны увидеть вывод, похожий на этот:
Stock('GOOG', 100, 490.1)
Cost: 49010.0
Обратите внимание, что строковые значения '100' и '490.1' были автоматически преобразованы в правильные типы (целое число и число с плавающей запятой). Это показывает, что наш метод from_row работает правильно.
- Наконец, давайте попробуем прочитать данные из файла CSV с помощью нашего модуля
reader.py. Выполните следующую команду в терминале:
cd ~/project
python3 -c "from stock import Stock; import reader; portfolio = reader.read_csv_as_instances('portfolio.csv', Stock); print(portfolio); print(f'Total value: {sum(s.cost for s in portfolio)}')"
Вы должны увидеть вывод, показывающий акции из файла CSV:
[Stock('GOOG', 100, 490.1), Stock('AAPL', 50, 545.75), Stock('MSFT', 200, 30.47)]
Total value: 82391.5
Метод from_row позволяет нам легко преобразовывать данные CSV в экземпляры класса Stock. В сочетании с функцией read_csv_as_instances мы получаем мощный способ загрузки и работы со структурированными данными.
Добавление валидации аргументов методов
В Python валидация данных является важной частью написания надежного кода. В этом разделе мы расширим нашу валидацию, автоматически проверяя аргументы методов. Файл validate.py уже содержит декоратор @validated. Декоратор в Python — это специальная функция, которая может изменять другую функцию. Декоратор @validated здесь может проверять аргументы функции на соответствие их аннотациям. Аннотации в Python — это способ добавления метаданных к параметрам и возвращаемым значениям функций.
Давайте модифицируем наш код, чтобы применить этот декоратор к методам с аннотациями:
- Сначала нам нужно понять, как работает декоратор
validated. Откройте файлvalidate.pyв вашем редакторе, чтобы ознакомиться с ним.
Декоратор validated использует аннотации функций для валидации аргументов. Прежде чем разрешить выполнение функции, он создает экземпляр класса валидатора для каждого аннотированного параметра и вызывает метод validate для проверки аргумента. Например, если аргумент аннотирован как PositiveInteger, декоратор создаст экземпляр PositiveInteger и проверит, действительно ли переданное значение является положительным целым числом. Если валидация не удалась, он собирает все ошибки и вызывает TypeError с подробными сообщениями об ошибках.
Теперь мы модифицируем функцию
validate_attributesвstructure.py, чтобы обернуть аннотированные методы декораторомvalidated. Это означает, что любые методы с аннотациями в классе будут автоматически проверять свои аргументы. Откройте файлstructure.pyв вашем редакторе.Обновите функцию
validate_attributes:
def validate_attributes(cls):
"""
Class decorator that:
1. Extracts Validator instances and builds _fields and _types lists
2. Applies @validated decorator to methods with annotations
"""
## Import the validated decorator
from validate import validated
## Process validator descriptors
validators = []
for name, val in vars(cls).items():
if isinstance(val, Validator):
validators.append(val)
## Set _fields based on validator names
cls._fields = [val.name for val in validators]
## Set _types based on validator expected_types
cls._types = [getattr(val, 'expected_type', lambda x: x) for val in validators]
## Apply @validated decorator to methods with annotations
for name, val in vars(cls).items():
if callable(val) and hasattr(val, '__annotations__'):
setattr(cls, name, validated(val))
## Create initialization method
cls.create_init()
return cls
Эта обновленная функция теперь выполняет следующее:
Обрабатывает дескрипторы валидаторов, как и раньше. Дескрипторы валидаторов используются для определения правил валидации для атрибутов класса.
Находит все методы с аннотациями в классе. Аннотации добавляются к параметрам методов для указания ожидаемого типа аргумента.
Применяет декоратор
@validatedк этим методам. Это гарантирует, что аргументы, передаваемые этим методам, будут валидированы в соответствии с их аннотациями.Сохраните файл после внесения этих изменений. Сохранение файла важно, поскольку оно гарантирует, что наши модификации будут сохранены и могут быть использованы позже.
Теперь давайте обновим метод
sellв классеStock, чтобы включить аннотацию. Аннотации помогают указать ожидаемый тип аргумента, который будет использоваться декоратором@validatedдля валидации. Откройте файлstock.pyв вашем редакторе.Измените метод
sell, чтобы включить аннотацию типа:
## stock.py
from structure import Structure
from validate import String, PositiveInteger, PositiveFloat
class Stock(Structure):
name = String()
shares = PositiveInteger()
price = PositiveFloat()
@property
def cost(self):
return self.shares * self.price
def sell(self, nshares: PositiveInteger):
self.shares -= nshares
Важное изменение — добавление : PositiveInteger к параметру nshares. Это указывает Python (и нашему декоратору @validated), чтобы валидировать этот аргумент с использованием валидатора PositiveInteger. Таким образом, при вызове метода sell аргумент nshares должен быть положительным целым числом.
- Снова запустите тесты, чтобы убедиться, что все по-прежнему работает. Запуск тестов — это хороший способ убедиться, что наши изменения не нарушили существующую функциональность.
cd ~/project
python3 teststock.py
Вы должны увидеть, что все тесты пройдены:
.........
----------------------------------------------------------------------
Ran 9 tests in 0.001s
OK
- Давайте протестируем нашу новую валидацию аргументов. Мы попробуем вызвать метод
sellс допустимыми и недопустимыми аргументами, чтобы увидеть, работает ли валидация должным образом.
cd ~/project
python3 -c "
from stock import Stock
s = Stock('GOOG', 100, 490.1)
s.sell(25)
print(s)
try:
s.sell(-25)
except Exception as e:
print(f'Error: {e}')
"
Вы должны увидеть вывод, похожий на:
Stock('GOOG', 75, 490.1)
Error: Bad Arguments
nshares: nshares must be >= 0
Это показывает, что валидация аргументов нашего метода работает! Первый вызов sell(25) успешен, потому что 25 — это положительное целое число. Но второй вызов sell(-25) завершается ошибкой, потому что -25 не является положительным целым числом.
Теперь вы реализовали полную систему для:
- Валидации атрибутов класса с использованием дескрипторов. Дескрипторы используются для определения правил валидации для атрибутов класса.
- Автоматического сбора информации о полях с использованием декораторов классов. Декораторы классов могут изменять поведение класса, например, собирать информацию о полях.
- Преобразования данных строк в экземпляры. Это полезно при работе с данными из внешних источников.
- Валидации аргументов методов с использованием аннотаций. Аннотации помогают указать ожидаемый тип аргумента для валидации.
Это демонстрирует мощь сочетания дескрипторов и декораторов в Python для создания выразительных, самовалидирующихся классов.
Резюме
В этой лабораторной работе вы научились комбинировать мощные возможности Python для создания чистого, самовалидирующегося кода. Вы освоили ключевые концепции, такие как использование дескрипторов для валидации атрибутов, создание декораторов классов для автоматизации генерации кода и автоматическое применение декораторов через наследование.
Эти методы являются мощными инструментами для создания надежного и поддерживаемого кода на Python. Они позволяют четко выражать требования к валидации и применять их во всей кодовой базе. Теперь вы можете применять эти шаблоны в своих собственных проектах на Python для повышения качества кода и уменьшения шаблонного кода.