Ленивые переменные модуля - это можно сделать? - PullRequest
30 голосов
/ 23 сентября 2009

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

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

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

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

Ответы [ 8 ]

55 голосов
/ 23 сентября 2009

Вы не можете сделать это с модулями, но вы можете замаскировать класс «как если бы» это был модуль, например, в itun.py, код ...:

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! _)

12 голосов
/ 15 января 2013

Я использовал реализацию Алекса на 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
    ...

Код можно поместить в другой модуль и расширить его свойствами как в примерах выше.

Может быть, это кому-нибудь пригодится.

4 голосов
/ 17 сентября 2018

Оказывается, что в 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}")
3 голосов
/ 23 сентября 2009

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

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

#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's current directory
        to find the 'Downloads' folder."""
    # expensive operation now here.

Используя этот метод, вы будете следовать рекомендациям.

2 голосов
/ 10 февраля 2019

Начиная с 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 доступно .

2 голосов
/ 05 июня 2013

Недавно я столкнулся с той же проблемой и нашел способ сделать это.

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

0 голосов
/ 25 августа 2018

Правильный способ сделать это, согласно документации 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
0 голосов
/ 23 сентября 2009

Если эта переменная находится в классе, а не в модуле, вы можете перегрузить getattr или, что еще лучше, заполнить его в init.

...