找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2102

积分

0

好友

298

主题
发表于 昨天 07:56 | 查看: 4| 回复: 0

很多人刚学 Python 的时候都会问一句:为啥这个语言这么火,居然连个 main 函数都没有?不科学啊,C 有、C++ 有、Java 也有,怎么到 Python 这就不要了。

我当时第一次写 Python,也是习惯性地在编辑器里敲了个:

int main() {
    // ...
}

然后一愣:诶?这语法直接红了。

先想想:别的语言为什么“必须有” main?

咱先别急着吐槽 Python,先回忆下其他几门常见语言里,main 是个啥角色。

在 C / C++ / Java 这类“编译型 + 运行时”的语言里,大致流程是这样的:

  1. 先把源代码编译成某种“可执行东西”(机器码 / 字节码);
  2. 程序启动时,运行时系统需要一个固定入口
  3. 这个入口就是 main——你必须告诉运行时:“从这里开始跑”。

比如 C:

int main(int argc, char *argv[]) {
    // 程序从这里开始执行
}

Java:

public class App {
    public static void main(String[] args) {
        // 程序从这里开始执行
    }
}

对这几门语言来说,main语言规范里写死的入口,没有就启动不了,你写得再漂亮也白搭。这背后涉及到语言设计哲学和程序启动流程的根本差异。

那 Python 程序的入口到底在哪?

重点来了:Python 根本没打算搞一个“固定函数入口”。

Python的执行模型更像是:我就拿着这个文件,从上到下一行一行解释执行

你在命令行敲:

python app.py

解释器内部做的事,大致可以粗暴地理解成:

  1. 先把 app.py 当成一个“模块对象”加载进来;
  2. 给这个模块准备一个全局作用域(一个字典);
  3. 从文件第一行开始往下执行,把遇到的函数定义、类定义、变量,全都丢到这个模块的全局命名空间里;
  4. 文件跑完,这个模块就算“初始化完成”了。

也就是说:对 Python 来说,一个 .py 文件本身就是“入口”。 你在最外层写的任何语句(不是函数里的那种),都会在运行时被直接执行:

# app.py

print("程序开始了")

x = 10

print("x =", x)

运行:

python app.py

终端上就会老老实实输出这两行,因为解释器真的就是“从头到尾”把这几句跑了一遍。

从这个角度看,Python 的“入口函数”其实就是:整个模块顶层代码,根本不需要一个专门的 main() 来兜底。

那大家老说的 if __name__ == "__main__" 是干嘛的?

很多人一看到这句就本能抵触感:这不还是 main 吗?只是换了个写法。

其实这句不是“语言强制的入口”,而是一种习惯用法,或者说“约定俗成的写法”。

先看一个最常见的模板:

# app.py

def main():
    print("hello python main")

if __name__ == "__main__":
    main()

你运行:

python app.py

会执行 main(),没问题。

但关键在于:这句 if 是可以删掉的,语言不会因此报错;只是你就少了一个“只在直接运行时才执行的入口”。

要理解它的意义,得知道 __name__ 到底是啥。

  • 每个 .py 文件在被加载成模块时,都会有个 __name__
  • 如果它是被别的模块 import 进来的,__name__ 等于模块名,比如 utils.math_utils
  • 如果它是被 直接当脚本运行 的那个文件,那它的 __name__ 就会被设置成 "__main__"

所以这句判断:

if __name__ == "__main__":
    main()

翻译成人话就是:

“只有当这个文件是被用户直接运行的时候,我才调 main();如果只是被别的地方 import,就别动。”

为什么要这么干?为了一份代码两种用法:既能当脚本跑,也能当库被复用

一个小例子:既是脚本又是库,怎么写最舒服?

比如现在你写了个图片压缩小工具,既想:

  • 直接 python compress.py input.jpg output.jpg 命令行用;
  • 又希望别的项目能通过 import compress 调你封装的函数。

用刚才那个模式就很顺:

# compress.py
from PIL import Image
import sys
from pathlib import Path

def compress_image(src, dst, quality=60):
    img = Image.open(src)
    img.save(dst, optimize=True, quality=quality)
    print(f"压缩完成: {src} -> {dst}")

def main():
    if len(sys.argv) < 3:
        print("用法: python compress.py <源文件> <目标文件> [质量(0-100)]")
        return

    src = Path(sys.argv[1])
    dst = Path(sys.argv[2])
    quality = int(sys.argv[3]) if len(sys.argv) > 3 else 60

    compress_image(src, dst, quality)

if __name__ == "__main__":
    main()

别人如果只想复用压缩逻辑:

# other_project.py
from compress import compress_image

compress_image("a.jpg", "b.jpg", quality=80)

这时候 compress.py 里的 main() 不会被执行,因为它被 import 进来的时候,__name__ 不是 "__main__"

所以你会发现:

  • 真正的业务入口逻辑可以写在 main() 里;
  • 但这个 main 完全是你自己取名,也可以叫 runclistart,语言不管;
  • if __name__ == "__main__" 只是一个“开关”:决定“直接运行时多做一件事”。

这和 C / Java 的那种“必须给我一个 main,不然我不启动”完全不是一个概念。

Python 为什么干脆就不规定“必须有 main”?

粗暴一点说,因为 Python 一开始定位就比较“脚本化、交互化”,它的哲学更偏:

“你写啥我就帮你从上到下跑啥,别让我多记一个固定名字。”

如果强行设一个固定入口函数,比如规定“必须有 def main()”,就会有几件事变得很累:

  1. 所有小脚本都得套一层模板: 原本一行 print(“hello”) 的事,非得写成:

    def main():
        print(“hello”)
    
    if __name__ == “__main__”:
        main()
  2. 交互式编程体验会变差: Python 的一大优势是 REPL(解释器里一行一行试),你在那种场景下是没有 main 一说的; 有了固定 main,概念上会很割裂。
  3. 模块级代码就不好用了: 现在你可以在模块顶层做一些初始化、注册、日志配置等等,写完即生效; 如果强制 main,大家就会为了“入口要干净”把这些东西塞进 main(),反而绕了一圈。

更重要的一个点:Python 的“程序 = 若干模块的组合”这件事,被设计得非常彻底

  • 每个模块都可以当脚本跑;
  • 每个模块又都可以被 import 成库;
  • 解释器只需要知道“从哪个模块开始跑”,而不是“从哪个函数开始”。

启动方式也很灵活,比如:

python app.py         # 从文件开始
python -m my_package  # 从包里的 __main__.py 开始

当你用 python -m my_package 时,其实是执行了包里的 __main__.py 这个模块,就相当于“这个包自己定义了一个入口脚本”。

my_package/__main__.py 里面爱写不写 main(),完全是你的自由。

那要不要自己“人为约定”一个 main 函数?

说句实话,我自己写项目的时候,还是挺喜欢搞一个 main 的,但那真的只是“架构层面的约定”,跟语言无关。

比如稍微正式一点的项目,我会搞成这样:

# app/main.py
import asyncio
from .config import settings
from .server import start_http_server

async def main():
    print(“启动配置:”, settings.model_dump())
    await start_http_server(settings.host, settings.port)

if __name__ == “__main__”:
    asyncio.run(main())

好处有:

  • 程序入口逻辑相对集中,方便你以后加启动日志、性能分析、异常捕获;
  • 对团队小伙伴来说,“从哪看起”非常明确:就看 main.py 里的那个 main
  • 以后如果要改成命令行工具,用 argparse 或者 click 也很好挂。

比如稍微加个参数解析:

# app/main.py
import argparse

def parse_args():
    parser = argparse.ArgumentParser()
    parser.add_argument(“--port”, type=int, default=8000)
    parser.add_argument(“--debug”, action=“store_true”)
    return parser.parse_args()

def main():
    args = parse_args()
    if args.debug:
        print(“调试模式开启”)
    print(f“服务启动在端口 {args.port}“)
    # TODO: 真正启动服务

if __name__ == “__main__”:
    main()

你看,这种写法其实跟 C / Java 的 main 挺像的,但关键差别在:

  • 语言没逼你这么写,是你自己为了可读性和结构化做的设计;
  • 你完全可以按项目风格,把入口拆成多个层次、多个文件,不受“必须叫 main”限制。这种项目结构的组织思路,在编写和维护高质量技术文档时也非常重要。

__main__.py:项目“入口脚本”的另一种玩法

还有个经常被忽略的点:Python 对包也提供了一种“入口约定”,就是 __main__.py

假设你有结构:

my_app/
    __init__.py
    __main__.py
    api.py
    models.py

__main__.py 写成这样:

# my_app/__main__.py
from .api import run_server

def main():
    print(“通过 -m 启动 my_app”)
    run_server()

if __name__ == “__main__”:
    main()

这时候,你可以直接在命令行里:

python -m my_app

解释器会自动去找 my_app.__main__ 模块来执行。 也就是说,包级别的“入口脚本”是通过 __main__.py 这个文件名约定好的,而不是通过某个函数名。

所以 Python 的思路一直是:用模块 / 文件名做入口约定,而不是函数名

Python 不是“没有 main”,而是“不需要规定 main”

如果一定要把上面这堆话压成一句话,大概就是:

Python 不是做不到 main,而是它把“程序入口”这件事设计成了“模块级的、文件级的约定”,而不是“强制一个叫 main 的函数”。

你可以:

  • 完全不写 main(),在文件顶层直接写脚本逻辑,小工具写起来飞快;
  • 也可以自己规整一个 main(),加上 if __name__ == “__main__”,让项目入口清晰;
  • 再高级一点,用 __main__.pypython -m,把整个包当成一个“可执行程序”。

这些都是 Python 给你的灵活性。

所以如果以后还有人问你:“Python 怎么连个 main 函数都没有?” 你就可以很淡定地说一句:

“不是没有,是根本用不着强制一个。你想写就自己约定一个,不想写就从文件第一行开始跑,这就是 Python 的风格。”

行,先这样,等哪天你写 CLI 工具或者 Web 服务的时候,咱再一起聊聊怎么设计一个“好用的 main 入口函数”和项目结构。




上一篇:技术总监因项目被撤离职?从职场八卦聊到LeetCode缺失区间算法
下一篇:企业级单点登录SSO:核心架构、原理与登录流程详解
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-11 19:43 , Processed in 0.196800 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表