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