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