web-dev-qa-db-fra.com

Pourquoi main () devrait-elle être courte?

Je programme depuis plus de 9 ans, et selon les conseils de mon premier professeur de programmation, je garde toujours ma fonction main() extrêmement courte.

Au début, je ne savais pas pourquoi. J'ai simplement obéi sans comprendre, pour le plus grand plaisir de mes professeurs.

Après avoir acquis de l'expérience, je me suis rendu compte que si j'avais conçu mon code correctement, une fonction main() courte se produisait. Écrire du code modulaire et suivre le principe de la responsabilité unique a permis à mon code d'être conçu en "grappes", et main() n'a servi de catalyseur qu'à faire fonctionner le programme.

Il y a quelques semaines, je regardais le code souce de Python et j'ai trouvé la fonction main():

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

...

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

Ouais python. Short main() function == Bon code.

Les professeurs de programmation avaient raison.

Voulant regarder plus en profondeur, j'ai jeté un œil à Py_Main. Dans son intégralité, il est défini comme suit:

/* 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;
}

Bon Dieu tout-puissant ... il est assez grand pour couler le Titanic.

Il semble que Python a fait l'astuce "Intro to Programming 101" et vient de déplacer tout le code de main() vers une fonction différente appelée quelque chose de très similaire à "main".

Voici ma question: ce code est-il terriblement écrit, ou y a-t-il d'autres raisons d'avoir une fonction principale courte?

Dans l'état actuel des choses, je ne vois absolument aucune différence entre le faire et simplement déplacer le code dans Py_Main() dans main(). Ai-je tort de penser cela?

89
riwalk

Vous ne pouvez pas exporter main depuis une bibliothèque, mais vous pouvez exporter Py_Main, puis toute personne utilisant cette bibliothèque peut "appeler" Python plusieurs fois avec différents arguments dans le même programme. À ce stade, python devient simplement un autre consommateur de la bibliothèque , un peu plus qu'un wrapper pour la fonction de bibliothèque; il appelle Py_Main comme tout le monde.

138
Rob Kennedy

Ce n'est pas que main ne devrait pas être aussi long que vous devriez éviter que la fonction any soit trop longue. main n'est qu'un cas particulier de fonction. Les fonctions plus longues deviennent très difficiles à gérer, diminuent la maintenabilité et sont généralement plus difficiles à utiliser. En raccourcissant les fonctions (et main), vous améliorez généralement la qualité de votre code.

Dans votre exemple, il n'y a aucun avantage à déplacer le code hors de main.

42
Mark B

L'une des raisons de raccourcir main() est le test unitaire. main() est la seule fonction qui ne peut pas être testée unitaire, il est donc logique d'extraire la majorité du comportement dans une autre classe qui peut être testée unitaire. Cela va de pair avec ce que vous avez dit

L'écriture de code modulaire et le respect du principe de responsabilité unique ont permis à mon code d'être conçu en "grappes", et main () n'a servi que de catalyseur pour faire fonctionner le programme.

Remarque: J'ai eu l'idée de ici .

28
Chance

C'est rarement une bonne idée que main soit long; comme avec la fonction any (ou la méthode) si elle est longue, vous manquez probablement des opportunités de refactoring.

Dans le cas spécifique que vous mentionnez ci-dessus, main est court car toute cette complexité est prise en compte dans Py_Main; si vous voulez que votre code se comporte comme un python Shell, vous pouvez simplement utiliser ce code sans trop de tripotage. (Il doit être pris en compte comme ça car il ne fonctionne pas bien si vous mettez main dans une bibliothèque; des choses étranges se produisent si vous le faites.)

ÉDITER:
Pour clarifier, main ne peut pas être dans une bibliothèque statique car elle n'a pas de lien explicite vers elle et ne sera donc pas liée correctement (sauf si vous la colocalisez dans un fichier objet avec quelque chose auquel il est fait référence, ce qui est tout simplement horrible!) Les bibliothèques partagées sont généralement traitées comme étant similaires (encore une fois, pour éviter toute confusion) bien que sur de nombreuses plates-formes, un facteur supplémentaire est qu'une bibliothèque partagée n'est qu'un exécutable sans bootstrap section (dont main n'est que la dernière partie la plus visible).

16
Donal Fellows

Main doit être court pour la même raison que toute fonction doit être courte. Le cerveau humain a du mal à conserver simultanément de grandes quantités de données non partitionnées. Divisez-le en morceaux logiques afin qu'il soit facile pour les autres développeurs (ainsi que vous-même!) De digérer et de raisonner.

Et oui, votre exemple est terrible et difficile à lire, encore moins à maintenir.

6
Ed S.

Certaines personnes bénéficient de plus de 50 fonctions qui ne font rien d'autre, mais terminent un appel vers une autre fonction. Je préférerais plutôt la fonction principale normale qui fait la logique du programme principal. Bien structuré bien sûr.

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

Je ne vois aucune raison pour laquelle je devrais envelopper quoi que ce soit dans un emballage.

C'est purement un goût personnel.

1
Coder

Sa meilleure pratique pour garder TOUTES vos fonctions courtes, pas seulement principales. Cependant "court" est subjectif, cela dépend de la taille de votre programme et de la langue que vous utilisez.

1
Mike Miller

Ne présumez pas que juste parce qu'un peu de logiciel est bon, tout le code derrière ce logiciel est bon. Un bon logiciel et un bon code ne sont pas la même chose et même lorsqu'un bon logiciel est soutenu par un bon code, il est inévitable que dans un grand projet, il y aura des endroits où les normes glisseront.

C'est une bonne pratique d'avoir une fonction courte main, mais ce n'est vraiment qu'un cas particulier de la règle générale selon laquelle il vaut mieux avoir des fonctions courtes. Les fonctions courtes sont plus faciles à comprendre et à déboguer, tout en étant plus adaptées au type de conception à "usage unique" qui rend les programmes plus expressifs. main est, peut-être, un endroit plus important pour s'en tenir à la règle car quiconque veut comprendre le programme doit comprendre main tandis que les coins les plus obscurs de la base de code peuvent être visités moins souvent.

Mais, la base de code Python ne pousse pas le code versPy_Mainpour jouer cette règle mais parce que vous ne pouvez pas exporter mainà partir d'une bibliothèque ni l'appeler en tant que fonction.

0
Jack Aidley

Voici une nouvelle raison pragmatique de garder le raccourci principal du GCC 4.6.1 Changelog :

Sur la plupart des cibles avec prise en charge de section nommée, les fonctions utilisées uniquement au démarrage (constructeurs statiques et main ), les fonctions utilisées uniquement à la sortie et les fonctions détectées comme froides sont placé dans des sous-sections de segment de texte distinctes . Cela étend la fonction -freorder-functions et est contrôlé par le même commutateur. L'objectif est d'améliorer le temps de démarrage des grands programmes C++.

Surlignage ajouté par moi.

0
Peter G.

Il n'est pas nécessaire que main ait une longueur quelconque, autre que les normes de codage. main est une fonction comme les autres, et en tant que telle, c'est complexité devrait être inférieur à 10 (ou ce que disent vos normes de codage). Voilà, tout le reste est plutôt argumentatif.

modifier

main ne devrait pas être court. Ou long. Il doit inclure les fonctionnalités qu'il doit exécuter en fonction de votre conception et respecter les normes de codage.

Quant au code spécifique dans votre question - oui, c'est moche.

Quant à votre deuxième question - oui, vous avez tort . Remettre tout ce code dans main ne vous permet pas de l'utiliser modulairement comme bibliothèque en liant Py_Main de dehors.

Maintenant, suis-je clair?

0
littleadv