Programuję od ponad 9 lat i zgodnie z radą mojego pierwszego nauczyciela programowania zawsze utrzymuję main()
bardzo krótką funkcję.
Na początku nie miałem pojęcia, dlaczego. Po prostu posłuchałem bez zrozumienia, ku zadowoleniu moich profesorów.
Po zdobyciu doświadczenia zdałem sobie sprawę, że jeśli poprawnie zaprojektowałem swój kod, posiadanie krótkiej main()
funkcji po prostu się wydarzyło. Pisanie modularnego kodu i przestrzeganie zasady pojedynczej odpowiedzialności pozwoliło na zaprojektowanie mojego kodu w „pęczkach” i main()
służyło jedynie jako katalizator do uruchomienia programu.
Szybko do przodu kilka tygodni temu, patrzyłem na kod Souce Pythona i znalazłem main()
funkcję:
/* Minimal main program -- everything is loaded from the library */
...
int
main(int argc, char **argv)
{
...
return Py_Main(argc, argv);
}
Tak, python. Krótka main()
funkcja == Dobry kod.
Nauczyciele programowania mieli rację.
Chcąc przyjrzeć się głębiej, spojrzałem na Py_Main. W całości jest on zdefiniowany w następujący sposób:
/* 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;
}
Dobry Boże Wszechmogący ... jest wystarczająco duży, aby zatopić Titanica.
Wygląda na to, że Python wykonał sztuczkę „Intro to Programming 101” i po prostu przeniósł cały main()
kod do innej funkcji, nazywając go czymś bardzo podobnym do „main”.
Oto moje pytanie: czy ten kod jest strasznie napisany, czy też istnieją inne powody, aby mieć krótką główną funkcję?
W obecnym stanie nie widzę absolutnie żadnej różnicy między zrobieniem tego a Py_Main()
ponownym przeniesieniem kodu main()
. Czy mylę się myśląc o tym?
źródło
options = ParseOptionFlags(argc,argv)
gdzieoptions
jest tostruct
, że zawiera zmiennePy_BytesWarningFlag
,Py_DebugFlag
itp ...Odpowiedzi:
Nie można eksportować
main
z biblioteki, ale można eksportowaćPy_Main
, a następnie każdy korzystający z tej biblioteki może „wywoływać” Pythona wiele razy z różnymi argumentami w tym samym programie. W tym momenciepython
staje się tylko kolejnym konsumentem biblioteki, niewiele więcej niż opakowaniem dla funkcji biblioteki; wzywaPy_Main
tak jak wszyscy inni.źródło
main
efektywnych wywołańexit
, których zwykle nie chce biblioteka.main
powoduje pozostawienie funkcji głównej… i wywołanieexit
z wartością zwracaną jako argumentem”. Zobacz także § 18.3 / 8, który wyjaśnia, że „obiekty o statycznym czasie przechowywania są niszczone” i „wszystkie otwarte strumienie C… są opróżniane” podczas wywoływaniaexit
. C99 ma podobny język.exit
liście niemain
mają znaczenia. Nie rozmawiamy o zachowaniuexit
. Omawiamy zachowaniemain
. A zachowaniemain
obejmuje zachowanieexit
, cokolwiek to może być. To sprawia, że niepożądany jest import i dzwonieniemain
(jeśli wykonanie takiej czynności jest w ogóle możliwe lub dozwolone).main
nie powoduje wywołaniaexit
kompilatora, oznacza to, że nie jest zgodny ze standardem. To, że standard dyktuje takie zachowanie,main
dowodzi, że jest w tym coś wyjątkowego. Szczególną rzecząmain
jest to, że powrót z niego powoduje wywołanieexit
. ( Jak to robi, zależy od autorów kompilatora. Kompilator może po prostu wstawić kod do epilogu funkcji, który niszczy obiekty statyczne, wywołujeatexit
procedury, opróżnia pliki i kończy działanie programu - co znowu nie jest czymś, czego chcesz w bibliotece .)To nie tak, że
main
nie powinien być długi tak jak należy unikać jakichkolwiek funkcji zbyt długo.main
to tylko specjalny przypadek funkcji. Dłuższe funkcje stają się bardzo trudne do opanowania, zmniejszają łatwość konserwacji i są ogólnie trudniejsze w obsłudze. Dziękimain
skróceniu funkcji (i ) ogólnie poprawiasz jakość kodu.W twoim przykładzie nie ma żadnej korzyści z przeniesienia kodu
main
.źródło
main
nie jest zbyt wielokrotnego użytku.Jednym z powodów
main()
skrócenia są testy jednostkowe.main()
to jedna funkcja, która nie może być testowana jednostkowo, więc sensowne jest wydobycie większości zachowań do innej klasy, która może być testowana jednostkowo. Jest to zgodne z tym, co powiedziałeśUwaga: stąd pomysł .
źródło
Rzadko dobrym pomysłem
main
jest być długim; jak w przypadku dowolnej funkcji (lub metody), jeśli jest długa, prawdopodobnie brakuje Ci możliwości refaktoryzacji.W konkretnym przypadku, o którym wspomniałeś powyżej,
main
jest krótki, ponieważ cała ta złożoność jest uwzględnionaPy_Main
; jeśli chcesz, aby Twój kod zachowywał się jak powłoka Pythona, możesz po prostu użyć tego kodu bez zbędnego manipulowania. (Trzeba to tak uwzględnić, ponieważ nie działa dobrze, jeśli umieściszmain
w bibliotece; jeśli to zrobisz, zdarzają się dziwne rzeczy).EDYCJA:
Aby wyjaśnić,
main
nie może znajdować się w bibliotece statycznej, ponieważ nie ma do niej wyraźnego łącza, więc nie zostanie poprawnie połączony (chyba że umieścisz go w pliku obiektowym z czymś, do czego się odwołuje, co jest po prostu okropne !) Biblioteki współdzielone są zwykle traktowane jako podobne (ponownie, aby zapobiec nieporozumieniom), chociaż na wielu platformach dodatkowym czynnikiem jest to, że biblioteka współdzielona jest tylko plikiem wykonywalnym bez sekcji ładowania (którejmain
jest tylko ostatnią i najbardziej widoczną częścią ).źródło
main
w bibliotece. To albo nie zadziała, albo strasznie cię zdezorientuje. Ale delegowanie praktycznie całej pracy do funkcji znajdującej się w bibliotece lib jest często rozsądne.Główna powinna być krótka z tego samego powodu, dla którego dowolna funkcja powinna być krótka. Ludzki mózg ma trudności z utrzymaniem w pamięci dużych ilości niepodzielonych danych. Podziel go na logiczne części, aby inni programiści (tak jak i Ty!) Mogli to łatwo przeanalizować.
I tak, twój przykład jest okropny i trudny do odczytania, nie mówiąc już o utrzymaniu.
źródło
Niektóre osoby korzystają z ponad 50 funkcji, które nie robią nic więcej, ale nawiązują połączenie z inną funkcją. Wolałbym normalną funkcję główną, która wykonuje logikę programu głównego. Oczywiście dobrze skonstruowany.
Nie widzę żadnego powodu, dla którego powinienem zawijać coś takiego w opakowaniu.
To czysto osobisty gust.
źródło
Najlepszą praktyką jest, aby WSZYSTKIE funkcje były krótkie, a nie tylko główne. Jakkolwiek „krótki” jest subiektywny, zależy od wielkości twojego programu i używanego języka.
źródło
Nie ma wymogu,
main
aby być dowolnej długości, poza standardami kodowania.main
jest funkcją jak każda inna i jako taka jej złożoność powinna być mniejsza niż 10 (lub cokolwiek, co mówią twoje standardy kodowania). To wszystko, wszystko inne jest raczej kłótliwe.edytować
main
nie powinno być krótkie. Lub długo. Powinien obejmować funkcje wymagane do wykonania w oparciu o projekt i przestrzegać standardów kodowania.Co do konkretnego kodu w twoim pytaniu - tak, to jest brzydkie.
Co do twojego drugiego pytania - tak, mylisz się . Przeniesienie całego tego kodu z powrotem do głównego nie pozwala na używanie go jako biblioteki poprzez linkowanie
Py_Main
z zewnątrz.Czy teraz jestem czysty?
źródło
main
nie różni się od żadnej innej funkcji w tym zakresie.Oto nowy pragmatyczny powód, aby trzymać się z dala od Listy zmian GCC 4.6.1 :
Podświetlanie dodane przeze mnie.
źródło
Nie zakładaj, że tylko dlatego, że trochę oprogramowania jest dobre, cały kod stojący za tym oprogramowaniem jest dobry. Dobre oprogramowanie i dobry kod to nie to samo, a nawet jeśli dobre oprogramowanie jest poparte dobrym kodem, nieuniknione jest, że w dużym projekcie pojawią się miejsca, w których standardy się pogarszają.
Dobrą praktyką jest posiadanie krótkiej
main
funkcji, ale tak naprawdę jest to tylko szczególny przypadek ogólnej zasady, że lepiej mieć krótkie funkcje. Krótkie funkcje są łatwiejsze do zrozumienia i łatwiejsze do debugowania, a także lepiej zachowują się w stylu „jednego celu”, który sprawia, że programy są bardziej wyraziste.main
jest być może ważniejszym miejscem do przestrzegania tej reguły, ponieważ każdy, kto chce zrozumieć program, musi zrozumieć,main
podczas gdy bardziej niejasne zakątki bazy kodu mogą być odwiedzane rzadziej.Ale baza kodów Pythona nie wypycha kodu
Py_Main
, aby zagrać w tę regułę, ale ponieważ nie można eksportowaćmain
z biblioteki ani wywoływać jej jako funkcji.źródło
Istnieje kilka technicznych odpowiedzi powyżej, odłóżmy to na bok.
Główny powinien być krótki, ponieważ powinien to być bootstrap. Główny powinien utworzyć instancję niewielkiej liczby obiektów, często takich, które wykonują pracę. Podobnie jak gdziekolwiek indziej, obiekty te powinny być dobrze zaprojektowane, spójne, luźno połączone, zamknięte ...
Chociaż mogą istnieć techniczne powody, aby mieć jedno-liniowe wywołanie główne innej metody potwora, w zasadzie masz rację. Z punktu widzenia inżynierii oprogramowania nic nie zostało uzyskane. Jeśli wybór jest między linią główną z jedną linią wywołującą metodę potwora, a samą magistralą będącą metodą potwora, ta druga jest ułamkowo słabsza.
źródło