Как правильно использовать геттеры и сеттеры в Python?
Я пытаюсь реализовать метод доступа (геттеры и сеттеры) для свойств в Python. Я пробовал несколько подходов, например:
def set_property(property, value):
# код установки значения свойства
def get_property(property):
# код получения значения свойства
Или использовал атрибуты объекта напрямую:
object.property = value
value = object.property
Но я не уверен, какой из этих способов является "питоническим" для реализации геттеров и сеттеров. Каково правильное и рекомендованное решение для этого в Python?
5 ответ(ов)
Какой «питонический» способ использовать геттеры и сеттеры?
«Питонический» способ — это не использование «геттеров» и «сеттеров», а использование обычных атрибутов, как показано в вопросе, и 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_...
— для этого предназначены свойства.
В вашем коде вы создаете класс test
, в котором реализованы свойства с помощью декораторов @property
и @<имя свойства>.setter
. Давайте разберем это по шагам:
В классе
test
в методе__init__
инициализируется атрибутpants
, которому присваивается строка'pants'
.Используя декоратор
@property
, вы определяете методp
, который возвращает текущее значениеself.pants
. Это позволяет вам получить значение свойстваp
как если бы это было обычное свойство объекта.Декоратор
@p.setter
позволяет вам задать значение для свойстваp
. В этом случае вы принимаетеvalue
, умножаете его на 2 и присваиваете результат атрибутуself.pants
.Пример использования:
- При создании экземпляра класса
t = test()
,t.p
вернет'pants'
(строка, заданная вself.pants
). - Когда вы присваиваете
t.p = 10
, это вызовет метод-сеттерp
, который пересчитает значениеpants
как10 * 2
, то есть20
. - Вызов
t.p
теперь вернет20
, так как вы изменили значениеself.pants
.
- При создании экземпляра класса
Таким образом, благодаря свойствам вы можете управлять атрибутами класса с использованием "чистого" синтаксиса, как в случае с обычными атрибутами.
Использование @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 # Вызовет исключение
Этот подход обеспечивает надежное управление данными классов, следя за тем, чтобы к ним не присваивались недопустимые значения.
Это старый вопрос, но тема остается крайне важной и актуальной. Если кто-то хочет выйти за рамки простых геттеров и сеттеров, я написал статью о "суперсвойствах" в 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 Автомобиль выключен, скорость не меняется
Более подробная информация о том, как и почему это работает, доступна здесь: ссылка на статью.
Свойства в 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/.
Использование @property против геттеров и сеттеров
Как изменить порядок столбцов в DataFrame?
Получение текущей даты в формате YYYY-MM-DD в Python
Как проверить тип NoneType в Python?
Как удалить пакеты, установленные с помощью easy_install в Python?