为什么main()应该很短?


87

我已经进行了9年以上的编程工作,并且根据我的第一位编程老师的建议,我始终将我的main()函数保持得非常简短。

起初我不知道为什么。我只是听从而不理解,这让我的教授们非常高兴。

在获得经验之后,我意识到,如果我正确地设计了代码,则具有短main()函数的情况就会发生。编写模块化代码并遵循单一职责原则,使我的代码可以“分批”设计,并且main()无非是推动程序运行的催化剂。

快到几周前,我正在查看Python的源代码,然后找到了该main()函数:

/* Minimal main program -- everything is loaded from the library */

...

int
main(int argc, char **argv)
{
    ...
    return Py_Main(argc, argv);
}

是的python。短main()函数==良好的代码。

编程老师是对的。

想深入了解一下,我看了一下Py_Main。整个定义如下:

/* Main program */

int
Py_Main(int argc, char **argv)
{
    int c;
    int sts;
    char *command = NULL;
    char *filename = NULL;
    char *module = NULL;
    FILE *fp = stdin;
    char *p;
    int unbuffered = 0;
    int skipfirstline = 0;
    int stdin_is_interactive = 0;
    int help = 0;
    int version = 0;
    int saw_unbuffered_flag = 0;
    PyCompilerFlags cf;

    cf.cf_flags = 0;

    orig_argc = argc;           /* For Py_GetArgcArgv() */
    orig_argv = argv;

#ifdef RISCOS
    Py_RISCOSWimpFlag = 0;
#endif

    PySys_ResetWarnOptions();

    while ((c = _PyOS_GetOpt(argc, argv, PROGRAM_OPTS)) != EOF) {
        if (c == 'c') {
            /* -c is the last option; following arguments
               that look like options are left for the
               command to interpret. */
            command = (char *)malloc(strlen(_PyOS_optarg) + 2);
            if (command == NULL)
                Py_FatalError(
                   "not enough memory to copy -c argument");
            strcpy(command, _PyOS_optarg);
            strcat(command, "\n");
            break;
        }

        if (c == 'm') {
            /* -m is the last option; following arguments
               that look like options are left for the
               module to interpret. */
            module = (char *)malloc(strlen(_PyOS_optarg) + 2);
            if (module == NULL)
                Py_FatalError(
                   "not enough memory to copy -m argument");
            strcpy(module, _PyOS_optarg);
            break;
        }

        switch (c) {
        case 'b':
            Py_BytesWarningFlag++;
            break;

        case 'd':
            Py_DebugFlag++;
            break;

        case '3':
            Py_Py3kWarningFlag++;
            if (!Py_DivisionWarningFlag)
                Py_DivisionWarningFlag = 1;
            break;

        case 'Q':
            if (strcmp(_PyOS_optarg, "old") == 0) {
                Py_DivisionWarningFlag = 0;
                break;
            }
            if (strcmp(_PyOS_optarg, "warn") == 0) {
                Py_DivisionWarningFlag = 1;
                break;
            }
            if (strcmp(_PyOS_optarg, "warnall") == 0) {
                Py_DivisionWarningFlag = 2;
                break;
            }
            if (strcmp(_PyOS_optarg, "new") == 0) {
                /* This only affects __main__ */
                cf.cf_flags |= CO_FUTURE_DIVISION;
                /* And this tells the eval loop to treat
                   BINARY_DIVIDE as BINARY_TRUE_DIVIDE */
                _Py_QnewFlag = 1;
                break;
            }
            fprintf(stderr,
                "-Q option should be `-Qold', "
                "`-Qwarn', `-Qwarnall', or `-Qnew' only\n");
            return usage(2, argv[0]);
            /* NOTREACHED */

        case 'i':
            Py_InspectFlag++;
            Py_InteractiveFlag++;
            break;

        /* case 'J': reserved for Jython */

        case 'O':
            Py_OptimizeFlag++;
            break;

        case 'B':
            Py_DontWriteBytecodeFlag++;
            break;

        case 's':
            Py_NoUserSiteDirectory++;
            break;

        case 'S':
            Py_NoSiteFlag++;
            break;

        case 'E':
            Py_IgnoreEnvironmentFlag++;
            break;

        case 't':
            Py_TabcheckFlag++;
            break;

        case 'u':
            unbuffered++;
            saw_unbuffered_flag = 1;
            break;

        case 'v':
            Py_VerboseFlag++;
            break;

#ifdef RISCOS
        case 'w':
            Py_RISCOSWimpFlag = 1;
            break;
#endif

        case 'x':
            skipfirstline = 1;
            break;

        /* case 'X': reserved for implementation-specific arguments */

        case 'U':
            Py_UnicodeFlag++;
            break;
        case 'h':
        case '?':
            help++;
            break;
        case 'V':
            version++;
            break;

        case 'W':
            PySys_AddWarnOption(_PyOS_optarg);
            break;

        /* This space reserved for other options */

        default:
            return usage(2, argv[0]);
            /*NOTREACHED*/

        }
    }

    if (help)
        return usage(0, argv[0]);

    if (version) {
        fprintf(stderr, "Python %s\n", PY_VERSION);
        return 0;
    }

    if (Py_Py3kWarningFlag && !Py_TabcheckFlag)
        /* -3 implies -t (but not -tt) */
        Py_TabcheckFlag = 1;

    if (!Py_InspectFlag &&
        (p = Py_GETENV("PYTHONINSPECT")) && *p != '\0')
        Py_InspectFlag = 1;
    if (!saw_unbuffered_flag &&
        (p = Py_GETENV("PYTHONUNBUFFERED")) && *p != '\0')
        unbuffered = 1;

    if (!Py_NoUserSiteDirectory &&
        (p = Py_GETENV("PYTHONNOUSERSITE")) && *p != '\0')
        Py_NoUserSiteDirectory = 1;

    if ((p = Py_GETENV("PYTHONWARNINGS")) && *p != '\0') {
        char *buf, *warning;

        buf = (char *)malloc(strlen(p) + 1);
        if (buf == NULL)
            Py_FatalError(
               "not enough memory to copy PYTHONWARNINGS");
        strcpy(buf, p);
        for (warning = strtok(buf, ",");
             warning != NULL;
             warning = strtok(NULL, ","))
            PySys_AddWarnOption(warning);
        free(buf);
    }

    if (command == NULL && module == NULL && _PyOS_optind < argc &&
        strcmp(argv[_PyOS_optind], "-") != 0)
    {
#ifdef __VMS
        filename = decc$translate_vms(argv[_PyOS_optind]);
        if (filename == (char *)0 || filename == (char *)-1)
            filename = argv[_PyOS_optind];

#else
        filename = argv[_PyOS_optind];
#endif
    }

    stdin_is_interactive = Py_FdIsInteractive(stdin, (char *)0);

    if (unbuffered) {
#if defined(MS_WINDOWS) || defined(__CYGWIN__)
        _setmode(fileno(stdin), O_BINARY);
        _setmode(fileno(stdout), O_BINARY);
#endif
#ifdef HAVE_SETVBUF
        setvbuf(stdin,  (char *)NULL, _IONBF, BUFSIZ);
        setvbuf(stdout, (char *)NULL, _IONBF, BUFSIZ);
        setvbuf(stderr, (char *)NULL, _IONBF, BUFSIZ);
#else /* !HAVE_SETVBUF */
        setbuf(stdin,  (char *)NULL);
        setbuf(stdout, (char *)NULL);
        setbuf(stderr, (char *)NULL);
#endif /* !HAVE_SETVBUF */
    }
    else if (Py_InteractiveFlag) {
#ifdef MS_WINDOWS
        /* Doesn't have to have line-buffered -- use unbuffered */
        /* Any set[v]buf(stdin, ...) screws up Tkinter :-( */
        setvbuf(stdout, (char *)NULL, _IONBF, BUFSIZ);
#else /* !MS_WINDOWS */
#ifdef HAVE_SETVBUF
        setvbuf(stdin,  (char *)NULL, _IOLBF, BUFSIZ);
        setvbuf(stdout, (char *)NULL, _IOLBF, BUFSIZ);
#endif /* HAVE_SETVBUF */
#endif /* !MS_WINDOWS */
        /* Leave stderr alone - it should be unbuffered anyway. */
    }
#ifdef __VMS
    else {
        setvbuf (stdout, (char *)NULL, _IOLBF, BUFSIZ);
    }
#endif /* __VMS */

#ifdef __APPLE__
    /* On MacOS X, when the Python interpreter is embedded in an
       application bundle, it gets executed by a bootstrapping script
       that does os.execve() with an argv[0] that's different from the
       actual Python executable. This is needed to keep the Finder happy,
       or rather, to work around Apple's overly strict requirements of
       the process name. However, we still need a usable sys.executable,
       so the actual executable path is passed in an environment variable.
       See Lib/plat-mac/bundlebuiler.py for details about the bootstrap
       script. */
    if ((p = Py_GETENV("PYTHONEXECUTABLE")) && *p != '\0')
        Py_SetProgramName(p);
    else
        Py_SetProgramName(argv[0]);
#else
    Py_SetProgramName(argv[0]);
#endif
    Py_Initialize();

    if (Py_VerboseFlag ||
        (command == NULL && filename == NULL && module == NULL && stdin_is_interactive)) {
        fprintf(stderr, "Python %s on %s\n",
            Py_GetVersion(), Py_GetPlatform());
        if (!Py_NoSiteFlag)
            fprintf(stderr, "%s\n", COPYRIGHT);
    }

    if (command != NULL) {
        /* Backup _PyOS_optind and force sys.argv[0] = '-c' */
        _PyOS_optind--;
        argv[_PyOS_optind] = "-c";
    }

    if (module != NULL) {
        /* Backup _PyOS_optind and force sys.argv[0] = '-c'
           so that PySys_SetArgv correctly sets sys.path[0] to ''
           rather than looking for a file called "-m". See
           tracker issue #8202 for details. */
        _PyOS_optind--;
        argv[_PyOS_optind] = "-c";
    }

    PySys_SetArgv(argc-_PyOS_optind, argv+_PyOS_optind);

    if ((Py_InspectFlag || (command == NULL && filename == NULL && module == NULL)) &&
        isatty(fileno(stdin))) {
        PyObject *v;
        v = PyImport_ImportModule("readline");
        if (v == NULL)
            PyErr_Clear();
        else
            Py_DECREF(v);
    }

    if (command) {
        sts = PyRun_SimpleStringFlags(command, &cf) != 0;
        free(command);
    } else if (module) {
        sts = RunModule(module, 1);
        free(module);
    }
    else {

        if (filename == NULL && stdin_is_interactive) {
            Py_InspectFlag = 0; /* do exit on SystemExit */
            RunStartupFile(&cf);
        }
        /* XXX */

        sts = -1;               /* keep track of whether we've already run __main__ */

        if (filename != NULL) {
            sts = RunMainFromImporter(filename);
        }

        if (sts==-1 && filename!=NULL) {
            if ((fp = fopen(filename, "r")) == NULL) {
                fprintf(stderr, "%s: can't open file '%s': [Errno %d] %s\n",
                    argv[0], filename, errno, strerror(errno));

                return 2;
            }
            else if (skipfirstline) {
                int ch;
                /* Push back first newline so line numbers
                   remain the same */
                while ((ch = getc(fp)) != EOF) {
                    if (ch == '\n') {
                        (void)ungetc(ch, fp);
                        break;
                    }
                }
            }
            {
                /* XXX: does this work on Win/Win64? (see posix_fstat) */
                struct stat sb;
                if (fstat(fileno(fp), &sb) == 0 &&
                    S_ISDIR(sb.st_mode)) {
                    fprintf(stderr, "%s: '%s' is a directory, cannot continue\n", argv[0], filename);
                    fclose(fp);
                    return 1;
                }
            }
        }

        if (sts==-1) {
            /* call pending calls like signal handlers (SIGINT) */
            if (Py_MakePendingCalls() == -1) {
                PyErr_Print();
                sts = 1;
            } else {
                sts = PyRun_AnyFileExFlags(
                    fp,
                    filename == NULL ? "<stdin>" : filename,
                    filename != NULL, &cf) != 0;
            }
        }

    }

    /* Check this environment variable at the end, to give programs the
     * opportunity to set it from Python.
     */
    if (!Py_InspectFlag &&
        (p = Py_GETENV("PYTHONINSPECT")) && *p != '\0')
    {
        Py_InspectFlag = 1;
    }

    if (Py_InspectFlag && stdin_is_interactive &&
        (filename != NULL || command != NULL || module != NULL)) {
        Py_InspectFlag = 0;
        /* XXX */
        sts = PyRun_AnyFileFlags(stdin, "<stdin>", &cf) != 0;
    }

    Py_Finalize();
#ifdef RISCOS
    if (Py_RISCOSWimpFlag)
        fprintf(stderr, "\x0cq\x0c"); /* make frontend quit */
#endif

#ifdef __INSURE__
    /* Insure++ is a memory analysis tool that aids in discovering
     * memory leaks and other memory problems.  On Python exit, the
     * interned string dictionary is flagged as being in use at exit
     * (which it is).  Under normal circumstances, this is fine because
     * the memory will be automatically reclaimed by the system.  Under
     * memory debugging, it's a huge source of useless noise, so we
     * trade off slower shutdown for less distraction in the memory
     * reports.  -baw
     */
    _Py_ReleaseInternedStrings();
#endif /* __INSURE__ */

    return sts;
}

全能的好上帝...它足以沉没泰坦尼克号。

似乎Python做了“编程入门101”的技巧,只是将所有main()的代码移到了一个不同的函数中,该函数与“ main”非常相似。

这是我的问题:这段代码编写得很糟糕,还是有其他原因使main函数简短?

就目前而言,我认为这样做与将代码Py_Main()移回绝对没有区别main()。我在想这个吗?



38
@Luzhin,不。我并没有要求任何人查看Python的源代码。这是编程问题。
riwalk

3
TBH,有一半的代码是选项处理,并随时程序支持的选项很多的,你写一个自定义的处理器,这是你最终做...

7
@Star不,Programmers.SE也适用于最佳实践,编码样式等。实际上,这就是我访问该网站的目的。
Mateen Ulhaq 2011年

4
@Nim,我明白这是它做什么,但没有理由不把它写成options = ParseOptionFlags(argc,argv)其中optionsstruct包含变量Py_BytesWarningFlagPy_DebugFlag等等...
riwalk

Answers:


137

您不能main从库中导出,但是可以导出Py_Main,然后使用该库的任何人都可以在同一程序中使用不同的参数多次“调用” Python。那时,python成为库的另一个使用者,仅是库函数的包装器;它调用Py_Main像其他人一样。


1
有一个很好的答案。
riwalk

26
我想说您不能导入它可能更准确,@ Shoosh。C ++标准禁止从您自己的代码中调用它。此外,其链接是实现定义的。另外,从main有效的调用返回exit,通常您不希望库执行此操作。
罗伯·肯尼迪

3
@Coder,请参见C ++ 03§3.6.1/ 5:“其中的return语句main具有离开主函数…并exit以返回值作为参数进行调用的效果。” 另请参见§18.3/ 8,它解释了在调用时“具有静态存储持续时间的对象已销毁”和“所有打开的C流...已刷新” exit。C99具有相似的语言。
罗伯·肯尼迪

1
@Coder,是否exit离开main无关紧要。我们不在讨论的行为exit。我们正在讨论的行为main。并且的行为main 包括的行为exit,无论可能是什么。就是不希望导入和调用的原因main(如果这样做是可能的或允许的话)。
罗伯·肯尼迪

3
@Coder,如果从返回main不具有调用exit您的编译器的效果,则您的编译器未遵循标准。该标准规定这种行为main证明了存在一些特别的东西吧。特殊的main是从它返回具有调用的效果exit。(如何它最多也就是编译器作者的编译器可以简单地在函数尾声,破坏静态对象,调用插入代码,atexit程序,刷新文件,并终止程序-这又是不是你想要的东西在图书馆。)
Rob Kennedy

42

不是main应该不那么长,因为您应该避免任何函数都太长。main只是功能的特例。较长的功能很难使用,降低了可维护性,并且通常很难使用。通过main缩短函数(和)的长度,通常可以提高代码的质量。

在您的示例中,将代码移出根本没有任何好处main


9
黄金字可能是“重用”。long main不是很可重用。
S.Lott

1
@S-这是一个黄金字。另一个是天哪!多动症刚刚踢!!或通俗易懂地讲:易读。
Edward Strange

3
main()还具有其他函数没有的一些限制。
马丁·约克

1
main()也没有实际含义。您的代码都应该对另一位程序员有意义。我使用main来解析参数,仅此而已-如果超过几行,我甚至会委托它。
比尔K

@Bill K:好点,仅使用main()解析参数(并启动程序的其余部分)也符合单一职责原则。
Giorgio

28

main()简短的原因之一涉及单元测试。 main()是无法进行单元测试的一个函数,因此将大多数行为提取到可以进行单元测试的另一类中是很有意义的。这跟你说的一样

编写模块化代码并遵循单一职责原则,使我的代码可以“束缚”地进行设计,而main()仅仅是促使程序运行的催化剂。

注意: 我从这里得到了这个主意。


另一个好人。从来没有想过那方面。
riwalk

16

main长期坚持这不是一个好主意;与任何函数(或方法)一样,如果很长的话,您可能会错过重构的机会。

在上面提到的特定情况下,main它很短,因为将所有复杂性都排除在了其中Py_Main;如果您希望您的代码表现得像python shell,则可以直接使用该代码而无需多花钱。(必须这样考虑,因为如果您将其放入main库中,它将无法正常工作;如果您这样做,则会发生奇怪的事情。)

编辑:
澄清main一下,不能在静态库中,因为它没有显式链接,因此无法正确链接(除非您将它与引用的对象一起放在对象文件中,这简直太可怕了!)共享库通常被视为相似(同样,防止混淆),尽管在许多平台上,另一个因素是,共享库只是一个没有引导程序部分的可执行文件(其main只是最后一个也是最可见的部分) )。


1
简而言之,不要放在main图书馆里。它要么行不通,要么会使您非常困惑。但委托几乎所有的工作提高到一个函数在一个lib,这是经常明智的。
Donal Fellows

6

Main应该短路,原因与任何功能都应该短路的原因相同。人脑很难一次将大量未分区的数据保存在内存中。将其分解为逻辑块,以便其他开发人员(以及您自己!)易于消化和推理。

是的,您的示例非常糟糕且难以阅读,更不用说维护了。


是的,我一直怀疑代码本身很糟糕(尽管问题只涉及代码的位置,而不是代码本身)。恐怕我的Python视线会因此受到固有的破坏……
riwalk

1
@stargazer:我不知道代码本身很糟糕,只是代码的组织方式不适合人类使用。就是说,这里有很多“丑陋”的代码,它们运作良好并且表现出色。代码美并不是全部,尽管我们应该始终尽最大努力编写尽可能干净的代码。
Ed S.

嗯 对我来说,它们是相同的。干净的代码往往更稳定。
riwalk11年

该代码并不可怕,主要是存在切换情况和多个平台的处理。您究竟发现什么可怕?
Francesco

@Francesco:抱歉,从维护和可读性的角度来看,“糟糕”,而不是功能上的。
Ed S.

1

有些人喜欢50多个函数,它们什么都不做,而是将调用包装到另一个函数。我宁愿选择执行主程序逻辑的普通主函数。当然结构良好。

int main()
{
CheckInstanceCountAndRegister();
InitGlobals();
ProcessCmdParams();
DoInitialization();
ProgramMainLoopOrSomething();
DeInit();
ClearGlobals();
UnregisterInstance();
return 0; //ToMainCRTStartup which cleans heap, etc.
}

我看不出有什么理由将任何东西都包裹在包装纸中。

纯粹是个人品味。


1
因为它是文件的代码。您可以以这种方式编写代码,而无需(几乎)编写注释。而且,当您更改代码时,文档会自动进行:-)更改。
奥利弗·韦勒

1

最佳做法是使所有功能(而不只是主要功能)保持简短。但是,“短”是主观的,它取决于程序的大小和所使用的语言。


0

main除了编码标准外,不需要任何长度。main是任何其他函数,因此它的复杂度应低于10(或您的编码标准所说的话)。就是这样,其他任何事情都颇具争议。

编辑

main不应该短。或长。它应包括根据您的设计执行所需的功能,并遵守编码标准。

至于您问题中的特定代码-是的,这很丑陋。

关于第二个问题-是的,你错了。将所有代码移回main不允许您通过Py_Main外部链接将其模块化地用作库。

现在知道了吗?


我没有问是否很长。我问为什么不应该那么长。
riwalk

“复杂度低于10”?是否有一个测量单位?
Donal Fellows

@ Stargazer712函数长度通常也由编码标准规定。这是一个可读性问题(并且复杂度高,通常将长的函数分支,以使复杂度远高于20),并且正如我所说的,main在这方面与任何其他函数没有什么不同。
littleadv

@Donal-是的,单击链接。
littleadv

我将不得不投票反对这一芽。您完全错过了问题的意图。
riwalk

0

这也是一个新的务实原因,也使main与GCC 4.6.1 Changelog保持简短:

在具有命名节支持的大多数目标上,仅在启动时使用的函数(静态构造函数和 main),仅在出口使用的函数和被检测为冷的函数被 放置在单独的文本段子节中。这扩展了-freorder-functions功能,并由同一开关控制。目的是缩短大型C ++程序的启动时间。

突出显示由我添加。


0

不要仅仅因为某些软件是好的就认为该软件背后的所有代码都是好的。好的软件和好的代码不是一回事,即使在好的软件有好的代码支持的情况下,不可避免的是,在大型项目中,也会有一些地方存在标准滑脱的地方。

具有短main函数是一种很好的做法,但这实际上只是一般规则的一个特例,即具有短函数会更好。简短的函数更易于理解和调试,并且更好地坚持使程序更具表现力的“单一目的”设计。main也许,这是遵守规则的一个更重要的地方,因为任何想了解程序的人都必须了解,main而对代码库更为模糊的角落的访问可能会减少。

但是,Python代码库并不是Py_Main为了遵守该规则而将代码推出,而是因为您无法main从库中导出或将其作为函数调用。


-1

上面有几个技术答案,让我们暂且不谈。

主电源应该简短,因为它应该是引导程序。主体应实例化少量对象,通常是一个完成工作的对象。像其他任何地方一样,这些对象应经过精心设计,结合,松散耦合,封装,...

尽管可能出于技术原因,让单行主调用另一个怪兽方法,但原则上您是正确的。从软件工程的角度来看,什么也没有得到。如果选择在一个主线调用一个Monster方法,而主线本身是一个Monster方法,则后者的坏处要小一些。


您假设“ C ++代码应仅使用对象,而仅使用对象”。事实并非如此,C ++是一种多范式语言,不会像其他任何语言一样将所有内容强加到OO模具中。
Ben Voigt
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.