Дескрипторы в Python
Есть два разных типа дескрипторов. Дескрипторы данных определяются как объекты , которые определяют одновременно __get__() и __set__() метод, тогда как дескрипторы без данных определить только __get__() метод. Это различие важно при рассмотрении переопределений и пространства имен словаря экземпляра. Если дескриптор данных и запись в словаре экземпляра имеют одно и то же имя, дескриптор данных будет иметь приоритет. Однако если вместо дескриптора не данных и записи в словаре экземпляра используется одно и то же имя, запись словаря экземпляра будет иметь приоритет.
Для того, чтобы дескриптор данных только для чтения, определяют как получить () и установить () с множеством () поднимая AttributeError при вызове. Определение метода набора () с исключением повышающего заполнителем достаточно , чтобы сделать его дескриптор данных.
descr.__get__(self, obj, type=None) --> value descr.__set__(self, obj, value) --> None descr.__delete__(self, obj) --> None
class DescPrinter(object): """A data descriptor that logs activity.""" _val = 7 def __get__(self, obj, objtype=None): print('Getting . ') return self._val def __set__(self, obj, val): print('Setting', val) self._val = val def __delete__(self, obj): print('Deleting . ') del self._val class Foo(): x = DescPrinter() i = Foo() i.x # Getting . # 7 i.x = 100 # Setting 100 i.x # Getting . # 100 del i.x # Deleting . i.x # Getting . # 7
Двусторонние преобразования
Объекты дескриптора могут позволить атрибутам связанных объектов автоматически реагировать на изменения.
Предположим, мы хотим смоделировать генератор с заданной частотой (в герцах) и периодом (в секундах). Когда мы обновляем частоту, мы хотим, чтобы период обновлялся, и когда мы обновляем период, мы хотим, чтобы частота обновлялась:
>>> oscillator = Oscillator(freq=100.0) # Set frequency to 100.0 Hz >>> oscillator.period # Period is 1 / frequency, i.e. 0.01 seconds 0.01 >>> oscillator.period = 0.02 # Set period to 0.02 seconds >>> oscillator.freq # The frequency is automatically adjusted 50.0 >>> oscillator.freq = 200.0 # Set the frequency to 200.0 Hz >>> oscillator.period # The period is automatically adjusted 0.005
Мы выбираем одно из значений (частота в герцах) в качестве «якоря», то есть того, которое можно установить без преобразования, и записываем для него класс дескриптора:
class Hertz(object): def __get__(self, instance, owner): return self.value def __set__(self, instance, value): self.value = float(value)
«Другое» значение (период в секундах) определяется в терминах привязки. Мы пишем класс дескриптора, который выполняет наши преобразования:
class Second(object): def __get__(self, instance, owner): # When reading period, convert from frequency return 1 / instance.freq def __set__(self, instance, value): # When setting period, update the frequency instance.freq = 1 / float(value)
Теперь мы можем написать класс Oscillator:
class Oscillator(object): period = Second() # Set the other value as a class attribute def __init__(self, freq): self.freq = Hertz() # Set the anchor value as an instance attribute self.freq = freq # Assign the passed value - self.period will be adjusted
ООП. Дескрипторы
В этой лекции мы рассмотрим такой важный механизм как дескрипторы, а также разберемся с тем как же устроены методы класса.
Свойства
Перед тем как говорить о дескрипторах давайте еще раз поговорим о свойствах (property). Рассмотрим следующий пример: пусть у нас есть класс «Профиль пользователя», который включает следующие поля: имя, фамилия и дата рождения.
import typing as tp class UserProfile: def __init__(self, user: "User", first_name: str = "", sur_name: str = "", bdate: tp.Optional[datetime.date] = None) -> None: self._user = user self.first_name = first_name self.sur_name = sur_name self.bdate = bdate self._age = None self._age_last_recalculated = None self._recalculate_age() def _recalculate_age(self) -> None: if self.bdate is None: return today = datetime.date.today() age = today.year - self.bdate.year if today datetime.date(today.year, self.bdate.month, self.bdate.day): age -= 1 self._age = age self._age_last_recalculated = today def age(self) -> tp.Optional[int]: if self._age is None: return None if datetime.date.today() > self._age_last_recalculated: self._recalculate_age() return self._age class User: def __init__(self, username: str, email: str, password: str) -> None: # . self.profile = UserProfile(self) # . >>> guido = User("guido", "guido@python.org", "python") >>> guido.profile.age() >>> guido.profile.bdate = datetime.date(1956, 1, 31) >>> guido.profile.age() 65
Из примера видно, что, во-первых, возраст пользователя вычисляется при каждом обращении, во-вторых, мы только получаем значение и никогда его не изменяем. Было бы логично, чтобы клиентский код работал с возрастом как с обычным атрибутом (свойством) доступным только для чтения и python предоставляет нам для этого механизм свойств (propertes):
class UserProfile: # . @property def age(self) -> Optional[int]: if self._age is None: return None if datetime.date.today() > self._age_last_recalculated: self._recalculate_age() return self._age >>> guido = User("guido", "guido@python.org", "python") >>> guido.profile.age >>> guido.profile.bdate = datetime.date(1956, 1, 31) >>> guido.profile.age 65 >>> guido.profile.age = 66 # . AttributeError: can't set attribute
Таким образом, свойства дают нам возможность создавать, аналогично другим языкам программирования (например, Java), сеттеры и геттеры, а также вычисляемые свойства (computed properties):
class UserProfile: # . @property def fullname(self) -> str: return f" ".title() @fullname.setter def fullname(self, value: str) -> None: name, surname = value.split(" ", maxsplit=1) self.first_name = name self.sur_name = surname @fullname.deleter def fullname(self) -> None: self.first_name = '' self.sur_name = '' >>> guido.profile.fullname = "Guido Van Rossum" >>> guido.profile.first_name 'Guido' >>> guido.profile.sur_name 'Van Rossum' >>> del guido.profile.fullname >>> guido.profile.first_name '' >>> guido.profile.sur_name ''
Чтобы понять как работают свойства необходимо разобраться с дескрипторами.
Дескрипторы
В документации дано следующее определение дескрипторов:
Дескриптор это любой объект, у которого определены методы __get__() , __set__() или __delete__() . Если дескриптором является атрибут класса, то для него определено специальное поведение при разшенении имени атрибута.
descr.__get__(self, obj, owner=None) -> value descr.__set__(self, obj, value) -> None descr.__delete__(self, obj) -> None
Дескрипторы, которые реализуют только __get__ называются дескрипторами не данных (non-data descriptors), а дескрипторы, которые реализуют __set__ и/или __delete__ называются дескрипторами данных (data descriptors). Рассмотрим следующий пример:
class D: def __get__(self, obj, owner=None): print(f"__get__ был вызван с аргументами obj= и owner=") def __set__(self, obj, value): print(f"__set__ был вызван с аргументами obj= и value=") class Klass: d1 = D() def __init__(self): self.d2 = D() >>> obj = Klass() >>> obj.d1 # == type(obj).__dict__['d1'].__get__(obj, type(obj)) __get__ был вызван с аргументами obj=__main__.Klass object at 0x1037d4c10> и owner=class '__main__.Klass'> >>> obj.d2 # == obj.__dict__['d2'] __main__.D at 0x1052521c0> >>> Klass.d1 # == Klass.__dict__['d1'].__get__(None, Klass) __get__ был вызван с аргументами obj=None и owner=class '__main__.Klass'> >>> obj.d1 = None # == type(obj).__dict__['d1'].__set__(obj, None) __set__ был вызван с аргументами obj=__main__.Klass object at 0x1037d4c10> и value=None >>> Klass.d1 = None # Klass.__dict__['d1'] = None
Из примера видно, что при обращении к d1 автоматически был вызван метод __get__ определенный на дескрипторе:
Поведением по умолчанию при доступе к атрибуту является обращение к словарю экземпляра, например, при обращении к a.x поиск начинается с a.__dict__[‘x’] , затем type(a).__dict__[‘x’] и так далее в порядке разрешения методов (mro). Когда же атрибут (класса/метакласса) является дескриптором, то Python изменяет путь поиска, сначала вызывая методы определенные у дескриптора.
Поэтому теперь должно быть понятно, почему при обращении к d2 мы получили просто экземпляр класса. Порядка разрешения имен атрибутов и методов мы коснемся в следующих лекциях.
В Python дескрипторы используются достаточно часто, в том числе и в самом языке, например, функции это дескрипторы:
PyTypeObject PyFunction_Type = PyVarObject_HEAD_INIT(&PyType_Type, 0) "function", sizeof(PyFunctionObject), 0, // . function_call, /* tp_call */ // . func_descr_get, /* tp_descr_get */ 0, /* tp_descr_set */ // . >;
Это позволяет автоматически передавать экземпляр класса в качестве первого аргумента ( self ), давайте посмотрим на вызов func_descr_get :
/* Bind a function to an object */ static PyObject * func_descr_get(PyObject *func, PyObject *obj, PyObject *type) if (obj == Py_None || obj == NULL) Py_INCREF(func); return func; > return PyMethod_New(func, obj); >
Если obj не был передан, то мы имеем дело с обычной функцией, в противном случае это метод и мы «биндим» объект в качестве первого аргумента. На python реализацию функций можно было бы записать так:
class Function: def __call__(self, *args, **kwargs): # тело функции def __get__(self, instance, owner): if instance is None: return self else: return functools.partial(self, instance)
А вот примеры реализаций декораторов @staticmethod и @classmethod:
import functools class Classmethod: def __init__(self, func): self.func = func def __get__(self, instance, owner): return functools.partial(self.func, owner) class Staticmethod: def __init__(self, func): self.func = func def __get__(self, instance, owner): return self.func
И наконец реализация @property:
class Property: def __init__(self, fget=None, fset=None, fdel=None): self.fget = fget self.fset = fset self.fdel = fdel def __get__(self, instance, owner): if instance is None: return self elif self.fget is None: raise AttributeError("Unreadable attribute") else: return self.fget(instance) def __set__(self, instance, value): if self.fset is None: raise AttributeError("Cant't set attribute") else: self.fset(instance, value) def __delete__(self, instance): if self.fdel is None: raise AttributeError("Can't delete attribute") else: self.fdel(instance) def getter(self, fget): return type(self)(fget, self.fset, self.fdel) def setter(self, fset): return type(self)(self.fget, fset, self.fdel) def deleter(self, fdel): return type(self)(self.fget, self.fset, fdel)
Пример простой ORM можно найти в репозитории с лекциями.
Базовый обзор дескриптора класса в Python
Этот материал дает базовый обзор дескриптора класса, плавно переходя от простых примеров, добавляя по одной функции за раз. Начните здесь, если вы новичок в дескрипторах.
Простой пример: дескриптор, возвращающий константу.
Класс Ten — это дескриптор, чей метод __get__() всегда возвращает константу 10:
class Ten: """Класс дескриптора""" def __get__(self, obj, objtype=None): return 10
Чтобы использовать дескриптор, он должен храниться как переменная класса в другом классе:
class A: # Обычный атрибут класса x = 5 # Экземпляр дескриптора y = Ten()
Сеанс интерпретатора Python показывает разницу между обычным поиском атрибутов и поиском дескриптора:
# экземпляр класса A >>> a = A() # поиск атрибута >>> a.x # 5 # поиск дескриптора >>> a.y # 10
При поиске атрибута a.x оператор точки находит ‘x’: 5 в словаре класса. При поиске a.y оператор точки находит экземпляр дескриптора, который распознается по методу __get__ . Вызов этого метода возвращает 10.
Обратите внимание, что значение 10 не сохраняется ни в словаре класса, ни в словаре экземпляра. Значение 10 вычисляется по требованию.
Этот пример показывает, как работает простой дескриптор, но он не очень полезен. Для извлечения констант лучше использовать обычные атрибуты класса.
Динамический поиск.
Полезные дескрипторы обычно выполняют вычисления, а не возвращают константы:
import os class DirectorySize: """Класс дескриптора""" def __get__(self, obj, objtype=None): return len(os.listdir(obj.dirname)) class Directory: # Экземпляр дескриптора size = DirectorySize() def __init__(self, dirname): # Обычный атрибут экземпляра self.dirname = dirname
Интерактивный сеанс интерпретатора показывает, что поиск является динамическим — он каждый раз вычисляет разные обновленные ответы:
>>> s = Directory('songs') >>> g = Directory('games') # папка `songs` содержит 20 файлов >>> s.size # 20 # папка `games` содержит 3 файла >>> g.size # 3 # например удалим файл `chess` из папки `games` >>> os.remove('games/chess') # кол-во файлов обновляется автоматически >>> g.size # 2
Помимо демонстрации того, как дескрипторы могут выполнять вычисления, этот пример показывает назначение аргументов для метода __get__() :
- Аргумент self — это экземпляр дескриптора DirectorySize , то есть атрибут size .
- Аргумент obj является либо экземпляром g , либо s , который позволяет методу __get__() узнать целевой экземпляр класса, в данном случае это Directory .
- Аргумент objtype — это класс Directory .
Управляемые атрибуты класса.
Популярным применением дескрипторов является управление доступом к данным экземпляра. Дескриптор назначается общедоступному атрибуту в словаре класса, в то время как фактические данные хранятся как частный атрибут в словаре экземпляра. Методы дескриптора __get__() и __set__() запускаются при доступе к общедоступному атрибуту.
В следующем примере age является общедоступным атрибутом, а _age — приватным атрибутом. При доступе к общедоступному атрибуту дескриптор регистрирует поиск или обновление:
import logging logging.basicConfig(level=logging.INFO) class LoggedAgeAccess: """Класс дескриптора""" def __get__(self, obj, objtype=None): value = obj._age logging.info('Доступ к %r дающий %r', 'age', value) return value def __set__(self, obj, value): logging.info('Обновление %r до %r', 'age', value) obj._age = value class Person: # Экземпляр дескриптора age = LoggedAgeAccess() def __init__(self, name, age): # Обычный атрибут экземпляра self.name = name # Вызов метода `__set__()` self.age = age def birthday(self): # Вызывает как `__get__()`, так и `__set__()` self.age += 1
Интерактивный сеанс показывает, что все обращения к возрасту управляемого атрибута регистрируются, но имя обычного атрибута не регистрируется:
# Записывается начальное обновление возраста >>> mary = Person('Mary M', 30) # INFO:root:Обновление 'age' до 30 >>> dave = Person('David D', 40) # INFO:root:Обновление 'age' до 40 # Фактические данные находятся в приватном атрибуте >>> vars(mary) # >>> vars(dave) # # Доступ к данным и регистрация поиска >>> mary.age # INFO:root:Доступ к 'age' дающий 30 # 30 # Обновления также регистрируются >>> mary.birthday() # INFO:root:Доступ к 'age' дающий 30 # INFO:root:Обновление 'age' до 31 # Обычный поиск атрибутов не регистрируется >>> dave.name # 'David D' # Регистрируется только управляемый атрибут >>> dave.age # INFO:root:Доступ 'age' дающий 40 # 40
Основная проблема, связанная с этим примером заключается в том, что приватное имя _age жестко запрограммировано в классе дескриптора LoggedAgeAccess . Это означает, что каждый экземпляр этого дескриптора может иметь только один зарегистрированный атрибут и что его имя остается неизменным. Другими словами, класс дескриптора LoggedAgeAccess заточен только на работу с атрибутом obj._age и не может, например хранить name в приватной переменной obj._name .
Автоматическое связывание дескриптора с именем переменной класса.
Когда класс использует дескрипторы, он может информировать каждый дескриптор о том, какое имя переменной было использовано.
В следующем примере класс Person имеет два экземпляра дескриптора LoggedAccess — это name и age . Когда класс Person определен, он выполняет обратный вызов метода __set_name__() , который определяется в классе LoggedAccess , присваивая каждому дескриптору собственное имя public_name и private_name :
import logging logging.basicConfig(level=logging.INFO) class LoggedAccess: """Класс дескриптора""" def __set_name__(self, owner, name): self.public_name = name self.private_name = '_' + name def __get__(self, obj, objtype=None): value = getattr(obj, self.private_name) logging.info('Доступ %r дающий %r', self.public_name, value) return value def __set__(self, obj, value): logging.info('Обновление %r до %r', self.public_name, value) setattr(obj, self.private_name, value) class Person: # Первый экземпляр дескриптора name = LoggedAccess() # Второй экземпляр дескриптора age = LoggedAccess() def __init__(self, name, age): # Вызывает первый дескриптор self.name = name # Вызывает второй дескриптор self.age = age def birthday(self): self.age += 1
Изменено в Python 3.12: исключения, возникающие в методе __set_name__ класса или типа, больше не переносятся с помощью RuntimeError . Контекстная информация добавляется к исключению в виде примечания. (Предоставлено Ирит Катриэль.)
При использовании интерактивного сеанса видно, что класс Person вызвал __set_name__() для записи имен полей. Для поиска дескриптора, без его физического вызова, можно использовать встроенную функцию vars() :
>>> vars(vars(Person)['name']) # >>> vars(vars(Person)['age']) #
Новый класс теперь регистрирует доступ как к имени, так и к возрасту:
>>> pete = Person('Peter P', 10) # INFO:root:Обновление 'name' до 'Peter P' # INFO:root:Обновление 'age' до 10 >>> kate = Person('Catherine C', 20) # INFO:root:Обновление 'name' до 'Catherine C' # INFO:root:Обновление 'age' до 20
Два экземпляра Person содержат только частные имена:
>>> vars(pete) # >>> vars(kate) #
Заключительные мысли о дескрипторах.
- Дескриптор — это любой объект, который определяет методы __get__() , __set__() или __delete__() .
- Опционально дескрипторы могут иметь метод __set_name__() . Этот метод используется только в тех случаях, когда дескриптору необходимо знать либо класс, в котором он был создан, либо имя переменной класса, которой он был присвоен. (Этот метод, если он присутствует, вызывается, даже если класс не является дескриптором.)
- Дескрипторы вызываются оператором точки во время поиска атрибута. Если к дескриптору обращаются косвенно с помощью vars(some_class)[descriptor_name] , экземпляр дескриптора возвращается без его фактического вызова.
- Дескрипторы работают только при использовании в качестве переменных класса. Когда они помещаются в экземпляры, они не имеют никакого эффекта.
- Основной мотивацией для дескрипторов является предоставление хука, позволяющего объектам, хранящимся в переменных класса, управлять тем, что происходит во время поиска атрибутов.
- Традиционно вызывающий класс контролирует, что происходит во время поиска.
- Дескрипторы используются во всем языке Python. Именно так функции превращаются в связанные методы. Встроенные функции, такие как classmethod() , staticmethod() , property() и functools.cached_property() , реализованы как дескрипторы.
- ОБЗОРНАЯ СТРАНИЦА РАЗДЕЛА
- Пространство имен и область видимости в классах
- Определение классов
- Объект класса и конструктор класса
- Создание экземпляра класса
- Метод экземпляра класса
- Что такое метод класса и зачем нужен
- Что такое статический метод в классах Python и зачем нужен
- Атрибуты класса и переменные экземпляра класса
- Кэширование методов экземпляра декоратором lru_cache
- Закрытые/приватные методы и переменные класса Python
- Наследование классов
- Множественное наследование классов
- Абстрактные классы
- Перегрузка методов в классе Python
- Что такое миксины и как их использовать
- Класс Python как структура данных, подобная языку C
- Создание пользовательских типов данных
- Специальные (магические) методы класса Python
- Базовая настройка классов Python магическими методами
- Настройка доступа к атрибутам класса Python
- Дескриптор класса для чайников
- Протокол дескриптора класса
- Практический пример дескриптора
- Использование метода .__new__() в классах Python
- Специальный атрибут __slots__ класса Python
- Специальный метод __init_subclass__ класса Python
- Определение метаклассов metaclass
- Эмуляция контейнерных типов в классах Python
- Другие специальные методы класса
- Как Python ищет специальные методы в классах
- Шаблон проектирования Фабрика и его реализация
Дескрипторы в Python
Сегодня мы поговорим про то, что такое дескрипторы в Python, когда следует их использовать и зачем это вообще нужно.
Дескрипторы Python или, в более общем смысле, просто дескрипторы предоставляют нам мощную технику для написания пригодного к повторному использованию кода, который можно использовать между классами. Они могут показаться похожими на концепцию наследования, но технически это не так. Это универсальный способ перехвата доступа к атрибутам. Дескрипторы — это механизм, лежащий в основе статических методов свойств, методов класса, суперметодов и т. д.
Дескрипторы были добавлены в Python версии 2.2, и с тех пор они считаются волшебными вещами, которые придали традиционным классам новый стиль. Это классы, которые позволяют вам делать управляемыми свойства в другом классе. В частности, они реализуют интерфейс для методов __get__() , __set__() и __delete__() , что делает их интересными по многим причинам.
Что такое дескриптор
Проще говоря, класс, который реализует метод __get__() , __set()__ или __delete()__ для объекта, известен как «дескриптор». Цитируя прямо из официальной документации Python, дескриптор — это атрибут объекта со связанным поведением, то есть такой атрибут, при доступе к которому его поведение переопределяется методом протокола дескриптора. Это методы __get__() , __set__() и __delete__() .
Связанное поведение (англ. binding behavior) применительно к дескрипторам означает привязку способа, которым может устанавливаться, запрашиваться (получаться) или удаляться значение, к данной переменной, объекту или набору данных. Это взаимодействие привязано к части данных, оно применяется только к тем данным, для которых вы его установили.
Дескрипторы можно дополнительно разделить на дескрипторы данных и дескрипторы не-данных. Если дескриптор, который вы пишете, имеет только метод __get__() , то это дескриптор не-данных. А реализация, включающая методы __set__() и __delete__() , называется дескриптором данных. Дескрипторы, не относящиеся к данным, доступны только для чтения, тогда как дескрипторы данных доступны как для чтения, так и для записи.
Важно отметить, что дескрипторы назначаются классу, а не экземпляру класса. При изменении класса перезаписывается или удаляется сам дескриптор, а не активируется его код.
Методы дескриптора
Наконец, класс дескриптора не ограничивается наличием только трех упомянутых методов. То есть он также может содержать любой другой атрибут, кроме методов __get__() , __set__() и __delete__() .
Давайте более подробно разберем методы get() , set() и delete() :
- self — это экземпляр создаваемого вами дескриптора
- object — это экземпляр объекта, к которому прикреплен ваш дескриптор
- type — это тип объекта, к которому присоединен дескриптор
- value — это значение, которое присваивается атрибуту дескриптора. К примеру, get(self, object, type) , set(self, object, value) или delete(self, object)
- __get__() обращается к атрибуту, когда вы хотите извлечь некоторую информацию. Он возвращает значение атрибута или вызывает исключение AttributeError , если запрошенный атрибут отсутствует
- __set__() вызывается в операции присвоения атрибута, которая устанавливает значение атрибута. Данный метод ничего не возвращает, но может вызвать исключение AttributeError
- __delete__() управляет операцией удаления, т. е. служит для удаления атрибута из объекта. Также ничего не возвращает
Теперь давайте разберемся с назначением дескрипторов и разберем это на нескольких примерах!
Назначение дескрипторов
Давайте определим класс Car , имеющий три атрибута, а именно марку, модель и объём топливного бака ( make , model и fuel_cap ). Мы будем использовать метод __init__() для инициализации атрибутов класса. Затем мы воспользуемся волшебной функцией __str__() , которая просто вернет вывод трех атрибутов, которые вы передадите классу при создании объекта.
Обратите внимание, что метод __str__() возвращает строковое представление объекта. Он вызывается, когда для объекта класса вызывается функция print() или str() .
class Car: def __init__(self,make,model,fuel_cap): self.make = make self.model = model self.fuel_cap = fuel_cap def __str__(self): return " model with a fuel capacity of ltr.".format(self.make,self.model,self.fuel_cap)
car1 = Car("BMW","X7",40) print(car1) # BMW model X7 with a fuel capacity of 40 ltr.
Все выглядит великолепно!
Теперь давайте изменим емкость топливного бака автомобиля на -40. Выглядеть это будет следующим образом:
car2 = Car("BMW","X7",-40) print(car2) # BMW model X7 with a fuel capacity of -40 ltr.
Подождите, что-то не так, не правда ли? Емкость топливного бака никак не может быть отрицательной. Однако Python принимает этот ввод без ошибок. Это связано с тем, что Python — динамический язык программирования, который не поддерживает явную проверку типов.
Чтобы избежать этой проблемы, давайте добавим условие if в метод __init__() и проверим, является ли введенный объем топливного бака валидным. Если введенный объем невалидный, создадим исключение ValueError . К примеру, это можно сделать так:
class Car: def __init__(self,make,model,fuel_cap): self.make = make self.model = model self.fuel_cap = fuel_cap if self.fuel_cap < 0: raise ValueError("Fuel Capacity can never be less than zero") def __str__(self): return "model with a fuel capacity of ltr.".format(self.make,self.model,self.fuel_cap)
car1 = Car("BMW","X7",40) print(car1) # BMW model X7 with a fuel capacity of 40 ltr. car2 = Car("BMW","X7",-40) print(car2)
---------------------------------------- ValueErrorTraceback (most recent call last) in ----> 1 car2 = Car("BMW","X7",-40) 2 print(car2) in __init__(self, make, model, fuel_cap) 5 self.fuel_cap = fuel_cap 6 if self.fuel_cap < 0: ---->7 raise ValueError("Fuel Capacity can never be less than zero") 8 9 def __str__(self): ValueError: Fuel Capacity can never be less than zero
Из приведенного выше вывода видно, что на данный момент все работает нормально, поскольку программа выдает ошибку ValueError , если объем топлива ниже нуля.
Однако может возникнуть другая проблема. К примеру, если введенный объем топлива является числом с плавающей запятой или строкой. Целочисленным значением может быть не только запас топлива, но и марка и модель автомобиля. Во всех этих случаях программа не сможет вызвать исключение.
class Car: def __init__(self,make,model,fuel_cap): self.make = make self.model = model self.fuel_cap = fuel_cap if self.fuel_cap < 0: raise ValueError("Fuel Capacity can never be less than zero") def __str__(self): return "model with a fuel capacity of ltr.".format(self.make,self.model,self.fuel_cap)
car2 = Car(-40,"X7",40) print(car2) # -40 model X7 with a fuel capacity of 40 ltr.
Чтобы справиться с таким случаем, вы можете подумать о добавлении еще одного условия if или, возможно, использовать метод isinstance() для проверки типов.
На этот раз давайте воспользуемся встроенным методом isinstance() для обработки ошибки. К примеру, проверку можно осуществить следующим образом:
class Car: def __init__(self,make,model,fuel_cap): self.make = make self.model = model self.fuel_cap = fuel_cap if isinstance(self.make, str): print(self.make) else: raise ValueError("Make of the car can never be an integer") if self.fuel_cap < 0: raise ValueError("Fuel Capacity can never be less than zero") def __str__(self): return "model with a fuel capacity of ltr.".format(self.make,self.model,self.fuel_cap)
car2 = Car("BMW","X7",40) print(car2) # BMW # BMW model X7 with a fuel capacity of 40 ltr. car2 = Car(-40,"X7",40) print(car2)
---------------------------------------- ValueErrorTraceback (most recent call last) in ----> 1 car2 = Car(-40,"X7",40) 2 print(car2) in __init__(self, make, model, fuel_cap) 7 print(self.make) 8 else: ----> 9 raise ValueError("Make of the car can never be an integer") 10 11 if self.fuel_cap < 0: ValueError: Make of the car can never be an integer
Здорово! Таким образом, мы смогли справиться и с этой ошибкой.
Использование дескрипторов
Однако, что делать, если позже вы захотите изменить атрибут топливной емкости на отрицательное значение -40? В данном случае это не сработает, так как проверка типов будет производиться в методе __init__() только один раз. Как вы знаете, метод __init__() является конструктором и вызывается только один раз при создании объекта класса. Следовательно, пользовательская проверка типов позже завершится ошибкой.
Давайте разберемся на примере:
class Car: def __init__(self,make,model,fuel_cap): self.make = make self.model = model self.fuel_cap = fuel_cap if isinstance(self.make, str): print(self.make) else: raise ValueError("Make of the car can never be an integer") if self.fuel_cap < 0: raise ValueError("Fuel Capacity can never be less than zero") def __str__(self): return "model with a fuel capacity of ltr.".format(self.make,self.model,self.fuel_cap)
car2 = Car("BMW","X7",40) print(car2) # BMW # BMW model X7 with a fuel capacity of 40 ltr. car2.make = -40 print(car2) # -40 model X7 with a fuel capacity of 40 ltr.
И вот! Вы смогли вырваться из проверки типов.
Теперь подумайте вот о чем. Что, если у вас есть много других атрибутов автомобиля? К примеру, пробег, цена, аксессуары и т. д., которые также требуют проверки типов, и вам также нужна функциональность, в которой некоторые из этих атрибутов имеют доступ только для чтения. Разве это не будет сильно раздражать?
Что ж, для решения всех вышеперечисленных проблем в Python есть дескрипторы!
Как вы узнали выше, любой класс, реализующий магические методы __get__() , __set__() или __delete__() для объекта протокола дескриптора, называется дескриптором. Они также дают вам дополнительный контроль над тем, как атрибут должен работать, например, должен ли он иметь доступ только для чтения или для чтения и записи.
Теперь давайте расширим приведенный выше пример, добавив методы дескриптора. Выглядеть это будет примерно следующим образом:
class Descriptor: def __init__(self): self.__fuel_cap = 0 def __get__(self, instance, owner): return self.__fuel_cap def __set__(self, instance, value): if isinstance(value, int): print(value) else: raise TypeError("Fuel Capacity can only be an integer") if value < 0: raise ValueError("Fuel Capacity can never be less than zero") self.__fuel_cap = value def __delete__(self, instance): del self.__fuel_cap class Car: fuel_cap = Descriptor() def __init__(self,make,model,fuel_cap): self.make = make self.model = model self.fuel_cap = fuel_cap def __str__(self): return "model with a fuel capacity of ltr.".format(self.make,self.model,self.fuel_cap) car2 = Car("BMW","X7",40) print(car2) # 40 # BMW model X7 with a fuel capacity of 40 ltr.
Не волнуйтесь, если дескриптор класса покажется сперва неясным. Давайте разобьем его на маленькие части и разберемся, что по сути делает каждый метод.
Работа каждого метода дескриптора
Метод __init__() класса дескриптора имеет нулевую локальную переменную __fuel_cap . Двойное подчеркивание в начале означает, что переменная является приватной. Оно нужно для того, чтобы отличить атрибут топливной емкости класса Descriptor от класса Car .
Как вы уже знаете, метод __get__() используется для получения атрибутов и возвращает переменную со значением емкости топлива. Он принимает три аргумента: объект дескриптора, экземпляр класса, содержащего экземпляр объекта дескриптора, т. е. car2 , и, наконец, владельца — класс, к которому принадлежит экземпляр, т. е. класс Car . В этом методе вы просто возвращаете атрибут value , т.е. fuel_cap , значение которого устанавливается в методе __set__() .
Метод __set__() вызывается, когда атрибуту присваивается значение, и, в отличие от метода __get__() , он ничего не возвращает. Он имеет два аргумента помимо самого объекта дескриптора, т. е. экземпляр, совпадающий с методом __get__() , и аргумент value — значение, которое вы присваиваете атрибуту. В этом методе вы проверяете, является ли значение, которое вы хотите присвоить атрибуту fuel_cap , целым числом или нет. Если нет, вы вызываете исключение TypeError . Затем в том же методе вы также проверяете, является ли значение отрицательным. И, если это так, вы вызываете исключение ValueError . После проверки на наличие ошибок вы обновляете атрибут fuel_cap равным значению.
Наконец, метод __delete__() . Он вызывается при удалении атрибута из объекта и аналогичен методу __set__() . Кроме того, он также ничего не возвращает.
Класс Car остается прежним. Единственное изменение, которое вы делаете, это добавление экземпляра fuel_cap класса Descriptor() . Обратите внимание, что, как упоминалось ранее, экземпляр класса дескриптора должен быть добавлен в класс как атрибут класса, а не как атрибут экземпляра.
Как только вы устанавливаете локальную переменную fuel_cap в методе __init__() на экземпляр fuel_cap , она вызывает метод __set__() класса Descriptor .
Тестирование дескриптора
Теперь давайте изменим запас топлива на отрицательное значение и посмотрим, вызовет ли программа исключение ValueError .
car2 = Car("BMW","X7",-40) print(car2)
-40 ---------------------------------------- ValueErrorTraceback (most recent call last) in ----> 1 car2 = Car("BMW","X7",-40) 2 print(car2) in __init__(self, make, model, fuel_cap) 4 self.make = make 5 self.model = model ----> 6 self.fuel_cap = fuel_cap 7 8 def __str__(self): in __set__(self, instance, value) 11 12 if value < 0: --->13 raise ValueError("Fuel Capacity can never be less than zero") 14 15 self.__fuel_cap = value ValueError: Fuel Capacity can never be less than zero
Если вы помните, раньше проверка типов при изменении значения атрибута на отрицательное число провалилась, поскольку осуществлялась только один раз, в методе __init__() . Давайте обновим значение fuel_cap до строкового значения и выясним, не приведет ли это к ошибке.
car2.fuel_cap = -1
-1 ---------------------------------------- ValueErrorTraceback (most recent call last) in ----> 1 car2.fuel_cap = -1 in __set__(self, instance, value) 11 12 if value < 0: --->13 raise ValueError("Fuel Capacity can never be less than zero") 14 15 self.__fuel_cap = value ValueError: Fuel Capacity can never be less than zero
car2.fuel_cap = "BMW"
---------------------------------------- TypeErrorTraceback (most recent call last) in ----> 1 car2.fuel_cap = "BMW" in __set__(self, instance, value) 8 print(value) 9 else: ---> 10 raise TypeError("Fuel Capacity can only be an integer") 11 12 if value < 0: TypeError: Fuel Capacity can only be an integer
Идеально! Как видите, когда вы позже обновляете атрибут топливной емкости, проверка работает.
Что ж, в дескрипторах есть небольшая проблема, заключающаяся в том, что при создании нового экземпляра или второго экземпляра класса предыдущее значение экземпляра переопределяется. Причина в том, что дескрипторы связаны с классом, а не с экземпляром.
Давайте разберемся в этом на примере ниже.
car3 = Car("BMW","X7",48) #created a new instance 'car3' with different values # 48
Когда вы распечатаете экземпляр car2 , вы заметите, что значения были переопределены для car3 .
print(car2) # BMW model X7 with a fuel capacity of 48 ltr.
Заключение
В этой статье мы познакомились с дескрипторами в Python, обсудили принципы их работы и особенности применения. Это руководство было предназначено для тех, кто знаком с Python и стремится освоить продвинутый уровень.
Надеемся, данная статья была вам полезна! Успехов в написании кода!