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