Ответ 5
[ 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
) не задано (где для целей этого примера "не задано", означает "set to 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]
и, конечно же, убедитесь, что наш сеттер знает, как извлечь первые три значения в качестве ключа для словаря и установить его значение в число или что-то еще.
Но даже если бы мы это сделали, мы все равно не могли поддерживать его с помощью свойств, потому что нет возможности получить значение, потому что мы не можем передавать параметры на getter. Поэтому нам пришлось вернуть все, вводя асимметрию.
Java-стиль getter/setter позволяет нам справиться с этим, но мы снова нуждаемся в getter/seters.
На мой взгляд, мы действительно хотим что-то, что улавливает следующие требования:
-
Пользователи определяют только один метод для данного атрибута и могут указывать, есть ли атрибут только для чтения или чтение-запись. Свойства не проверяются, если атрибут доступен для записи.
-
Пользователю нет необходимости определять дополнительную переменную, лежащую в основе функции, поэтому нам не нужен __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
и убедиться, что он обрабатывает как uberProperty
и 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
.
Нам нужно каким-то образом динамически связать экземпляр 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
-
Нам нужно, чтобы ex
возвращал exgetValue()
по умолчанию.
- То, что мы на самом деле увидим, - это та область, где модель терпит неудачу.
- Оказывается, нам всегда нужно использовать вызов функции для получения значения.
- Но мы можем сделать его похожим на обычный вызов функции и не использовать
exgetValue()
. (Выполнение этого очевидно, если вы еще не установили его.)
-
Нам нужно поддерживать настройку ex directly
, как в ex = <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] Я могу быть в стороне от того, все ли так.