Свойства (property) и инкапсуляция в Python
Геттеры и сеттеры: основы и варианты реализации
Геттеры и сеттеры позволяют контролировать доступ к атрибутам объекта, добавляя логику при чтении или записи. В Python основным средством является декоратор @property, но существуют и другие подходы.
Основное решение: декоратор @property
Самый простой и рекомендуемый способ - использовать встроенный декоратор @property для создания свойства, доступного как атрибут, но с методами-обработчиками.
class Person:
def __init__(self, name):
self._name = name
@property
def name(self):
"""Геттер для имени"""
return self._name
@name.setter
def name(self, value):
if not isinstance(value, str):
raise ValueError("Имя должно быть строкой")
self._name = value
Get set python (геттеры и сеттеры в python)
Пояснение: атрибут _name считается защищённым (одно подчёркивание). Геттер name просто возвращает его значение. Сеттер name проверяет тип перед присваиванием. Пример использования:
p = Person("Алиса")
print(p.name) # Алиса
p.name = "Боб"
print(p.name) # Боб
# p.name = 123 # ValueError: Имя должно быть строкой
Python имя метода (имя метода в python)
Типичные ошибки:
- Забыть поставить декоратор
@propertyперед сеттером - сеттер не сработает и будет переопределён. - Использовать одинаковые имена для атрибута и свойства (например,
self.nameвместоself._name) - возникает бесконечная рекурсия.
Цели: контроль доступа, валидация, вычисляемые значения. Особенно полезно при переходе от прямого доступа к атрибутам к контролируемому.
Как создать свойство без использования декораторов?
Функция property() позволяет задать геттер, сеттер, удалитель и документацию в виде аргументов.
class Circle:
def __init__(self, radius):
self._radius = radius
def get_radius(self):
return self._radius
def set_radius(self, value):
if value < 0:
raise ValueError("Радиус не может быть отрицательным")
self._radius = value
def del_radius(self):
del self._radius
radius = property(get_radius, set_radius, del_radius, "Радиус окружности")
Python print self (использование self в print python)
Порядок аргументов: fget, fset, fdel, doc. Если какой-то аргумент не нужен, передаётся None.
c = Circle(5)
print(c.radius) # 5
c.radius = 10
print(c.radius) # 10
метод remove python (метод remove в python)
Проблемы:
- Легко перепутать порядок аргументов - вместо сеттера случайно передать удалитель.
- Свойство, созданное этим способом, менее читаемо, чем вариант с декораторами.
Случаи использования: когда свойство создаётся динамически, например, в метапрограммировании.
Как переиспользовать логику геттеров и сеттеров для разных атрибутов?
Дескрипторы - это классы, реализующие протокол __get__ и/или __set__. Они позволяют вынести общую логику в отдельный класс.
class PositiveNumber:
def __set_name__(self, owner, name):
self.private_name = f"_{name}"
def __get__(self, instance, owner):
return getattr(instance, self.private_name)
def __set__(self, instance, value):
if value <= 0:
raise ValueError("Значение должно быть положительным")
setattr(instance, self.private_name, value)
class Product:
price = PositiveNumber()
quantity = PositiveNumber()
def __init__(self, price, quantity):
self.price = price
self.quantity = quantity
какие есть методы в python (методы в python)
Теперь любой атрибут, назначенный на экземпляр PositiveNumber, автоматически проверяется на положительность.
prod = Product(100, 5)
print(prod.price) # 100
prod.price = 200
print(prod.price) # 200
# prod.price = -1 # ValueError: Значение должно быть положительным
метод в программе python (методы в программе на python)
Проблемы:
- Необходимость использовать
__set_name__для привязки имени атрибута. - Дескрипторы сложнее для понимания начинающими.
Цели: повторное использование кода валидации, создание доменно-специфичных типов атрибутов (например, Email, Phone).
Как перехватывать любые обращения к атрибутам?
Магические методы __getattr__ и __setattr__ позволяют контролировать доступ ко всем атрибутам объекта, что полезно для прокси-объектов или динамических хранилищ.
class SmartDict:
def __init__(self):
self._data = {}
def __getattr__(self, name):
if name.startswith('_'): # защищённые атрибуты не трогаем
raise AttributeError(name)
return self._data.get(name, None)
def __setattr__(self, name, value):
if name.startswith('_'):
# обычное присваивание для служебных атрибутов
super().__setattr__(name, value)
else:
self._data[name] = value
Python определение методов (определение методов в python)
Теперь можно обращаться к несуществующим атрибутам как к ключам словаря:
sd = SmartDict()
sd.name = "Алиса"
print(sd.name) # Алиса
print(sd.age) # None (нет ключа)
Python print class (печать класса в python)
Проблемы:
- В
__setattr__важно отличать служебные атрибуты (начинающиеся с подчёркивания) от обычных, иначе возникает бесконечная рекурсия при присваиванииself._data. - Не вызывает исключение при попытке записи в несуществующий атрибут (по умолчанию).
Случаи использования: прокси, обёртки, объекты с динамическими полями (как в ORM).
Как упростить объявление свойств с помощью сторонних библиотек?
Библиотеки attrs и pydantic предоставляют декларативный способ описания атрибутов с валидацией, автоматически генерируя геттеры, сеттеры и методы инициализации.
# pip install attrs
import attr
@attr.s
class Point:
x = attr.ib(validator=attr.validators.instance_of(int))
y = attr.ib(validator=attr.validators.instance_of(int))
функции языка python (функции в python)
Класс Point автоматически получает корректные геттеры и сеттеры с проверкой типов.
Использование:
p = Point(1, 2)
print(p.x, p.y) # 1 2
# p.x = "string" # TypeError: ...
Проблемы:
- Внешняя зависимость - не подходит для простых проектов или библиотек.
- Меньший контроль над поведением свойства (например, нельзя легко добавить вычисляемое поле).
Цели: быстрое прототипирование, уменьшение шаблонного кода, строгая валидация.
Ниже приведены расширенные примеры использования геттеров и сеттеров, демонстрирующие различные сценарии.
Вычисляемое свойство (площадь круга)
import math
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value < 0:
raise ValueError("Радиус не может быть отрицательным")
self._radius = value
@property
def area(self):
return math.pi * self._radius ** 2
Свойство area не хранит значение, а вычисляет его каждый раз при обращении. Изменение радиуса автоматически влияет на площадь.
c = Circle(10) print(c.area) # 314.1592653589793 c.radius = 5 print(c.area) # 78.53981633974483
Свойство только для чтения (без сеттера)
class ReadOnlyConfig:
def __init__(self):
self._config = {"host": "localhost", "port": 8080}
@property
def host(self):
return self._config["host"]
@property
def port(self):
return self._config["port"]
Сеттер не определён, поэтому попытка присвоить значение вызовет AttributeError.
cfg = ReadOnlyConfig() print(cfg.host) # localhost # cfg.host = "newhost" # AttributeError: can't set attribute
Ленивая загрузка (вычисление при первом обращении)
class LazyData:
def __init__(self):
self._heavy_data = None
@property
def data(self):
if self._heavy_data is None:
print("Вычисление данных...")
self._heavy_data = [x ** 2 for x in range(1000)]
return self._heavy_data
ld = LazyData()
print("До обращения")
print(len(ld.data)) # Вычисление данных... 1000
print(len(ld.data)) # 1000 (без повторного вычисления)
Валидация с несколькими условиями
class Temperature:
def __init__(self, celsius):
self._celsius = None
self.celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if not isinstance(value, (int, float)):
raise TypeError("Температура должна быть числом")
if value < -273.15:
raise ValueError("Температура ниже абсолютного нуля")
self._celsius = value
@property
def fahrenheit(self):
return self.celsius * 9 / 5 + 32
t = Temperature(100) print(t.fahrenheit) # 212.0 # t.celsius = -300 # ValueError: Температура ниже абсолютного нуля
Свойства в наследовании (переопределение геттера/сеттера)
class Base:
def __init__(self):
self._value = 0
@property
def value(self):
return self._value
@value.setter
def value(self, val):
self._value = val
class Derived(Base):
@property
def value(self):
return super().value * 2
@value.setter
def value(self, val):
super(Derived, type(self)).value.__set__(self, val + 1)
d = Derived() d.value = 5 print(d.value) # 12 ( (5+1)*2 )
Обратите внимание на вызов сеттера базового класса через super(Derived, type(self)).value.__set__.
Использование deleter
class Secure:
def __init__(self, key):
self._key = key
@property
def key(self):
return "***"
@key.deleter
def key(self):
if input("Введите пароль для удаления ключа: ") == "admin":
del self._key
print("Ключ удалён")
else:
print("Доступ запрещён")
s = Secure("secret")
print(s.key) # ***
del s.key # запрос пароля …
Свойство с кэшированием результата (мемоизация)
class Fibonacci:
def __init__(self):
self._cache = {0: 0, 1: 1}
@property
def fib_10(self):
if 10 not in self._cache:
print("Вычисление fib(10)...")
self._cache[10] = self._fib(10)
return self._cache[10]
def _fib(self, n):
if n in self._cache:
return self._cache[n]
self._cache[n] = self._fib(n-1) + self._fib(n-2)
return self._cache[n]
f = Fibonacci() print(f.fib_10) # Вычисление fib(10)... 55 print(f.fib_10) # 55 (из кэша)
Геттер и сеттер с логированием
import logging
logging.basicConfig(level=logging.INFO)
class LoggerAttr:
def __init__(self, value):
self._value = value
@property
def value(self):
logging.info(f"Чтение value: {self._value}")
return self._value
@value.setter
def value(self, new_val):
logging.info(f"Запись value: {new_val}")
self._value = new_val
la = LoggerAttr(10) print(la.value) # INFO:root:Чтение value: 10 -> 10 la.value = 20 # INFO:root:Запись value: 20
Такой подход используется для отладки или аудита изменений.