Не так давно пришлось столкнуться со следующей ситуацией: имелся довольно сложный и затратный по ресурсам алгоритм сбора таблицы соответствий (читай dict), при этом, хотелось иметь к элементам этой таблицы достаточно удобный доступ (по имени), так как в дальнейшем они довольно таки активно использовались в различных частях кода. Что так же немаловажно, сценариев исполнения самого кода присутствует великое множество и далеко не во всех из них эта таблица понадобится.
Приведу пример, чтобы стало немного понятнее:
UPD: переместил функцию load_available_services_from_external_site в модуль thirdparty, чтобы сделать более явным то, что она является сторонней и недоступна для модификаций.
Приведу пример, чтобы стало немного понятнее:
class SocialServices(object):
def init(self):
facebook, twitter = thirdparty.load_available_services_from_external_site()
self.facebook = facebook
self.twitter = twitter
social_services = SocialServices()
social_services.init()
def like_it_on_facebook():
global social_services
social_services.facebook.like_it
В этом примере можно увидеть все обозначенные выше проблемы:
1. Все социальные сервисы получаются из одного источника одновременно
2. Сервисы загружаются с внешнего сайта, а это достаточно медленная операция.
3. Сервис понадобится только, если кто-нибудь захочет сделать like на facebook.
Один из возможных вариантов, это использовать "ленивые" свойства, тогда код мог бы выглядеть следующим образом:
class SocialServices(object):
@lazy_property
def _services(self):
return thirdparty.load_available_services_from_external_site()
def facebook(self):
return self._services[0]
def twitter(self):
return self._services[1]
social_services = SocialServices()
def like_it_on_facebook():
global social_services
social_services.facebook.like_it
В этом случае сервисы загрузятся при первом обращении, а в дальнейшем будет использоваться их закэшированная версия. Единственное, чем этот код страдает, это резким снижением читабельности. Может быть это и не так заметно, при наличии всего двух сервисов, но, если их станет 10 или 20... Читабельность можно повысить, введя дополнительный result-объект, к которому мы сможем обращаться не по индексам, а по именам компонентов, но, тогда, это будет практически копия наших социальных сервисов. Так же этот метод будет плохо работать, если инициализация включает не только запрос, но и какое-то дополнительное изменение состояния объекта.
Другой вариант, это проверять состояние объекта каждый раз при обращении к свойству, которое этого требует:
class SocialServices(object):
initialized = False
def init(self):
facebook, twitter = thirdparty.load_available_services_from_external_site()
self._facebook = facebook
self._twitter = twitter
def ensure_initialized(self):
if not self.initialized:
self.init()
self.initialized = True
@property
def facebook(self):
self.ensure_initialized()
return self._facebook
@property
def twitter(self):
self.ensure_initialized()
return self._twitter
social_services = SocialServices()
def like_it_on_facebook():
global social_services
social_services.facebook.like_it
Это метод решает все вышеперечисленные проблемы и можно было бы на этом остановиться, смирившись даже с тем, что необходимо дополнительно вводить для каждого атрибута свойство, содержащее проверку состояния объекта, но, постойте, мы ведь программируем на Питоне... На помощь нам приходят декораторы.
Не буду и дальше растягивать интригу и приведу окончательное решение:
def auto_init_by_access_to(class_, *fields):
for field in fields:
getter = lambda instance: instance.__dict__[field]
getter.__name__ = field
setattr(class_, field, auto_init_property(getter))
class auto_init_property(object):
def __init__(self, func):
self._func = func
self.__name__ = func.__name__
self.__doc__ = func.__name__
def __get__(self, instance, _):
if instance is None: return None
if not hasattr(instance, 'initialized') or not instance.initialized:
instance.init()
instance.initialized = True
result = instance.__dict__[self.__name__] = self._func(instance)
return result
class SocialServices(object):
def init(self):
facebook, twitter = thirdparty.load_available_services_from_external_site()
self.facebook = facebook
self.twitter = twitter
social_services = SocialServices()
auto_init_by_access_to(SocialServices, "facebook", "twitter")
def like_it_on_facebook():
global social_services
social_services.facebook.like_it
Нам пришлось добавить один вспомогательный класс и одну вспомогательную функцию. auto_init_property по сути выполняет роль свойства из предыдущего примера и мы могли бы использовать этот класс следующим образом:
@auto_init_property
def facebook(self):
return self._facebook
при этом при первом же обращении свойство будет заменено значением self._facebook.
Функция auto_init_by_access_to по сути создает эти свойства в указанном классе, при этом сам наш класс SocialServices вернулся к тому виду, в котором он впервые предстал в этом посте, давая нам возможность выбирать между автоматической и явной инициализацией.
UPD: переместил функцию load_available_services_from_external_site в модуль thirdparty, чтобы сделать более явным то, что она является сторонней и недоступна для модификаций.