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

Ответ 1

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

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

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

Ответ 2

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

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

Это не код Python.

Ответ 4

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

Иногда возникает потребность в геттерах и сеттерах, но даже тогда я "спрячу" их во внешнем мире. В 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.

Ответ 5

[ 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) не задано (где для целей этого примера "не задано", означает "set to 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. Поэтому нам пришлось вернуть все, вводя асимметрию.

Java-стиль getter/setter позволяет нам справиться с этим, но мы снова нуждаемся в 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! Точка!). Решение, к которому я придумал, заключается в следующем.

Мы создаем новый объект для замены понятия свойства. Объект предназначен для хранения значения переменной, установленной для него, но также поддерживает дескриптор кода, который знает, как вычислять значение по умолчанию. Его задача - сохранить заданное 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 и убедиться, что он обрабатывает как uberProperty и 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.

Нам нужно каким-то образом динамически связать экземпляр 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
    
  • Нам нужно, чтобы ex возвращал exgetValue() по умолчанию.

    • То, что мы на самом деле увидим, - это та область, где модель терпит неудачу.
    • Оказывается, нам всегда нужно использовать вызов функции для получения значения.
    • Но мы можем сделать его похожим на обычный вызов функции и не использовать exgetValue(). (Выполнение этого очевидно, если вы еще не установили его.)
  • Нам нужно поддерживать настройку ex directly, как в ex = <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] Я могу быть в стороне от того, все ли так.

Ответ 6

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

Ответ 7

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

Сравнение

o.x = 5
ox = o.x

против.

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

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

Ответ 8

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

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

Ответ 9

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

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

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

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

Ответ 10

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

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

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

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

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

Ответ 11

В сложных проектах я предпочитаю использовать свойства только для чтения (или геттеры) с явной функцией 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()

Ответ 12

Оба @property и традиционные методы получения и установки имеют свои преимущества. Это зависит от вашего варианта использования.

Преимущества @property

  • Вам не нужно менять интерфейс при изменении реализации доступа к данным. Когда ваш проект мал, вы, вероятно, хотите использовать прямой доступ к атрибутам для доступа к члену класса. Например, пусть у вас есть объект foo типа Foo, который имеет член num. Тогда вы можете просто получить этот член с num = foo.num. По мере роста вашего проекта вы можете почувствовать, что для простого доступа к атрибутам должны быть некоторые проверки или отладки. Тогда вы можете сделать это с помощью @property внутри класса. Интерфейс доступа к данным остается таким же, поэтому нет необходимости изменять код клиента.

    Цитируется по PEP-8:

    Для простых атрибутов публичных данных лучше всего просто указать имя атрибута без сложных методов accessor/mutator. Имейте в виду, что Python обеспечивает легкий путь к будущему усовершенствованию, если вы обнаружите, что простой атрибут данных должен вырабатывать функциональное поведение. В этом случае используйте свойства, чтобы скрыть функциональную реализацию за простым синтаксисом доступа к атрибутам данных.

  • Использование @property для доступа к данным в Python рассматривается как Pythonic:

    • Он может усилить вашу самоидентификацию как программиста Python (не Java).

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

Преимущества традиционных геттеров и сеттеров

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

    def Foo:
        def set_num(self, num, force=False):
            ...
    
  • Традиционные геттеры и сеттеры делают его явным, что доступ к членам класса осуществляется с помощью метода. Это означает:

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

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

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

  • Как упоминалось @NeilenMarais, и в этом посте расширение традиционных геттеров и сеттеров в подклассах проще, чем расширение свойств.

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

Предостережения

  • Ни @property ни традиционные геттеры и сеттеры не делают члена класса закрытым, даже если вы используете двойное подчеркивание перед его именем:

    class Foo:
        def __init__(self):
            self.__num = 0
    
        @property
        def num(self):
            return self.__num
    
        @num.setter
        def num(self, num):
            self.__num = num
    
        def get_num(self):
            return self.__num
    
        def set_num(self, num):
            self.__num = num
    
    foo = Foo()
    print(foo.num)          # output: 0
    print(foo.get_num())    # output: 0
    print(foo._Foo__num)    # output: 0