Python @property против геттеров и сеттеров

Вот простой вопрос, связанный с Python:

class MyClass(object):
    ...
    def get_my_attr(self):
        ...

    def set_my_attr(self, value):
        ...

и

class MyClass(object):
    ...        
    @property
    def my_attr(self):
        ...

    @my_attr.setter
    def my_attr(self, value):
        ...

Python позволяет нам делать это в любом случае. Если бы вы разработали программу Python, какой подход вы бы использовали и почему?

621
задан Zaur Nasibov 08 июля '11 в 1:42
источник поделиться

11 ответов

Предпочитает свойства. Это то, для чего они предназначены.

Причина в том, что все атрибуты являются общедоступными в Python. Исходные имена с подчеркиванием или двумя являются лишь предупреждением о том, что данный атрибут является деталью реализации, которая не может оставаться прежней в будущих версиях кода. Это не мешает вам получать или устанавливать этот атрибут. Следовательно, стандартный доступ к атрибутам является обычным, путинским способом доступа к атрибутам.

Преимущество свойств заключается в том, что они синтаксически идентичны доступу к атрибутам, поэтому вы можете менять один на другой без каких-либо изменений в клиентский код. У вас может даже быть одна версия класса, который использует свойства (например, для поэтапного или отладки) и тот, который не предназначен для производства, без изменения кода, который его использует. В то же время вам не нужно писать геттеры и сеттеры для всего на всякий случай, если вам может понадобиться более эффективный контроль доступа позже.

534
ответ дан kindall 08 июля '11 в 2:06
источник поделиться

В Python вы не используете геттеры или сеттеры или свойства только для удовольствия. Сначала вы просто используете атрибуты, а затем позже, только при необходимости, в конечном итоге переходите к свойству без изменения кода с помощью своих классов.

Существует действительно много кода с расширением .py, который использует геттеры и сеттеры, наследования и бессмысленные классы везде, где, например, простой кортеж, но это код от людей, пишущих на С++ или Java, используя Python.

Это не код Python.

131
ответ дан 6502 08 июля '11 в 2:08
источник поделиться

Использование свойств позволяет вам начать с обычных доступов к атрибутам, а затем заново создать их с помощью геттеров и сеттеров.

112
ответ дан Ignacio Vazquez-Abrams 08 июля '11 в 1:57
источник поделиться

Короткий ответ: свойства выигрывают руки вниз. Всегда.

Иногда возникает потребность в геттерах и сеттерах, но даже тогда я "спрячу" их во внешнем мире. В Python есть много способов сделать это (getattr, setattr, __getattribute__ и т.д.), Но очень кратким и чистым является:

def set_email(self, value):
    if '@' not in value:
        raise Exception("This doesn't look like an email address.")
    self._email = value

def get_email(self):
    return self._email

email = property(get_email, set_email)

Вот краткая статья, в которой вводится тема геттеров и сеттеров в Python.

65
ответ дан mac 08 июля '11 в 1:52
источник поделиться

[ 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. Причина в том, что для нашего случая я хочу предположить, что методы someDefaultComput возвращают значения, которые меняются со временем, говорят временную метку, и всякий раз, когда 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} as before.

Что мы получили? Мы получили возможность ссылаться на эти атрибуты как на атрибуты, хотя за кулисами мы заканчиваем запущенные методы.

Конечно, реальная сила свойств заключается в том, что мы обычно хотим, чтобы эти методы делали что-то помимо просто получения и установки значений (в противном случае нет смысла использовать свойства). Я сделал это в своем примере с геттером. Мы в основном запускаем тело функции, чтобы получить значение по умолчанию, когда значение не задано. Это очень распространенная картина.

Но что мы теряем, и что мы не можем сделать?

Основное раздражение, на мой взгляд, заключается в том, что если вы определяете геттер (как мы здесь делаем), вам также нужно определить сеттер. [1] Этот дополнительный шум, который загромождает код.

Другая досада заключается в том, что нам еще нужно инициализировать значения 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]

и, конечно же, убедитесь, что наш сеттер знает, как извлечь первые три значения в качестве ключа для словаря и установить его значение в число или что-то еще.

Но даже если бы мы это сделали, мы все равно не смогли бы поддерживать его со свойствами, потому что нет возможности получить значение, потому что мы не можем передавать параметры на getter. Поэтому нам пришлось возвращать все, вводя асимметрию.

Атрибут getter/setter в стиле Java позволяет нам справиться с этим, но мы снова нуждаемся в использовании getter/seters.

В моем сознании, что мы действительно хотим, это то, что улавливает следующие требования:

  • Пользователи определяют только один метод для данного атрибута и могут указывать там является ли атрибут "только для чтения" или "чтение-запись". Свойства не проверяются если атрибут доступен для записи.

  • Пользователю не нужно определять дополнительную переменную, лежащую в основе функции, поэтому нам не нужен init или setattr в коде. Переменная существует только потому, что мы создали этот атрибут нового стиля.

  • Любой код по умолчанию для атрибута выполняется в самом теле метода.

  • Мы можем установить атрибут как атрибут и ссылаться на него как на атрибут.

  • Мы можем параметризовать атрибут.

В терминах кода нам нужен способ записи:

def x(self, *args):
    return defaultX()

и тогда вы сможете:

print e.x     -> The default at time T0
e.x = 1
print e.x     -> 1
e.x = None
print e.x     -> The default at time T1

и т.д.

Мы также хотим сделать это для специального случая параметризуемого атрибута, но все же разрешить работу по умолчанию. Вы увидите, как я это рассмотрел ниже.

Теперь до точки (yay! точка!). Решение, для которого я пришел, заключается в следующем.

Мы создаем новый объект для замены понятия свойства. Объект предназначен для хранения значения переменной, установленной для него, но также поддерживает дескриптор кода, который знает, как вычислять значение по умолчанию. Его задача - сохранить заданное значение или запустить метод, если это значение не установлено.

Назовите это 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

Я предполагаю, что метод здесь - это метод класса, значение - значение UberProperty, и я добавил isSet, потому что None может быть реальным значением, и это позволяет нам чистый способ объявить, что на самом деле "нет значения". Другой способ - какой-то часового.

Это в основном дает нам объект, который может делать то, что мы хотим, но как мы действительно наносим его на наш класс? Ну, свойства используют декораторы; почему мы не можем? Посмотрим, как это может выглядеть (здесь я буду придерживаться только одного "атрибута", x).

class Example(object):

    @uberProperty
    def x(self):
        return defaultX()

Это, конечно, пока не работает. Мы должны внедрить uberProperty и убедитесь, что он обрабатывает и получает, и устанавливает.

Пусть начнется с get.

Моя первая попытка состояла в том, чтобы просто создать новый объект UberProperty и вернуть его:

def uberProperty (f):   return UberProperty (f)

Я быстро обнаружил, что это не работает: Python никогда не связывает вызываемый объект, и мне нужен объект, чтобы вызвать функцию. Даже создание декоратора в классе не работает, поскольку, хотя теперь у нас есть класс, мы по-прежнему не имеем объекта для работы.

Итак, нам нужно будет иметь возможность делать больше здесь. Мы знаем, что метод должен быть представлен только один раз, поэтому отпустите и сохраните наш декоратор, но измените UberProperty только для хранения ссылки на метод:

class UberProperty(object):

    def __init__(self, method):
        self.method = method 

Он также не является вызываемым, поэтому на данный момент ничего не работает.

Как мы завершаем картину? Итак, что получилось, когда мы создаем класс example с помощью нашего нового декоратора:

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 и обновить любые атрибуты, где значение атрибута имеет тип UberProperty.

Теперь uber-свойства классные, и мы, вероятно, захотим использовать их много, поэтому имеет смысл просто создать базовый класс, который делает это для всех подклассов. Я думаю, вы знаете, что будет называться базовый класс.

class UberObject(object):
    def __init__(self):
        for k in dir(self):
            v = getattr(self, k)
            if isinstance(v, UberProperty):
                v = BoundUberProperty(self, v)
                setattr(self, k, v)

Мы добавим это, изменим наш пример на наследование из UberObject и...

e = Example()
print e.x               -> <__main__.BoundUberProperty object at 0x104604c90>

После изменения x:

@uberProperty
def x(self):
    return *datetime.datetime.now()*

Мы можем выполнить простой тест:

print e.x.getValue()
print e.x.getValue()
e.x.setValue(datetime.date(2013, 5, 31))
print e.x.getValue()
e.x.clearValue()
print e.x.getValue()

И мы получим требуемый результат:

2013-05-31 00:05:13.985813
2013-05-31 00:05:13.986290
2013-05-31
2013-05-31 00:05:13.986310

(Ге, я работаю допоздна.)

Обратите внимание, что здесь я использовал getValue, setValue и clearValue. Это связано с тем, что я еще не связал средства, чтобы они автоматически возвращались.

Но я думаю, что сейчас это прекрасное место, потому что я устаю. Вы также можете видеть, что основная функциональность, которую мы хотели, на месте; остальное - украшение в стиле. Важное удобство использования, но это может подождать, пока у меня не будет изменений, чтобы обновить сообщение.

Я закончу пример в следующей публикации, обратившись к этим вещам:

  • Нам нужно убедиться, что UberObject init всегда вызывается подклассами.

    • Таким образом, мы либо заставляем его называться где-то, либо препятствуем его реализации.
    • Мы посмотрим, как это сделать с помощью метакласса.
  • Нам нужно убедиться, что мы обрабатываем общий случай, когда кто-то "псевдонимы", функция для чего-то еще, например:

      class Example(object):
          @uberProperty
          def x(self):
              ...
    
          y = x
    
  • Нам нужно e.x для возврата e.x.getValue() по умолчанию.

    • То, что мы на самом деле увидим, это та область, где модель терпит неудачу.
    • Оказывается, нам всегда нужно использовать вызов функции для получения значения.
    • Но мы можем сделать его похожим на обычный вызов функции и не использовать e.x.getValue(). (Выполнение этого очевидно, если вы еще не установили его.)
  • Нам нужно поддерживать установку e.x напрямую, как в e.x = <newvalue>. Мы можем сделать это и в родительском классе, но нам нужно обновить наш init код, чтобы обработать его.

  • Наконец, мы добавим параметризованные атрибуты. Должно быть довольно очевидно, как мы это сделаем.

Здесь код, который существует до сих пор:

import datetime

class UberObject(object):
    def uberSetter(self, value):
        print 'setting'

    def uberGetter(self):
        return self

    def __init__(self):
        for k in dir(self):
            v = getattr(self, k)
            if isinstance(v, UberProperty):
                v = BoundUberProperty(self, v)
                setattr(self, k, v)


class UberProperty(object):
    def __init__(self, method):
        self.method = method

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

    def uberProperty(f):
        return UberProperty(f)

class Example(UberObject):

    @uberProperty
    def x(self):
        return datetime.datetime.now()

Адам

[1] Я могу быть в стороне от того, все ли так.

50
ответ дан Adam Donahue 31 мая '13 в 7:27
источник поделиться

Я думаю, что оба имеют свое место. Одна из проблем с использованием @property заключается в том, что трудно расширить поведение геттеров или сеттеров в подклассах с использованием стандартных механизмов классов. Проблема в том, что фактические функции getter/setter скрыты в свойстве.

Вы можете фактически получить функции, например. с

class C(object):
    _p = 1
    @property
    def p(self):
        return self._p
    @p.setter
    def p(self, val):
        self._p = val

вы можете получить доступ к функциям getter и setter как C.p.fget и C.p.fset, но вы не можете легко использовать обычные методы наследования (например, супер) для их расширения. После некоторого вникания в тонкости супер, вы действительно можете использовать супер таким образом:

# Using super():
class D(C):
    # Cannot use super(D,D) here to define the property
    # since D is not yet defined in this scope.
    @property
    def p(self):
        return super(D,D).p.fget(self)

    @p.setter
    def p(self, val):
        print 'Implement extra functionality here for D'
        super(D,D).p.fset(self, val)

# Using a direct reference to C
class E(C):
    p = C.p

    @p.setter
    def p(self, val):
        print 'Implement extra functionality here for E'
        C.p.fset(self, val)

Использование super(), однако, довольно неуклюжие, так как свойство должно быть переопределено, и вам нужно использовать слегка контринтуитивный механизм super (cls, cls) для получения несвязанной копии p.

24
ответ дан NeilenMarais 23 дек. '11 в 15:09
источник поделиться

Использование свойств для меня более интуитивно и лучше подходит для большинства кодов.

Сравнение

o.x = 5
ox = o.x

против.

o.setX(5)
ox = o.getX()

для меня совершенно очевидно, что легче читать. Кроме того, свойства позволяют частным переменным намного проще.

19
ответ дан Hobblin 08 июля '11 в 1:53
источник поделиться

Я бы предпочел не использовать ни в большинстве случаев. Проблема со свойствами заключается в том, что они делают класс менее прозрачным. В частности, это проблема, если вы должны были создать исключение из сеттера. Например, если у вас есть свойство Account.email:

class Account(object):
    @property
    def email(self):
        return self._email

    @email.setter
    def email(self, value):
        if '@' not in value:
            raise ValueError('Invalid email address.')
        self._email = value

тогда пользователь класса не ожидает, что присвоение значения этому свойству может вызвать исключение:

a = Account()
a.email = 'badaddress'
--> ValueError: Invalid email address.

В результате исключение может быть необработанным и либо слишком распространено в цепочке вызовов, чтобы обрабатываться должным образом, либо приводило к очень бесполезной трассировке, предоставляемой пользователю программы (что, к сожалению, слишком распространено в мире python и java).

Я бы также избегал использования геттеров и сеттеров:

  • потому что определение их для всех свойств заранее очень трудоемко,
  • лишний раз увеличивает количество кода, что затрудняет понимание и поддержание кода.
  • если вы определяли их для свойств только по мере необходимости, интерфейс класса изменился бы, повредив всех пользователей класса

Вместо свойств и геттеров/сеттеров я предпочитаю выполнять сложную логику в четко определенных местах, например, в методе проверки:

class Account(object):
    ...
    def validate(self):
        if '@' not in self.email:
            raise ValueError('Invalid email address.')

или аналогичный метод Account.save.

Обратите внимание, что я не пытаюсь сказать, что нет случаев, когда свойства полезны, только чтобы вам было лучше, если вы можете сделать ваши классы простыми и прозрачными, чтобы вам не нужны.

11
ответ дан user2239734 03 апр. '13 в 12:28
источник поделиться

Я чувствую, что свойства - это позволить вам получить накладные расходы на создание геттеров и сеттеров только тогда, когда они вам действительно нужны.

Культура Java программирования настоятельно советует никогда не предоставлять доступ к свойствам, а вместо этого идти через геттеры и сеттеры и только те, которые на самом деле нужны. Немного подробней всегда писать эти очевидные фрагменты кода и заметить, что 70% времени они никогда не заменяются какой-то нетривиальной логикой.

В Python люди действительно заботятся о таких накладных расходах, чтобы вы могли принять следующую практику:

  • Не используйте геттеры и сеттеры сначала, если они не нужны
  • Используйте @property для их реализации без изменения синтаксиса остальной части вашего кода.
9
ответ дан fulmicoton 08 июля '11 в 2:02
источник поделиться

Я удивлен, что никто не упомянул, что свойства связаны с методами класса дескриптора, Adam Donohue и NeilenMarais получают именно эту идею в своих сообщениях - что получатели и сеттеры являются функциями и могут быть использованы для:

  • Validate
  • изменить данные
  • тип утки (тип принуждения для другого типа)

Это представляет собой удобный способ скрыть детали реализации и код, например, регулярное выражение, тип приведения, try.. кроме блоков, утверждений или вычисленных значений.

В общем случае CRUD на объекте часто может быть довольно мирским, но рассмотрим пример данных, которые будут сохраняться в реляционной базе данных. ORM может скрывать детали реализации конкретных SQL-запросов в методах, связанных с fget, fset, fdel, определенных в классе свойств, который будет управлять ужасными if.. elif.. else-лестницами, которые настолько уродливы в OO-коде, - подвергая простой и элегантный self.variable = something и удалить детали для разработчика с помощью ORM.

Если кто-то думает о свойствах только как некоторый скучный остаток языка Bondage и Discipline (т.е. Java), им не хватает точки дескрипторов.

8
ответ дан fiacre 01 июня '15 в 9:54
источник поделиться

В сложных проектах я предпочитаю использовать свойства только для чтения (или геттеры) с явной функцией setter:

class MyClass(object):
...        
@property
def my_attr(self):
    ...

def set_my_attr(self, value):
    ...

В длинных проектах жизни отладка и рефакторинг занимает больше времени, чем сама запись кода. Есть несколько недостатков для использования @property.setter, что делает отладку еще сложнее:

1) python позволяет создавать новые атрибуты для существующего объекта. Это делает следующую опечатку трудной для отслеживания:

my_object.my_atttr = 4.

Если ваш объект является сложным алгоритмом, вы потратите довольно много времени, пытаясь выяснить, почему он не сходится (обратите внимание на дополнительную "t" в строке выше)

2) сеттер иногда может перейти к сложному и медленному методу (например, ударить базу данных). Для другого разработчика было бы сложно понять, почему следующая функция работает очень медленно. Он мог бы потратить много времени на профилирование метода do_something():

def slow_function(my_object):
    my_object.my_attr = 4.
    my_object.do_something()
4
ответ дан otognan 25 мая '17 в 4:52
источник поделиться

Другие вопросы по меткам