Использование @property против геттеров и сеттеров

729

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

Заур Насибов
источник

Ответы:

614

Предпочитают свойства . Это то, что они там.

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

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

Kindall
источник
90
Имена атрибутов с двойным подчеркиванием обрабатываются специально Python; это не просто соглашение. См. Docs.python.org/py3k/tutorial/classes.html#private-variables
6502
63
Они обрабатываются по-разному, но это не мешает вам получить к ним доступ. PS: 30 н.э.
добро пожаловать
4
и потому что символы "@" в коде Python безобразны, а разыменование @decorators дает то же чувство, что и спагетти-код.
Ягода Цакала
18
Я не согласна Насколько структурированный код равен коду спагетти? Python - прекрасный язык. Но было бы еще лучше с лучшей поддержкой простых вещей, таких как правильная инкапсуляция и структурированные классы.
69
Хотя в большинстве случаев я согласен, будьте осторожны, скрывая медленные методы за декоратором @property. Пользователь вашего API ожидает, что доступ к свойству работает как переменный доступ, и отклонение слишком далеко от этого ожидания может сделать ваш API неприятным для использования.
defrex
154

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

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

Это не код Python.

6502
источник
46
@ 6502, когда вы сказали «[…] бессмысленные классы везде, где, например, подойдет простой кортеж»: преимущество класса перед кортежем заключается в том, что экземпляр класса предоставляет явные имена для доступа к его частям, а кортеж - нет. , Имена лучше читаются и позволяют избежать ошибок, чем подписка на кортежи, особенно когда это должно быть передано за пределы текущего модуля.
Hibou57
15
@ Hibou57: я не говорю, что класс бесполезен. Но иногда кортежа более чем достаточно. Проблема, однако, в том, что, скажем, у Java или C ++ нет выбора, кроме как создавать классы для всего, потому что другие возможности просто раздражают в этих языках. Другим типичным признаком программирования на Java / C ++ с использованием Python является создание абстрактных классов и сложных иерархий классов без какой-либо причины, когда в Python вы можете просто использовать независимые классы благодаря утилитарной типизации.
6502
39
@ Hibou57 для этого вы также можете использовать namedtuple: doughellmann.com/PyMOTW/collections/namedtuple.html
hugo24
5
@JonathonReinhart: это IS в стандартной библиотеке , так как 2,6 ... см docs.python.org/2/library/collections.html
6502
1
Когда «в конечном итоге происходит переход к свойству, если это необходимо», вполне вероятно, что вы нарушаете код, используя ваши классы. Свойства часто вводят ограничения - любой код, который не ожидал этих ограничений, сломается, как только вы их введете.
Yaccob
118

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

Игнасио Васкес-Абрамс
источник
3
@GregKrsak Это кажется странным, потому что это так. «Согласная вещь для взрослых» была мемом-питоном еще до того, как были добавлены свойства. Это был стандартный ответ людям, которые жаловались на отсутствие модификаторов доступа. Когда свойства были добавлены, внезапная инкапсуляция становится желательной. То же самое произошло с абстрактными базовыми классами. «Python всегда был в состоянии войны с нарушением инкапсуляции. Свобода - это рабство. Лямбды должны располагаться только на одной линии».
Джонсип
71

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

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

макинтош
источник
1
@BasicWolf - я думал, что это было неявно ясно, что я на стороне собственности забора! :) Но я добавляю параграф к своему ответу, чтобы прояснить это.
Мак
9
СОВЕТ: Слово «всегда» - это намек на то, что автор пытается убедить вас с помощью утверждения, а не аргумента. Так же как наличие жирного шрифта. (Я имею в виду, если вместо этого вы видите CAPS, тогда - нууу, - это должно быть правильно.) Посмотрите, функция «свойства», оказывается, отличается от Java (почему-то Немезида Python почему-то), и, следовательно, групповое мышление сообщества Python объявляет это лучше. В действительности свойства нарушают правило «Явное лучше, чем неявное», но никто не хочет этого признавать. Он сделал это на языке, поэтому теперь он объявляется "Pythonic" через тавтологический аргумент.
Стюарт Берг
3
Нет чувств обиженных. :-P Я просто пытаюсь указать, что «Pythonic» соглашения несовместимы в этом случае: «Explicit лучше, чем неявное» находится в прямом конфликте с использованием a property. (Это выглядит как простое присвоение, но вызывает функцию.) Следовательно, «Pythonic» по сути является бессмысленным термином, за исключением тавтологического определения: «Pythonic условные обозначения - это вещи, которые мы определили как Pythonic».
Стюарт Берг
1
Теперь идея иметь ряд соглашений, которые следуют за темой, великолепна . Если бы такой набор соглашений существовал, то вы могли бы использовать его как набор аксиом, чтобы направлять ваше мышление, а не просто длинный контрольный список трюков для запоминания, который значительно менее полезен. Аксиомы могут быть использованы для экстраполяции и помогут вам подойти к проблемам, которые еще никто не видел. Обидно, что эта propertyособенность угрожает сделать идею аксиом Питона почти бесполезной. Итак, все, что нам осталось, это контрольный список.
Стюарт Берг
1
Я не согласна Я предпочитаю свойства в большинстве ситуаций, но когда вы хотите подчеркнуть, что установка чего-либо имеет побочные эффекты, кроме изменения selfобъекта , явные установщики могут быть полезны. Например, user.email = "..."не похоже, что это может вызвать исключение, потому что это похоже на установку атрибута, тогда user.set_email("...")как ясно, что могут быть побочные эффекты, такие как исключения.
bluenote10
65

[ 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} 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()

и сможет тогда сделать:

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и убедиться, что он обрабатывает как получает, так и устанавливает.

Давайте начнем с получения.

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

Адам Донахью
источник
53
Да, это «Tldr». Не могли бы вы подвести итог, что вы пытаетесь сделать здесь?
пул
9
@ Адам, return self.x or self.defaultX()это опасный код. Что происходит когда self.x == 0?
Келли Томас,
К вашему сведению, вы можете сделать так, чтобы вы могли параметризовать геттер, вроде. Это будет включать в себя превращение переменной в пользовательский класс, __getitem__метод которого вы переопределили . Это было бы странно, потому что тогда у вас был бы совершенно нестандартный питон.
будет
2
@KellyThomas Просто пытаюсь сделать пример простым. Чтобы сделать это правильно, вам нужно создать и удалить запись x dict в целом, потому что даже значение None могло быть специально установлено. Но да, вы абсолютно правы, это то, что вам нужно учитывать в случае производственного использования.
Адам Донахью
Java-подобные методы получения позволяют вам делать точно такие же вычисления, не так ли?
Qed
26

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

# 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.

NeilenMarais
источник
20

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

Сравнение

o.x = 5
ox = o.x

против

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

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

Hobblin
источник
12

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

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


источник
3
@ user2239734 Я думаю, вы неправильно понимаете понятие свойств. Хотя вы можете проверить значение при установке свойства, в этом нет необходимости. Вы можете иметь как свойства, так и validate()метод в классе. Свойство просто используется, когда у вас есть сложная логика за простым obj.x = yприсваиванием, и это зависит от того, что такое логика.
Заур Насибов
12

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

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

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

  • Не используйте геттеры и сеттеры сначала, если они не нужны
  • Используйте @propertyдля их реализации без изменения синтаксиса остальной части вашего кода.
fulmicoton
источник
1
«и обратите внимание, что в 70% случаев они никогда не заменяются какой-то нетривиальной логикой». - это довольно конкретное число, оно исходит из какого-то места, или вы намереваетесь использовать его как волнистое «подавляющее большинство» (я не шучу, если есть исследование, которое количественно оценивает это число, я бы искренне заинтересован в чтении)
Адам Паркин
1
О, нет, извини. Похоже, у меня есть кое-какое исследование, чтобы сделать резервную копию этого числа, но я имел в виду только «большую часть времени».
fulmicoton
7
Дело не в том, что люди заботятся о накладных расходах, а в том, что в Python вы можете перейти от прямого доступа к методам доступа без изменения клиентского кода, так что вам нечего терять, сначала напрямую выставляя свойства.
Нил Г
10

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

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

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

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

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

фиакр
источник
6

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

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()метод профилирования , хотя my_object.my_attr = 4.на самом деле это является причиной замедления:

def slow_function(my_object):
    my_object.my_attr = 4.
    my_object.do_something()
otognan
источник
5

И у @propertyтрадиционных, и у сеттеров есть свои преимущества. Это зависит от вашего варианта использования.

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

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

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

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

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

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

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

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

  • Традиционные методы получения и установки обеспечивают более сложный доступ к данным, чем простой доступ к атрибутам. Например, когда вы устанавливаете члена класса, иногда вам нужен флаг, указывающий, где вы хотели бы принудительно выполнить эту операцию, даже если что-то выглядит не идеально. Хотя не совсем очевидно, как расширить прямой доступ к членам, например 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
Cyker
источник
2

Вот выдержки из «Эффективного Python: 90 конкретных способов написать лучший Python» (Удивительная книга. Я очень рекомендую это).

То, что нужно запомнить

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

@ Используйте @property для определения особого поведения при доступе к атрибутам ваших объектов, если это необходимо.

✦ Следуйте правилу наименьшего удивления и избегайте странных побочных эффектов в ваших методах @property.

✦ Убедитесь, что методы @property быстрые; для медленной или сложной работы, особенно с вводом-выводом или побочными эффектами, вместо этого используйте обычные методы.

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

Мне особенно нравится @property, потому что он позволяет постепенно продвигаться к лучшей модели данных.
@property - это инструмент, помогающий решать проблемы, с которыми вы можете столкнуться в реальном коде. Не злоупотребляйте этим. Когда вы обнаруживаете, что постоянно расширяете методы @property, возможно, пришло время реорганизовать ваш класс, вместо того, чтобы продолжать разбирать плохой дизайн кода.

@ Используйте @property, чтобы дать существующим атрибутам экземпляра новую функциональность.

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

✦ Рассмотрите возможность рефакторинга класса и всех сайтов вызовов, когда вы слишком интенсивно используете @property.

Влад Безден
источник