Не так давно пришлось столкнуться со следующей ситуацией: имелся довольно сложный и затратный по ресурсам алгоритм сбора таблицы соответствий (читай 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.