С помощью property() в Python вы можете создавать управляемые атрибуты в своих классах. Управляемые атрибуты, также известные как свойства (англ. properties), используются, если вам нужно изменять их внутреннюю реализацию без изменения общедоступного API класса. Когда пользователи полагаются на ваши классы и объекты, важно, чтобы ваш API был стабильным.
Свойства, вероятно, самый популярный способ быстрого создания управляемых атрибутов в абсолютно питоническом стиле.
Начало работы с property() в Python
property() позволяет вам превращать атрибуты класса в свойства или управляемые атрибуты. Поскольку это встроенная функция, вы можете использовать ее, ничего не импортируя. Для обеспечения оптимальной производительности property() была реализована на языке C .
С помощью property() вы можете присоединить геттеры и сеттеры к атрибутам класса. Это даст возможность обрабатывать внутреннюю реализацию этого атрибута, не раскрывая геттеры и сеттеры в вашем API. Вы также можете указать способ управления удалением атрибута и предоставить соответствующую строку документации для ваших свойств.
Вот полная сигнатура property:
property(fget=None, fset=None, fdel=None, doc=None)
Первые два аргумента принимают объекты функции, которые будут играть роль геттера (fget) и сеттера (fset). Вот краткое описание того, что делает каждый аргумент:
| Аргумент | Описание |
|---|---|
fget | Функция, возвращающая значение управляемого атрибута |
fset | Функция, позволяющая установить значение управляемого атрибута |
fdel | Функция для определения того, как управляемый атрибут обрабатывает удаление |
doc | Строка, представляющая docstring свойства |
Возвращаемое значение property() – это сам управляемый атрибут. Если вы обращаетесь к управляемому атрибуту, как в obj.attr, Python автоматически вызывает fget(). Если вы присваиваете атрибуту новое значение, как в obj.attr = value, Python вызывает fset(), используя входное значение в качестве аргумента. Наконец, если вы запустите оператор del obj.attr, Python автоматически вызовет fdel().
Вы можете использовать doc, чтобы предоставить соответствующую строку документации для ваших свойств. Вы и ваши коллеги-программисты сможете прочитать ее с помощью help(). Аргумент doc также полезен, когда вы работаете с редакторами кода и IDE, которые поддерживают доступ к строкам документации.
Вы можете использовать property() как функцию или как декоратор для создания ваших свойств. В следующих двух разделах вы познакомитесь с обоими подходами. Но стоит сразу отметить, что подход с декоратором более популярен в сообществе Python.
Создание атрибутов с помощью property()
Вы можете создать свойство, вызвав property() с соответствующим набором аргументов и присвоив возвращаемое значение атрибуту класса. Все аргументы для property() необязательны.
В следующем примере показано, как создать класс Circle с удобным свойством для управления его радиусом radius:
# circle.py
class Circle:
def __init__(self, radius):
self._radius = radius
def _get_radius(self):
print("Get radius")
return self._radius
def _set_radius(self, value):
print("Set radius")
self._radius = value
def _del_radius(self):
print("Delete radius")
del self._radius
radius = property(
fget=_get_radius,
fset=_set_radius,
fdel=_del_radius,
doc="The radius property."
)
В этом фрагменте кода вы создаете Circle. Инициализатор класса .__init__() принимает радиус в качестве аргумента и сохраняет его в закрытом атрибуте с именем ._radius. Затем вы определяете три закрытых метода:
._get_radius()возвращает текущее значение._radius._set_radius()принимает значение в качестве аргумента и присваивает его._radius._del_radius()удаляет атрибут экземпляра._radius
Чтобы инициализировать свойство, вы передаете три метода в качестве аргументов в property(). Вы также передаете соответствующую строку документации.
Запустим следующий код, чтобы посмотреть, как работает Circle:
>>> from circle import Circle
>>> circle = Circle(42.0)
>>> circle.radius
Get radius
42.0
>>> circle.radius = 100.0
Set radius
>>> circle.radius
Get radius
100.0
>>> del circle.radius
Delete radius
>>> circle.radius
Get radius
Traceback (most recent call last):
...
AttributeError: 'Circle' object has no attribute '_radius'
>>> help(circle)
Help on Circle in module __main__ object:
class Circle(builtins.object)
...
| radius
| The radius property.
Свойство .radius скрывает закрытый атрибут экземпляра ._radius, который теперь является вашим управляемым атрибутом. Вы можете получить доступ и назначить .radius напрямую. Под капотом Python при необходимости автоматически вызывает ._get_radius() и ._set_radius(). Когда вы выполняете del circle.radius, Python вызывает ._del_radius(), который удаляет базовый ._radius.
Свойства имеют приоритет над дескрипторами. Если вы используете dir() для проверки внутренних членов данного свойства, вы найдете в списке .__set__() и .__get__(). Эти методы обеспечивают реализацию протокола дескриптора по умолчанию.
Использование property() в качестве декоратора
Декораторы в Python используются повсюду. Это функции, которые принимают другую функцию в качестве аргумента и возвращают новую функцию с добавленным функционалом. С помощью декоратора вы можете присоединить операции предварительной и постобработки к существующей функции.
property() также может работать как декоратор, поэтому вы можете использовать синтаксис @property для быстрого создания свойств:
# circle.py
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
"""The radius property."""
print("Get radius")
return self._radius
@radius.setter
def radius(self, value):
print("Set radius")
self._radius = value
@radius.deleter
def radius(self):
print("Delete radius")
del self._radius
Теперь код выглядит более питоническим и чистым. Вам больше не нужно использовать такие имена методов, как ._get_radius(), ._set_radius() и ._del_radius(). И у вас есть три метода с одинаковым понятным и описательным именем, похожим на атрибут.
Подход декоратора для создания свойств требует определения первого метода, использующего публичное имя для нижележащего управляемого атрибута, которым в данном случае является .radius. Этот метод должен реализовывать логику получения – геттер. В приведенном выше примере этот метод реализуется в строках с 7 по 11.
Строки с 13 по 16 определяют сеттер для .radius. В этом случае синтаксис сильно отличается. Вместо того, чтобы снова использовать @property, вы используете @radius.setter. Почему? Посмотрим на вывод dir():
>>> dir(Circle.radius) [..., 'deleter', ..., 'getter', 'setter']
Помимо .fget, .fset, .fdel и множества других специальных атрибутов и методов, property также предоставляет .deleter(), .getter() и .setter(). Каждый из этих трех методов возвращает новое свойство.
Когда вы декорируете второй метод .radius() с помощью @radius.setter, вы создаете новое свойство и переназначаете имя уровня класса .radius для его хранения. Это новое свойство содержит тот же набор методов, что и начальное, с добавлением нового сеттера, представленного в строке 14. Наконец, синтаксис декоратора переназначает новое свойство на имя уровня класса .radius. Механизм определения метода удаления аналогичен.
Новая реализация Circle работает так же, как пример в разделе выше:
>>> from circle import Circle
>>> circle = Circle(42.0)
>>> circle.radius
Get radius
42.0
>>> circle.radius = 100.0
Set radius
>>> circle.radius
Get radius
100.0
>>> del circle.radius
Delete radius
>>> circle.radius
Get radius
Traceback (most recent call last):
...
AttributeError: 'Circle' object has no attribute '_radius'
>>> help(circle)
Help on Circle in module __main__ object:
class Circle(builtins.object)
...
| radius
| The radius property.
Вам не нужно использовать пару круглых скобок для вызова .radius() в качестве метода. Вместо этого вы можете получить доступ к .radius, как к обычному атрибуту. Это, собственно, основной вариант использования свойств. Они позволяют относиться к методам, как к атрибутам, и автоматически вызывают базовый набор методов.
Предоставление атрибутов только для чтения
Вероятно, простейший вариант использования property() – предоставление атрибутов только для чтения. Допустим, вам нужен неизменяемый класс Point, который не позволяет пользователю изменять исходное значение его координат x и y. Для этого вы можете создать Point, как в следующем примере:
# point.py
class Point:
def __init__(self, x, y):
self._x = x
self._y = y
@property
def x(self):
return self._x
@property
def y(self):
return self._y
Здесь вы сохраняете входные аргументы в атрибутах ._x и ._y. Кроме того, вы определяете два метода-геттера и декорируете их с помощью @property.
Теперь у вас есть два свойства только для чтения, .x и .y, в качестве ваших координат:
>>> from point import Point
>>> point = Point(12, 5)
>>> # Read coordinates
>>> point.x
12
>>> point.y
5
>>> # Write coordinates
>>> point.x = 42
Traceback (most recent call last):
...
AttributeError: can't set attribute
Здесь point.x и point.y – простые примеры свойств, доступных только для чтения. Их поведение зависит от базового дескриптора, который предоставляет свойство. Реализация же по умолчанию .__set__() вызывает AttributeError, если вы не определяете правильный метод-сеттер.
Вы можете пойти дальше этой реализации Point и предоставить явные сеттеры, которые вызывают настраиваемое исключение с более сложными и конкретными сообщениями:
# point.py
class WriteCoordinateError(Exception):
pass
class Point:
def __init__(self, x, y):
self._x = x
self._y = y
@property
def x(self):
return self._x
@x.setter
def x(self, value):
raise WriteCoordinateError("x coordinate is read-only")
@property
def y(self):
return self._y
@y.setter
def y(self, value):
raise WriteCoordinateError("y coordinate is read-only")
В этом примере вы определяете настраиваемое исключение с именем WriteCoordinateError. Это исключение позволяет вам настроить способ реализации неизменяемого класса Point. Теперь оба сеттера вызывают ваше настраиваемое исключение с более явным сообщением.
Создание атрибутов, доступных для чтения и записи
Вы также можете использовать property() для предоставления управляемых атрибутов с возможностью чтения и записи. На практике вам просто нужно предоставить соответствующие геттер (чтение) и сеттер (запись) для ваших свойств, чтобы создать управляемые атрибуты чтения-записи.
Допустим, вы хотите, чтобы ваш класс Circle имел атрибут .diameter. Однако использование и радиуса, и диаметра в инициализаторе класса кажется ненужным, потому что вы можете вычислить одно, используя другое. Вот Circle, который управляет .radius и .diameter как атрибутами чтения и записи:
# circle.py
import math
class Circle:
def __init__(self, radius):
self.radius = radius
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
self._radius = float(value)
@property
def diameter(self):
return self.radius * 2
@diameter.setter
def diameter(self, value):
self.radius = value / 2
Здесь вы создаете класс Circle с .radius, доступным для чтения и записи. В этом случае геттер просто возвращает значение радиуса. Сеттер же преобразует входное значение для радиуса и присваивает его закрытому ._radius, который является переменной, используемой для хранения окончательных данных.
В этой новой реализации Circle и его атрибута .radius есть небольшая деталь. В этом случае инициализатор класса присваивает входное значение свойству .radius напрямую, вместо того, чтобы сохранять его в выделенном непубличном атрибуте, таком как ._radius.
Почему? Потому что вам нужно убедиться, что каждое значение, указанное в виде радиуса, включая значение инициализации, проходит через сеттер и преобразуется в число с плавающей точкой.
Circle также реализует атрибут .diameter как свойство. Геттер рассчитывает диаметр, используя радиус. Метод-сеттер делает кое-что любопытное. Вместо того, чтобы сохранять входное значение диаметра в специальном атрибуте, он вычисляет радиус и записывает результат в .radius.
Вот как работает наш Circle:
>>> from circle import Circle >>> circle = Circle(42) >>> circle.radius 42.0 >>> circle.diameter 84.0 >>> circle.diameter = 100 >>> circle.diameter 100.0 >>> circle.radius 50.0
И .radius, и .diameter работают как обычные атрибуты в этих примерах, обеспечивая чистый и общедоступный API для вашего класса Circle.
Предоставление атрибутов только для записи
Вы также можете создать атрибуты только для записи, изменив реализацию метода-геттера в ваших свойствах. Например, вы можете заставить свой геттер генерировать исключение каждый раз, когда пользователь обращается к значению базового атрибута.
Вот пример обработки паролей со свойством только для записи:
# users.py
import hashlib
import os
class User:
def __init__(self, name, password):
self.name = name
self.password = password
@property
def password(self):
raise AttributeError("Password is write-only")
@password.setter
def password(self, plaintext):
salt = os.urandom(32)
self._hashed_password = hashlib.pbkdf2_hmac(
"sha256", plaintext.encode("utf-8"), salt, 100_000
)
Инициализатор User принимает имя пользователя и пароль в качестве аргументов и сохраняет их в .name и .password соответственно. Вы используете свойство для управления тем, как ваш класс обрабатывает входной пароль. Геттер вызывает ошибку AttributeError всякий раз, когда пользователь пытается получить текущий пароль. Это превращает .password в атрибут только для записи:
>>> from users import User
>>> john = User("John", "secret")
>>> john._hashed_password
b'b\xc7^ai\x9f3\xd2g ... \x89^-\x92\xbe\xe6'
>>> john.password
Traceback (most recent call last):
...
AttributeError: Password is write-only
>>> john.password = "supersecret"
>>> john._hashed_password
b'\xe9l$\x9f\xaf\x9d ... b\xe8\xc8\xfcaU\r_'
В этом примере вы создаете john как экземпляр User с начальным паролем. Сеттер хеширует пароль и сохраняет его в ._hashed_password. Обратите внимание, что когда вы пытаетесь получить доступ к .password напрямую, вы получаете AttributeError. Наконец, присвоение нового значения .password запускает метод установки и создает новый хешированный пароль.
В методе установки .password вы используете os.urandom() для генерации 32-байтовой случайной строки. Чтобы сгенерировать хешированный пароль, вы используете hashlib.pbkdf2_hmac(). Затем вы сохраняете полученный хешированный пароль в закрытом атрибуте ._hashed_password. Это гарантирует, что вы никогда не сохраните пароль в виде открытого текста ни в каком извлекаемом атрибуте.
property() в действии
Иногда вам нужно отслеживать, что делает ваш код и как работают ваши программы. Один из способов сделать это в Python – использовать logging. Этот модуль предоставляет все функции, которые могут потребоваться для логирования вашего кода. Это позволит вам постоянно следить за кодом и генерировать полезную информацию о том, как он работает.
Если вам когда-либо понадобится отслеживать, как и когда вы получаете доступ к данному атрибуту и изменяете его, то вы также можете воспользоваться для этого функцией property():
# circle.py
import logging
logging.basicConfig(
format="%(asctime)s: %(message)s",
level=logging.INFO,
datefmt="%H:%M:%S"
)
class Circle:
def __init__(self, radius):
self._msg = '"radius" was %s. Current value: %s'
self.radius = radius
@property
def radius(self):
"""The radius property."""
logging.info(self._msg % ("accessed", str(self._radius)))
return self._radius
@radius.setter
def radius(self, value):
try:
self._radius = float(value)
logging.info(self._msg % ("mutated", str(self._radius)))
except ValueError:
logging.info('validation error while mutating "radius"')
Здесь вы сначала импортируете logging и определяете базовую конфигурацию. Затем вы реализуете Circle с управляемым атрибутом .radius. После чего геттер генерирует информацию каждый раз, когда вы обращаетесь к .radius в своем коде. Кроме того, сеттер регистрирует каждое изменение в .radius. Он также регистрирует те ситуации, в которых вы получаете ошибку из-за неверных входных данных.
Вот как можно использовать Circle в своем коде:
>>> from circle import Circle >>> circle = Circle(42.0) >>> circle.radius 14:48:59: "radius" was accessed. Current value: 42.0 42.0 >>> circle.radius = 100 14:49:15: "radius" was mutated. Current value: 100 >>> circle.radius 14:49:24: "radius" was accessed. Current value: 100 100 >>> circle.radius = "value" 15:04:51: validation error while mutating "radius"
Заключение
property в Python – это особый тип члена класса, обеспечивающий функциональность, занимающую место где-то между обычными атрибутами и методами. Более того, свойства позволяют изменять реализацию атрибутов экземпляра без изменения общедоступного API класса.
В этом руководстве мы разобрали, что такое property() и как эта функция работает. Кроме того, мы рассмотрели на примерах, как использовать property в Python в качестве декоратора.
Надеемся данная статья была вам полезна! Успехов в написании кода!
Сокращенный перевод статьи «Python’s property(): Add Managed Attributes to Your Classes».

