7

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

1

Я пытаюсь реализовать метод доступа (геттеры и сеттеры) для свойств в Python. Я пробовал несколько подходов, например:

def set_property(property, value):  
    # код установки значения свойства

def get_property(property):  
    # код получения значения свойства

Или использовал атрибуты объекта напрямую:

object.property = value  
value = object.property

Но я не уверен, какой из этих способов является "питоническим" для реализации геттеров и сеттеров. Каково правильное и рекомендованное решение для этого в Python?

5 ответ(ов)

6

Какой «питонический» способ использовать геттеры и сеттеры?

«Питонический» способ — это не использование «геттеров» и «сеттеров», а использование обычных атрибутов, как показано в вопросе, и del для удаления (но имена изменены, чтобы защитить невинных... встроенные функции):

value = 'что-то'

obj.attribute = value  
value = obj.attribute
del obj.attribute

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

class Obj:
    """демонстрация property"""
    #
    @property            # сначала декорируем метод-геттер
    def attribute(self): # Имя этого метода-геттера — *то* имя
        return self._attribute
    #
    @attribute.setter    # теперь свойство декорируется с помощью `.setter`
    def attribute(self, value):   # имя, т.е. "attribute", остается тем же
        self._attribute = value   # имя "value" не является специальным
    #
    @attribute.deleter     # декорируем с помощью `.deleter`
    def attribute(self):   # снова имя метода остается тем же
        del self._attribute

(Каждое использование декоратора копирует и обновляет предыдущее свойство, поэтому обратите внимание, что вам следует использовать одно и то же имя для каждой функции/метода установки, получения и удаления.)

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

obj = Obj()
obj.attribute = value  
the_value = obj.attribute
del obj.attribute

Вам следует избегать следующего:

def set_property(property, value):  
def get_property(property):  

Во-первых, приведенный выше код не работает, потому что вы не предоставляете аргумент для экземпляра, которому свойство будет установлено (обычно self), но это может быть оформлено так:

class Obj:

    def set_property(self, property, value): # не делайте этого
        ...
    def get_property(self, property):        # не делайте этого тоже
        ...

Во-вторых, это дублирует назначение двух специальных методов, __setattr__ и __getattr__.

В-третьих, у нас также есть встроенные функции setattr и getattr.

setattr(object, 'property_name', value)
getattr(object, 'property_name', default_value)  # default - необязателен

Декоратор @property предназначен для создания геттеров и сеттеров.

Например, мы могли бы изменить поведение установки, чтобы ограничить устанавливаемое значение:

class Protective(object):

    @property
    def protected_value(self):
        return self._protected_value

    @protected_value.setter
    def protected_value(self, value):
        if acceptable(value): # например, проверка типа или диапазона
            self._protected_value = value

В общем, мы хотим избегать использования property и просто использовать прямые атрибуты.

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

Демонстрация

Например, предположим, нам нужно, чтобы защищаемый атрибут нашего объекта был целым числом от 0 до 100 включительно и предотвращал его удаление, с соответствующими сообщениями, информирующими пользователя о правильном использовании:

class Protective(object):
    """демонстрация защищенного свойства"""
    #
    def __init__(self, start_protected_value=0):
        self.protected_value = start_protected_value
    # 
    @property
    def protected_value(self):
        return self._protected_value
    #
    @protected_value.setter
    def protected_value(self, value):
        if value != int(value):
            raise TypeError("protected_value должно быть целым числом")
        if 0 <= value <= 100:
            self._protected_value = int(value)
        else:
            raise ValueError("protected_value должно быть " +
                             "в пределах от 0 до 100 включительно")
    #
    @protected_value.deleter
    def protected_value(self):
        raise AttributeError("не удаляйте, protected_value может быть установлено в 0")

(Обратите внимание, что __init__ ссылается на self.protected_value, но методы свойства ссылаются на self._protected_value. Это сделано так, чтобы __init__ использовал свойство через публичный API, обеспечивая его «защиту».)

И использование:

>>> p1 = Protective(3)
>>> p1.protected_value
3
>>> p1 = Protective(5.0)
>>> p1.protected_value
5
>>> p2 = Protective(-5)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in __init__
  File "<stdin>", line 15, in protected_value
ValueError: protected_value должно быть в пределах от 0 до 100 включительно
>>> p1.protected_value = 7.3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 17, in protected_value
TypeError: protected_value должно быть целым числом
>>> p1.protected_value = 101
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 15, in protected_value
ValueError: protected_value должно быть в пределах от 0 до 100 включительно
>>> del p1.protected_value
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 18, in protected_value
AttributeError: не удаляйте, protected_value может быть установлено в 0

Имеют ли значение имена?

Да, они имеют. .setter и .deleter создают копии оригинального свойства. Это позволяет подклассам правильно изменять поведение, не изменяя поведение в родительском классе.

class Obj:
    """демонстрация свойства"""
    #
    @property
    def get_only(self):
        return self._attribute
    #
    @get_only.setter
    def get_or_set(self, value):
        self._attribute = value
    #
    @get_or_set.deleter
    def get_set_or_delete(self):
        del self._attribute

Теперь для этого кода необходимо использовать соответствующие имена:

obj = Obj()
# obj.get_only = 'value' # вызовет ошибку
obj.get_or_set = 'value'  
obj.get_set_or_delete = 'new value'
the_value = obj.get_only
del obj.get_set_or_delete
# del obj.get_or_set # вызовет ошибку

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

Заключение

Начинайте с простых атрибутов.

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

Избегайте функций с именами set_... и get_... — для этого предназначены свойства.

0

В вашем коде вы создаете класс test, в котором реализованы свойства с помощью декораторов @property и @<имя свойства>.setter. Давайте разберем это по шагам:

  1. В классе test в методе __init__ инициализируется атрибут pants, которому присваивается строка 'pants'.

  2. Используя декоратор @property, вы определяете метод p, который возвращает текущее значение self.pants. Это позволяет вам получить значение свойства p как если бы это было обычное свойство объекта.

  3. Декоратор @p.setter позволяет вам задать значение для свойства p. В этом случае вы принимаете value, умножаете его на 2 и присваиваете результат атрибуту self.pants.

  4. Пример использования:

    • При создании экземпляра класса t = test(), t.p вернет 'pants' (строка, заданная в self.pants).
    • Когда вы присваиваете t.p = 10, это вызовет метод-сеттер p, который пересчитает значение pants как 10 * 2, то есть 20.
    • Вызов t.p теперь вернет 20, так как вы изменили значение self.pants.

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

0

Использование @property и @name.setter позволяет не только следовать "питоновскому" стилю, но и проверять корректность атрибутов как при создании объекта, так и при их изменении.

class Person(object):
    def __init__(self, p_name=None):
        self.name = p_name

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, new_name):
        if isinstance(new_name, str):  # проверка типа для атрибута name
            self._name = new_name
        else:
            raise Exception("Недопустимое значение для имени")

Таким образом, вы фактически "скрываете" атрибут _name от разработчиков, использующих ваш класс, и выполняете проверки типа для свойства name. Обратите внимание, что, следуя этому подходу, даже во время инициализации вызывается сеттер. Поэтому:

p = Person(12)

Приведет к следующему исключению:

Exception: Недопустимое значение для имени

А вот:

p = Person('Mike')
print(p.name)  # Выведет: Mike
p.name = 'George'
print(p.name)  # Выведет: George
p.name = 2.3  # Вызовет исключение

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

0

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

Вот пример класса Car, который демонстрирует использование таких свойств:

from objects import properties, self_properties

class Car:
    with properties(locals(), 'meta') as meta:

        @meta.prop(read_only=True)
        def brand(self) -> str:
            """Марка автомобиля"""

        @meta.prop(read_only=True)
        def max_speed(self) -> float:
            """Максимальная скорость автомобиля"""

        @meta.prop(listener='_on_acceleration')
        def speed(self) -> float:
            """Скорость автомобиля"""
            return 0  # По умолчанию остановлен

        @meta.prop(listener='_on_off_listener')
        def on(self) -> bool:
            """Состояние двигателя"""
            return False

    def __init__(self, brand: str, max_speed: float = 200):
        self_properties(self, locals())

    def _on_off_listener(self, prop, old, on):
        if on:
            print(f"{self.brand} Включен, Поехали!")
        else:
            self._speed = 0
            print(f"{self.brand} Выключен.")

    def _on_acceleration(self, prop, old, speed):
        if self.on:
            if speed > self.max_speed:
                print(f"{self.brand} {speed}км/ч Упс! Двигатель взорвался!")
                self.on = False
            else:
                print(f"{self.brand} Новая скорость: {speed}км/ч")
        else:
            print(f"{self.brand} Автомобиль выключен, скорость не меняется")

Этот класс можно использовать следующим образом:

mycar = Car('Ford')

# Автомобиль выключен
for speed in range(0, 300, 50):
    mycar.speed = speed

# Автомобиль включен
mycar.on = True
for speed in range(0, 350, 50):
    mycar.speed = speed

Результатом выполнения этого кода будет следующий вывод:

Ford Автомобиль выключен, скорость не меняется
Ford Автомобиль выключен, скорость не меняется
Ford Автомобиль выключен, скорость не меняется
Ford Автомобиль выключен, скорость не меняется
Ford Автомобиль выключен, скорость не меняется
Ford Автомобиль выключен, скорость не меняется
Ford Включен, Поехали!
Ford Новая скорость: 0км/ч
Ford Новая скорость: 50км/ч
Ford Новая скорость: 100км/ч
Ford Новая скорость: 150км/ч
Ford Новая скорость: 200км/ч
Ford 250км/ч Упс! Двигатель взорвался!
Ford Выключен.
Ford Автомобиль выключен, скорость не меняется

Более подробная информация о том, как и почему это работает, доступна здесь: ссылка на статью.

0

Свойства в Python действительно очень полезны, так как они позволяют использовать присвоение значений, в то же время обеспечивая возможность валидации. В приведенном коде показано, как использовать декораторы @property для создания метода-геттера и @<property_name>.setter для создания метода-сеттера:

# Программа на Python, демонстрирующая использование @property 
class AgeSet:
    def __init__(self):
        self._age = 0

    # использование декоратора property для геттера
    @property
    def age(self):
        print("Вызван метод геттера")
        return self._age

    # сеттер
    @age.setter
    def age(self, a):
        if a < 18:
            raise ValueError("Извините, ваш возраст ниже критериев допуска")
        print("Вызван метод сеттера")
        self._age = a

pkj = AgeSet()

pkj.age = int(input("Установите возраст с помощью сеттера: "))

print(pkj.age)

Если вам нужны дополнительные детали, я написал об этом в статье, доступной по следующей ссылке: https://pythonhowtoprogram.com/how-to-create-getter-setter-class-properties-in-python-3/.

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь