Share This
Связаться со мной
Крути в низ
Categories
//property() в Python

property() в Python

01.02.2022Category : Python

С помощью 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'bxc7^aix9f3xd2g ... x89^-x92xbexe6'  >>> john.password Traceback (most recent call last):     ... AttributeError: Password is write-only  >>> john.password = "supersecret" >>> john._hashed_password b'xe9l$x9fxafx9d ... bxe8xc8xfcaUr_'

В этом примере вы создаете 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».

  • 0 views
  • 0 Comment

Leave a Reply

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.

Связаться со мной
Close