每当你尝试从其他文件夹导入代码时,都会输入一个空的__init__.py
。它几乎已经成为大多数 Python 开发人员(无论是初学者还是高手)的肌肉记忆。但我们真的知道吗__init__.py
?
在这篇博文中,云朵君将和大家一起深入了解__init__.py
非空的工作原理以及它如何以三种方式__init__.py
帮助我们开发 Python 程序的。
什么是__init__.py
?
__init__.py
是一个 Python 文件,它告诉 Python 解释器该文件夹应该被视为一个包。
与 C 和 C++ 等编译语言不同,在使用之前必须预编译依赖项,而 Python 的解释器会即时获取依赖项。要向 Python 发出信号,告知某个文件夹包含将在其他地方使用的代码,可以通过__init__.py
。__init__.py
将你的文件夹变成可导入的 Python 包。
在 Python 中创建一个类时,你通常还需要创建一个__init__函数。这将定义如何构造对象,并且是创建类的对象时首先要执行的操作。类似地,__init__.py
是 Python 包的构造函数。每当导入包时,它都会首先执行。空__init__.py
表示__init__
Python 包的构造函数方法为空。这很好,但这并不意味着我们不能用它做更多的事情。
谨慎使用 __init__.py
由于__init__.py
是 Python 包的构造函数,因此我们需要谨慎放置__init__.py的位置。
如果我们有一个名为的文件夹datetime
,其中有一些用于处理日期格式的自定义实用程序函数:
# ./datetime/utils.py
def increment_date(date: int, increment: int) -> int:
"""以毫秒为单位创建时间戳"""
return date + increment
然后我们添加一个__init__.py
,这样就可以在main.py中导入代码:
myfolder
│
├── datetime
│ ├── __init__.py
│ └── utils.py
└── main.py
# main.py
from datetime.utils import increment_date
from datetime import datetime
def main():
timestamp = datetime.timestamp(datetime(2024, 2, 27))
print(increment_date(timestamp, increment=10))
if __name__ == "__main__":
main()
如果运行main.py会发生什么?
ImportError: cannot import 'datetime' from 'datetime'
通常,Python 解释器将按照(1)本地目录、(2)标准库和(3)已安装的 Python 模块的顺序对包发现过程进行优先排序。
通过将__init__.py放入名为datetime的文件夹,我们覆盖了名为的 Python 标准库datetime
,导致我们的导入语句datetime.datetime
失败。
避免此问题仅需要一个简单的修复:不要将__init__.py放在与其他 Python 标准库或已安装的 Python 模块同名的任何文件夹下。
了解了它的工作原理后__init__.py
,可以用它做一些更奇特的事情!
定义Package级别配置
想象一下,如果代码中的所有 Python 文件都共享类似的配置:日志记录级别、常量、环境变量等。你不必将其设置在包中每个 Python 文件的顶部,而是可以将它们全部包含在__init__.py
.
# myfolder/__init__.py
import os
import logs
# 为整个包加载环境变量
OPENAI_API_KEY = os.getenv( "OPENAI_API_KEY" , None )
# 设置包级别常量
MODEL = "gpt-4"
# 你还可以设置日志配置。
logging.basicConfig(level=logging.INFO)
# 添加可在整个包中方便重用的自定义装饰器
def foo(func):
@wraps(func)
def wrapper(*args, **kwargs):
logging.info(f"Calling function: {func.__name__}")
result = func(*args, **kwargs)
logging.info(f"Function {func.__name__} completed")
return result
return wrapper
在你的代码库中,你可以执行以下操作:
#myfolder/bar.py
#从你的包__init__.py 相对导入来自
from . import OPENAI_API_KEY, MODEL, foo
from openai import OpenAI
from typing import Optional
client = OpenAI()
@foo
def chat_with_openai(prompt: str, llm: Optional[str] = None) -> Any:
"""Send a prompt to an LLM and return the response"""
llm = llm or MODEL
return client.chat.completions.create(
model=llm,
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": prompt}
]
)
基于上述内容,你还可以执行以下操作:
功能标记 高级记录器配置(例如,将所有日志保存到文件夹) 设置默认参数(例如,如果未提供输出目录,则保存到 X) 使用情况监控(例如,将所有函数调用发送到你的云实例的自定义装饰器) 主题定制/定义(例如语言区域、明/暗主题) 使用自定义装饰器集中处理错误
简化包导入
随着代码库变得越来越复杂,你将添加更多类和函数。按照经验法则将代码分成内聚单元,你最终可能会得到以下结果:
foo
│
├── llm_email_responder
│ ├── __init__.py
│ ├── base_email_responder.py
│ ├── mail_chimp_responder.py
│ ├── zoho_mail_responder.py
│ └── send_grid_responder.py
└── __init__.py
如果想使用 MailChimp 的 LLM 电子邮件回复器:
from foo.llm_email_responder.mail_chimp_responder import MailChimpResponder
最终得到了多行看起来非常相似的导入语句,此时需要记住代码库的内部结构。
__init__.py
可用于管理导入并简化开发人员体验,同时屏蔽代码库中更私密/较低级别的部分。
# ./foo/llm_email_responder/__init__.py
# 跳过最低级别的基本电子邮件回复器,
# 因为用户不需要像使用更高级别的实现那样频繁地使用它。
from .mail_chimp_responder import MailChimpResponder
from .zoho_mail_responder import ZohoResponder
from .send_grid_responder import SendGridResponder
__all__ = [
"MailChimpResponder" ,
"ZohoResponder" ,
"SendGridResponder" ,
]
通过上述操作,你现在可以__init__.py访问MailChimpResponder:
from foo.llm_email_responder import MailChimpResponder
虽然这是一种在开发人员友好界面中公开低级代码的便捷方法,但它也会增加维护工作量,因为对低级代码库的任何更改都必须与__init__.py 的更改相匹配。我们还必须评估设计模式,以确保公开低级代码库形成一个有凝聚力的架构。
Langchain 经常使用这个技巧来确保我们不需要记住错综复杂的内部代码结构。
单例模式
你还可以使用__init__.py 强制实施单例设计模式。这将强制整个包使用在 __init__.py 中实例化的相同实例。
如果你的软件包需要建立与远程服务的连接、加载大型数据集或任何需要繁重工作量的先决条件,那么这可能会非常方便。
假设我需要连接到 Gemini Pro 来处理我处理财务报告的所有 LLM 需求。
foo
│
├──report_analysisr
│ ├── __init__.py
│ ├──outline_extraction.py
│ └──entity_extraction.py
└── __init__.py
# .foo/report_analyser/outline_extraction
from langchain_google_vertexai import VertexAI
def extract_outline ( page: str , **kwargs ) -> str :
llm = VertexAI(
model_name= "gemini-pro" , location= "europe-west2" , **kwargs
)
prompt_template = """...<你的提示模板在此>..."""
prompt = prompt_template.format_prompt(page=page)
return llm.invoke(prompt)
# .foo/report_analyser/entity_extraction
from langchain_google_vertexai import VertexAI
def extract_entities ( page: str , **kwargs ) -> str :
llm = VertexAI(
model_name= "gemini-pro" , location= "europe-west2" , **kwargs
)
prompt_template = """...<你的提示模板在此>..."""
prompt = prompt_template.format_prompt(page=page)
return llm.invoke(prompt)
# .foo/report_analyser/entity_extraction
from foo.report_analyser.outline_extraction import extract_outline
from foo.report_analyser.entity_extraction import extract_entities
def main ():
page = "此处为文档的长页"
# 将创建与 VertexAI 的连接以提取大纲
outline = extract_outline(page,temperature= 0.2 )
# 将创建与 VertexAI 的另一个连接以提取实体
entities = extract_entities(page,temperature= 0.2 )
每次运行操作时,代码库都必须与 VertexAI 建立新连接,从而产生不必要的网络开销。如果需要对 Gemini Pro 使用相同的配置,你可以改为执行以下操作:
# .foo/report_analyser/__init__.py
from langchain_google_vertexai import VertexAI
DEFAULT_LLM = VertexAI(
model_name="gemini-pro",
location="europe-west2",
temperature=0.2,
)
# .foo/report_analyser/outline_extraction
from . import DEFAULT_LLM
from typing import Dict
def extract_outline(
page: str,
custom_llm_parameters: Optional[Dict] = None
) -> str:
if custom_llm_parameters is None:
llm = DEFAULT_LLM
else:
llm = VertexAI(
model_name="gemini-pro",
location="europe-west2",
**custom_llm_parameters
)
prompt_template = """...<你的提示模板在此>..."""
prompt = prompt_template.format_prompt(page=page)
return llm.invoke(prompt)
如果你想要更好的代码配置,可以将创建 LLM 连接的部分拆分为单独的__init__.py文件,本质上是创建 LLM 连接管理器。将其与@lru_cache结合使用,可以更有效地处理自定义配置。
🏴☠️宝藏级🏴☠️ 原创公众号『数据STUDIO』内容超级硬核。公众号以Python为核心语言,垂直于数据科学领域,包括可戳👉 Python|MySQL|数据分析|数据可视化|机器学习与数据挖掘|爬虫 等,从入门到进阶!
长按👇关注- 数据STUDIO -设为星标,干货速递