Використання @property проти getters та setters


727

Ось чітке специфічне для дизайну 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, який би ви підходили і чому?

Відповіді:


613

Віддайте перевагу властивостям . Це для чого вони там.

Причина в тому, що всі атрибути є загальнодоступними в Python. Почати імена з підкресленням або двома - це лише попередження про те, що даний атрибут є деталлю реалізації, яка може не залишатися такою ж у майбутніх версіях коду. Це не заважає вам фактично отримати або встановити цей атрибут. Отже, стандартний доступ до атрибутів - це звичайний, пітонічний спосіб, ну а доступ до атрибутів.

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


90
Імена атрибутів з подвійним підкресленням спеціально обробляються Python; це не просто звичайна умова. Дивіться docs.python.org/py3k/tutorial/classes.html#private-variables
6502

63
З ними поводиться по-різному, але це не перешкоджає доступу до них. PS: AD 30 C0
kindall

4
і тому, що символи "@" некрасиві в коді python, а перенаправлення @decorators викликає те саме відчуття, як і спагеті-код.
Беррі Цакала

18
Я не згоден. Як структурований код дорівнює коду спагетті? Пітон - прекрасна мова. Але було б ще краще з кращою підтримкою таких простих речей, як правильна інкапсуляція та структуровані класи.

69
Хоча я згоден у більшості випадків, будьте обережні, ховаючи повільні методи за декоратором @property. Користувач вашого API очікує, що доступ до власності виконує як змінний доступ, і відхилення занадто далеко від цього очікування може зробити ваш API неприємним у використанні.
дефрекс

153

У Python ви не використовуєте геттери, сетери та властивості лише для задоволення. Спочатку ви просто використовуєте атрибути, а потім пізніше, лише за потреби, з часом переходите до властивості, не змінюючи код за допомогою своїх класів.

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

Це не код Python.


46
@ 6502, коли ви говорили "[…] безглузді класи скрізь, де, наприклад, робився би простий кортеж": перевага класу над кортежем полягає в тому, що екземпляр класу надає явні імена для доступу до його частин, а кортеж - не . Імена краще в читанні та уникненні помилок, ніж підписання кортежів, особливо коли це потрібно передавати за межі поточного модуля.
Hibou57

15
@ Hibou57: Я не кажу, що заняття марні. Але іноді кортеж більш ніж достатньо. Однак проблема полягає в тому, що той, хто походить від скажімо Java або C ++, не має іншого вибору, крім створення класів для всього, оскільки інші можливості просто дратують використання в цих ланагуах. Ще одним типовим симптомом програмування Java / C ++ за допомогою Python є створення абстрактних класів та складних ієрархій класів без будь-якої причини, де в Python ви могли просто використовувати незалежні класи завдяки типінгу качки.
6502 р.

39
@ Hibou57 для цього ти можеш також використовувати nametuple: doughellmann.com/PyMOTW/collections/namedtuple.html
hugo24,

5
@JonathonReinhart: це IS в стандартній бібліотеці , так як 2,6 ... см docs.python.org/2/library/collections.html
6502

1
Коли "врешті-решт мігрується до ресурсу за потреби", цілком ймовірно, що ви зламаєте код за допомогою своїх класів. Властивості часто вводять обмеження - будь-який код, який не очікував, що ці обмеження порушиться, як тільки ви введете їх.
yaccob

118

Використання властивостей дозволяє починати з звичайних атрибутів доступу, а потім створювати резервну копію за допомогою getters та setters після необхідності .


3
@GregKrsak Це здається дивним, оскільки це так. "Реч, яка погоджується з дорослими", була пам’яткою пітона раніше, ніж були додані властивості. Це відповідь на акції людей, які скаржилися на відсутність модифікаторів доступу. Коли властивості були додані, раптом капсуляція стає бажаною. Те ж саме сталося з абстрактними базовими класами. "Питон завжди воював з порушенням інкапсуляції. Свобода - це рабство. Лямбди повинні знаходитися лише на одній лінії".
johncip

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 - я подумав, що неявно зрозуміло, що я перебуваю на стороні власності паркану! :) Але я додаю пункт до своєї відповіді, щоб уточнити це.
mac

9
Підказка: Слово "завжди" - це натяк на те, що автор намагається переконати вас у твердженні, а не аргумент. Як і наявність шрифту жирного шрифту. (Я маю на увазі, якщо ви бачите замість цього CAPS, то - whoa - це повинно бути правильно.) Подивіться, функція "властивості", можливо, відрізняється від Java (чомусь фактично неметазія Python), а отже, спільнота Python's groupthryth заявляє, що це краще. Насправді властивості порушують правило "Явне краще, ніж неявне", але ніхто не хоче цього визнавати. Це зробило це мовою, тому тепер воно оголошується "пітонічним" через тавтологічний аргумент.
Стюарт Берг

3
Ніякі почуття не болять. :-P Я просто намагаюся зазначити, що в цьому випадку "піфонічні" конвенції суперечать: "Явне краще, ніж неявне" знаходиться в прямому конфлікті з використанням a property. (Це схоже на просту задачу, але вона називає функцію.) Тому «Пітонічний» є по суті безглуздим терміном, за винятком тавтологічного визначення: «Піфонічні умовності - це речі, які ми визначили як піфонічні».
Стюарт Берг

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's (або 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.

Що ми здобули? Ми отримали можливість посилатися на ці атрибути як на атрибути, навіть незважаючи на те, що за кадром ми закінчуємо застосовані методи.

Звичайно, реальна сила властивостей полягає в тому, що ми, як правило, хочемо, щоб ці методи щось робили, окрім отримання та встановлення значень (інакше немає сенсу використовувати властивості). Я зробив це на своєму прикладі геттера. В основному ми працюємо з функціональним органом, щоб вибрати типовий раз, коли значення не встановлено. Це дуже поширена закономірність.

Але що ми втрачаємо, а що не можемо зробити?

На мою думку, головне роздратування полягає в тому, що якщо ви визначаєте геттер (як у нас тут), ви також повинні визначити сеттера. [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та переконатися, що він обробляє як комплекти, так і набори.

Почнемо з 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] Я, можливо, позаду, чи це все-таки так.


53
Так, це 'tldr'. Чи можете ви, будь ласка, підсумувати те, що ви намагаєтеся зробити тут?
poolie

9
@Адам return self.x or self.defaultX()це небезпечний код. Що відбувається, коли self.x == 0?
Келлі Томас

FYI, ви можете зробити так, щоб ви могли параметризувати геттер, на зразок. Це передбачало б перетворення змінної на спеціальний клас, з якого ви перекрили __getitem__метод. Це було б дивно, як тоді у вас був би абсолютно нестандартний пітон.
буде

2
@KellyThomas Просто намагаюся простий приклад. Щоб правильно це зробити, вам знадобиться створити та видалити запис x dict взагалі, тому що навіть значення None могло бути спеціально встановлено. Але так, ви абсолютно праві, це те, що вам потрібно буде врахувати у випадку використання виробництва.
Адам Донахю

Java-подібні геттери дозволяють робити саме такі обчислення, чи не так?
qed

26

Я думаю, що обидва мають своє місце. Одне з питань використання @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, але ви не можете легко використовувати звичайні засоби успадкування (наприклад, супер) засоби для їх розширення. Після деякого копання в тонкощах супер, ви дійсно можете використовувати супер таким чином:

# 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

Використання властивостей мені інтуїтивніше і краще вписується в більшість кодів.

Порівнюючи

o.x = 5
ox = o.x

vs.

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

для мене цілком очевидно, що легше читати. Також властивості дозволяють приватні змінні набагато простіше.


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

Я б також уникав використання геттерів та сетерів:

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

Замість властивостей та getters / setters я віддаю перевагу виконанню складної логіки у чітко визначених місцях, таких як метод перевірки:

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для їх реалізації без зміни синтаксису решти коду.

1
"і зауважте, що 70% часу вони ніколи не замінюються якоюсь нетривіальною логікою". - це досить конкретне число, чи воно походить звідкись, чи ви маєте намір це як рукохватка "переважна більшість" штук (я не буду уважним, якщо є дослідження, яке оцінює цю кількість, я б щиро зацікавлений у його читанні)
Адам Паркін

1
О, не вибачте. Це звучить так, ніби я маю деякі дослідження, щоб створити резервну копію цього номера, але я мав на увазі це лише "більшу частину часу".
фульмікотон

7
Справа не в тому, що люди піклуються про накладні витрати, це те, що в Python ви можете переходити від прямого доступу до методів аксесуара, не змінюючи клієнтський код, тож вам нічого втрачати, спочатку безпосередньо виставляючи властивості.
Ніл G

10

Я здивований, що ніхто не згадував про те, що властивості пов'язані з методами дескрипторного класу, Адам Донохуе та НіленМарайс точно отримують цю ідею у своїх повідомленнях - що геттери та сетери є функціями і їх можна використовувати для:

  • підтвердити
  • змінити дані
  • тип качки (тип примусу до іншого типу)

Це представляє розумний спосіб приховати деталі реалізації та сукупність коду, як звичайний вираз, вводити касти, спробувати .. крім блоків, тверджень чи обчислених значень.

Як правило, CRUD на об'єкті часто може бути досить звичним, але розглянемо приклад даних, які зберігатимуться у реляційній базі даних. ORM може приховати деталі реалізації конкретних версій SQL в методах, пов'язаних з fget, fset, fdel, визначеними у класі властивостей, який керуватиме жахливими if .. elif .. else драбинками, які такі некрасиві в коді OO - викриваючи прості і вишукано self.variable = somethingта не дозволяють деталі розробника використовувати ORM.

Якщо ви думаєте про властивості лише як про якийсь грізний залишок мови неволі та дисципліни (тобто Java), їм не вистачає точки дескрипторів.


6

У складних проектах я вважаю за краще використовувати властивості лише для читання (або getters) з явною функцією 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()метод профілювання , хоча my_object.my_attr = 4.насправді є причиною уповільнення:

def slow_function(my_object):
    my_object.my_attr = 4.
    my_object.do_something()

5

І @propertyтрадиційні геттери, і сетери мають свої переваги. Це залежить від вашого випадку використання.

Переваги @property

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

    Цитується з PEP-8 :

    Для простих атрибутів загальнодоступних даних найкраще розкрити лише ім’я атрибута без складних методів доступу / мутатора. Майте на увазі, що Python надає простий шлях до подальшого вдосконалення, якщо ви виявите, що простий атрибут даних потребує зростання функціональної поведінки. У цьому випадку використовуйте властивості, щоб приховати функціональну реалізацію за простим синтаксисом доступу до атрибутів даних.

  • Використання @propertyдля доступу до даних в Python вважається пітонічним :

    • Це може посилити вашу самоідентифікацію як програміста 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

2

Ось уривки з "Ефективний Python: 90 конкретних способів написати кращий Python" (Дивовижна книга. Я дуже рекомендую це).

Що потрібно пам’ятати

Визначте інтерфейси нового класу за допомогою простих загальнодоступних атрибутів та уникайте визначення методів setter та getter.

✦ Використовуйте @property, щоб визначити особливу поведінку, коли доступ до атрибутів на ваших об'єктах, якщо це необхідно.

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

Переконайтесь, що @property методи швидкі; для повільної або складної роботи, особливо із залученням вводу-виводу або спричиненням побічних ефектів, використовуйте замість цього звичайні методи.

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

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

✦ Використовуйте @property, щоб надати існуючим атрибутам примірника нову функціональність.

Досягніть поступового прогресу в напрямку кращих моделей даних, використовуючи @property.

✦ Подумайте про рефакторинг класу та всіх сайтів для викликів, коли ви занадто сильно використовуєте @property.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.