Python: хороший способ получить функцию и все зависимости в одном файле?

Я работаю над большой базой кода Python, которая растет, растет и растет. Это не одно приложение — это скорее набор экспериментов с общим кодом.

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

Сейчас я использую PyCharm и не смог найти эту функциональность. Есть ли какой-нибудь инструмент, который это делает?

Изменить: я создал public-release для решения этой проблемы. Имея основной модуль, он просматривает зависимые модули и копирует их в новый репозиторий.


person Peter    schedule 07.11.2016    source источник
comment
Все ваши пользовательские модули находятся в общем каталоге (или подкаталогах общего каталога)? И были бы вы счастливы, если бы все зависимые модули были упакованы в один zip-файл вместе с вашим скриптом, или вам нужно извлечь только соответствующий код из этих модулей? Было бы довольно легко пройти через sys.modules и найти модули, которые вы используете, которые находятся в определенном каталоге или каталогах, но было бы сложнее извлечь только те подразделы, которые вам нужны.   -  person Matthias Fripp    schedule 23.04.2017
comment
Другим вариантом может быть запуск вашего эксперимента с помощью инструмента профилирования, который регистрирует каждый вызов функции. Тогда вы, вероятно, могли бы использовать сценарий, чтобы найти код для каждой функции в каждом файле модуля. Если вы обычно импортируете свои функции с помощью from mymodule import func и не используете глобальные переменные и не дублируете имена функций, то, вероятно, вы могли бы безопасно собрать все функции в один скрипт. Если вы обычно используете import mymodule, а затем mymodule.func(), тогда вы можете настроить сценарий для создания версий оболочки каждого модуля только с соответствующими функциями.   -  person Matthias Fripp    schedule 23.04.2017


Ответы (5)


Если вам нужны только модули, вы можете просто запустить код и новый сеанс и пройти sys.modules для любого модуля в вашем пакете.

Чтобы переместить все зависимости с помощью PyCharm, вы можете создать макрос, который перемещает выделенный объект в предопределенный файл, прикрепить макрос к сочетанию клавиш, а затем быстро рекурсивно переместить любой импорт в проекте. Например, я сделал макрос под названием export_func, который перемещает функцию в to_export.py, и добавил ярлык для F10:

Макродействия

Учитывая функцию, которую я хочу переместить в файл, например

from utils import factorize


def my_func():
    print(factorize(100))

и utils.py выглядит примерно так

import numpy as np
from collections import Counter
import sys
if sys.version_info.major >= 3:
    from functools import lru_cache
else:
    from functools32 import lru_cache


PREPROC_CAP = int(1e6)


@lru_cache(10)
def get_primes(n):
    n = int(n)
    sieve = np.ones(n // 3 + (n % 6 == 2), dtype=np.bool)
    for i in range(1, int(n ** 0.5) // 3 + 1):
        if sieve[i]:
            k = 3 * i + 1 | 1
            sieve[k * k // 3::2 * k] = False
            sieve[k * (k - 2 * (i & 1) + 4) // 3::2 * k] = False
    return list(map(int, np.r_[2, 3, ((3 * np.nonzero(sieve)[0][1:] + 1) | 1)]))


@lru_cache(10)
def _get_primes_set(n):
    return set(get_primes(n))


@lru_cache(int(1e6))
def factorize(value):
    if value == 1:
        return Counter()
    if value < PREPROC_CAP and value in _get_primes_set(PREPROC_CAP):
        return Counter([value])
    for p in get_primes(PREPROC_CAP):
        if p ** 2 > value:
            break
        if value % p == 0:
            factors = factorize(value // p).copy()
            factors[p] += 1
            return factors
    for p in range(PREPROC_CAP + 1, int(value ** .5) + 1, 2):
        if value % p == 0:
            factors = factorize(value // p).copy()
            factors[p] += 1
            return factors
    return Counter([value])

Я могу выделить my_func и нажать F10, чтобы создать to_export.py:

from utils import factorize


def my_func():
    print(factorize(100))

Выделите factorize в to_export.py и нажмите F10, чтобы получить

from collections import Counter
from functools import lru_cache

from utils import PREPROC_CAP, _get_primes_set, get_primes


def my_func():
    print(factorize(100))


@lru_cache(int(1e6))
def factorize(value):
    if value == 1:
        return Counter()
    if value < PREPROC_CAP and value in _get_primes_set(PREPROC_CAP):
        return Counter([value])
    for p in get_primes(PREPROC_CAP):
        if p ** 2 > value:
            break
        if value % p == 0:
            factors = factorize(value // p).copy()
            factors[p] += 1
            return factors
    for p in range(PREPROC_CAP + 1, int(value ** .5) + 1, 2):
        if value % p == 0:
            factors = factorize(value // p).copy()
            factors[p] += 1
            return factors
    return Counter([value])

Затем выделите каждый из PREPROC_CAP, _get_primes_set и get_primes, а затем нажмите F10, чтобы получить

from collections import Counter
from functools import lru_cache

import numpy as np


def my_func():
    print(factorize(100))


@lru_cache(int(1e6))
def factorize(value):
    if value == 1:
        return Counter()
    if value < PREPROC_CAP and value in _get_primes_set(PREPROC_CAP):
        return Counter([value])
    for p in get_primes(PREPROC_CAP):
        if p ** 2 > value:
            break
        if value % p == 0:
            factors = factorize(value // p).copy()
            factors[p] += 1
            return factors
    for p in range(PREPROC_CAP + 1, int(value ** .5) + 1, 2):
        if value % p == 0:
            factors = factorize(value // p).copy()
            factors[p] += 1
            return factors
    return Counter([value])


PREPROC_CAP = int(1e6)


@lru_cache(10)
def _get_primes_set(n):
    return set(get_primes(n))


@lru_cache(10)
def get_primes(n):
    n = int(n)
    sieve = np.ones(n // 3 + (n % 6 == 2), dtype=np.bool)
    for i in range(1, int(n ** 0.5) // 3 + 1):
        if sieve[i]:
            k = 3 * i + 1 | 1
            sieve[k * k // 3::2 * k] = False
            sieve[k * (k - 2 * (i & 1) + 4) // 3::2 * k] = False
    return list(map(int, np.r_[2, 3, ((3 * np.nonzero(sieve)[0][1:] + 1) | 1)]))

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

person Colin    schedule 18.04.2017
comment
Отлично, это очень полезно, и sys.modules — хороший совет. Большое спасибо. Присуждает награду, если не придет какой-то чудодейственный ответ - person Peter; 19.04.2017
comment
Правильно ли я понимаю, если это означает, что вы просто вручную нажимаете на каждую функцию и говорите PyCharm скопировать ее? - person jpmc26; 19.04.2017
comment
Ага, похоже. Хотя лучше всего было бы полностью автоматизированное сканирование зависимостей и копирование, похоже, что это самая быстрая альтернатива. - person Peter; 25.04.2017

Запихивать весь код в один модуль — не лучшая идея. Хорошим примером причины является случай, когда один из ваших экспериментов зависит от двух модулей с разными определениями для одного и того же имени функции. С отдельными модулями ваш код легко различает их; чтобы поместить их в один и тот же модуль, редактор должен был бы сделать какое-то хакерское переименование функций (например, добавить к ним старое имя модуля или что-то в этом роде), и ситуация становится еще хуже, если какая-то другая функция в модуле вызывает ту, с конфликтующим названием. Для этого вам фактически необходимо полностью заменить механизм области видимости модуля.

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

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

  1. Отделите экспериментальный код от кода, готового к выпуску.
  2. Упакуйте свой стабильный код.

Разделение вашего экспериментального кода

Управление версиями — это решение вашей первой проблемы. Это позволит вам создать любой экспериментальный код на вашем локальном компьютере, и пока вы его не зафиксируете, вы не будете загрязнять свою кодовую базу экспериментальным кодом. Если вы действительно хотите зафиксировать этот код для резервного копирования, отслеживания или обмена, вы можете использовать ветвление здесь. Определите ветку как свою стабильную ветку (обычно это магистраль в SVN и master в git) и отправляйте только экспериментальный код в другие ветки. Затем вы можете объединить ветки экспериментальных функций со стабильной веткой, когда они станут достаточно зрелыми для публикации. Такая конфигурация ветвления имеет дополнительное преимущество, позволяя вам отделить ваши эксперименты друг от друга, если вы того пожелаете.

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

Упаковка вашего стабильного кода

Один очень простой вариант — просто попросить пользователей проверить стабильную ветку из репозитория. Распространение таким способом далеко не неслыханно. Это все еще немного лучше, чем ваша текущая ситуация, поскольку вам больше не нужно вручную собирать все ваши файлы; однако вам может понадобиться немного документации. В качестве альтернативы вы можете использовать встроенную функцию управления версиями, чтобы проверить всю фиксацию в виде zip-файла или аналогичного (export в SVN и archive в git), если вы не хотите делать свой репозиторий общедоступным; они могут быть загружены в любом месте.

Если этого кажется недостаточно, и вы можете сэкономить время прямо сейчас, setuptools, вероятно, является хорошим решением этой проблемы. Это позволит вам сгенерировать колесо, содержащее ваш стабильный код. У вас может быть сценарий setup.py для каждого пакета кода, который вы хотите выпустить; сценарий setup.py определит, какие пакеты и модули следует включить. Вы должны управлять этим сценарием вручную, но если вы настроите его так, чтобы он включал целые пакеты и каталоги, а затем установили хорошие проектные соглашения для организации вашего кода, вам не придется изменять его слишком часто. Это также дает вашим конечным пользователям стандартный механизм установки вашего кода. Вы даже можете опубликовать его на PyPI, если хотите широко распространить его.

Если вы дойдете до использования инструментов настройки, вы также можете рассмотреть возможность использования сервера сборки, который может отслеживать новые коммиты и запускать сценарии для переупаковки и потенциальной публикации вашего кода.

person jpmc26    schedule 17.04.2017
comment
Спасибо за ваш подробный ответ. (1) Контроль исходного кода: я уже использую git, ветвление и т. д. Я думаю, проблема в том, что мне нужно различать стабильный код (который большой и имеет много несвязанных вещей, но проходит тесты) и код выпуска, который включает только необходимые модули, необходимые для запуска некоторых функций. Я ищу автоматизированный способ создания этой ветки релиза из стабильного кода. (2): Упаковка. Я посмотрю, могут ли здесь помочь setuptools, я не думал об этом. - person Peter; 19.04.2017
comment
@Peter Это стоящий вопрос, чтобы спросить, сколько проблем вы готовы пройти. Насколько сложно вашим пользователям проверить всю ветку и просто удалить то, что им не нужно? У вас достаточно широкое использование, что дополнительные усилия с их стороны были бы слишком большими? Возможно, пока достаточно задокументировать, что требуется, а что можно выбросить. Если вы решите, что этого недостаточно, я думаю, что setuptools будет вашим лучшим выбором. Как широко используемый инструмент, он очень хорош в том, что он делает, и вы найдете больше всего информации в Интернете, и мне кажется, что он хорошо подходит для вашей проблемы. - person jpmc26; 19.04.2017
comment
@Peter Тем не менее, если вы пойдете по пути setuptools, вам может потребоваться реорганизовать свои пакеты и модули, чтобы упростить упаковку. Это неплохо, но вы должны знать, что для того, чтобы найти организацию, которая работает гладко, могут потребоваться некоторые усилия и небольшие пробы и ошибки. В любом случае удачи вашему проекту. знак равно - person jpmc26; 19.04.2017
comment
Одна из проблем заключается в том, что кодовая база является частной, и все должно быть одобрено до публичного выпуска. Я посмотрю больше на то, как setuptools может помочь с этим. Спасибо. - person Peter; 19.04.2017
comment
@Peter Как я уже упоминал, вы можете распространять выпуски, используя git archive, если вы не хотите делать репозиторий общедоступным. Использование setuptools для создания пакетов, конечно же, избавляет от необходимости делать репозиторий доступным напрямую. - person jpmc26; 13.02.2018

Итак, в конце концов, чтобы решить нашу проблему, я сделал инструмент под названием public-release, который собирает все зависимости для модуля, который вы хотите выпустить, выбрасывает их в отдельный репозиторий со сценариями установки и всем остальным, чтобы ваш код можно было легко запустить позже.

person Peter    schedule 13.06.2017

К сожалению, динамические возможности Python делают это вообще невозможным. (Например, вы можете вызывать функции по именам, взятым из произвольного источника.)

Вы можете думать наоборот, что означает, что вы должны удалить все неиспользуемые части кода.

Согласно этот вопрос PyCharm делает не поддерживать это. Пакет vulture обеспечивает функцию обнаружения мертвого кода.

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

person Imre Piller    schedule 17.04.2017

В PyCharm вы можете выбрать код, который хотите переместить в новый модуль, и в главном меню выбрать - Рефакторинг -> Копировать (у меня F6, но я не могу вспомнить, является ли это настраиваемым ярлыком). Это дает вам возможность скопировать код в новый (или существующий файл) в каталог по вашему выбору. Он также добавит весь соответствующий импорт.

person Mat    schedule 07.11.2016
comment
Это не то, что я ищу. Я хочу скопировать функцию и все функции, которые она использует, в один файл, чтобы выпустить его как отдельный проект. - person Peter; 07.11.2016