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

вот чистый вопрос дизайна, специфичный для 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 ответов


предпочитаете свойства. Для этого они и существуют.

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

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


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

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

Это не код Python.


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


короткий ответ: свойства побеждает. Всегда.

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

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.


[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) не установлен (где, для целей этого примера "not set" означает "set to None") я хочу значение xs (или y's) вычисление по умолчанию.

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

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.

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

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

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

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

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

  • пользователю не нужно определять дополнительную переменную, лежащую в основе функции, поэтому нам не нужно __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 и убедитесь, что он обрабатывает как Get, так и sets.

давайте начнем с gets.

моей первой попыткой было просто создать новый Uberproperty объект и вернуть его:

def uberProperty(f):
    return UberProperty(f)

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

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

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

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__ всегда вызывается подклассами.

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

      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] я, возможно, отстаю от того, является ли это все тот же случай.


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

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

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, вы can действительно используйте 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.


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

сравнение

o.x = 5
ox = o.x

и

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

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


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

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.')

или аналогичная учетная запись.метод Save.

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


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

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

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

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

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

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

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

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

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


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

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

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

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

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

    взято с PEP-8:

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

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

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

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

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

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

    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