Использование @property против геттеров и сеттеров - PullRequest
696 голосов
/ 08 июля 2011

Вот чистый вопрос разработки 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, какой подход вы бы использовали и почему?

Ответы [ 12 ]

583 голосов
/ 08 июля 2011

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

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

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

146 голосов
/ 08 июля 2011

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

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

Это не код Python.

114 голосов
/ 08 июля 2011

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

69 голосов
/ 08 июля 2011

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

Иногда нужны геттеры и сеттеры, но даже тогда я бы «спрятал» их во внешний мир. Есть много способов сделать это в 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.

60 голосов
/ 31 мая 2013

[ 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) не устанавливается (где, для этой цели Например, «не установлено» означает «установлено на Нет») Я хочу значение вычисления 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]

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

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

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

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

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

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

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

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

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

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

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

и затем иметь возможностьdo:

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

и т. д.

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

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

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

Давайте назовем его 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 и убедиться, что он обрабатывает как get, так и set.

Давайте начнем с get.

Моей первой попыткой было просто создать новый объект 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__ и обновить любые атрибуты, где значение атрибута имеет тип 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 directly, как в 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] Я могуотстать от того, так ли это до сих пор.

24 голосов
/ 23 декабря 2011

Я думаю, что у обоих есть свое место.Одна проблема с использованием @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 таким образом:

# 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 (), однако, довольно неуклюже, поскольку свойство должно бытьпереопределен, и вы должны использовать слегка нелогичный супер (cls, cls) механизм, чтобы получить несвязанную копию p.

20 голосов
/ 08 июля 2011

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

Сравнение

o.x = 5
ox = o.x

против

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

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

12 голосов
/ 03 апреля 2013

Я бы предпочел использовать ни в большинстве случаев. Проблема со свойствами заключается в том, что они делают класс менее прозрачным. Особенно это проблема, если вы должны поднять исключение из сеттера. Например, если у вас есть свойство 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.

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

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

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

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

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

или похожий метод Account.save.

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

9 голосов
/ 01 июня 2015

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

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

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

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

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

9 голосов
/ 08 июля 2011

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

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

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

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