[ 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] Я могу отстать от того, так ли это до сих пор.