С помощью 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».