Код, який боляче читати: як ми провели конкурс на найкращий спагеті-код
Привіт! Мене звати Дмитро Пилипенко, уже два роки я працюю Instructional Designer в освітній платформі robot_dreams, тепер — як Senior. Так-так, у нас теж є така градація, адже ми теж в ITшці.
Instructional Designer займається тим, що формує і оформлює навчальні програми разом із лекторами в такий спосіб, щоби програма була максимально ефективною та цікавою для студентів. У школах, де навчають саме IT-спеціальностей, технічний бекграунд буде плюсом. Так тандем лектор (досвід у своїй галузі) плюс ID (досвід у розробці програм і загальний технічний рівень) підсилюється, і програма отримує шанси бути більш якісною, ефективною та приносити практичний досвід.
Нещодавно на честь дня народження платформи ми провели конкурс на найкращий лайнокод. Зазвичай студенти вчаться писати код чітко, структуровано, із дотриманням стандартів. Та відсвяткувати річницю хотілося більш фаново й розслаблено, весело і з користю. Я брав участь у етапі відбору як представник від команди, а також кординував конкурс. Тож хочу поділитися, що цікавого зробили наші учасники.
Що таке лайнокод, або спагеті-код
Так називають погано структурований, заплутаний і нечитабельний код. Як правило, він написаний в один рядок, без переносів, розділень на функції чи цикли. Виглядає це приблизно так само, як сплутані між собою спагеті: розібрати й відокремити частини майже неможливо.
Початківці часто припускаються цієї помилки: можуть писати лайнокоди на старті своєї карʼєри. Із часом вони вчаться робити більш читабельні й зрозумілі іншим розробникам коди. Та ми навмисно пожартували над самими собою і використали цей термін у назві конкурсу. Що більше ти знаєш і вмієш, то легше зізнаватися в помилках минулого.
Код в один рядок
Головним завданням конкурсу було не просто написати спагеті-код, а й зробити так, щоб він працював. І це вже завдання із зірочкою.
На першому етапі відбору перевірялося виконання умов конкурсу, а саме:
- один рядок коду — не два, не три, і вже точно не «if-else» на двох сторінках;
- мова програмування — будь-яка: від Python до Brainfuck;
- функціональність — код має працювати;
- чесність — це має бути авторський код учасника, жодних хакерських трюків;
- все в одному місці — у репозиторії;
- документація — розвʼязок має містити на репозиторії документацію в довільному стилі, з якої зрозуміла ідея, постановка задачі, як скомпілювати/інтерпретувати і виконати код.
Чесно кажучи, навіть при невиконанні умов ми розглядали роботи, щоб не пропустити щось визначне та унікальне. Але такі учасники втрачали рейтинг, що мало значення при переході на наступний етап оцінювання.
Ми відзначали, чи власну ідею використав учасник, чи розв’язував одну із запропонованих нами задач:
- перевірка на високосний рік;
- унікальні елементи в масиві та їхнє сортування;
- перевірка на паліндром;
- власний генератор випадкових чисел;
- стиснення тексту.
Після двох попередніх підетапів відбору ми дивилися саме в код. Після його відкриття в журі мало виникати бажжання не те, що звернути чи закрити його, а рубанути тумблер зі світлом і хоча б пʼять хвилин полежати в тиші й темноті. Оце основний критерій! 🤣
Але, хоч код і мав бути в один рядок (фактично в один рядок відформатований) і містити велику кількість заплутаних конструкцій, та все ж він мав працювати й розв’язувати поставлену задачу.
До фіналу потрапило десять робіт, за які голосували учасники нашого ком’юніті, а точніше всі охочі, після доєднання до нашого чат-боту. Коротенько оглянемо роботи призерів за результатами голосування.
Дисклеймер
Далі в статті будемо оглядати речі, які можуть викликати циклічне запитання «навіщо», а деякі з них — травмувати психіку ще психологічно не сформованих розробників. Будьте обережні й читайте далі відповідально!
Переможець (або Selfie-random)
Ну, звісно, оглянемо рішення нашого переможця.
Обрана задача із запропонованих: напишіть код, що генерує абсолютно випадкове число від 1 до 100... але без генератора випадкових чисел.
Код переможця в оригінальному форматуванні ви можете переглянути на репозиторії.
Давайте ризикнемо трішки його відформатувати:
__builtins__.__import__(«lorem ipsum»[-3] + «y» + «lorem ipsum»[-3]).stdout.write( f«Shitty {'number' if '666'.isdigit() else 'string'}: " f»{(lambda _: _())(lambda: (lambda sasori: (lambda f: (lambda hidan: hidan% 100 + 1)( int(getattr(__import__('hashlib'), 'sha256')( getattr(__import__('base{count}'.format(count=8 * 8)), 'b64encode')( ( getattr(getattr(__import__('cv2'), 'imencode')('.jpg', getattr(f, 'read')()[1])[1], 'tobytes')() + ( str(len(getattr(__import__('psutil'), 'pids')())) + str(getattr(getattr(__import__('psutil'), 'virtual_memory')(), 'available')) ).encode() ) ) ).hexdigest()[:8], 16) ))(sasori) if getattr(sasori, 'isClosed'.replace('Closed', 'OPENED'.lower().title()))() else getattr((x for x in ()), 'throw')(IOError('Your camera is a shit!')) ))(getattr(__import__('cv2'), 'VideoCapture')(0)) )}\n» )
Учасник не стандартно, а оригінально й динамічно імпортує модулі через генерацію рядка sys для того, щоб додати більше заплутаності:
__builtins__.__import__("lorem ipsum«[-3] + «y» + "lorem ipsum"[-3])
Налаштовуємося на правильні логічні хвилі конкурсу: фрагмент "lorem ipsum«[-3] дає літеру ’s’, і додавання ’y’ знову + «s» також через "lorem ipsum«[-3] дає ’sys’.
Далі використовуємо цікавий прийом на трушну перевірку if ’666’.isdigit() для того, щоб завжди використовувати «number».
Поки все логічно, скажете ви... 😅
Ініціалізуємо вкладену лямбда-функцію, яка обчислює значення для вставки у форматований рядок і взагалі робить магію (lambda _: _())(lambda:...)
Тут використовуємо всього потроху:
- cv2 — для захоплення кадру з камери:
- якщо камера доступна, виконує f.read()[1] для захоплення кадру з відеопотоку, cv2.imencode(’.jpg’, кадр)[1] кодує кадр як зображення JPEG, а.tobytes() перетворює його на байти. Якщо не доступна, отримаєте досить толерантний ерор меседж, який охарактеризує вашу камеру практично одним словом.
- psutil — для отримання інформації про процеси й доступну пам’ять:
- psutil.pids() повертає список усіх PID активних процесів.
- psutil.virtual_memory().available дає обсяг доступної пам’яті. Обидва значення конвертуються в рядки й конкатенуються.
- base64 — для кодування зображення:
- base64.b64encode()- кодує зображення та системну інформацію в Base64.
- hashlib — для створення SHA256 хешу:
- hashlib.sha256(...) — створює SHA-256-хеш з отриманого Base64-рядка.
Потім беремо вісім символів отриманого хеш і кастуємо.
Ще цікавий момент: функція isOpened() (переписана через isClosed.replace(’Closed’, ’OPENED’)) перевіряє, чи підключено камеру. Якщо камера недоступна, маємо IOError.
Формула всього цього задуму в результаті виходить int((...), 16)% 100 + 1 і перетворює результат на число в межах від 1 до 100 згідно з умовою задачі конкурсу.
І, звісно, виводимо це все, хоч тут через стандартний вивід, а не сигналами системних звуків в абетці Морзе. Хоча це гарна ідея для такого конкурсу, і тут явно такого бракує 😅 Але ідея залучити апаратні модулі вашого комп’ютера теж цікава і прийшлася до душі всім, хто віддав голос.
Цікава історія одного стажера (із використанням C, Python, Shell)
Цікава історія й цікаве рішення. Загальне враження — творчий підхід. Учасник обрав один із варіантів завдань, але ж як це все оформив. Обрана задача із запропонованих — така ж, як і попередня: власний генератор випадкових чисел.
Історія розповідає нам про такого собі стажера Ваню, який отримав задачу на створення генератора випадкових чисел, що потрібно інтегрувати до продукту (так співпало із завданням конкурсу 😅) мовою програмування С. Але Ваня не шукає простих шляхів. Легенда говорить про те, що стажер ще й додумався все оформити TXT-файлами в різних місцях репозиторію, що явно доповнює код із погляду документації.
Найкраще опише задум стажера його ж нотатка:
... усі мої колеги розповідають, за скільки строк коду їм вдалося вирішити задачі. Я буду кращим за всіх! Я настільки крутий кодер, що можу написати найкраще рішення, і використати всього одну строчку коду!!!
Задача полягає в тому, щоб написати генератор випадкових чисел на.с, бо це низькорівнева мова програмування, і код на ній працює швидко.
Я, як мене вчили, провів ресерч, і дізнався, що для цієї задачі є готові рішення. Перше, яке мені вибилось — це сайт random.org. Я запропонував використати його, але мені сказали, що ми не можемо: їх АПІ з лімітами, і нам потрібне своє рішення. Тому використати random.org хоч і не жахлива ідея, але не варіант, враховуючи потреби компанії.
Але я знайшов, як викорситати random.org так, щоб не залежати від їх лімітів! Хмм, але я не знаюЮ як робити network request на.c, хоча знаю, як це зробити на пітоні.
Еврика! Можна інтегрувати пітон в.c.
Тоді треба спочатку написати пітонівську частину...
Чи доводилося вам інтегрувати Python в C? Я думаю, такі випадки дійсно зустрічаються в житті, але на цьому моменті точно стає цікаво, як же це буде.
Тож починаємо із Python:
from time import time from concurrent.futures import ThreadPoolExecutor,as_completed # Треба зробити код набагато рандомніше, тому будемо паралелити для отримання найрандомнішого рандому import requests eval=exec # Евал така собі назва, хочу іншу. Більше того, для exec є краща назва exec=ThreadPoolExecutor(5) # краща назва. dict={} # так буде зрозуміліше # в одну строчку, дефайн не зробити, наче, тому треба адаптуватися під мій виклик # NOTE: функція дуже тонко налаштована, змінювати значення заборонено! # NOTE: якщо у вас дуже слабкий інтернет, вам заборонено(!!!) використовувати цей генератор, бо є вірогідність отримати OOM str=«def fetch_url(*args): \ args=args[0];\ start_time,end_time=args[1](),args[2].get(args[0]).elapsed.total_seconds()+args[1]();\ return(end_time-start_time)*2718432483+543543» # Тут, сама логіка # Ми надсилаємо Get запит на рандоморг, і записуємо за скільки часу ми отримали відповідь # код чітко налаштований так, щоб усі функції дефайнились як ми хочемо, і відпрацьовували рівно 5(!) разів, після чого результати сумуються у фінальний результат print(sum([list.result()for list in list(map(lambda _:exec.submit((lambda f:(eval(f,{},dict),dict[«fetch_url»])[1])(str),(«<a href="https://www.random.org">https://www.random.org</a>»,time,requests)),range(len(«55555»))))]))
Звісно, учасник додав коментарі, але ж погляньте на елегантність цього рішення.
Паралельно будемо виконувати це пʼять 5 разів на всяк випадок для покращення нашого рандому, точніше його результату:
eval=exec exec=ThreadPoolExecutor(5)
Ну а самі перевизначення то окрема історія, захотілося людині 😅
Визначаємо функцію через рядок, а вже потім його виконаємо через exec. І самий сік нашої пайтон-частини:
print(sum([list.result()for list in list(map(lambda _:exec.submit((lambda f:(eval(f,{},dict),dict[«fetch_url»])[1])(str),(«<a href="https://www.random.org">https://www.random.org</a><a href="about:blank">»,time,requests</a>)),range(len(«55555»))))]))
Тут ми завуальовано визначаємо ітератор range(len("55555″), щоби потім використати його в map
map(lambda _:exec.submit((lambda f:(eval(f,{},dict),dict[«fetch_url»])[1])(str),(«<a href="https://www.random.org">https://www.random.org</a>»,time,requests)),range(len(«55555»)))
У рамках map ми визначаємо хитрим способом функцію, про яку говорили раніше, і яка в нас була просто рядком. І вносимо цю функцію до глобального словника. Далі виконуємо пʼять разів запити, кожен із результатів (час запиту як різницю таймстемпів початку й закінчення запиту) домножаємо на щось схоже на MAX_INT (але щось інше), додаємо магічне 543543 (мабуть, універсальний пароль засвітив стажер), суму виводимо просто print.
Отака простенька вступна частина Python-коду.
Рухаємось далі. Інтегруємо Python код в C:
int main() { // я запитав у чатджіпіті, як можна це зробити, і мені порекомендували створити файл пітону. не знаю поки що далі, але він точно допоможе // для читабельності, назву файл так, як він має запускатись FILE *f=fopen(«python.exe», «w»); fprintf(f, "%s», «from time import time;from concurrent.futures import ThreadPoolExecutor,as_completed;import requests;eval=exec;exec=ThreadPoolExecutor(5);dict={};str=\«def fetch_url(*args):args=args[0];start_time,end_time=args[1](),args[2].get(args[0]).elapsed.total_seconds()+args[1]();return(end_time-start_time)*2718432483+543543\";print(sum([list.result()for list in list(map(lambda _:exec.submit((lambda f:(eval(f,{},dict),dict[\«fetch_url\"])[1])(str),(\«<a href="https://www.random.org">https://www.random.org</a>\",time,requests)),range(len(\«55555\"))))]))"); fclose(f); char command[256]; // наче так можна запустити команду… snprintf(command, sizeof(command), «python3 python.exe»); // попен, хе-хе f = popen(command, «r»); double _; // тут записуємо значення з пітонівсьоко коду fscanf(f, "%lf», &_); // а тут я подумав, що все таки, якось недостатньо виходить рандому, тому, було розроблена рандомізація рандомного числа // логіка дуже проста — ми беремо адресу флоата в якому ми зберігаємо результат із пітону. потім переводимо її, ніби це андерса Інта. Потім, беремо Інт із цієї Адреси. Потім, проводимо XOR з інтовим time(NULL) (чому б і ні) // потім, приводимо модульну операцію для того щоб звести результат до очікуваного ренджу від 1 до 100 // з якоїсь причини, значення можуть бути мінусовими. чому — я хз, але можна просто 100 додати, і так буде норміс //а вже потім, проводимо останній модуло, і приводимо вже до реального ренджу 1 до 100 printf("%d\n», ((((int)(int*)&_ ^ (int)time(NULL))% 100) + 100)% 100 + 1); }
Тут усе зрозуміло із коментарів учасника. Але все ж таки особливий підхід, коли ми розглянутий вище код на Python записуємо до файлу саме із С-коду:
FILE *f=fopen(«python.exe», «w»); fprintf(f, "%s», «from time import time;from concurrent.futures import ThreadPoolExecutor,as_completed;import requests;eval=exec;exec=ThreadPoolExecutor(5);dict={};str=\«def fetch_url(*args):args=args[0];start_time,end_time=args[1](),args[2].get(args[0]).elapsed.total_seconds()+args[1]();return(end_time-start_time)*2718432483+543543\";print(sum([list.result()for list in list(map(lambda _:exec.submit((lambda f:(eval(f,{},dict),dict[\«fetch_url\"])[1])(str),(\«<a href="https://www.random.org">https://www.random.org</a><a href="about:blank">\",time,requests</a>)),range(len(\«55555\"))))]))"); fclose(f);
А потім із цього ж коду C ми виконуємо код на Python і за посередництвом Shell за кадром отримуємо нарешті наші дані, які для нас згенерував Python. Ну, начебто все знову логічно до цього моменту... Що ж далі?
А далі ще простіше. Але є новий гравець у цій історії — Shell 💪 Хоча насправді він зустрічався і раніше, але не так явно, як обгортка для двох попередніх складових.
Для чого нам баш, запитаєте ви? Для того, щоб зробити цей код однорядковим, оскільки include в один рядок із головною функцією компілятор не пропускає:
# Треба створити файл із сі кодом, який ми запустимо # Називаємо gcc.exe, щоб усім було зрозуміло як його запускати echo '#include <stdio.h>' > gcc.exe && \ echo '#include <stdlib.h>' >> gcc.exe && \ echo '#include <time.h>' >> gcc.exe && \ echo 'int main(){FILE *f=fopen(«python.exe», «w»);fprintf(f, "%s», «from time import time;from concurrent.futures import ThreadPoolExecutor,as_completed;import requests;eval=exec;exec=ThreadPoolExecutor(5);dict={};str=\«def fetch_url(*args):args=args[0];start_time,end_time=args[1](),args[2].get(args[0]).elapsed.total_seconds()+args[1]();return(end_time-start_time)*2718432483+543543\";print(sum([list.result()for list in list(map(lambda _:exec.submit((lambda f:(eval(f,{},dict),dict[\«fetch_url\"])[1])(str),(\«<a href="https://www.random.org">https://www.random.org</a>\",time,requests)),range(len(\«55555\"))))]))");fclose(f);char command[256];snprintf(command, sizeof(command), «python3 python.exe»);f = popen(command, «r»);double _;fscanf(f, "%lf», &_);printf("%d\n», ((((int)(int*)&_ ^ (int)time(NULL))% 100) + 100)% 100 + 1);}' >> gcc.exe && \ gcc -x c -o exe.exe gcc.exe && \ ./exe.exe # в кінці компілюємо, btw ці дебіли розробники gcc, без додаткових параметрів не дають.exe сприймати як.c… # приводимо до exe.exe, щоб усім було зрозуміло що його треба запускати з exe # ну й запускаємо
Таким чином у цьому рішенні поєдналося явно три технології. Однозначно цікавий підхід, який демонструє не тільки навички й широкий технічний погляд учасника, але і його творчі здібності.
Морзянка (або закодований код)
Розглянемо ще одне цікаве рішення з використанням абетки Морзе.
Код виглядає наступним чином, як наче трішки засніжило:
exec((lambda:''.join([{'.-': 'a', '-…': 'b', '-.': 'd', '.': 'e', '.-.': 'f', '.': 'i', '.-.': 'l', '--': 'm', '-.': 'n', '---': 'o', '.--.': 'p', '.-.': 'r', '…': 's', '-': 't', '.-': 'u', '-.--': 'y', '-----': '0', '.----': '1', '…-': '4', '-.-.--': '!', '-.--.': '(', '-.--.-': ')', '---…': ':', '-…-': '=', '.-.-.': '"','------':' ','…':'%'}[c]for c in '.--.-.. -. — -.--. -.--.-..- -- -… -..- ------ -.-- ---….-.-. -.--.….-.-...-. -.--. -.--…...- -…- -…- ----- ------.- -. -. ------ -.--….---- ----- -.-.-- -…- ----- -.--.- ---.-. -.--. -.--…...- ----- ----- -…- -…- ----- -.--.-.-.….-.-. -. ---.-.-. -.--.- -.--.. -. — -.--.. -.--..- — -.--.-.-. -. -.-. ------ -.--.-.-. ---… ------.-.-. -.--.- -.--.- -.--.- -.--.-'.split()]))())
Але якщо придивитися уважніше, то цей код уже не такий і складний.
У нас є словничок:
{'.-': 'a', '-…': 'b', '-.': 'd', '.': 'e', '.-.': 'f', '.': 'i', '.-.': 'l', '--': 'm', '-.': 'n', '---': 'o', '.--.': 'p', '.-.': 'r', '…': 's', '-': 't', '.-': 'u', '-.--': 'y', '-----': '0', '.----': '1', '…-': '4', '-.-.--': '!', '-.--.': '(', '-.--.-': ')', '---…': ':', '-…-': '=', '.-.-.': '"','------':' ','…':'%'}
І відповідно сам код абеткою Морзе, у яку додали два додаткових символи, оскільки азбука по дефолту їх не враховує — це ’------’ -> ’ ’ (пробіл) ’...’ -> ’%’ (відсоток):
'.--.-.. -. — -.--. -.--.-..- -- -… -..- ------ -.-- ---….-.-. -.--.….-.-...-. -.--. -.--…...- -…- -…- ----- ------.- -. -. ------ -.--….---- ----- -.-.-- -…- ----- -.--.- ---.-. -.--. -.--…...- ----- ----- -…- -…- ----- -.--.-.-.….-.-. -. ---.-.-. -.--.- -.--.. -. — -.--.. -.--..- — -.--.-.-. -. -.-. ------ -.--.-.-. ---… ------.-.-. -.--.- -.--.- -.--.- -.--.-'.split()
Весь цей задум приведе нас до доволі елегантного рішення, якщо перевести його в Python =-код:
print((lambda y:«yes» if (y% 4 == 0 and y% 10!= 0) or (y% 400 == 0) else «no»)(int(input(«enter year: "))))
Так, звісно, в Python це все виглядає не так лайново, як того вимагав конкурс. А от усе ж таки оформлене рішення цікаве, особливо для тих, хто не зустрічався раніше з абеткою Морзе. Щонайменше в програмуванні.
Навіщо це все
Насправді ми віримо, що лайнокод може принести користь для розвитку програміста та прокачати певні навички. Наприклад, лайнокод може стимулювати креативне мислення та здатність шукати рішення, які виходять за межі звичайного підходу.
Створення складного та неефективного коду дає змогу краще зрозуміти, як саме погана оптимізація або неправильна структура можуть вплинути на продуктивність програми. У процесі написання складного та нечитабельного коду програмісти часто стикаються з необхідністю ускладнювати прості речі. Це тренує око та мозок розпізнавати складне й робити його одразу простішим.
Можна навчитися цінувати компроміси між швидкістю розробки, читабельністю та ефективністю. Навіть якщо початковий код складний і важкочитабельний, важливим є процес його рефакторингу — перетворення в більш зрозумілий і ефективний.
Створення складного коду часто пов’язане з використанням нестандартних алгоритмів або конструкцій. Це може сприяти глибшому розумінню коду в майбутньому, бо хтозна із чим ще доведеться зустрітися в житті, з якими стажерами. Написання складного коду також зазвичай вимагає роботи з абстракціями та створення багатьох рівнів, що може допомогти в розвитку навичок роботи з більш складними концепціями та паттернами проєктування в майбутньому.
Навіщо ми провели конкурс на найкращий лайнокод? Здебільшого для розваги. Але, чесно кажучи, стикаючись із таким кодом, ти переосмислюєш деякі моменти й зосереджуєшся на тому, як все ж таки важливо не ускладнювати. Усе геніальне насправді просто!
35 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів