[ 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
) не устанавливается (где, для этой цели Например, «не установлено» означает «установлено на Нет») Я хочу значение вычисления 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()
и затем иметь возможностьdo:
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, так и set.
Давайте начнем с 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] Я могуотстать от того, так ли это до сих пор.