Ленивые переменные модуля - это можно сделать?
Я пытаюсь найти способ ленивой загрузки переменной уровня модуля.
В частности, я написал небольшую библиотеку Python для общения с iTunes, и я хочу иметь переменную модуля DOWNLOAD_FOLDER_PATH
. К сожалению, iTunes не скажет вам, где находится его папка загрузки, поэтому я написал функцию, которая захватывает путь к файлу нескольких треков подкастов и поднимается вверх по дереву каталогов, пока не найдет каталог "Загрузки".
Это займет секунду или два, поэтому я бы хотел, чтобы он оценивался лениво, а не во время импорта модуля.
Есть ли способ лениво назначить переменную модуля при первом доступе или мне придется полагаться на функцию?
Ответы
Ответ 1
Вы не можете делать это с помощью модулей, но вы можете замаскировать класс "как если бы" это был модуль, например, в itun.py
, code...:
import sys
class _Sneaky(object):
def __init__(self):
self.download = None
@property
def DOWNLOAD_PATH(self):
if not self.download:
self.download = heavyComputations()
return self.download
def __getattr__(self, name):
return globals()[name]
# other parts of itun that you WANT to code in
# module-ish ways
sys.modules[__name__] = _Sneaky()
Теперь любой может import itun
... и получить на самом деле ваш экземпляр itun._Sneaky()
. __getattr__
позволяет вам получить доступ к чему-либо еще в itun.py
, который может быть более удобным для вас, чтобы закодировать как объект модуля верхнего уровня, чем внутри _Sneaky
! _)
Ответ 2
Я использовал реализацию Alex 'на Python 3.3, но это ужасно срывается:
Код
def __getattr__(self, name):
return globals()[name]
неверен, потому что должен быть поднят AttributeError
, а не KeyError
.
Это немедленно сработало под Python 3.3, потому что много интроспекции сделано
во время импорта, ищет атрибуты типа __path__
, __loader__
и т.д.
Вот версия, которую мы используем сейчас в нашем проекте, чтобы обеспечить ленивый импорт
в модуле. Модуль __init__
модуля задерживается до момента доступа первого атрибута
который не имеет специального имени:
""" config.py """
# lazy initialization of this module to avoid circular import.
# the trick is to replace this module by an instance!
# modelled after a post from Alex Martelli :-)
Ленивые переменные модуля - это можно сделать?
class _Sneaky(object):
def __init__(self, name):
self.module = sys.modules[name]
sys.modules[name] = self
self.initializing = True
def __getattr__(self, name):
# call module.__init__ after import introspection is done
if self.initializing and not name[:2] == '__' == name[-2:]:
self.initializing = False
__init__(self.module)
return getattr(self.module, name)
_Sneaky(__name__)
Теперь модуль должен определить функцию init. Эта функция может использоваться
для импорта модулей, которые могут импортировать себя:
def __init__(module):
...
# do something that imports config.py again
...
Код может быть помещен в другой модуль и может быть расширен с помощью свойств
как в приведенных выше примерах.
Может быть, это полезно для кого-то.
Ответ 3
Есть ли способ лениво назначить переменную модуля при первом доступе или мне придется полагаться на функцию?
Я думаю, вы правы в том, что функция - лучшее решение вашей проблемы здесь.
Я приведу вам краткий пример для иллюстрации.
#myfile.py - an example module with some expensive module level code.
import os
# expensive operation to crawl up in directory structure
Дорогая операция будет выполнена при импорте, если она находится на уровне модуля. Невозможно остановить это, не дожидаясь ленивого импорта всего модуля!
#myfile2.py - a module with expensive code placed inside a function.
import os
def getdownloadsfolder(curdir=None):
"""a function that will search upward from the user current directory
to find the 'Downloads' folder."""
# expensive operation now here.
При использовании этого метода вы будете следовать лучшей практике.
Ответ 4
Оказывается, что в Python 3.7 это можно сделать чисто, определив __getattr__()
на уровне модуля, как указано в PEP 562.
# mymodule.py
from typing import Any
DOWNLOAD_FOLDER_PATH: str
def _download_folder_path() -> str:
global DOWNLOAD_FOLDER_PATH
DOWNLOAD_FOLDER_PATH = ... # compute however ...
return DOWNLOAD_FOLDER_PATH
def __getattr__(name: str) -> Any:
if name == "DOWNLOAD_FOLDER_PATH":
return _download_folder_path()
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
Ответ 5
Начиная с Python 3.7 (и в результате PEP-562), это теперь возможно с модульным уровнем __getattr__
:
Внутри вашего модуля поместите что-то вроде:
def _long_function():
# print() function to show this is called only once
print("Determining DOWNLOAD_FOLDER_PATH...")
# Determine the module-level variable
path = "/some/path/here"
# Set the global (module scope)
globals()['DOWNLOAD_FOLDER_PATH'] = path
# ... and return it
return path
def __getattr__(name):
if name == "DOWNLOAD_FOLDER_PATH":
return _long_function()
# Implicit else
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
Из этого должно быть ясно, что _long_function()
не выполняется при импорте модуля, например:
print("-- before import --")
import somemodule
print("-- after import --")
приводит только к:
-- before import --
-- after import --
Но когда вы пытаетесь получить доступ к имени из модуля, будет вызван уровень модуля __getattr__
, который, в свою очередь, вызовет _long_function
, который будет выполнять долгосрочную задачу, кэшируя его как модуль- переменная уровня и возвращает результат обратно к коду, который ее вызвал.
Например, с первым блоком выше внутри модуля "somemodule.py", следующий код:
import somemodule
print("--")
print(somemodule.DOWNLOAD_FOLDER_PATH)
print('--')
print(somemodule.DOWNLOAD_FOLDER_PATH)
print('--')
производит:
--
Determining DOWNLOAD_FOLDER_PATH...
/some/path/here
--
/some/path/here
--
или, более четко:
# LINE OF CODE # OUTPUT
import somemodule # (nothing)
print("--") # --
print(somemodule.DOWNLOAD_FOLDER_PATH) # Determining DOWNLOAD_FOLDER_PATH...
# /some/path/here
print("--") # --
print(somemodule.DOWNLOAD_FOLDER_PATH) # /some/path/here
print("--") # --
Наконец, вы также можете реализовать __dir__
, как описывает PEP, если вы хотите указать (например, инструментам интроспекции кода), что DOWNLOAD_FOLDER_PATH
доступен.
Ответ 6
Недавно я столкнулся с той же проблемой и нашел способ сделать это.
class LazyObject(object):
def __init__(self):
self.initialized = False
setattr(self, 'data', None)
def init(self, *args):
#print 'initializing'
pass
def __len__(self): return len(self.data)
def __repr__(self): return repr(self.data)
def __getattribute__(self, key):
if object.__getattribute__(self, 'initialized') == False:
object.__getattribute__(self, 'init')(self)
setattr(self, 'initialized', True)
if key == 'data':
return object.__getattribute__(self, 'data')
else:
try:
return object.__getattribute__(self, 'data').__getattribute__(key)
except AttributeError:
return super(LazyObject, self).__getattribute__(key)
С помощью этого LazyObject
вы можете определить метод init
для объекта, и объект будет инициализирован лениво, пример кода выглядит так:
o = LazyObject()
def slow_init(self):
time.sleep(1) # simulate slow initialization
self.data = 'done'
o.init = slow_init
объект o
, указанный выше, будет иметь точно такие же методы, какие бы объекты 'done'
не имели, например:
# o will be initialized, then apply the `len` method
assert len(o) == 4
полный код с тестами (работает в версии 2.7) можно найти здесь:
https://gist.github.com/observerss/007fedc5b74c74f3ea08
Ответ 7
Правильный способ сделать это, согласно документации по Python, это создать подкласс types.ModuleType
, а затем динамически обновить модуль __class__
. Итак, вот решение, не совсем подходящее для ответа Кристиана Тисмера, но, вероятно, совсем не похожее на него:
import sys
import types
class _Sneaky(types.ModuleType):
@property
def DOWNLOAD_FOLDER_PATH(self):
if not hasattr(self, '_download_folder_path'):
self._download_folder_path = '/dev/block/'
return self._download_folder_path
sys.modules[__name__].__class__ = _Sneaky
Ответ 8
Если эта переменная проживает в классе, а не в модуле, вы можете перегрузить getattr или, еще лучше, заполнить его в init.