Добре известен факт е, че можете да „предефинирате ключови думи, знаци, числа“ и други аспекти на някои езици за програмиране. Например, едно от нещата, които можете да направите в C++, е да разменяте различни ключови думи за емотикони.

Тъй като избраният от мен език е Python, исках да видя дали мога да внедря нещо също толкова забавно. Ще правя всичко тук в Python 3.9 x64 като справка, тъй като възползването от странности на ниско ниво като това има тенденция да се променя с времето.

За съжаление начинът, по който Python обработва Unicode знаците, го ограничава от възможността да прави „много неща“. Не можем да използваме емотикони като имена на променливи (😢), да не говорим за ключови думи, и със сигурност не можем да предефинираме проста логика като 2+2=4...нали?

Причината, поради която C, C++ и други езици могат да извършат този вид измама, е, че те имат начин да кажат на своя предпроцесор какво да прави. Python няма препроцесор, има интерпретатор. Това е част от това, което прави езика толкова лесен за използване - той абстрахира много от тези досадни детайли, за да измести фокуса от скоростта на изпълнение на програмата към скоростта на разработка на софтуерния разработчик. За нормални случаи на употреба това функционира като универсална и лесна за използване платформа за разработване на софтуер. Нещата стават трудни, когато искате да промените стандартната функционалност, но ииманачин да я хакнете.

Нека се опитаме да направим нещо забавно, нека се опитаме да накараме интерпретатора на Python да направи 2+2equal 5. Нямам предвид нещо кофти като print("2+2=5"), имам предвид да вземете цялото число 2 и да го комбинирате с друго цяло число 2 и изходът да е равен на 5.

За да направим това, трябва да променим фундаментален аспект на Python в начина, по който интерпретира числата. Python е изграден с C, което означава, че ако имаме достъп до Python на достатъчно ниско ниво, може да сме в състояние да емулираме #define поведението на C. За наше щастие има вградена библиотека, наречена ctypes, която ни позволява достъп до python на това ниво. Така че основно всяко число в Python има специфичен идентификатор, свързан с него. Можем да получим достъп до този идентификатор, като направим id(x):

>>> id(4)
2077461146000

Това, което ще направим, е да злоупотребим с факта, че имаме достъп до тези идентификатори на ниво C, така че всеки път, когато интерпретаторът на Python се опита да получи достъп до числото 4, той незабавно се пренасочва към 5. Честно казано, всъщност е доста просто. Всичко, което трябва да направим, е да зададем местоположението на паметта от 4 равно на 5 и готово!

import ctypes
offset = 24
ctypes.c_int8.from_address(id(4) + offset).value = 5

Сега, ако въведете 2 + 2 в Python, той ще върне 5!... но се случва нещо странно. Първо, какво става с това число на компенсиране? Идентификационните номера за 4 и 5 са на 32 числа едно от друго и въпреки това добавихме само 24:

>>> id(4)
2077461146000
>>> id(5)
2077461146032

Разяснявайки това, логично е да зададем стойността на 4 директно равна на идентификатора на 5, вместо изобщо да добавяме отместване, но за съжаление това няма ефект върху това, което 2+2 оценява. Различните степени на отмествания произвеждат различни ефекти, някои причиняват целочислени преливания, а други направо сриват програмата, без да се хвърлят изключения. Така че засега нека продължим с идеята, че добавянето на offset=24 кара 2+2 да стане равно на 5.

Въпреки че първоначално нещата изглежда са работили, когато работим с това предположение, че 4 безпроблемно е предефинирано като 5, при по-внимателно разглеждане откриваме, че има някои пропуски в тази теория.

print(4)
print(2 + 2)
print(6 - 2)
print(2 * 2)

Всички те оценяват правилния отговор от 4 като 5, но когато го превърнем в плаващ елемент:

print(8 / 2)

Правилно се оценява на 4.0. Това се случва, защото id(4) не е в същото място в паметта като id(float(4)). Там, където нещата наистина стават интересни, е когато го правим

pow(2, 4)

Обикновено това би се изчислило като 2⁴=16, но с нашата нова математика то оценява 2⁴=50, което на теория би поставило нашето ново 4 число равно на 5.6438561898. Колкото и да е странно, когато се използва 4 както като основа, така и като показател, изглежда, че се оценява според очакванията:

>>> pow(4, 4)
3125 

Нека пренесем това на следващото ниво

Вместо просто предефиниране на едно число, какво ще кажете за предефиниране на диапазон от числа?

import struct
import ctypes
import random

offset = 24
# amount of numbers to redefine
num = 100
nums = list(range(num))
addresses = [id(x) + offset for x in nums]
random.shuffle(nums)

for a, n in zip(addresses, nums):
    ctypes.c_ssize_t.from_address(a).value = n
# Remember, whatever num is equal to changes each time, so the loop length will change
for redefined_number in range(num):
    print(redefined_number)
print('2 + 2 =', 2+2)
print('9 - 4 =', 9-4)
print('5 * 6 =', 5*6)
# print('1 / 0 =\n', 1/0)

Този скрипт взема първите сто цели числа и произволно им присвоява нова стойност. Функционално това би трябвало да ви позволява да делите на „нула“, но изглежда има допълнителна проверка от страна на Python, която автоматично хвърля изключение „деление на нула“. Друго нещо, което трябва да се отбележи тук е, че предефинираните числа не са непременно уникални, което означава, че няколко числа могат да бъдат предефинирани като едно число, оставяйки някои да не бъдат дефинирани изобщо. 9 и 12 могат да бъдат нанесени на 33, но 33 може да бъде нанесено само на 9 или 12, но не и на двете.

Практическото приложение на това знание е оставено като упражнение на читателя.

Ако харесвате този вид съдържание, препоръчвам ви да ме последвате за още