Python:如何以单个文件的形式获取函数及其所有依赖项?

11
我正在处理一个庞大的 Python 代码库,它不断地增长。这不是一个单一的应用程序,更像是一堆共享某些公共代码的实验。
偶尔,我想发布一个特定实验的公共版本。我不想发布整个可怕的代码库,只想发布运行特定实验所需的部分。因此,基本上我希望有一些工具可以遍历所有导入的内容,并复制调用的任何函数(或至少导入的所有模块)到一个单独的文件中,以便我可以发布为演示版。当然,我只想对当前项目中定义的文件执行此操作(而不是类似 numpy 这样的依赖包)。
我现在正在使用 PyCharm,但没有找到这个功能。是否有任何工具可以做到这一点?
编辑:我创建了 public-release 包来解决这个问题。给定一个主模块,它遍历依赖模块并将它们复制到一个新的存储库中。

您所有的定制模块是否在一个公共目录(或公共目录的子目录)中?并且您是否愿意只将所有所依赖的模块与您的脚本一起打包到单个zip文件中,或者您需要从这些模块中只提取相关代码?通过sys.modules并查找位于特定目录或目录下使用的模块将会相对容易,但提取您所需的子部分则会更加困难。 - Matthias Fripp
另一个选择是在记录每个函数调用的分析工具下运行实验。然后,您可以使用脚本找到每个模块文件中每个函数的代码。如果您通常使用 from mymodule import func 导入函数,并且不使用任何全局变量或重复任何函数名称,则可能可以安全地将所有函数收集到单个脚本中。如果您通常使用 import mymodule 然后 mymodule.func(),那么您可以让脚本创建每个模块的 shell 版本,只包含相关函数。 - Matthias Fripp
5个回答

7

最终,为了解决我们的问题,我做了一个叫做public-release的工具,它会收集想要发布的模块的所有依赖项,将它们放入一个单独的仓库中,并提供设置脚本等功能,以便后续可以轻松运行您的代码。


7

如果您只想要模块,可以运行代码并通过sys.modules在您的包中查找任何模块。

要将所有依赖项移动到PyCharm中,您可以创建一个宏,将突出显示的对象移动到预定义文件中,并将宏附加到键盘快捷方式,然后快速递归地移动任何项目内导入。例如,我创建了一个名为export_func的宏,将函数移动到to_export.py中,并添加了F10的快捷方式:

Macro Actions

给定一个要移动的函数,例如:

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)]))

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


太好了,这非常有帮助,sys.modules 的提示很不错。非常感谢。除非出现奇迹般的答案,否则将授予悬赏。 - Peter
如果我理解正确的话,这是否意味着您只是手动点击每个函数并告诉PyCharm将其复制? - jpmc26
是的,看起来是这样。虽然完全自动化的“依赖关系爬取和复制”可能是最好的选择,但似乎这是最快的替代方案。 - Peter

6
把所有的代码都塞到一个模块里并不是一个好主意。一个很好的例子是,如果你的实验依赖于两个具有相同函数名称但定义不同的模块之一,使用单独的模块很容易区分它们;如果把它们放在同一个模块中,编辑器就必须进行某种 hacky 函数重命名(例如,在旧模块名称前面添加它们),如果模块中的其他函数调用了具有冲突名称的函数,则情况会变得更糟。你实际上需要完全替换模块作用域机制才能做到这一点。
构建模块依赖列表也是一个非常棘手的任务。考虑一个依赖于依赖于 numpy 模块的实验。你几乎肯定希望你的最终用户实际安装 numpy 包而不是捆绑它,因此现在编辑器必须有某种方法来区分哪些模块应包含在内,哪些模块你希望以其他方式安装。除此之外,你还必须考虑像一个函数在行内导入模块和其他非正常情况等。
你对你的编辑器要求太多了。你确实有两个问题:
1. 将实验代码与发布准备好的代码分离。 2. 打包稳定代码。
将实验代码与发布准备好的代码分离
源代码控制是你第一个问题的答案。这将允许你在本地机器上创建任何你想要的实验性代码,只要你不提交它,你就不会污染你的代码库。如果你想要提交此代码进行备份、跟踪或共享目的,你可以在此处使用分支。将一个分支标识为你的稳定分支(通常是 SVN 中的 trunk 和 git 中的 master),并且仅将实验性代码提交到其他分支。然后,当它们足够成熟以发布时,你可以将实验性功能分支合并到稳定分支中。这样的分支设置还有一个额外的好处,即允许你隔离你的实验。
托管在服务器上的源代码控制系统通常会使事情变得更简单、更安全,但如果你是唯一的开发人员,你仍然可以在没有服务器的情况下本地使用 git。如果你不是唯一的开发人员,那么托管在服务器上的存储库也使协调变得更容易。
打包稳定代码
一个非常简单的选择是告诉你的用户从存储库中检出稳定分支。这种方式的分发并不罕见。这比你目前的情况要好一点,因为你不再需要手动收集所有文件;但你可能需要做一些文档工作。你也可以使用你的源代码控制内置的功能将整个提交检出为 zip 文件或类似文件(在 SVN 中是导出,在 git 中是归档),如果你不想公开你的存储库,这些文件可以上传到任何地方。
如果你觉得这还不够,并且现在有时间的话,setuptools 可能是解决这个问题的好方法。它可以生成一个包含稳定代码的 wheel 文件。你可以为想要发布的每个代码包编写一个 setup.py 脚本;setup.py 脚本会确定需要包含哪些包和模块。虽然你需要手动管理这个脚本,但是如果你将它配置为包含整个包和目录并建立良好的项目规范来组织你的代码,就应该不需要经常更改了。这样做的好处是为你的最终用户提供了一个标准的安装机制。如果你愿意共享它,甚至可以在 PyPI 上发布它。
如果你采用 setuptools,可能还需要考虑使用构建服务器,它可以捕捉新的提交并运行脚本来重新打包和发布你的代码。

1
感谢您详细的回复。(1)源代码控制:我已经使用git、分支等。我认为问题在于我需要区分“稳定”的代码(它很大,有很多无关的东西,但是通过了测试)和“发布”的代码,后者只包括运行某些功能所需的必要模块。我正在寻找一种自动化的方法来从稳定的代码生成这个“发布”分支。(2)打包。我会研究一下setuptools是否可以帮助解决这个问题,我之前没有想到过。 - Peter
@Peter 这是一个值得问的问题,你愿意付出多少麻烦。对于你的用户来说,检查整个分支并删除他们不需要的东西有多难?你的使用范围是否足够广泛,以至于他们额外的努力会太多?现在记录所需和可以丢弃的内容可能已经足够了。如果你确定这还不够,我认为setuptools将是你最好的选择。作为一个广泛使用的工具,它非常擅长它所做的事情,并且你会在网上找到最多的信息,而且听起来它很适合你的问题。 - jpmc26
@Peter 话虽如此,如果你选择使用setuptools,你可能需要重新组织你的包和模块以使打包更简单。这并不是什么坏事,但你应该意识到这可能需要一些努力和一点试错来找到一个运作顺畅的组织方式。无论如何,祝你的项目好运。=) - jpmc26
一个问题是代码库是私有的,发布前必须经过批准。我将进一步研究setuptools如何帮助解决这个问题。谢谢。 - Peter
@Peter 正如我所提到的,如果您不希望使存储库本身公开可访问,可以使用 git archive 来分发发布版本。当然,使用 setuptools 生成软件包也可以避免必须直接提供存储库的需要。 - jpmc26

1
很不幸,Python 的动态特性使其总体上不可能实现。(例如,你可以通过来自任意来源的名称调用函数。)
你可以从反面思考,也就是删除代码中所有未使用的部分。
根据 这个问题 ,PyCharm 不支持此功能。vulture 包提供了死代码检测功能。
因此,我建议您复制项目,并将所需的函数收集到一个模块中。之后,检测演示代码中的所有未使用部分并将其删除。

0
在PyCharm中,您可以选择要移动到新模块的代码,然后从主菜单中选择“重构”->复制(我的是F6,但我不记得那是否是自定义快捷键)。这将使您能够将代码复制到您选择的目录中的新文件(或现有文件)。它还会添加所有相关的导入。

这不是我要找的。我想将一个函数及其使用的所有函数复制到单个文件中,以作为一个独立项目发布。 - Peter

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接