Модуль, по-видимому, повторно импортирован в мемоизированную функцию Python

Я пишу утилиту командной строки Python, которая включает преобразование строки в TextBlob, т.е. часть модуля обработки естественного языка. Импорт модуля очень медленный, ~300 мс в моей системе. Для ускорения я создал мемоизированную функцию, которая преобразует текст в TextBlob только при первом вызове функции. Важно отметить, что если я дважды запускаю свой сценарий над одним и тем же текстом, я хочу избежать повторного импорта TextBlob и повторного вычисления большого двоичного объекта, вместо этого извлекая его из кеша. Это все сделано и работает нормально, за исключением того, что по какой-то причине функция все еще очень медленная. На самом деле, это так же медленно, как и раньше. Я думаю, это должно быть потому, что модуль повторно импортируется, даже если функция запоминается, а оператор импорта происходит внутри запоминаемой функции.

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

Вот минимальный пример основного кода:

@memoize
def make_blob(text):
     import textblob
     return textblob.TextBlob(text)


if __name__ == '__main__':
    make_blob("hello")

А вот декоратор мемоизации:

import os
import shelve
import functools
import inspect


def memoize(f):
    """Cache results of computations on disk in a directory called 'cache'."""
    path_of_this_file = os.path.dirname(os.path.realpath(__file__))
    cache_dirname = os.path.join(path_of_this_file, "cache")

    if not os.path.isdir(cache_dirname):
        os.mkdir(cache_dirname)

    cache_filename = f.__module__ + "." + f.__name__
    cachepath = os.path.join(cache_dirname, cache_filename)

    try:
        cache = shelve.open(cachepath, protocol=2)
    except:
        print 'Could not open cache file %s, maybe name collision' % cachepath
        cache = None

    @functools.wraps(f)
    def wrapped(*args, **kwargs):
        argdict = {}

        # handle instance methods
        if hasattr(f, '__self__'):
            args = args[1:]

        tempargdict = inspect.getcallargs(f, *args, **kwargs)

        for k, v in tempargdict.iteritems():
            argdict[k] = v

        key = str(hash(frozenset(argdict.items())))

        try:
            return cache[key]
        except KeyError:
            value = f(*args, **kwargs)
            cache[key] = value
            cache.sync()
            return value
        except TypeError:
            call_to = f.__module__ + '.' + f.__name__
            print ['Warning: could not disk cache call to ',
                   '%s; it probably has unhashable args'] % (call_to)
            return f(*args, **kwargs)

    return wrapped

И вот демонстрация того, что мемоизация в настоящее время не экономит время:

❯ time python test.py
python test.py  0.33s user 0.11s system 100% cpu 0.437 total

~/Desktop
❯ time python test.py
python test.py  0.33s user 0.11s system 100% cpu 0.436 total

Это происходит, несмотря на то, что функция правильно запоминается (операторы печати, помещаемые внутрь запоминаемой функции, выдают результат только при первом запуске сценария).

Я собрал все вместе в GitHub Gist на случай, если это будет полезно.


person JWS    schedule 06.03.2015    source источник
comment
Можете ли вы изменить способ работы вашего скрипта, чтобы амортизировать стоимость времени импорта по нескольким конверсиям? Медленно только импорт или конвертация?   -  person Nick T    schedule 06.03.2015
comment
Python импортирует модуль только один раз, мемоизация ничего не меняет.   -  person stranac    schedule 06.03.2015
comment
@NickT, это медленный импорт.   -  person JWS    schedule 06.03.2015
comment
@stranac, вы хотите сказать, что модуль импортируется только один раз ВСЕГДА или один раз при каждом запуске скрипта? Потому что здесь я запускаю сценарий дважды.   -  person JWS    schedule 06.03.2015
comment
Один раз, когда вы запускаете скрипт. Я не заметил, что вы пытались сохранить кеш с помощью полки.   -  person stranac    schedule 06.03.2015
comment
@JWS Действительно ли время запуска в 300 мс так важно? Если вы время от времени вызываете свой скрипт только через 300 мс, это не кажется большой проблемой. Если вы часто вызываете его, вы можете оставить его бездействующим в фоновом режиме, избегая времени запуска, когда это необходимо.   -  person syntonym    schedule 06.03.2015
comment
@syntonym, это важно — мне нужно, чтобы вся программа работала максимальное количество времени, поэтому долгий импорт меня убьет. Теперь я вижу, что импорт TextBlob идет медленно, потому что зависит от NLTK. Поэтому я думаю, что мой ответ — разветвить NLTK и создать версию с ленивым импортом или что-то в этом роде. Я не знаком с созданием сценариев, которые простаивают в фоновом режиме, но похоже, что это может сработать для моих целей.   -  person JWS    schedule 06.03.2015


Ответы (1)


Как насчет другого подхода:

import pickle

CACHE_FILE = 'cache.pkl'

try:
    with open(CACHE_FILE) as pkl:
        obj = pickle.load(pkl)
except:
    import slowmodule
    obj = "something"
    with open(CACHE_FILE, 'w') as pkl:
        pickle.dump(obj, pkl)

print obj

Здесь мы кэшируем объект, а не модуль. Обратите внимание, что это не даст никакой экономии, если для кэшируемого объекта требуется slowmodule. Таким образом, в приведенном выше примере вы увидите экономию, поскольку "something" является строкой и не требует, чтобы модуль slowmodule понимал ее. Но если вы сделали что-то вроде

obj = slowmodule.Foo("bar")

Процесс распаковки автоматически импортирует slowmodule, сводя на нет все преимущества кэширования.

Поэтому, если вы можете манипулировать textblob.TextBlob(text) во что-то, что при распаковывании не требует модуля textblob, то вы увидите экономию, используя этот подход.

person jedwards    schedule 06.03.2015