Использование @property против геттеров и сеттеров
Описание проблемы:
Я столкнулся с вопросом о преимуществах использования синтаксиса @property
в Python по сравнению с традиционными методами получения и установки значений (геттерами и сеттерами). В каких конкретных ситуациях программисту следует предпочесть один подход другому?
С использованием свойств:
class MyClass(object):
@property
def my_attr(self):
return self._my_attr
@my_attr.setter
def my_attr(self, value):
self._my_attr = value
Без использования свойств:
class MyClass(object):
def get_my_attr(self):
return self._my_attr
def set_my_attr(self, value):
self._my_attr = value
Есть ли у @property
явные преимущества, которые делают его более предпочтительным? Буду признателен за ваши рекомендации и примеры из практики!
5 ответ(ов)
Предпочитайте свойства. Именно для этого они и предназначены.
Причина заключается в том, что все атрибуты в Python являются публичными. Начинание имен с одного или двух подчеркиваний всего лишь предупреждает о том, что данный атрибут является деталью реализации, которая может измениться в будущих версиях кода. Это не предотвращает доступ к атрибуту или его изменение. Таким образом, стандартный доступ к атрибутам является нормальным, "питоничным" способом обращения к ним.
Преимущество свойств заключается в том, что они синтаксически идентичны доступу к атрибутам, поэтому вы можете переходить от одного к другому без изменений в клиентском коде. Вы даже можете иметь одну версию класса, использующую свойства (например, для контрактного программирования или отладки), и другую версию для продакшена, не изменяя код, который его использует. В то же время, вам не нужно писать геттеры и сеттеры для всего на свете, на случай, если вам может понадобиться лучше контролировать доступ в будущем.
В Python нет необходимости использовать геттеры, сеттеры или свойства просто ради этого. Сначала вы просто используете атрибуты, а затем, только если это действительно необходимо, можете перейти на свойства, не изменяя код, который использует ваши классы.
Действительно, существует много кода с расширением .py, где используются геттеры и сеттеры, наследование и бессмысленные классы, когда, например, можно было бы обойтись простым кортежем. Однако такой код обычно пишут люди, пришедшие из C++ или Java и использующие Python.
Это не является типично "питоновским" кодом.
Я думаю, что оба подхода имеют свое место. Одна из проблем использования @property
заключается в том, что сложно расширять поведение геттеров или сеттеров в подклассах, используя стандартные механизмы классов. Дело в том, что реальные функции геттера и сеттера скрыты в свойстве.
Тем не менее, вы можете получить доступ к этим функциям, например, так:
class C(object):
_p = 1
@property
def p(self):
return self._p
@p.setter
def p(self, val):
self._p = val
Вы можете получить доступ к функциям геттера и сеттера как C.p.fget
и C.p.fset
, но вам будет сложно использовать обычные механизмы наследования методов (например, super
) для их расширения. После некоторого исследования тонкостей работы super
вы можете на самом деле использовать его таким образом:
# Использование super():
class D(C):
# Нельзя использовать super(D, D) здесь для определения свойства
# поскольку D еще не определен в этом контексте.
@property
def p(self):
return super(D, D).p.fget(self)
@p.setter
def p(self, val):
print('Реализуйте дополнительную функциональность здесь для D')
super(D, D).p.fset(self, val)
# Используя прямую ссылку на C
class E(C):
p = C.p
@p.setter
def p(self, val):
print('Реализуйте дополнительную функциональность здесь для E')
C.p.fset(self, val)
Использование super()
довольно громоздко, поскольку свойство нужно переопределить, и вам нужно использовать несколько неинтуитивный механизм super(cls, cls)
, чтобы получить не привязанную копию p
.
TL;DR? Вы можете перейти к концу для примера кода.
Я на самом деле предпочитаю использовать другой подход, который может показаться сложным для одноразового использования, но отлично подходит для более сложных случаев.
Сначала немного предыстории.
Свойства полезны тем, что позволяют нам управлять как установкой, так и получением значений программистским способом, при этом позволяя обращаться к атрибутам как к обычным атрибутам. Мы можем преобразовать "получения" в "вычисления" и "установки" в "события". Предположим, у нас есть следующий класс, который я закодировал с использованием геттеров и сеттеров, похожих на Java.
class Example(object):
def __init__(self, x=None, y=None):
self.x = x
self.y = y
def getX(self):
return self.x or self.defaultX()
def getY(self):
return self.y or self.defaultY()
def setX(self, x):
self.x = x
def setY(self, y):
self.y = y
def defaultX(self):
return someDefaultComputationForX()
def defaultY(self):
return someDefaultComputationForY()
Возможно, вы задаетесь вопросом, почему я не вызываю defaultX
и defaultY
в методе __init__
объекта. Причина заключается в том, что для нашего случая я хочу предположить, что методы someDefaultComputation
возвращают значения, которые со временем изменяются, скажем, временную метку, и всякий раз, когда x
(или y
) не установлен (где, для целей этого примера, "не установлен" означает "установлен в None"), я хочу получить значение обычного вычисления для x
(или y
).
Это решение имеет свои недостатки, о которых я говорил выше. Я перепишу его, используя свойства:
class Example(object):
def __init__(self, x=None, y=None):
self._x = x
self._y = y
@property
def x(self):
return self._x or self.defaultX()
@x.setter
def x(self, value):
self._x = value
@property
def y(self):
return self._y or self.defaultY()
@y.setter
def y(self, value):
self._y = value
# default{XY} как и раньше.
Что мы выиграли? Мы получили возможность обращаться к этим атрибутам как к атрибутам, даже несмотря на то, что за кулисами мы запускаем методы.
Конечно, настоящая сила свойств заключается в том, что обычно мы хотим, чтобы эти методы делали что-то дополнительное, кроме простого получения и установки значений (в противном случае нет смысла использовать свойства). Я это делал в своём примере геттера. По сути, мы запускаем тело функции, чтобы получить значение по умолчанию, когда значение не установлено. Это очень распространённая практика.
Но что мы теряем и что мы не можем сделать?
Главное неудобство, на мой взгляд, заключается в том, что если вы определяете геттер (как мы делаем здесь), вы также должны определить сеттер. Это лишний шум, который захламляет код.
Ещё одно неудобство — это то, что мы по-прежнему должны инициализировать значения x
и y
в __init__
. (Ну, конечно, мы могли бы добавить их, используя setattr()
, но это будет больше лишнего кода.)
Третье, в отличие от примера с Java-подобной реализацией, геттеры не могут принимать другие параметры. Теперь я вас уже слышу: если он принимает параметры, это не геттер! В официальном смысле это правда. Но в практическом смысле нет причин, по которым мы не можем параметризовать именованный атрибут — как x
— и установить его значение для определенных параметров.
Было бы неплохо, если бы мы могли сделать что-то вроде:
e.x[a,b,c] = 10
e.x[d,e,f] = 20
Например. Ближайшее, что мы можем сделать — это переопределить присваивание, чтобы подразумевать некоторую специальную семантику:
e.x = [a,b,c,10]
e.x = [d,e,f,30]
И, конечно, убедиться, чтобы наш сеттер знал, как извлечь первые три значения в качестве ключа для словаря и установить его значение как число или что-то подобное.
Но даже если мы это сделаем, мы всё равно не сможем поддерживать это с помощью свойств, потому что нет возможности передать параметры геттеру. Поэтому нам пришлось бы возвращать всё, вводя асимметрию.
Java-стиль геттеров/сеттеров позволяет это обрабатывать, но мы снова возвращаемся к необходимости иметь геттеры/сеттеры.
На мой взгляд, то, что нам действительно нужно, это что-то, что охватывает следующие требования:
- Пользователи определяют лишь один метод для данного атрибута и могут указать, является ли атрибут доступным только для чтения или доступным для записи. Свойства не проходят этот тест, если атрибут записываемый.
- Нет необходимости для пользователя определять дополнительную переменную, лежащую в основе функции, поэтому нам не нужны
__init__
илиsetattr
в коде. Переменная просто существует, поскольку мы создали этот атрибут нового стиля. - Любой код по умолчанию для атрибута выполняется в теле метода.
- Мы можем установить атрибут как атрибут и ссылаться на него как на атрибут.
- Мы можем параметризовать атрибут.
Что касается кода, мы хотим возможность записать:
def x(self, *args):
return defaultX()
И затем мы можем сделать:
print(e.x) # -> Значение по умолчанию в момент T0
e.x = 1
print(e.x) # -> 1
e.x = None
print(e.x) # -> Значение по умолчанию в момент T1
И так далее.
Мы также хотим сделать это для специального случая параметризуемого атрибута, но при этом позволить работать и обычному присваиванию по умолчанию. Вы увидите, как я с этим справился далее.
Теперь к сути (ура! к сути!). Решение, которое я предложил для этой задачи, выглядит следующим образом.
Мы создаем новый объект, заменяющий понятие свойства. Этот объект предназначен для хранения значения переменной, установленного в нем, но также удерживает ссылку на код, который знает, как вычислить значение по умолчанию. Его задача — хранить установленное значение или вызывать метод, если значение не установлено.
Назовём его UberProperty
.
class UberProperty(object):
def __init__(self, method):
self.method = method
self.value = None
self.isSet = False
def setValue(self, value):
self.value = value
self.isSet = True
def clearValue(self):
self.value = None
self.isSet = False
Я подразумеваю, что method
здесь это метод класса, value
— значение UberProperty
, и я добавил isSet
, потому что None
может быть реальным значением, и это позволяет нам четко указать, что "значения нет". Другой способ — использовать какую-то отправную метку.
Это в основном дает нам объект, который может делать то, что нам нужно, но как мы на самом деле можем его использовать в нашем классе? Ну, свойства используют декораторы; почему бы и нам не использовать? Давайте посмотрим, как это может выглядеть (отсюда я буду придерживаться только одного 'атрибута', x
).
class Example(object):
@uberProperty
def x(self):
return defaultX()
Это пока не работает, конечно. Нам нужно реализовать uberProperty
и убедиться, что он обрабатывает как получение, так и установку значения.
Начнем с получения.
Моя первая попытка заключалась в том, чтобы просто создать новый объект UberProperty
и вернуть его:
def uberProperty(f):
return UberProperty(f)
Я быстро понял, что это не работает: Python никогда не связывает вызываемый объект с объектом, и мне нужен объект, чтобы вызвать функцию. Даже создание декоратора в классе не работает, так как, хотя теперь у нас есть класс, у нас все равно нет объекта, с которым можно работать.
Таким образом, нам нужно будет сделать больше. Мы знаем, что метод необходимо представлять только один раз, поэтому давайте сохраним наш декоратор, но изменим UberProperty
, чтобы он только хранил ссылку на method
:
class UberProperty(object):
def __init__(self, method):
self.method = method
Он также не является вызываемым, так что в данный момент ничего не работает.
Как нам завершить картину? Что мы получаем, когда создаем класс-пример, используя наш новый декоратор:
class Example(object):
@uberProperty
def x(self):
return defaultX()
print(Example.x) # <__main__.UberProperty object at 0x10e1fb8d0>
print(Example().x) # <__main__.UberProperty object at 0x10e1fb8d0>
В обоих случаях мы получаем UberProperty
, который, конечно же, не является вызываемым, так что это не очень полезно.
Что нам нужно, так это способ динамически привязывать экземпляр UberProperty
, созданный декоратором, к объекту класса до того, как этот объект будет возвращен пользователю для использования. Эм, да, это вызов __init__
, парень.
Давайте сначала запишем, каким мы хотим видеть наш окончательный результат. Мы связываем UberProperty
с экземпляром, так что очевидно, что необходимо вернуть BoundUberProperty
. Это то, где мы на самом деле будем хранить состояние для атрибута x
.
class BoundUberProperty(object):
def __init__(self, obj, uberProperty):
self.obj = obj
self.uberProperty = uberProperty
self.isSet = False
def setValue(self, value):
self.value = value
self.isSet = True
def getValue(self):
return self.value if self.isSet else self.uberProperty.method(self.obj)
def clearValue(self):
del self.value
self.isSet = False
Теперь у нас есть представление; как нам это привязать к объекту? Есть несколько подходов, но самый простой, который можно объяснить, — это использовать метод __init__
для этого отображения. К моменту вызова __init__
наши декораторы уже выполнены, поэтому нам просто нужно пройтись по __dict__
объекта и обновить любые атрибуты, где значение атрибута является экземпляром `Uber
Использование свойств для меня кажется более интуитивным и лучше вписывается в большинство кодов.
Если сравнить
o.x = 5;
ox = o.x;
с
o.setX(5);
ox = o.getX();
то очевидно, что первый вариант проще для чтения. Кроме того, свойства позволяют намного удобнее работать с приватными переменными.
Как клонировать список, чтобы он не изменялся неожиданно после присваивания?
Преобразование списка словарей в DataFrame pandas
Как отсортировать список/кортеж списков/кортежей по элементу на заданном индексе
Как отменить последнюю миграцию?
Как явно освободить память в Python?