如何使用可选导入进行类型提示?

17

当使用可选导入时,将包仅作为我想要它成为我的软件包的可选依赖项而在函数内导入时,有没有一种方法可以将函数的返回类型标注为属于此可选依赖项之一的类呢?

pandas作为可选依赖项的简单示例:

def my_func() -> pd.DataFrame:                                                  
    import pandas as pd                                                         
    return pd.DataFrame()                                                       

df = my_func()
在这种情况下,由于import语句位于my_func内部,因此这段代码会不出所料地引发以下错误:

NameError: name 'pd' is not defined

如果改用字符串字面量类型提示,则如下所示:i.e.
def my_func() -> 'pd.DataFrame':                                                
    import pandas as pd                                                         
    return pd.DataFrame()                                                       

df = my_func()

现在可以执行该模块,但mypy会抱怨:

错误:名称 'pd' 未定义

我如何使该模块成功执行并保留静态类型检查功能,同时又可以选择性地进行导入?

4个回答

14
尝试将您的导入放置在文件顶部的if typing.TYPE_CHECKING语句内。该变量始终为false,但出于类型提示目的,它被视为始终为true。
例如:
# Lets us avoid needing to use forward references everywhere
# for Python 3.7+
from __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    import pandas as pd

def my_func() -> pd.DataFrame:  
    import pandas as pd                                                 
    return pd.DataFrame()

你也可以使用if False:,但我认为这会让别人更难理解。

需要注意的是,这意味着尽管 pandas 在运行时可以是可选依赖项,但对于类型检查来说仍然是必需的。

另一个选项是使用mypy的--always-true--always-false标志。这将使您对代码的哪些部分进行类型检查具有更细粒度的控制。例如,您可以像这样做:

try:
    import pandas as pd
    PANDAS_EXISTS = True
except ImportError:
    PANDAS_EXISTS = False

if PANDAS_EXISTS:
    def my_func() -> pd.DataFrame:                                                   
        return pd.DataFrame()

...然后运行 mypy --always-true=PANDAS_EXISTS your_code.py,假设已经导入pandas,并将其用于类型检查;运行 mypy --always-false=PANDAS_EXISTS your_code.py,假设未导入pandas并用于类型检查。

这有助于发现您在不需要使用pandas的函数中意外地使用了需要它的函数的情况--尽管需要注意的是(a)这只是mypy的解决方案,(b)在库中仅有时存在的函数可能会对最终用户造成困惑。


在您提出的第一个解决方案中,布尔常量应该是 TYPE_CHECKING。在这种情况下,由于未导入 pandas ,代码将在运行时失败。这是否仍需要在 my_func 中导入? - dspencer
1
@dspencer -- 很好的发现。我用TYPE_CHECKING替换了TYPE_HINTING,并在第一个示例中添加了缺失的导入。 - Michael0x2a
一个有诱惑力的改进是使用 PANDAS_EXISTS = bool(importlib.util.find_spec('pandas')) 或更糟糕的 PANDAS_EXISTS = not not importlib.util.find_spec('pandas'),其中find_spec返回 importlib.machinery.ModuleSpec 或 None。当然,错误捕获看起来不太优雅,但完全符合PEP20(Python之禅)的指南,因为EAFP是其关键原则,另外使用importlib.util不太容易理解(与PEP8、可读性相比),尤其是如果已经安装了pandas但由于缺少依赖项而无法导入它,则在导入时会引发 ImportError,但是 find_spec会毫不知情。 - Matteo Ferla

1
这是我暂时使用的解决方案,似乎在PyCharm的类型检查器中可以工作,但我还没有尝试MyPy。
from typing import TypeVar, TYPE_CHECKING

PANDAS_CONFIRMED = False
if TYPE_CHECKING:
    try:
        import pandas as pd
        PANDAS_CONFIRMED = True
    except ImportError:
        pass 

if PANDAS_CONFIRMED:
    DataFrameType = pd.DataFrame
else:
    DataFrameType = TypeVar('DataFrameType')

def my_func() -> DataFrameType:  
    import pandas as pd                                                 
    return pd.DataFrame()

这样做的好处是始终定义函数(因此,如果有人运行调用my_func的代码,他们将得到一个信息丰富的ImportError而不是误导性的AttributeError)。即使没有安装pandas,这也总是提供某种类型提示,而不会在运行时过早地尝试导入pandas。if-else结构使得PyCharm将一些DataFrameType实例视为Union[DataFrame, DataFrameType],但它仍然提供适合DataFrame的linting信息,并且在某些情况下,例如my_func的输出,它会推断DataFrameType实例将始终是DataFrame。


0
遇到了类似的问题,通过使用Any对包进行类型提示,并在实际导入之前将其设置为None来解决。
from typing import Any

OpenAIResource: Any = None
try:
    from promptchain_ext.openai.resource import OpenAIResource
except ImportError:
    pass

0
另一种可以避免与某些代码检查工具(例如Pylance)出现问题的方法是这样的:
from typing import Any, TYPE_CHECKING

DataFrame = Any

if TYPE_CHECKING:
    try:
        from pandas import DataFrame

    except ImportError:
        pass

DataFrameType = TypeVar("DataFrameType", bound=DataFrame)

def my_func() -> DataFrameType:  
    import pandas as pd                                                 
    return pd.DataFrame()

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