Отмена импорта модуля *

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

from scipy import *
from numpy import *

... Это, конечно, загрязняет пространство имен и затрудняет определение происхождения атрибута в модуле.

Есть ли способ заставить Python проанализировать и исправить это для меня? Кто-нибудь делал утилиту для этого? Если нет, то как можно сделать такую ​​утилиту?


person Kelketek    schedule 07.03.2013    source источник
comment
Я сочувствую тебе. Надеюсь, вы найдете хороший инструмент. (+1)   -  person NPE    schedule 07.03.2013
comment
Более того, я надеюсь, что вы напишете хороший инструмент (независимо от того, основан ли он на моем ответе или нет) и опубликуете его на PyPI, поэтому, если мне когда-нибудь понадобится такая вещь, мне не придется делать это самому. :)   -  person abarnert    schedule 07.03.2013


Ответы (4)


Я думаю, что решения PurityLake и Martijn Pieters с вспомогательным ручным управлением, вероятно, являются лучшим способом. Но невозможно сделать это программно.

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

Затем вам нужно перебрать их, используя inspect.getmodule(), чтобы узнать, в каком модуле был изначально определен каждый объект. И я предполагаю, что вы не используете ничего, что было дважды from foo import *-ed. Составьте список всех имен, которые были определены в модулях numpy и scipy.

Теперь вы можете взять этот вывод и просто заменить каждый foo на numpy.foo.

Итак, собирая это, что-то вроде этого:

for modname in sys.argv[1:]:
    with open(modname + '.py') as srcfile:
        src = srcfile.read()
    src = src.replace('from numpy import *', 'import numpy')
    src = src.replace('from scipy import *', 'import scipy')
    mod = __import__(modname)
    for name in dir(mod):
        original_mod = inspect.getmodule(getattr(mod, name))
        if original_mod.__name__ == 'numpy':
            src = src.replace(name, 'numpy.'+name)
        elif original_mod.__name__ == 'scipy':
            src = src.replace(name, 'scipy.'+name)
    with open(modname + '.tmp') as dstfile:
        dstfile.write(src)
    os.rename(modname + '.py', modname + '.bak')
    os.rename(modname + '.tmp', modname + '.py')

Если одно из предположений неверно, изменить код несложно. Кроме того, вы можете использовать tempfile.NamedTemporaryFile и другие улучшения, чтобы случайно не перезаписать временные файлы. (Я просто не хотел иметь дело с головной болью написания чего-то кросс-платформенного; если вы не работаете в Windows, это легко.) И, очевидно, добавить некоторую обработку ошибок и, возможно, некоторые отчеты.

person abarnert    schedule 07.03.2013
comment
Я не думаю, что это будет работать. Как насчет переменных с именем my_foo. Внезапно вы получаете my_numpy.foo. Упс. Конечно, при наличии надлежащего синтаксического анализатора (я думаю, ast) вы, вероятно, могли бы это сделать. - person mgilson; 07.03.2013
comment
Это, вероятно, больше, что я ищу. У парня иногда были модули, в которых было сделано пять разных импортов подстановочных знаков. Это безумие. Поиск и замена здесь немного опасны, так как делают некоторые предположения, но эта идея направляет меня в правильном направлении. - person Kelketek; 07.03.2013
comment
В конечном счете, это разумное начало для инструмента, который может быть полезен для всего сообщества. Мне также интересно, как такие вещи будут взаимодействовать с __all__. Чтобы сделать это на самом деле правильно, вы, вероятно, захотите отфильтровать материал, которого нет в module.__all__, если он существует. - person mgilson; 07.03.2013
comment
@mgilson: Вы правы, и если у вас есть оба my_foo и foo, невозможно исправить одно, не сломав другое. Здесь вам даже не нужен настоящий синтаксический анализатор, просто лексер, который намного проще. Но вы правы, у нас уже есть правильный синтаксический анализатор в ast, так что, возможно, это правильный путь. - person abarnert; 07.03.2013
comment
@mgilson: Моя первоначальная мысль заключалась в том, что я не буду делать это таким образом - я буду полагаться на линтер, модульные тесты и ручное редактирование, как в ответе Мартина Питерса. Но теперь, когда вы упомянули об этом, если бы кто-то взял это и отшлифовал, это могло бы быть полезным инструментом общего назначения. - person abarnert; 07.03.2013
comment
@mgilson, даже при наличии надлежащего парсера вы не обязательно сможете это сделать. Если вы импортируете имя foo, у вас все еще может быть локальная переменная с именем foo в одной из функций, которую вы не можете заменить на bar.foo - person shx2; 07.03.2013
comment
@mgilson: Между тем, __all__ здесь не проблема. Я заменяю не все, что экспортирует numpy, а все, что импортируется из numpy. Обратный путь может быть проще (numpy.__all__, если он существует, dir(numpy) в противном случае), но он не будет работать в случаях, когда есть несколько from foo import * строк с конфликтующими именами. Как узнать, является ли экземпляр, скажем, log10 numpy.log10 или scipy.log10, не глядя на реальный объект? - person abarnert; 07.03.2013
comment
@shx2 - правильный синтаксический анализатор сообщит вам, что foo внутри функции local. Это одна из причин, по которой вам нужен правильный синтаксический анализатор. - person mgilson; 07.03.2013
comment
@abarnert - в зависимости от того, что называется вторым :) - person mgilson; 07.03.2013
comment
@abarnert: вы можете записать список имен в id ({k:id(v) for k,v in globals().items()}) до первого импорта и снова после первого from foo import * и проверить, какие элементы в вашем словаре изменились. Затем еще раз после второго (from bar import *) и еще раз проверьте. - person shx2; 07.03.2013

да. Удалите импорт и запустите линтер на модуле.

Я рекомендую использовать flake8, хотя это также может создать много шума из-за ошибок стиля.

Просто удалить импорт и попытаться запустить код, вероятно, будет недостаточно, так как многие ошибки имен не будут возникать до тех пор, пока вы не запустите только правильную строку кода с правильным вводом. Вместо этого линтер проанализирует код путем синтаксического анализа и обнаружит потенциальные NameError без необходимости запуска кода.

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

В этом случае, когда есть несколько from module import * строк, это становится немного более болезненным, поскольку вам нужно выяснить для каждого отсутствующего имени, какой модуль предоставил это имя. Это потребует ручной работы, но вы можете просто импортировать модуль в интерпретатор Python и проверить, определено ли отсутствующее имя в этом модуле:

>>> import scipy, numpy
>>> 'loadtxt' in dir(numpy)
True

Вам нужно принять во внимание, что в этом конкретном случае между модулями numpy и scipy есть перекрытие; для любого имени, определенного в обоих модулях, выигрывает модуль, импортированный последним.

Обратите внимание, что оставление любой строки from module import * на месте означает, что линтер не сможет определить, какие имена могут вызывать ошибки NameError!

person Martijn Pieters    schedule 07.03.2013
comment
Если я удалю импорт, как это скажет мне, из какого модуля взяты имена? Не будет ли это просто ошибкой имени для всех атрибутов, которых сейчас нет в списке? - person Kelketek; 07.03.2013
comment
@Kelketek: Да, и вам придется выяснить для каждого из них, из какого модуля он предполагается исходить. Это не так сложно, к счастью. - person Martijn Pieters; 07.03.2013
comment
Если вы удалите их по одному, это, вероятно, будет проще. - person Wooble; 07.03.2013
comment
Предположительно, я должен сначала запустить линтер, чтобы получить какие-либо общие проблемы, которые он может найти, решить их, удалить одну строку импорта, запустить его снова, чтобы увидеть, какие имена появляются как ошибки имени, а затем добавить их в импорт "из" и затем повторить процесс с каждым импортом подстановочных знаков? - person Kelketek; 07.03.2013
comment
@MartijnPieters: На самом деле это может быть удивительно болезненным. Чтобы взять пример OP с from scipy import * и from numpy import *, семантика нескольких функций, таких как log10, будет зависеть от того, какой импорт был последним (они существуют в обоих модулях, но ведут себя по-разному). См. stackoverflow.com/questions/6200910/ - person NPE; 07.03.2013
comment
@Kelketek: с этим есть проблема: если оставить любую из строк from module import * на месте, линтер не сможет определить, какие имена вызывают ошибки имен. Статический линтер не может видеть, какой модуль имеет какое имя. - person Martijn Pieters; 07.03.2013
comment
Хм... Где обязательный +3? Умный ... не хочу слишком часто запускать свой хак. В любом случае, +1 от меня. - person mgilson; 07.03.2013
comment
Я даю вам голос за этот ответ, так как это, вероятно, самый чистый способ сделать это. Тем не менее, я чувствую, что метод абамерта (в целом) — это то, что мне понадобится — у предыдущего разработчика было очень много таких операторов импорта в одном модуле. Иногда до пяти. - person Kelketek; 07.03.2013
comment
@Kelketek: Честно говоря, даже если бы я доработал свой инструмент до такой степени, что он стал надежным (или вы сделали это сами), вам, вероятно, все равно пришлось бы обрабатывать некоторые случаи вручную. (По крайней мере, при отладке инструмента!) Так что это важный ответ в любом случае. - person abarnert; 07.03.2013
comment
Да, я понимаю это. Тем не менее, это значительно упростит решение проблемы. Я имею дело с десятками и десятками таких модулей. Это кошмар. - person Kelketek; 07.03.2013
comment
@Kelketek: Зачем менять их все сразу? Почему бы не подойти к этому на основе модуля за модулем? Они все сломаны? Если нет, не исправлять, пока не понадобится! - person Martijn Pieters; 07.03.2013
comment
Я все еще решаю свой точный практический подход. Но в любом случае, у меня их слишком много, чтобы не думать об автоматической помощи в отслеживании имен. - person Kelketek; 07.03.2013

Для этого я сделал небольшую утилиту, которую я называю «dedazzler». Он найдет строки «из импорта модуля *», а затем расширит «каталог» целевых модулей, заменив строки.

После его запуска вам все равно нужно запустить линтер. Вот особенно интересная часть кода:

import re

star_match = re.compile('from\s(?P<module>[\.\w]+)\simport\s[*]')
now = str(time.time())
error = lambda x: sys.stderr.write(x + '\n')

def replace_imports(lines):
    """
    Iterates through lines in a Python file, looks for 'from module import *'
    statements, and attempts to fix them.
    """
    for line_num, line in enumerate(lines):
        match = star_match.search(line)
        if match:
            newline = import_generator(match.groupdict()['module'])
            if newline:
                lines[line_num] = newline
    return lines

def import_generator(modulename):
    try:
        prop_depth = modulename.split('.')[1:]
        namespace = __import__(modulename)
        for prop in prop_depth:
            namespace = getattr(namespace, prop)
    except ImportError:
        error("Couldn't import module '%s'!" % modulename)
        return
    directory = [ name for name in dir(namespace) if not name.startswith('_') ]
    return "from %s import %s\n"% (modulename, ', '.join(directory))

Я поддерживаю это в более полезной автономной форме утилиты здесь:

https://github.com/USGM/dedazzler/

person Kelketek    schedule 17.05.2013

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

РЕДАКТИРОВАТЬ: ах да, линтер, я об этом не подумал.

person PurityLake    schedule 07.03.2013