Не так давно пришлось столкнуться со следующей ситуацией: имелся довольно сложный и затратный по ресурсам алгоритм сбора таблицы соответствий (читай 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, чтобы сделать более явным то, что она является сторонней и недоступна для модификаций.
а пацему такой працедурный стиль примера. можно ведь сделать базовый клас SocialService от него фб. или там твиттер дочернии. а там внутри каждого пусть будет все лениво. ни какой магии джаст ооп. и понятно будет школяру, а не только посвященным :)
ОтветитьУдалитьВопрос не в том как этими сервисами потом пользоваться, а в том как их изначально получить. SocialServices скорее инкапсулируют алгоритм получения этих сервисов, с помощью которого теоретический FacebookService и TwitterService смогли бы получить биндинг для дальнейшего использования. Описанная проблема заключается в том, что все биндинги получаются одновременно и ресурсы затрачиваемые на каждое такое получение велики, поэтому, если бы теоретический FacebookService и TwitterService каждый самостоятельно инициализировал бы эти биндинги, то это удвоило бы работу (в этом случае, когда сервисов всего два, а их могло бы быть и больше).
ОтветитьУдалитьВозможно, из примера не совсем понятно, что load_available_services_from_external_site является внешним методом по отношению к нашему теоретическому приложению и поменять его реализацию таким образом, чтобы он получал отдельно биндинг для twitter и отдельно биндинг для facebook, не представляется возможным. Таким образом, единственный способ получить доступ к одному из социальных сервисов это воспользоваться этим методом. Но, если его использовать несколько раз, то это создаст ненужный overhead.