Python: 在单个文件中获取函数和所有依赖项的好方法?

Python: Good way to get function and all dependencies in a single file?

我正在开发一个庞大的 Python 代码库,它会不断发展壮大。它不是一个单一的应用程序——更多的是共享一些通用代码的一堆实验。

每隔一段时间,我想发布一个给定实验的 public 版本。我不想发布整个糟糕的代码库,只发布 运行 给定实验所需的部分。所以基本上我想要一些东西来遍历所有导入并将调用的任何函数(或至少所有导入的模块)复制到一个文件中,我可以将其作为演示发布。我当然只想对当前项目中定义的文件执行此操作(而不是像 numpy 这样的依赖包)。

我现在正在使用 PyCharm,但一直找不到该功能。有什么工具可以做到这一点吗?

编辑:我创建了 public-release 包来解决这个问题。给定一个主模块,它会爬取依赖模块并将它们复制到一个新的 repo 中。

在 PyCharm 中,您可以 select 您希望从主菜单中移入新模块的代码 select - 重构 -> 复制(我的 F6,但我可以记住这是否是定制的快捷方式)。这使您可以选择将代码复制到您选择的目录中的新文件(或现有文件)。它还将添加所有相关的导入。

不幸的是,Python的动态特性使得一般情况下无法实现。 (例如,您可以通过来自任意来源的名称调用函数。)

你可以反其道而行之,也就是说,你应该删除所有未使用的代码部分。

根据this question PyCharm does not support this. The vulture包提供死代码检测功能。

因此,我建议将您收集所需功能的项目复制到一个模块中。之后,检测所有未使用的演示代码并删除它们。

将所有代码塞入一个模块中并不是一个好主意。一个很好的例子是,当您的一个实验依赖于两个对相同函数名称具有不同定义的模块时。使用单独的模块,您的代码很容易区分它们;要将它们塞进同一个模块中,编辑器将不得不进行某种骇人听闻的函数重命名(例如,在它们前面加上旧的模块名称或其他名称),如果模块中的其他函数调用该函数,情况会变得更糟与冲突的名称。您实际上必须完全替换模块范围机制才能执行此操作。

构建模块依赖项列表也是一项艰巨的任务。考虑进行一个依赖于 numpy 的模块的实验。你几乎肯定希望你的最终用户实际安装 numpy 包而不是捆绑它,所以现在编辑器必须有一些方法来区分要包含哪些模块以及你希望以其他方式安装哪些模块。最重要的是,你必须考虑一些事情,比如当一个函数在行中导入一个模块而不是在你的模块的顶部和其他非常规的情况下。

你对你的编辑要求太高了。你真的有两个问题:

  1. 将实验代码与发布就绪代码分开。
  2. 打包你的稳定代码。

分离你的实验代码

源代码控制 是您第一个问题的答案。这将允许您在本地计算机上创建您希望的任何实验代码,只要您不提交它,您就不会用实验代码污染您的代码库。如果您确实想要提交此代码以用于备份、跟踪或共享目的,您可以在此处使用分支。确定一个分支作为你的 stable 分支(通常在 SVN 中为 t运行k,在 git 中为 master),并且只将实验代码提交到其他分支。然后,当它们变得足够成熟可以发布时,您可以将实验性功能分支合并到稳定分支中。如果您愿意,这样的分支设置还有一个额外的好处,即允许您将实验彼此隔离。

服务器托管的源代码控制系统通常会使事情变得更简单和更安全,但如果您是唯一的开发人员,您仍然可以在没有服务器的情况下在本地使用 git。如果您不是唯一的开发人员,服务器托管的存储库还可以更轻松地与其他人进行协调。

打包你的稳定代码

要考虑的一个非常简单的选择是告诉您的用户从存储库中检查稳定分支。以这种方式分发绝非闻所未闻。这仍然比您当前的情况好一点,因为您不再需要手动收集所有文件;不过,您可能需要做一些文档。如果您不想,您也可以使用源代码管理的内置功能将整个提交签出为 zip 文件或类似文件(SVN 中的 export 和 git 中的 archive)公开您的存储库;这些可以上传到任何地方。

如果这还不够,您现在可以抽出时间,setuptools 可能是解决此问题的好方法。这将允许您生成一个包含稳定代码的轮子。您可以为每个要发布的代码包创建一个 setup.py 脚本; setup.py 脚本将确定要包含哪些包和模块。您必须手动管理此脚本,但如果您将其配置为包括整个包和目录,然后建立良好的项目约定来组织代码,则不必经常更改它。这也有利于为您的最终用户提供代码的标准安装机制。如果你想广泛分享它,你甚至可以在 PyPI 上发布它。

如果您使用设置工具,您可能还需要考虑一个 构建服务器,它可以接收新的提交并且可以 运行 脚本重新打包并可能发布您的代码。

如果您只想要模块,您可以只 运行 代码和一个新会话,然后 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))

to_export.py 中突出显示 factorize 并按 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_setget_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)]))

即使你有很多代码要复制,它也会非常快。

所以最后,为了解决我们的问题,我制作了一个名为 public-release 的工具,它收集您要发布的模块的所有依赖项,将它们放入一个单独的 repo 中,其中包含设置脚本和所有,让你以后的代码可以轻松运行。