Дескрипторы Python: введение

Оглавление

Дескрипторы - это специфическая функция Python, которая обеспечивает большую часть волшебства, скрытого под оболочкой языка. Если вы когда-нибудь думали, что дескрипторы Python - это сложная тема с небольшим количеством практических приложений, то это руководство - идеальный инструмент, который поможет вам разобраться в этой мощной функции. Вы придете к пониманию того, почему дескрипторы Python являются такой интересной темой и к каким вариантам использования вы можете их применить.

К концу этого урока вы будете знать:

  • Что такое Дескрипторы Python
  • Где они используются во внутренних компонентах Python
  • Как реализовать свои собственные дескрипторы
  • Когда использовать Дескрипторы Python

Это руководство предназначено для разработчиков на Python от среднего до продвинутого уровня, поскольку оно касается внутренних компонентов Python. Однако, если вы еще не достигли этого уровня, просто продолжайте читать! Вы найдете полезную информацию о Python и цепочке поиска.

Что такое дескрипторы Python?

Дескрипторы - это объекты Python, которые реализуют метод протокола дескрипторов , который дает вам возможность создавать объекты, которые имеют особое поведение при обращении к ним как к атрибутам других объектов. Здесь вы можете увидеть правильное определение протокола дескриптора:

__get__(self, obj, type=None) -> object
__set__(self, obj, value) -> None
__delete__(self, obj) -> None
__set_name__(self, owner, name)

Если ваш дескриптор реализует только .__get__(), то считается, что это дескриптор, не содержащий данных. Если он реализует .__set__() или .__delete__(), то считается, что это дескриптор данных. Обратите внимание, что это различие касается не только названия, но и поведения. Это связано с тем, что дескрипторы данных имеют приоритет в процессе поиска, как вы увидите позже.

Взгляните на следующий пример, который определяет дескриптор, регистрирующий что-либо в консоли при обращении к нему:

# descriptors.py
class Verbose_attribute():
    def __get__(self, obj, type=None) -> object:
        print("accessing the attribute to get the value")
        return 42
    def __set__(self, obj, value) -> None:
        print("accessing the attribute to set the value")
        raise AttributeError("Cannot change the value")

class Foo():
    attribute1 = Verbose_attribute()

my_foo_object = Foo()
x = my_foo_object.attribute1
print(x)

В приведенном выше примере Verbose_attribute() реализует протокол дескриптора. Как только он создан как атрибут Foo, его можно считать дескриптором.

Как дескриптор, он имеет поведение привязки при доступе к нему с использованием точечной записи. В этом случае дескриптор регистрирует сообщение на консоли каждый раз, когда к нему обращаются, чтобы получить или задать значение:

  • При обращении к .__get__() значению оно всегда возвращает значение 42.
  • При обращении к .__set__() определенному значению возникает AttributeError исключение, которое является рекомендуемым способ реализации доступных только для чтения дескрипторов.

Теперь запустите приведенный выше пример, и вы увидите, что дескриптор регистрирует доступ к консоли перед возвратом постоянного значения:

$ python descriptors.py
accessing the attribute to get the value
42

Здесь, когда вы пытаетесь получить доступ к attribute1, дескриптор регистрирует этот доступ к консоли, как определено в .__get__().

Как работают дескрипторы во внутренней части Python

Если у вас есть опыт работы в качестве объектно-ориентированного разработчика на Python, то вам может показаться, что подход, описанный в предыдущем примере, несколько излишен. Вы могли бы добиться того же результата, используя свойства. Хотя это действительно так, вы можете быть удивлены, узнав, что свойства в Python - это просто... дескрипторы! Позже вы увидите, что свойства - не единственная функция, использующая дескрипторы Python.

Дескрипторы Python в свойствах

Если вы хотите получить тот же результат, что и в предыдущем примере, без явного использования дескриптора Python, то наиболее простым подходом является использование свойства . В следующем примере используется свойство, которое регистрирует сообщение на консоли при обращении к нему:

# property_decorator.py
class Foo():
    @property
    def attribute1(self) -> object:
        print("accessing the attribute to get the value")
        return 42

    @attribute1.setter
    def attribute1(self, value) -> None:
        print("accessing the attribute to set the value")
        raise AttributeError("Cannot change the value")

my_foo_object = Foo()
x = my_foo_object.attribute1
print(x)

В приведенном выше примере используются декораторы для определения свойства с присоединенными методами получения и установки. Но, как вы, наверное, знаете, декораторы - это всего лишь синтаксический сахар. Приведенный выше пример, на самом деле, можно записать следующим образом:

# property_function.py
class Foo():
    def getter(self) -> object:
        print("accessing the attribute to get the value")
        return 42

    def setter(self, value) -> None:
        print("accessing the attribute to set the value")
        raise AttributeError("Cannot change the value")

    attribute1 = property(getter, setter)

my_foo_object = Foo()
x = my_foo_object.attribute1
print(x)

Теперь вы можете видеть, что свойство было создано с помощью property(). Подпись этой функции выглядит следующим образом:

property(fget=None, fset=None, fdel=None, doc=None) -> object

property() возвращает объект property, который реализует протокол дескриптора. В нем используются параметры fget, fset и fdel для фактической реализации трех методов протокола.

Дескрипторы Python в методах и функциях

Если вы когда-либо писали объектно-ориентированную программу на Python, то вы наверняка использовали методы. Это обычные функции, первый аргумент которых зарезервирован для экземпляра объекта. Когда вы обращаетесь к методу с использованием точечной записи, вы вызываете соответствующую функцию и передаете экземпляр объекта в качестве первого параметра.

Волшебство, которое преобразует ваш obj.method(*args) вызов в method(obj, *args), находится внутри .__get__() реализации function объекта, который, по сути, является дескриптор, не содержащий данных. В частности, объект function реализует .__get__() таким образом, что он возвращает связанный метод, когда вы обращаетесь к нему с помощью точечной записи. Следующие (*args) вызывают функции, передавая все необходимые дополнительные аргументы.

Чтобы получить представление о том, как это работает, взгляните на этот пример на чистом Python из официальных документов :

import types

class Function(object):
    ...
    def __get__(self, obj, objtype=None):
        "Simulate func_descr_get() in Objects/funcobject.c"
        if obj is None:
            return self
        return types.MethodType(self, obj)

В приведенном выше примере, когда доступ к функции осуществляется с помощью точечной записи, вызывается .__get__() и возвращается связанный метод.

Это работает для обычных методов экземпляра точно так же, как и для методов класса или статических методов. Итак, если вы вызываете статический метод с помощью obj.method(*args), то он автоматически преобразуется в method(*args). Аналогично, если вы вызываете метод класса с помощью obj.method(type(obj), *args), то он автоматически преобразуется в method(type(obj), *args).

Примечание: Чтобы узнать больше о *args, ознакомьтесь с Аргументами и kwargs в Python: демистифицировано.

В официальных документах вы можете найти несколько примеров того, как статические методы и методы класса были бы реализованы, если бы они были написаны на чистый Python вместо фактической реализации C. Например, возможная реализация статического метода может быть такой:

class StaticMethod(object):
    "Emulate PyStaticMethod_Type() in Objects/funcobject.c"
    def __init__(self, f):
        self.f = f

    def __get__(self, obj, objtype=None):
        return self.f

Аналогично, это может быть возможной реализацией метода класса:

class ClassMethod(object):
    "Emulate PyClassMethod_Type() in Objects/funcobject.c"
    def __init__(self, f):
        self.f = f

    def __get__(self, obj, klass=None):
        if klass is None:
            klass = type(obj)
        def newfunc(*args):
            return self.f(klass, *args)
        return newfunc

Обратите внимание, что в Python метод класса - это просто статический метод, который принимает ссылку на класс в качестве первого аргумента в списке аргументов.

Как осуществляется Доступ К Атрибутам С помощью Цепочки Поиска

Чтобы немного больше понять о дескрипторах Python и внутренних компонентах Python, вам нужно понять, что происходит в Python при обращении к атрибуту. В Python каждый объект имеет встроенный атрибут __dict__. Это словарь, содержащий все атрибуты, определенные в самом объекте. Чтобы увидеть это в действии, рассмотрим следующий пример:

class Vehicle():
    can_fly = False
    number_of_weels = 0

class Car(Vehicle):
    number_of_weels = 4

    def __init__(self, color):
        self.color = color

my_car = Car("red")
print(my_car.__dict__)
print(type(my_car).__dict__)

Этот код создает новый объект и выводит содержимое атрибута __dict__ как для объекта, так и для класса. Теперь запустите скрипт и проанализируйте выходные данные, чтобы увидеть набор атрибутов __dict__:

{'color': 'red'}
{'__module__': '__main__', 'number_of_weels': 4, '__init__': <function Car.__init__ at 0x10fdeaea0>, '__doc__': None}

Атрибуты __dict__ установлены так, как ожидалось. Обратите внимание, что в Python все является объектом. Класс на самом деле тоже является объектом, поэтому у него также будет атрибут __dict__, который содержит все атрибуты и методы класса.

Итак, что происходит, когда вы получаете доступ к атрибуту в Python? Давайте проведем несколько тестов с измененной версией предыдущего примера. Рассмотрим этот код:

# lookup.py
class Vehicle(object):
    can_fly = False
    number_of_weels = 0

class Car(Vehicle):
    number_of_weels = 4

    def __init__(self, color):
        self.color = color

my_car = Car("red")

print(my_car.color)
print(my_car.number_of_weels)
print(my_car.can_fly)

В этом примере вы создаете экземпляр класса Car, который наследуется от класса Vehicle. Затем вы получаете доступ к некоторым атрибутам. Если вы запустите этот пример, то увидите, что получите все ожидаемые значения:

$ python lookup.py
red
4
False

Здесь, когда вы обращаетесь к атрибуту color экземпляра my_car, вы фактически обращаетесь к единственному значению атрибута __dict__ объекта my_car. Когда вы обращаетесь к атрибуту number_of_wheels объекта my_car, вы на самом деле обращаетесь к единственному значению атрибута __dict__ класса Car. Наконец, когда вы обращаетесь к атрибуту can_fly, вы фактически обращаетесь к нему с помощью атрибута __dict__ класса Vehicle.

Это означает, что приведенный выше пример можно переписать следующим образом:

# lookup2.py
class Vehicle():
    can_fly = False
    number_of_weels = 0

class Car(Vehicle):
    number_of_weels = 4

    def __init__(self, color):
        self.color = color

my_car = Car("red")

print(my_car.__dict__['color'])
print(type(my_car).__dict__['number_of_weels'])
print(type(my_car).__base__.__dict__['can_fly'])

Когда вы протестируете этот новый пример, вы должны получить тот же результат:

$ python lookup2.py
red
4
False

Итак, что происходит, когда вы обращаетесь к атрибуту объекта с помощью точечной записи? Как интерпретатор узнает, что вам действительно нужно? Итак, вот где используется концепция, называемая цепочкой поиска :

  • Сначала вы получите результат, возвращаемый методом __get__ дескриптора данных , названного в честь искомого атрибута.

  • Если это не удастся, то вы получите значение __dict__ вашего объекта для ключа, названного в честь искомого атрибута.

  • Если это не удастся, то вы получите результат, возвращаемый методом __get__ дескриптора , не содержащего данных, , названного в честь искомого атрибута.

  • Если это не удастся, то вы получите значение вашего типа объекта __dict__ для ключа, названного в честь искомого атрибута.

  • Если это не удастся, то вы получите значение родительского типа вашего объекта __dict__ для ключа, названного в честь искомого атрибута.

  • Если это не удается, то предыдущий шаг повторяется для всех родительских типов в порядке разрешения метода вашего объекта.

  • Если все остальное не сработало, то вы получите исключение AttributeError.

Теперь вы можете понять, почему важно знать, является ли дескриптор дескриптором данных или дескриптором, не содержащим данных? Они находятся на разных уровнях цепочки поиска, и позже вы увидите, что эта разница в поведении может быть очень удобной.

Как правильно использовать дескрипторы Python

Если вы хотите использовать дескрипторы Python в своем коде, то вам просто нужно реализовать протокол дескрипторов . Наиболее важными методами этого протокола являются .__get__() и .__set__(), которые имеют следующую сигнатуру:

__get__(self, obj, type=None) -> object
__set__(self, obj, value) -> None

При внедрении протокола имейте в виду следующее:

  • self это экземпляр дескриптора, который вы пишете.
  • obj является экземпляром объекта, к которому привязан ваш дескриптор.
  • type это тип объекта, к которому прикреплен дескриптор.

В .__set__() у вас нет переменной type , потому что вы можете вызвать только .__set__() для объекта. Напротив, вы можете вызвать .__get__() как для объекта, так и для класса.

Еще одна важная вещь, которую следует знать, это то, что дескрипторы Python создаются только один раз для каждого класса. Это означает, что каждый отдельный экземпляр класса, содержащий дескриптор , совместно использует этот экземпляр дескриптора. Это то, чего вы, возможно, не ожидаете, и может привести к классической ловушке, подобной этой:

# descriptors2.py
class OneDigitNumericValue():
    def __init__(self):
        self.value = 0
    def __get__(self, obj, type=None) -> object:
        return self.value
    def __set__(self, obj, value) -> None:
        if value > 9 or value < 0 or int(value) != value:
            raise AttributeError("The value is invalid")
        self.value = value

class Foo():
    number = OneDigitNumericValue()

my_foo_object = Foo()
my_second_foo_object = Foo()

my_foo_object.number = 3
print(my_foo_object.number)
print(my_second_foo_object.number)

my_third_foo_object = Foo()
print(my_third_foo_object.number)

Здесь у вас есть класс Foo, который определяет атрибут number, который является дескриптором. Этот дескриптор принимает однозначное числовое значение и сохраняет его в свойстве самого дескриптора. Однако этот подход не сработает, поскольку каждый экземпляр Foo использует один и тот же экземпляр дескриптора. По сути, вы создали всего лишь новый атрибут уровня класса.

Попробуйте запустить код и проверить результат:

$ python descriptors2.py
3
3
3

Вы можете видеть, что все экземпляры Foo имеют одинаковое значение атрибута number, несмотря на то, что последний экземпляр был создан после установки атрибута my_foo_object.number.

Итак, как вы можете решить эту проблему? Вы можете подумать, что было бы неплохо использовать словарь для сохранения всех значений дескриптора для всех объектов, к которым он привязан. Это кажется хорошим решением, поскольку .__get__() и .__set__() имеют атрибут obj, который является экземпляром объекта, к которому вы привязаны. Вы могли бы использовать это значение в качестве ключа для словаря.

К сожалению, у этого решения есть большой недостаток, который вы можете увидеть в следующем примере:

# descriptors3.py
class OneDigitNumericValue():
    def __init__(self):
        self.value = {}

    def __get__(self, obj, type=None) -> object:
        try:
            return self.value[obj]
        except:
            return 0

    def __set__(self, obj, value) -> None:
        if value > 9 or value < 0 or int(value) != value:
            raise AttributeError("The value is invalid")
        self.value[obj] = value

class Foo():
    number = OneDigitNumericValue()

my_foo_object = Foo()
my_second_foo_object = Foo()

my_foo_object.number = 3
print(my_foo_object.number)
print(my_second_foo_object.number)

my_third_foo_object = Foo()
print(my_third_foo_object.number)

В этом примере вы используете словарь для хранения значения атрибута number для всех ваших объектов внутри вашего дескриптора. Когда вы запустите этот код, вы увидите, что он работает нормально и что поведение соответствует ожиданиям:

$ python descriptors3.py
3
0
0

К сожалению, недостатком здесь является то, что дескриптор сохраняет строгую ссылку на объект owner. Это означает, что если вы уничтожите объект, то память не освободится, потому что сборщик мусора продолжает находить ссылку на этот объект внутри дескриптора!

Вы можете подумать, что решением здесь могло бы быть использование слабых ссылок. Хотя это возможно, вам придется смириться с тем фактом, что не на все объекты можно ссылаться как на слабые и что, когда ваши объекты собираются, они исчезают из вашего словаря.

Лучшим решением здесь будет просто не сохранять значения в самом дескрипторе, а сохранять их в объекте, который содержит дескриптор. прикреплен к . Попробуйте следующий подход:

# descriptors4.py
class OneDigitNumericValue():
    def __init__(self, name):
        self.name = name

    def __get__(self, obj, type=None) -> object:
        return obj.__dict__.get(self.name) or 0

    def __set__(self, obj, value) -> None:
        obj.__dict__[self.name] = value

class Foo():
    number = OneDigitNumericValue("number")

my_foo_object = Foo()
my_second_foo_object = Foo()

my_foo_object.number = 3
print(my_foo_object.number)
print(my_second_foo_object.number)

my_third_foo_object = Foo()
print(my_third_foo_object.number)

В этом примере, когда вы присваиваете значение атрибуту number вашего объекта, дескриптор сохраняет его в атрибуте __dict__ объекта, к которому он присоединен, используя то же имя, что и сам дескриптор.

Единственная проблема здесь заключается в том, что при создании экземпляра дескриптора вы должны указать имя в качестве параметра:

number = OneDigitNumericValue("number")

Не лучше ли было бы просто написать number = OneDigitNumericValue()? Возможно, но если вы используете версию Python ниже 3.6, то вам понадобится немного волшебства с помощью метаклассов и декораторов. Однако, если вы используете Python 3.6 или более поздней версии, то в протоколе дескриптора есть новый метод .__set_name__(), который выполняет все эти волшебные действия за вас, как предложено в PEP 487:

__set_name__(self, owner, name)

С помощью этого нового метода всякий раз, когда вы создаете экземпляр дескриптора, вызывается этот метод и автоматически устанавливается параметр name.

Теперь попробуйте переписать предыдущий пример для Python 3.6 и выше:

# descriptors5.py
class OneDigitNumericValue():
    def __set_name__(self, owner, name):
        self.name = name

    def __get__(self, obj, type=None) -> object:
        return obj.__dict__.get(self.name) or 0

    def __set__(self, obj, value) -> None:
        obj.__dict__[self.name] = value

class Foo():
    number = OneDigitNumericValue()

my_foo_object = Foo()
my_second_foo_object = Foo()

my_foo_object.number = 3
print(my_foo_object.number)
print(my_second_foo_object.number)

my_third_foo_object = Foo()
print(my_third_foo_object.number)

Теперь .__init__() был удален, и был реализован .__set_name__(). Это позволяет создать свой дескриптор без указания имени внутреннего атрибута, который вам нужно использовать для хранения значения. Теперь ваш код также выглядит лучше и чище!

Запустите этот пример еще раз, чтобы убедиться, что все работает:

$ python descriptors5.py
3
0
0

Этот пример должен выполняться без проблем, если вы используете Python версии 3.6 или выше.

Зачем использовать дескрипторы Python?

Теперь вы знаете, что такое дескрипторы Python и как сам Python использует их для реализации некоторых своих функций, таких как методы и свойства. Вы также увидели, как создать дескриптор Python, избежав при этом некоторых распространенных ошибок. Теперь все должно быть ясно, но вы все еще можете задаваться вопросом, зачем вам их использовать.

По моему опыту, я знаю многих продвинутых разработчиков на Python, которые никогда раньше не использовали эту функцию и которым она не нужна. Это вполне нормально, потому что не так много случаев использования, когда необходимы дескрипторы на Python. Однако это не означает, что дескрипторы Python - это просто академическая тема для продвинутых пользователей. Все еще есть несколько хороших примеров использования, которые могут оправдать затраты на обучение их использованию.

Отложенные свойства

Первый и самый простой пример - это отложенные свойства. Это свойства, начальные значения которых не загружаются до тех пор, пока к ним не будет выполнен первый доступ. Затем они загружают свое начальное значение и сохраняют его в кэше для последующего повторного использования.

Рассмотрим следующий пример. У вас есть класс DeepThought, содержащий метод meaning_of_life(), который возвращает значение после долгого времени, проведенного в напряженной концентрации:

# slow_properties.py
import time

class DeepThought:
    def meaning_of_life(self):
        time.sleep(3)
        return 42

my_deep_thought_instance = DeepThought()
print(my_deep_thought_instance.meaning_of_life())
print(my_deep_thought_instance.meaning_of_life())
print(my_deep_thought_instance.meaning_of_life())

Если вы запустите этот код и попытаетесь получить доступ к методу три раза, то будете получать ответ каждые три секунды, что соответствует продолжительности спящего режима внутри метода.

Теперь свойство lazy может вычислять этот метод только один раз при его первом выполнении. Затем оно кэширует результирующее значение, чтобы, если оно вам понадобится снова, вы могли получить его в кратчайшие сроки. Вы можете добиться этого с помощью дескрипторов Python:

# lazy_properties.py
import time

class LazyProperty:
    def __init__(self, function):
        self.function = function
        self.name = function.__name__

    def __get__(self, obj, type=None) -> object:
        obj.__dict__[self.name] = self.function(obj)
        return obj.__dict__[self.name]

class DeepThought:
    @LazyProperty
    def meaning_of_life(self):
        time.sleep(3)
        return 42

my_deep_thought_instance = DeepThought()
print(my_deep_thought_instance.meaning_of_life)
print(my_deep_thought_instance.meaning_of_life)
print(my_deep_thought_instance.meaning_of_life)

Не торопитесь изучать этот код и понимать, как он работает. Можете ли вы оценить мощь дескрипторов Python здесь? В этом примере, когда вы используете дескриптор @LazyProperty, вы создаете экземпляр дескриптора и передаете ему .meaning_of_life(). Этот дескриптор хранит как метод, так и его имя в качестве переменных экземпляра.

Поскольку это дескриптор, не связанный с данными, при первом обращении к значению атрибута meaning_of_life автоматически вызывается .__get__() и выполняется .meaning_of_life() на my_deep_thought_instance объект. Результирующее значение сохраняется в атрибуте __dict__ самого объекта. Когда вы снова получите доступ к атрибуту meaning_of_life, Python будет использовать цепочку поиска , чтобы найти значение для этого атрибута внутри атрибута __dict__, и это значение будет немедленно возвращено.

Обратите внимание, что это работает, потому что в этом примере вы использовали только один метод .__get__() протокола дескрипторов. Вы также реализовали дескриптор, не содержащий данных. Если бы вы реализовали дескриптор данных, то этот трюк не сработал бы. Следуя цепочке поиска, он имел бы приоритет над значением, хранящимся в __dict__. Чтобы проверить это, запустите следующий код:

# wrong_lazy_properties.py
import time

class LazyProperty:
    def __init__(self, function):
        self.function = function
        self.name = function.__name__

    def __get__(self, obj, type=None) -> object:
        obj.__dict__[self.name] = self.function(obj)
        return obj.__dict__[self.name]

    def __set__(self, obj, value):
        pass

class DeepThought:
    @LazyProperty
    def meaning_of_life(self):
        time.sleep(3)
        return 42

my_deep_thought_instance = DeepThought()
print(my_deep_thought_instance.meaning_of_life)
print(my_deep_thought_instance.meaning_of_life)
print(my_deep_thought_instance.meaning_of_life)

В этом примере вы можете видеть, что простая реализация .__set__(), , даже если она вообще ничего не делает, создает дескриптор данных. Теперь трюк с ленивым свойством перестает работать.

Код D.R.Y.

Другим типичным вариантом использования дескрипторов является написание повторно используемого кода и придание вашему коду четкости. Дескрипторы Python предоставляют разработчикам отличный инструмент для написания повторно используемого кода, который может быть разделен между различными свойствами или даже разными классы.

Рассмотрим пример, в котором у вас есть пять разных свойств с одинаковым поведением. Каждому свойству может быть присвоено определенное значение, только если это четное число. В противном случае его значение устанавливается равным 0:

# properties.py
class Values:
    def __init__(self):
        self._value1 = 0
        self._value2 = 0
        self._value3 = 0
        self._value4 = 0
        self._value5 = 0

    @property
    def value1(self):
        return self._value1

    @value1.setter
    def value1(self, value):
        self._value1 = value if value % 2 == 0 else 0

    @property
    def value2(self):
        return self._value2

    @value2.setter
    def value2(self, value):
        self._value2 = value if value % 2 == 0 else 0

    @property
    def value3(self):
        return self._value3

    @value3.setter
    def value3(self, value):
        self._value3 = value if value % 2 == 0 else 0

    @property
    def value4(self):
        return self._value4

    @value4.setter
    def value4(self, value):
        self._value4 = value if value % 2 == 0 else 0

    @property
    def value5(self):
        return self._value5

    @value5.setter
    def value5(self, value):
        self._value5 = value if value % 2 == 0 else 0

my_values = Values()
my_values.value1 = 1
my_values.value2 = 4
print(my_values.value1)
print(my_values.value2)

Как вы можете видеть, здесь много дублированного кода. Можно использовать дескрипторы Python для разделения поведения между всеми свойствами. Вы можете создать дескриптор EvenNumber и использовать его для всех свойств следующим образом:

# properties2.py
class EvenNumber:
    def __set_name__(self, owner, name):
        self.name = name

    def __get__(self, obj, type=None) -> object:
        return obj.__dict__.get(self.name) or 0

    def __set__(self, obj, value) -> None:
        obj.__dict__[self.name] = (value if value % 2 == 0 else 0)

class Values:
    value1 = EvenNumber()
    value2 = EvenNumber()
    value3 = EvenNumber()
    value4 = EvenNumber()
    value5 = EvenNumber()

my_values = Values()
my_values.value1 = 1
my_values.value2 = 4
print(my_values.value1)
print(my_values.value2)

Теперь этот код выглядит намного лучше! Дубликаты устранены, а логика реализована в одном месте, так что, если вам нужно что-то изменить, вы можете легко это сделать.

Заключение

Теперь, когда вы знаете, как Python использует дескрипторы для реализации некоторых своих замечательных функций, вы станете более сознательным разработчиком, который поймет, почему некоторые функции Python были реализованы именно так, как они есть.

Ты научился:

  • Что такое дескрипторы Python и когда их использовать
  • Где используются дескрипторы во внутренней части Python
  • Как реализовать свои собственные дескрипторы

Более того, теперь вы знаете о некоторых конкретных вариантах использования, где дескрипторы Python особенно полезны. Например, дескрипторы полезны, когда у вас есть общее поведение, которое должно быть общим для множества свойств, даже относящихся к разным классам.

Back to Top