Подтвердить что ты не робот

Ленивые переменные модуля - это можно сделать?

Я пытаюсь найти способ ленивой загрузки переменной уровня модуля.

В частности, я написал небольшую библиотеку Python для общения с iTunes, и я хочу иметь переменную модуля DOWNLOAD_FOLDER_PATH. К сожалению, iTunes не скажет вам, где находится его папка загрузки, поэтому я написал функцию, которая захватывает путь к файлу нескольких треков подкастов и поднимается вверх по дереву каталогов, пока не найдет каталог "Загрузки".

Это займет секунду или два, поэтому я бы хотел, чтобы он оценивался лениво, а не во время импорта модуля.

Есть ли способ лениво назначить переменную модуля при первом доступе или мне придется полагаться на функцию?

4b9b3361

Ответ 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.