Handles in Python

Basic methods

The handle protocol in Python is defined by the presence of methods __get__, __set__ And __delete__ in class. These methods allow objects to control how attribute values ​​are retrieved, set, or removed. There is also an optional method __set_name__which allows the handle to recognize the name of the attribute to which it is assigned in the class.

Method __get__ called when the attribute's value is retrieved, it takes two arguments: self And instance. instance is the instance of the object through which the handle is accessible, or None, if the call goes through the class. The return value of this method will be the value of the specified attribute:

class Descriptor:
    def __get__(self, instance, owner):
        return 'значение'

class MyClass:
    attr = Descriptor()

my_object = MyClass()
print(my_object.attr)  # выведет 'значение'

__set__ allows you to control changes in the attribute value. It takes three arguments: self, instance And valueWhere value is the new attribute value:

class Descriptor:
    def __set__(self, instance, value):
        print(f"Установка значения {value}")
        self.__value = value

class MyClass:
    attr = Descriptor()

my_object = MyClass()
my_object.attr = 10  # выведет 'Установка значения 10'

__delete__ called when an attribute is removed using the operator del. It takes two arguments: self And instance:

class Descriptor:
    def __delete__(self, instance):
        print("Удаление атрибута")
        del self.__value

class MyClass:
    attr = Descriptor()

my_object = MyClass()
del my_object.attr  # выведет 'Удаление атрибута'

Optional method __set_name__ called at class creation time for each descriptor, allowing the descriptor to know the name of the attribute it is bound to. This method takes two arguments: self And nameWhere name is the attribute name:

class Descriptor:
    def __set_name__(self, owner, name):
        self.public_name = name
        self.private_name="_" + name

    def __get__(self, instance, owner):
        return getattr(instance, self.private_name, 'еще не установлено')

    def __set__(self, instance, value):
        setattr(instance, self.private_name, value)

class MyClass:
    attr = Descriptor()

my_object = MyClass()
print(my_object.attr)  # выведет 'еще не установлено'
my_object.attr = 99
print(my_object.attr)  # выведет 99

Examples of using

Let's create a data validation descriptor that will check that the user's age cannot be a negative number and cannot exceed 100 years:

class ValidateAge:
    def __set_name__(self, owner, name):
        self.private_name="_" + name

    def __get__(self, instance, owner):
        return getattr(instance, self.private_name, None)

    def __set__(self, instance, value):
        if not (0 <= value <= 100):
            raise ValueError("Возраст должен быть между 0 и 100 годами")
        setattr(instance, self.private_name, value)

class Person:
    age = ValidateAge()

    def __init__(self, name, age):
        self.name = name
        self.age = age

try:
    p = Person("Kolya", 30)  # валидный возраст
    print(p.age)
    p.age = -5  # невалидный возраст, будет вызвано исключение ValueError
except ValueError as e:
    print(e)

Now let's create a descriptor for caching the results of heavy calculations. Suppose there is a function that takes a significant amount of time to execute, and ye;yj caches its result for the same input data:

import time

class CachedAttribute:
    def __init__(self, method):
        self.method = method
        self.cache = {}

    def __get__(self, instance, owner):
        if instance not in self.cache:
            self.cache[instance] = self.method(instance)
        return self.cache[instance]

class HeavyComputation:
    @CachedAttribute
    def compute(self):
        # имитация длительного вычисления
        time.sleep(2)
        return "Результат вычисления"

hc = HeavyComputation()
start_time = time.time()
print(hc.compute)  # первый вызов занимает время
print(f"Выполнено за {time.time() - start_time} секунд")

start_time = time.time()
print(hc.compute)  # второй вызов мгновенный, использует кэшированный результат
print(f"Выполнено за {time.time() - start_time} секунд")

Let's create a descriptor that will log any changes to attribute values:

class LoggedAttribute:
    def __set_name__(self, owner, name):
        self.private_name="_" + name

    def __get__(self, instance, owner):
        return getattr(instance, self.private_name, None)

    def __set__(self, instance, value):
        print(f"Установка {self.private_name} в {value}")
        setattr(instance, self.private_name, value)

class User:
    name = LoggedAttribute()
    age = LoggedAttribute()

    def __init__(self, name, age):
        self.name = name
        self.age = age

u = User("Katya", 30)
u.name = "Katyuha"  # Логируется изменение
u.age = 31  # Логируется изменение

Implementation of Singleton and Factory patterns

We will not explain in the context of this article why the Singleton pattern is needed. But in short, it ensures that a class has only one instance and provides a global access point to that instance.

Let's create a descriptor Singletonwhich will manage the creation of instances of another class, ensuring that only one instance is created:

class Singleton:
    def __init__(self, cls):
        self.cls = cls
        self.instance = None

    def __get__(self, instance, owner):
        if self.instance is None:
            self.instance = self.cls()
        return self.instance

class Database:
    def __init__(self):
        print("Создание базы данных")

# применение дескриптора Singleton
class AppConfig:
    db = Singleton(Database)

# тестирование паттерна Singleton
config1 = AppConfig()
config2 = AppConfig()
db1 = config1.db  # создание БД
db2 = config2.db  # не создает новый экземпляр, использует существующий

print(db1 is db2)  # выведет True, подтверждая, что db1 и db2 - один и тот же объект

Factory is a design pattern that is used to create objects without specifying specific object classes.

To implement this pattern, you can create a handle that will dynamically determine which object to create based on some condition or configuration:

class VehicleFactory:
    def __init__(self, cls):
        self.cls = cls

    def __get__(self, instance, owner):
        return self.cls()

class Car:
    def drive(self):
        print("Вождение автомобиля")

class Bike:
    def ride(self):
        print("Езда на велосипеде")

# фабрика, создающая автомобили
class AppConfigCar:
    vehicle = VehicleFactory(Car)

# фабрика, создающая велосипеды
class AppConfigBike:
    vehicle = VehicleFactory(Bike)

# создание и использование автомобиля
car_config = AppConfigCar()
car = car_config.vehicle  # создает объект Car
car.drive()

# создание и использование велосипеда
bike_config = AppConfigBike()
bike = bike_config.vehicle  # создает объект Bike
bike.ride()

Built-in functions property, classmethod and staticmethod

property is a built-in Python function that can be used to create an attribute whose value is generated dynamically through the getter and setter methods. Usable when you need to add validation logic when assigning a value to an attribute or when the attribute value depends on other attributes.

property works as a handle using methods __get__, __set__ And __delete__ to control access to an attribute:

class Celsius:
    def __init__(self, temperature=0):
        self.temperature = temperature

    def to_fahrenheit(self):
        return (self.temperature * 1.8) + 32

    def get_temperature(self):
        print("Получение значения")
        return self._temperature

    def set_temperature(self, value):
        if value < -273.15:
            raise ValueError("Температура не может быть ниже -273.15 градусов Цельсия")
        print("Установка значения")
        self._temperature = value

    temperature = property(get_temperature, set_temperature)

c = Celsius(37)
print(c.temperature)
c.temperature = -300  # вызовет исключение

classmethod is a decorator that modifies a method so that it receives a class (rather than an instance of the class) as its first argument.

Internally classmethod implemented as a handle. When a method is decorated like classmethodcalling it results in calling the method __get__ handle which returns bound method – a function whose first argument automatically becomes a class:

class A:
    @classmethod
    def method(cls):
        return f"вызван classmethod класса {cls}"

print(A.method())  # вызван classmethod класса <class '__main__.A'>

staticmethod is a decorator that changes a class method so that it behaves like a regular function that does not accept any selfneither cls as the first argument.

staticmethod also implemented as a handle. When using this method __get__ handle simply returns a function without being bound to an instance or class:

class Math:
    @staticmethod
    def add(x, y):
        return x + y

print(Math.add(5, 7))  # 12

Several features

Data Descriptors __get__ And __set__ take precedence over instance attributes, whereas descriptors without data __get__ without __set__ inferior to them. This is important to consider when designing classes and their behavior.

__set__ will not be called if you try to change the attribute through a direct call to __dict__ copy. To avoid this error, it is important to always use normal assignment to set attribute values, thereby ensuring that the method is called __set__.

If a class defines a handle and an instance attribute with the same name, this may result in unexpected behavior. In such a case, the handle will take precedence when accessed through the class, but the instance attribute may peculiar hide handle when accessed directly. To avoid this situation, you should avoid naming instance attributes and descriptors with the same name.

When implementing the method __get__ It is important to remember that it must correctly handle the situation when instance argument is equal to None. This happens when the attribute is accessed through the class itself rather than an instance of it. In this case it is usually recommended to return the handle itself (i.e. self).

Handles in Python are a convenient way to add logic to attribute access.

The article was prepared in anticipation of the launch of a new stream of Python Developer specialization.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *