Введение
В этом практическом занятии (лабораторной работе) вы узнаете о дескрипторах в Python, мощном механизме для настройки доступа к атрибутам объектов. Дескрипторы позволяют определить, как осуществляется доступ к атрибутам, как они устанавливаются и удаляются, что дает вам контроль над поведением объектов и позволяет реализовать логику валидации.
Цели этого практического занятия включают понимание протокола дескрипторов, создание и использование пользовательских дескрипторов, реализацию валидации данных с помощью дескрипторов и оптимизацию реализации дескрипторов. В ходе практического занятия вы создадите несколько файлов, в том числе descrip.py, stock.py и validate.py.
Понимание протокола дескрипторов
На этом этапе мы узнаем, как работают дескрипторы в Python, создав простой класс Stock. Дескрипторы в Python - это мощная возможность, которая позволяет настроить, как осуществляется доступ к атрибутам, как они устанавливаются и удаляются. Протокол дескрипторов состоит из трех специальных методов: __get__(), __set__() и __delete__(). Эти методы определяют, как дескриптор ведет себя при доступе к атрибуту, при присвоении ему значения или при удалении, соответственно.
Сначала нам нужно создать новый файл с именем stock.py в директории проекта. Этот файл будет содержать наш класс Stock. Вот код, который вы должны поместить в файл stock.py:
## stock.py
class Stock:
__slots__ = ['_name', '_shares', '_price']
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
@property
def name(self):
return self._name
@name.setter
def name(self, value):
if not isinstance(value, str):
raise TypeError('Expected a string')
self._name = value
@property
def shares(self):
return self._shares
@shares.setter
def shares(self, value):
if not isinstance(value, int):
raise TypeError('Expected an integer')
if value < 0:
raise ValueError('Expected a positive value')
self._shares = value
@property
def price(self):
return self._price
@price.setter
def price(self, value):
if not isinstance(value, (int, float)):
raise TypeError('Expected a number')
if value < 0:
raise ValueError('Expected a positive value')
self._price = value
def cost(self):
return self.shares * self.price
def sell(self, amount):
self.shares -= amount
def __repr__(self):
return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})'
В этом классе Stock мы используем декоратор property для определения методов-геттеров и сеттеров для атрибутов name, shares и price. Эти методы-геттеры и сеттеры действуют как дескрипторы, что означает, что они контролируют, как к этим атрибутам осуществляется доступ и как они устанавливаются. Например, методы-сеттеры валидируют входные значения, чтобы убедиться, что они имеют правильный тип и находятся в приемлемом диапазоне.
Теперь, когда наш файл stock.py готов, откроем оболочку Python, чтобы поэкспериментировать с классом Stock и увидеть, как работают дескрипторы на практике. Для этого откройте терминал и выполните следующие команды:
cd ~/project
python3 -i stock.py
Опция -i в команде python3 сообщает Python запустить интерактивную оболочку после выполнения файла stock.py. Таким образом, мы можем напрямую взаимодействовать с классом Stock, который мы только что определили.
В оболочке Python создадим объект акции и попробуем получить доступ к его атрибутам. Вот как это можно сделать:
s = Stock('GOOG', 100, 490.10)
s.name ## Should return 'GOOG'
s.shares ## Should return 100
Когда вы получаете доступ к атрибутам name и shares объекта s, Python на самом деле использует метод __get__ дескриптора в фоновом режиме. Декораторы property в нашем классе реализованы с использованием дескрипторов, что означает, что они контролируют доступ к атрибутам и их присваивание.
Давайте взглянем более детально на словарь класса, чтобы увидеть объекты дескрипторов. Словарь класса содержит все атрибуты и методы, определенные в классе. Вы можете просмотреть ключи словаря класса с помощью следующего кода:
Stock.__dict__.keys()
Вы должны увидеть вывод, похожий на следующий:
dict_keys(['__module__', '__slots__', '__init__', 'name', '_name', 'shares', '_shares', 'price', '_price', 'cost', 'sell', '__repr__', '__doc__'])
Ключи name, shares и price представляют объекты дескрипторов, созданные декораторами property.
Теперь давайте рассмотрим, как работают дескрипторы, вызвав их методы вручную. В качестве примера мы будем использовать дескриптор shares. Вот как это можно сделать:
## Get the descriptor object for 'shares'
q = Stock.__dict__['shares']
## Use the __get__ method to retrieve the value
q.__get__(s, Stock) ## Should return 100
## Use the __set__ method to set a new value
q.__set__(s, 75)
s.shares ## Should now be 75
## Try to set an invalid value
try:
q.__set__(s, '75') ## Should raise TypeError
except TypeError as e:
print(f"Error: {e}")
Когда вы получаете доступ к атрибуту, такому как s.shares, Python вызывает метод __get__ дескриптора, чтобы получить значение. Когда вы присваиваете значение, как в s.shares = 75, Python вызывает метод __set__ дескриптора. Затем дескриптор может проверить данные и вызвать ошибки, если входное значение не является допустимым.
После того, как вы закончите экспериментировать с классом Stock и дескрипторами, вы можете выйти из оболочки Python, выполнив следующую команду:
exit()
Создание пользовательских дескрипторов
На этом этапе мы создадим собственный класс дескриптора. Но сначала разберемся, что такое дескриптор. Дескриптор - это объект Python, реализующий протокол дескрипторов, состоящий из методов __get__, __set__ и __delete__. Эти методы позволяют дескриптору управлять доступом к атрибуту, его установкой и удалением. Создав собственный класс дескриптора, мы сможем лучше понять, как работает этот протокол.
Создайте новый файл с именем descrip.py в директории проекта. Этот файл будет содержать наш пользовательский класс дескриптора. Вот код:
## descrip.py
class Descriptor:
def __init__(self, name):
self.name = name
def __get__(self, instance, cls):
print(f'{self.name}:__get__')
## In a real descriptor, you would return a value here
def __set__(self, instance, value):
print(f'{self.name}:__set__ {value}')
## In a real descriptor, you would store the value here
def __delete__(self, instance):
print(f'{self.name}:__delete__')
## In a real descriptor, you would delete the value here
В классе Descriptor метод __init__ инициализирует дескриптор с именем. Метод __get__ вызывается при доступе к атрибуту, метод __set__ - при установке атрибута, а метод __delete__ - при удалении атрибута.
Теперь создадим тестовый файл, чтобы поэкспериментировать с нашим пользовательским дескриптором. Это поможет нам увидеть, как дескриптор ведет себя в различных сценариях. Создайте файл с именем test_descrip.py со следующим кодом:
## test_descrip.py
from descrip import Descriptor
class Foo:
a = Descriptor('a')
b = Descriptor('b')
c = Descriptor('c')
## Create an instance and try accessing the attributes
if __name__ == '__main__':
f = Foo()
print("Accessing attribute f.a:")
f.a
print("\nAccessing attribute f.b:")
f.b
print("\nSetting attribute f.a = 23:")
f.a = 23
print("\nDeleting attribute f.a:")
del f.a
В файле test_descrip.py мы импортируем класс Descriptor из файла descrip.py. Затем создаем класс Foo с тремя атрибутами a, b и c, каждый из которых управляется дескриптором. Создаем экземпляр класса Foo и выполняем такие операции, как доступ к атрибутам, их установка и удаление, чтобы увидеть, как вызываются методы дескриптора.
Теперь запустим этот тестовый файл, чтобы увидеть, как работают дескрипторы на практике. Откройте терминал, перейдите в директорию проекта и запустите тестовый файл с помощью следующих команд:
cd ~/project
python3 test_descrip.py
Вы должны увидеть такой вывод:
Accessing attribute f.a:
a:__get__
Accessing attribute f.b:
b:__get__
Setting attribute f.a = 23:
a:__set__ 23
Deleting attribute f.a:
a:__delete__
Как вы можете видеть, каждый раз, когда вы обращаетесь к атрибуту, управляемому дескриптором, устанавливаете его значение или удаляете, вызывается соответствующий магический метод (__get__, __set__ или __delete__).
Давайте также проверим наш дескриптор в интерактивном режиме. Это позволит нам протестировать дескриптор в реальном времени и сразу увидеть результаты. Откройте терминал, перейдите в директорию проекта и запустите интерактивную сессию Python с файлом descrip.py:
cd ~/project
python3 -i descrip.py
Теперь введите следующие команды в интерактивной сессии Python, чтобы увидеть, как работает протокол дескрипторов:
class Foo:
a = Descriptor('a')
b = Descriptor('b')
c = Descriptor('c')
f = Foo()
f.a ## Should call __get__
f.b ## Should call __get__
f.a = 23 ## Should call __set__
del f.a ## Should call __delete__
exit()
Основная идея здесь заключается в том, что дескрипторы предоставляют способ перехватывать и настраивать доступ к атрибутам. Это делает их мощными инструментами для реализации валидации данных, вычисляемых атрибутов и других продвинутых поведений. Используя дескрипторы, вы можете лучше контролировать, как к атрибутам вашего класса осуществляется доступ, как они устанавливаются и удаляются.
Реализация валидаторов с использованием дескрипторов
На этом этапе мы создадим систему валидации с использованием дескрипторов. Но сначала разберемся, что такое дескрипторы и почему мы их используем. Дескрипторы - это объекты Python, реализующие протокол дескрипторов, который включает методы __get__, __set__ или __delete__. Они позволяют настроить, как осуществляется доступ к атрибуту объекта, как он устанавливается или удаляется. В нашем случае мы используем дескрипторы для создания системы валидации, которая обеспечивает целостность данных. Это означает, что данные, хранящиеся в наших объектах, всегда будут соответствовать определенным критериям, например, иметь определенный тип или быть положительными значениями.
Теперь приступим к созданию нашей системы валидации. Создадим новый файл с именем validate.py в директории проекта. Этот файл будет содержать классы, реализующие наши валидаторы.
## validate.py
class Validator:
def __init__(self, name):
self.name = name
@classmethod
def check(cls, value):
return value
def __set__(self, instance, value):
instance.__dict__[self.name] = self.check(value)
class String(Validator):
expected_type = str
@classmethod
def check(cls, value):
if not isinstance(value, cls.expected_type):
raise TypeError(f'Expected {cls.expected_type}')
return value
class PositiveInteger(Validator):
expected_type = int
@classmethod
def check(cls, value):
if not isinstance(value, cls.expected_type):
raise TypeError(f'Expected {cls.expected_type}')
if value < 0:
raise ValueError('Expected a positive value')
return value
class PositiveFloat(Validator):
expected_type = float
@classmethod
def check(cls, value):
if not isinstance(value, (int, float)):
raise TypeError('Expected a number')
if value < 0:
raise ValueError('Expected a positive value')
return float(value)
В файле validate.py мы сначала определяем базовый класс Validator. Этот класс имеет метод __init__, который принимает параметр name, который будет использоваться для идентификации атрибута, подлежащего валидации. Метод check - это метод класса, который просто возвращает переданное ему значение. Метод __set__ - это метод дескриптора, который вызывается при установке атрибута объекта. Он вызывает метод check для валидации значения, а затем сохраняет валидированное значение в словаре объекта.
Затем мы определяем три подкласса Validator: String, PositiveInteger и PositiveFloat. Каждый из этих подклассов переопределяет метод check для выполнения конкретных проверок валидации. Класс String проверяет, является ли значение строкой, класс PositiveInteger проверяет, является ли значение положительным целым числом, а класс PositiveFloat проверяет, является ли значение положительным числом (целым или вещественным).
Теперь, когда мы определили наши валидаторы, изменим наш класс Stock, чтобы использовать эти валидаторы. Создадим новый файл с именем stock_with_validators.py и импортируем валидаторы из файла validate.py.
## stock_with_validators.py
from validate import String, PositiveInteger, PositiveFloat
class Stock:
name = String('name')
shares = PositiveInteger('shares')
price = PositiveFloat('price')
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
def cost(self):
return self.shares * self.price
def sell(self, amount):
self.shares -= amount
def __repr__(self):
return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})'
В файле stock_with_validators.py мы определяем класс Stock и используем валидаторы в качестве атрибутов класса. Это означает, что каждый раз, когда устанавливается атрибут объекта Stock, будет вызван метод __set__ соответствующего валидатора для валидации значения. Метод __init__ инициализирует атрибуты объекта Stock, а методы cost, sell и __repr__ предоставляют дополнительную функциональность.
Теперь протестируем наш класс Stock, основанный на валидаторах. Откроем терминал, перейдем в директорию проекта и запустим файл stock_with_validators.py в интерактивном режиме.
cd ~/project
python3 -i stock_with_validators.py
После запуска интерпретатора Python мы можем попробовать несколько команд для тестирования системы валидации.
## Create a stock with valid values
s = Stock('GOOG', 100, 490.10)
print(s.name) ## Should return 'GOOG'
print(s.shares) ## Should return 100
print(s.price) ## Should return 490.1
## Try changing to valid values
s.shares = 75
print(s.shares) ## Should return 75
## Try setting invalid values
try:
s.shares = '75' ## Should raise TypeError
except TypeError as e:
print(f"Error setting shares to string: {e}")
try:
s.shares = -50 ## Should raise ValueError
except ValueError as e:
print(f"Error setting negative shares: {e}")
exit()
В тестовом коде мы сначала создаем объект Stock с допустимыми значениями и выводим его атрибуты, чтобы убедиться, что они установлены правильно. Затем мы пытаемся изменить атрибут shares на допустимое значение и выводим его снова, чтобы подтвердить изменение. Наконец, мы пытаемся установить атрибут shares в недопустимое значение (строку и отрицательное число) и перехватываем исключения, которые вызываются валидаторами.
Обратите внимание, как наш код стал намного чище. Класс Stock больше не нуждается в реализации всех этих методов-свойств - валидаторы обрабатывают все проверки типов и ограничения.
Дескрипторы позволили нам создать повторно используемую систему валидации, которая может быть применена к любому атрибуту класса. Это мощный шаблон для обеспечения целостности данных в вашем приложении.
Улучшение реализации дескрипторов
На этом этапе мы усовершенствуем нашу реализацию дескрипторов. Возможно, вы заметили, что в некоторых случаях мы избыточно указываем имена. Это может сделать наш код немного запутанным и трудным для поддержки. Чтобы решить эту проблему, мы воспользуемся методом __set_name__, полезной функцией, введенной в Python 3.6.
Метод __set_name__ вызывается автоматически при определении класса. Его основная задача - установить имя дескриптора за нас, так что нам не нужно делать это вручную каждый раз. Это сделает наш код чище и более эффективным.
Теперь обновим файл validate.py, чтобы он включал метод __set_name__. Вот как будет выглядеть обновленный код:
## validate.py
class Validator:
def __init__(self, name=None):
self.name = name
def __set_name__(self, cls, name):
## This gets called when the class is defined
## It automatically sets the name of the descriptor
if self.name is None:
self.name = name
@classmethod
def check(cls, value):
return value
def __set__(self, instance, value):
instance.__dict__[self.name] = self.check(value)
class String(Validator):
expected_type = str
@classmethod
def check(cls, value):
if not isinstance(value, cls.expected_type):
raise TypeError(f'Expected {cls.expected_type}')
return value
class PositiveInteger(Validator):
expected_type = int
@classmethod
def check(cls, value):
if not isinstance(value, cls.expected_type):
raise TypeError(f'Expected {cls.expected_type}')
if value < 0:
raise ValueError('Expected a positive value')
return value
class PositiveFloat(Validator):
expected_type = float
@classmethod
def check(cls, value):
if not isinstance(value, (int, float)):
raise TypeError('Expected a number')
if value < 0:
raise ValueError('Expected a positive value')
return float(value)
В приведенном выше коде метод __set_name__ в классе Validator проверяет, является ли атрибут name равным None. Если это так, он устанавливает name равным фактическому имени атрибута, используемому в определении класса. Таким образом, нам не нужно явно указывать имя при создании экземпляров классов дескрипторов.
Теперь, когда мы обновили файл validate.py, мы можем создать улучшенную версию нашего класса Stock. В этой новой версии нам не нужно будет избыточно указывать имена. Вот код улучшенного класса Stock:
## improved_stock.py
from validate import String, PositiveInteger, PositiveFloat
class Stock:
name = String() ## No need to specify 'name' anymore
shares = PositiveInteger()
price = PositiveFloat()
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
def cost(self):
return self.shares * self.price
def sell(self, amount):
self.shares -= amount
def __repr__(self):
return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})'
В этом классе Stock мы просто создаем экземпляры классов дескрипторов String, PositiveInteger и PositiveFloat без указания имен. Метод __set_name__ в классе Validator автоматически позаботится о установке имен.
Давайте протестируем наш улучшенный класс Stock. Сначала откройте терминал и перейдите в директорию проекта. Затем запустите файл improved_stock.py в интерактивном режиме. Вот команды для этого:
cd ~/project
python3 -i improved_stock.py
Как только вы войдете в интерактивную сессию Python, вы можете попробовать следующие команды, чтобы протестировать функциональность класса Stock:
s = Stock('GOOG', 100, 490.10)
print(s.name) ## Should return 'GOOG'
print(s.shares) ## Should return 100
print(s.price) ## Should return 490.1
## Try changing values
s.shares = 75
print(s.shares) ## Should return 75
## Try invalid values
try:
s.shares = '75' ## Should raise TypeError
except TypeError as e:
print(f"Error: {e}")
try:
s.price = -10.5 ## Should raise ValueError
except ValueError as e:
print(f"Error: {e}")
exit()
Эти команды создают экземпляр класса Stock, выводят его атрибуты, изменяют значение атрибута, а затем пытаются установить недопустимые значения, чтобы проверить, вызываются ли соответствующие ошибки.
Метод __set_name__ автоматически устанавливает имя дескриптора при определении класса. Это делает ваш код чище и менее избыточным, так как вам больше не нужно указывать имя атрибута дважды.
Это улучшение демонстрирует, как протокол дескрипторов Python продолжает развиваться, делая проще написание чистого и поддерживаемого кода.
Резюме
В этом практическом занятии вы узнали о дескрипторах Python - мощной функции, которая позволяет настраивать доступ к атрибутам в классах. Вы изучили протокол дескрипторов, включая методы __get__, __set__ и __delete__. Вы также создали базовый класс дескриптора для перехвата доступа к атрибутам и использовали дескрипторы для реализации системы валидации данных для обеспечения их целостности.
Кроме того, вы улучшили свои дескрипторы с помощью метода __set_name__ для уменьшения избыточности. Дескрипторы широко используются в библиотеках и фреймворках Python, таких как Django и SQLAlchemy. Понимание их дает более глубокое понимание Python и помогает вам писать более элегантный и поддерживаемый код.