Rysowanie własnego modułu.Czas na Secunda Aprilis (no OK, bardziej Quarta Aprilis), czyli wytłumaczenie/sprostowanie Prima Aprilisowego postu. W dużym skrócie, w tym roku na pierwszego kwietnia opublikowałem "pierwszą część wideo-tutoriala języka Python", w której pokazałem jak "w prosty sposób" stworzyć funkcje literującą podany napis, a następnie jak narysować (sic) moduł Pythona z ową funkcją w programie Microsoft Paint. Oba podane przykłady były pokazane w bardzo bardzo krzywym zwierciadle, niemniej jednak oba były w pełni działające - stąd też postanowiłem poświęcić trochę miejsca, żeby dokładnie(j) wytłumaczyć co tam się w zasadzie działo. Dodam, że żart wyszedł mi lepiej niż się spodziewałem, co wnioskuje po tym, że kilka osób zorientowało się, że "coś jest nie tak" dopiero jak odpaliłem Painta :)

Na początek dwa linki - z oryginalnym wideo oraz materiałami na nim prezentowanymi:
Materiały: https://github.com/gynvael/stream/tree/master/007-python-101
Wideo: https://youtu.be/7VJaprmuHcw


Wideo trafiło m.in. na Wykop, gdzie kilka osób skomentowało je na serio, co mam wrażenie, że dodało wiarygodności mojemu nagraniu. Tu i tam pojawiło się również kilka świetnych komentarzy utrzymanych w podobnym tonie jak wideo (lub po prostu zabawnych) - kilka moich ulubionych poniżej:


Pierwsza połowa wideo: Tworzenie funkcji

Przechodząc do technicznej części posta - w pierwszej części pokazałem jak w języku Python 2.7 zrobić funkcję, która wypisuje napis znak po znaku, z niewielkim odstępem czasowym pomiędzy kolejnymi znakami.


Standardowo funkcja tego typu wygląda tak:

def SlowPrint(txt):
 for ch in txt:
   sys.stdout.write(ch)
   sys.stdout.flush()
   time.sleep(0.5)

Powyższa funkcja była również dla mnie punktem wyjścia do stworzenia potworka pokazanego na wideo, który był właśnie powyższą funkcją, tyle że w postaci, której CPython 2.7 używa wewnętrznie w trakcie wykonania programu. Dodam, że ta część wideo była ogólnie rzecz biorąc poprawna, tj. wszystkie opisy pól i typów o których mówiłem były raczej po stronie prawidłowej (choć pominąłem dokładne wytłumaczenie wielu detali).

Wyjdźmy więc od powyższej funkcji i przeprowadźmy jej inspekcję. Na początku zaznaczę, że w CPython funkcja składa się w zasadzie z dwóch głównych obiektów:
  • "zewnętrznego" function, który jest w zasadzie związany z tym jak dana funkcja ma zostać wywołana, tj. przechowuje wartości domyślnych parametrów, referencje do zmiennych lokalnych (w przypadku funkcji zagnieżdżonych), czy do zestawy obiektów globalnych (w końcu funkcje z innych modułów widzą swoje własne zestawy "globali");
  • oraz "wewnętrznego" code, który zawiera zarówno kod bajtowy funkcji, jak i nazwy używanych zmiennych lokalnych, globalnych, lokalnych-z-innego-kontekstu (w przypadku funkcji zagnieżdżonych), etc.
Sama nazwa funkcji jest zmienną zawierającą referencję do* etykietą dołączoną do obiektu typu function (co można łatwo sprawdzić pisząc type(SlowPrint)), zacznijmy więc od inspekcji tego obiektu.

* EDIT: Jak słusznie zwrócił uwagę jell na #python.pl@freenode (podziękowania!), w Pythonie nie ma "zmiennych zawierających referencje". Zamiast tego są nazwy, które są jedynie etykietkami konkretnych obiektów tworzonymi za pomocą różnych operacji powiązania (ang. binding). W oficjalnej dokumentacji można o tym poczytać w sekcji 4.1. Naming and binding; oprócz tego asdf z #python.pl@freenode podrzucił kilka innych fajnych linków w tym temacie: Other languages have "variables" (z bardzo wymownymi obrazkami) oraz Facts and myths about Python names and values - dzięki! /EDIT

W tym celu w zasadzie wystarczy użyć dir(SlowPrint), natomiast warto przy okazji odfiltrować nieistotne dla nas pola, których nazwa zaczyna się od dunder, czyli dwóch znaków podkreślenia:

>>>[x for x in dir(SlowPrint) if not x.startswith("__")]
['func_closure', 'func_code', 'func_defaults', 'func_dict', 'func_doc', 'func_globals', 'func_name']

Pól nie ma zbyt wiele, więc możemy je omówić po kolei - w kolejności od najistotniejszych dla nas:
  • func_code - referencja do obiektu code, który ma zostać użyty w przypadku uruchomienia danej funkcji;
  • func_name - nazwa obiektu funkcji (zazwyczaj tożsama z pierwotną nazwą zdefiniowanej funkcji), dostępna również w polu __name__;
  • func_globals - referencja do zestawu obiektów globalnych, z których funkcja będzie korzystać po uruchomieniu (atrybut ten jest tylko do odczytu)
  • func_defaults - tablica domyślnych wartości (patrz również: /n/Python_default_replacement);
  • func_closure - lista obiektów typu cell, które są de facto referencjami do zmiennych lokalnych z kontekstu utworzenia funkcji zagnieżdżonej;
  • func_dict - referencja do obiektu słownika, który zawiera "customowe" pola danej funkcji (patrz również PEP 232)
  • func_doc - referencja do stringu z dokumentacją funkcji, czyli de facto do wieloliniowego komentarza na początku funkcji (patrz również PEP 257

[VERBOSE] Obiekt function w przykładach

Stwierdziłem, że na blogu również zacznę wrzucać ramki w podobnej konwencji jak w mojej książce, tj. VERBOSE z dodatkowymi informacjami, oraz BEYOND z informacjami wychodzącymi poza zakres danego materiału. Ramki te nie są istotne dla głównego tematu danego postu, więc można je spokojnie pominąć. Ale ad meritum; poniżej znajduje się kilka bardzo krótkich kodów, które pokazują zależność między tym co robimy z funkcją (lub w jakim funkcja jest środowisku), a tym co znajduje się w danym polu obiektu function (pominąłem func_name - tam chyba nic ciekawego nie ma).

func_code
>>> def a(txt):
...   print txt
...
>>> def b(txt):
...   print txt[::-1]
...
>>> a.func_code = b.func_code
>>> a("asdf")
fdsa
Obiekty code niekoniecznie muszą być ze sobą "kompatybilne" - wszystko zależy od ilości zmiennych, skomplikowania operacji matematycznych (wysokość stosu), etc. W tym wypadku akurat były.

func_globals
>>> b = 1337
>>> def a():
...   print b
...
>>> a()
1337
>>> a.func_globals
{'a': , 'b': 1337, '__builtins__': , '__package__': None, '__name__': '__main__', '__doc__': None}
>>> id(a.func_globals) == id(globals())
True
Niestety to pole jest read-only. No fun.

func_defaults
>>> def a(txt="asdf"):
...   print txt
...
>>> a()
asdf
>>> a.func_defaults
('asdf',)
>>> a.func_defaults = ('xyz',)
>>> a()
xyz
Patrz również wspomniany wcześniej /n/Python_default_replacement.

func_closure
>>> def a():
...   b = 10
...   def c():
...     print b
...   return c
...
>>> a().func_closure
(,)
>>> a().func_closure[0].cell_contents
10

func_dict
>>> def a():
...   pass
...
>>> a.costam = "nic tam"
>>> a.func_dict
{'costam': 'nic tam'}
>>> a.func_dict["xyz"] = 1234
>>> a.xyz
1234

func_doc
>>> def a():
...   """dokumentacja funkcji"""
...   pass
...
>>> a.func_doc
'dokumentacja funkcji'
>>> a.__doc__
'dokumentacja funkcji'
>>> help(a)
Help on function a in module __main__:

a()
   dokumentacja funkcji


Jeśli chodzi o stworzenie nowego obiektu typu function, to, jak pokazałem w Prima Aprilisowym wideo, konstruktor przyjmuje dwa argumenty: obiekt typu code, oraz referencje do słownika ze zmiennymi globalnymi, np. (korzystając z obiektu code istniejącej funkcji):

>>> b = 123
>>> def a():
...   print b
...
>>> f = type(a)(a.func_code, globals())
>>> f()
123
>>> f = type(a)(a.func_code, {"b": 1337})
>>> f()
1337

Przechodząc do obiektu code, ten jest trochę bardziej skomplikowany. Zacznijmy od inspekcji, ponownie odrzucając wszystkie pola zaczynające się od dundera.

>>> [x for x in dir(f.func_code) if x[:2] != '__']
['co_argcount', 'co_cellvars', 'co_code', 'co_consts', 'co_filename', 'co_firstlineno', 'co_flags', 'co_freevars', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals', 'co_stacksize', 'co_varnames']

Znaczenie większości pól wyjaśniłem już na wideo, ale dla przypomnienia (w kolejności od najbardziej, do najmniej istotnych dla nas w tym momencie):
  • co_code - string z kodem bajtowym CPython;
  • co_argcount - całkowita liczba argumentów funkcji;
  • co_names - tuple z nazwami "zewnętrznych" (globalnych) obiektów/zmiennych/modułów/pól/itp. używanych przez kod bajtowy;
  • co_nlocals - sumaryczna liczba parametrów oraz zmiennych lokalnych;
  • co_varnames - tuple z nazwami parametrów oraz zmiennych lokalnych funkcji;
  • co_consts - tuple ze stałymi używanymi w funkcji - tu pojawią się wszelkie None, False, True, konkretne liczby, literały tekstowe, itp.;
  • co_flags - flagi, czyli specjalne ustawienia dla maszyny wirutalnej;
  • co_name - pierwotna nazwa funkcji;
  • co_filename - nazwa pliku z którego pochodzi funkcja - pole używane w komunikatach o błędach;
  • co_firstlineno - numer pierwszej linii funkcji w pliku źródłowym - pole używane przy wyświetlaniu komunikatów o błędach;
  • co_lnotab - "skompresowana" lista mapująca kolejne opkody w kodzie bajtowym do odpowiadających im numerów linii w kodzie źródłowym;
  • co_stacksize - dokładna wymagana wielkość stosu obliczeniowego funkcji (CPython VM jest maszyną stosową) ;
  • co_freevars - w przypadku funkcji zagnieżdżonych, tuple z nazwami używanych zmiennych lokalnych pochodzących "z zewnątrz" (tj. z funkcji-w-której-ta-funkcja-została-zagnieżdżona);
  • co_cellvars - w przypadku funkcji posiadających inne, zagnieżdżone funkcje, lista nazw zmiennych lokalnych, które są przez owe zagnieżdżone funkcje "dziedziczone" (używane);

Należy dodać, że wszystkie powyższe pola są tylko-do-odczytu, tj. gdyby chcieć któreś zmienić, trzeba by stworzyć nowy obiekt code.

[VERBOSE] Obiekt code w przykładach

Poniżej znajduje się kilka przykładów różnych definicji funkcji oraz zawartości poszczególnych pól obiektu code - podobnie jak w poprzedniej ramce, starałem się uwidocznić konkretne elementy funkcji, które mają wpływ na odpowiadające im pola. Pominąłem jednak pola związane z komunikatami o błędach.

co_code
>>> def inc(a):
...   return a + 1
...
>>> inc.func_code.co_code
'|\x00\x00d\x01\x00\x17S'
>>> import dis
>>> dis.dis(inc)
 2           0 LOAD_FAST                0 (a)
             3 LOAD_CONST               1 (1)
             6 BINARY_ADD
             7 RETURN_VALUE
Opis instrukcji można znaleźć np. w oficjalnej dokumentacji, choć korzystając z wyszukiwarki można znaleźć również ciekawsze wyniki. Dodam, że bajtkod niekoniecznie musi być kompatybilny pomiędzy różnymi wersjami Pythona (nawet małymi). Dodam, że istnieją również dekompilatory od kodu bajtowego Pythona, które w wyniku działania dają kod Pythona.

co_argcount
>>> def a(x): pass
...
>>> def b(x, y): pass
...
>>> a.func_code.co_argcount
1
>>> b.func_code.co_argcount
2

co_names
>>> import sys
>>> def a():
...   sys.stdout.write("asdf")
...
>>> a.func_code.co_names
('sys', 'stdout', 'write')

co_nlocals
>>> def a():
...   x, y = 1, 2
...
>>> def b(x):
...   print x
...
>>> a.func_code.co_nlocals
2
>>> b.func_code.co_nlocals
1
>>> def c(x):
...   y, z = 1, 2
...
>>> c.func_code.co_nlocals
3

co_varnames
>>> def a(param1, param2):
...   loc1, loc2 = 1, 2
...
>>> a.func_code.co_varnames
('param1', 'param2', 'loc1', 'loc2')

co_consts
>>> def a():
...   a = 5
...   b = 1.2
...   print "wynik:", a + b
...
>>> a.func_code.co_consts
(None, 5, 1.2, 'wynik:')
Stała None znajduje się zawsze na liście na pierwszym miejscu (a przynajmniej tak mi się wydaje; wcześniej myślałem, że to wynik domyślnego
co_freevars oraz co_cellvars
>>> def a():
...   b = 1
...   def c():
...     print b
...   return c
...
>>> c = a()
>>> a.func_code.co_cellvars
('b',)
>>> c.func_code.co_freevars
('b',)


Jeśli chodzi o utworzenie obiektu code, to przyznaję, że musiałem się trochę naszukać jakie parametry w jakiej kolejności przyjmuje konstruktor - ostatecznie zagadkę rozwiązało rzucenie okiem na funkcje code_new w Objects/codeobject.c (źródła CPython):

   if (!PyArg_ParseTuple(args, "iiiiSO!O!O!SSiS|O!O!:code",
                         &argcount, &nlocals, &stacksize, &flags,
                         &code,
                         &PyTuple_Type, &consts,
                         &PyTuple_Type, &names,
                         &PyTuple_Type, &varnames,
                         &filename, &name,
                         &firstlineno, &lnotab,
                         &PyTuple_Type, &freevars,
                         &PyTuple_Type, &cellvars))
       return NULL;

Jak można się domyślić patrząc na powyższy format parametrów, ostatnie dwa argumenty są opcjonalne (znak | przed ostatnimi dwoma O!, które zapewne oznaczają tuple).

Na nagraniu obiekt code, oraz powiązany z nim function, stworzyłem w następujący sposób:

hello_code = types.CodeType(
   1, # il arg
   2, # il zmiennych lokalnych
   3, # wielkosc stosu
   67, # flagi
   ''.join(map(chr, c)), # kod
   (None, 1.0), # stale
   ('stdout', 'write', 'flush', 'sleep'), # nazwy / funkcje / obiekty
   ('b', 'c'), # zmienne lokalne
   "",
   "hello", # nazwa funkcji
   1, "")

hello = types.FunctionType(hello_code, globals())

Dodam, że pewną zagwozdką dla mnie było samo odwołanie się do omawianych typów code i function - nie występują one domyślnie w globalnej przestrzeni nazw, więc w powyższym kodzie musiałem posłużyć się biblioteką types. Miałem jednak w zapasie alternatywny trik, znany mi z CTFów. Mianowicie, klasy w Pythonie posiadają metodę __subclasses__, która zwraca listę wszystkich klas dziedziczących po danej klasie. Co więcej, wszystkie klasy w Pythonie dziedziczą domyślnie po klasie object (wszystkie, łącznie z code oraz function), więc mogłem sobie interesujące mnie typy po prostu wydobyć korzystając z klasy object (która jak najbardziej znajduje się w przestrzeni nazw globalnych):

types = object.__subclasses__()
types_names = map(repr, types)
code = types[types_names.index(">type 'code'<")]
function = types[types_names.index(">type 'function'<")]

Innym planem było użycie function = types(jakaś_istniejąca_funkcja) oraz code = types(jakaś_istniejąca_funkcja.func_code), ale miałem problem ze znalezieniem jakiejkolwiek referencji do typu function w domyślnych importach - większość wbudowanych funkcji jest typu builtin_function_or_method (czyli "wewnętrzna/wbudowana w interpreter funkcja lub metoda"), który to typ nie nadaje się do tego typu zabaw.

Jeśli chodzi o samo pole co_code, to podczas video wypełniłem je stringiem stworzonym z tablicy wartości bajtów zapisanych bit po bicie:

c = [
   0b1111000, # 0
   0b101111,  # 1
   0b0,       # 2
   0b1111100, # 3
...
   0b0,       # 52
   0b1010011, # 53
]
...
   ''.join(map(chr, c)), # kod
...

W tym momencie należy wyjaśnić kilka rzeczy. Po pierwsze, kod bajtowy nie był wpisywany przeze mnie, tylko przez skrypt AutoIt3, co zresztą kilka osób od razu zauważyło, wskazując, że np. na czas 8:28 na video widać obie moje ręce w kamerce, ale kod się jakoś magicznie wpisuje (pierwszy wyłapał to ℕ𝕠𝕆𝕟𝕖; swoją drogą, wow, co ten unicode...). Fragment skryptu:

Opt("SendKeyDelay", 150)
Sleep(2500)

Send("0b1111000, # 0", 1)
Send("{ENTER}")
Send("0b101111,  # 1", 1)
Send("{ENTER}")
Send("0b0,       # 2", 1)
...

Sam skrypt był wygenerowany Pythonem - ot zwykła pętla po kolejnych bajtach z co_code wypisująca je w postaci wymaganej przez AutoIt (jakieś 'Send("%-10s # %i", 1)\nSend("{ENTER}")\n' % (bin(ord(nth_opcode))+",", n)).

Dodam, że na początku rozważałem wpisanie tego ręcznie, ale ostatecznie stwierdziłem, że niepotrzebnie wydłuży to wideo - a część z Paintem i tak zajmie masę czasu, więc zdałem się jednak na skrypt wprowadzający 6-7 znaków na sekundę bez pomyłek).

Jeśli chodzi o moją narrację podczas gdy wartości binarne były wprowadzane, to jest mniej lub bardziej poprawna (choć zdecydowanie niekompletna) - na drugim monitorze miałem otwarty wynik dis.dis() z tej funkcji i zerkałem na niego okiem żeby coś sensownego opowiadać czekając aż skrypt skończy (stąd numery bajtów w komentarzach powyżej - żebym wiedział na którą część listingu patrzeć).

Tak więc ostatecznie cały "trik" polegał na ręcznym odtworzeniu funkcji, wprowadzając ją od razu w postaci, którą oczekuje CPython 2.7. Gdyby to jeszcze nie było oczywiste, to nie, zdecydowanie tak się funkcji w Pythonie nie tworzy :)

Druga połowa wideo: Rysowanie modułu

Pomysł z "rysowaniem", a raczej wstawianiem kolorowych pixeli w celu stworzenia pliku nie-graficznego nie jest ani oryginalny, ani nowy - np. KrzaQ podrzucił mi link do tego GIFa (wie ktoś może kto to nagrał? wg. stackoverflow jest to Overv, ale potwierdzenie mile widziane), którego zresztą widziałem już wcześniej. Problemem w tego typu hackach jest jednak nagłówek pliku BMP, który raczej nie jest kompatybilny z nagłówkiem formatu, który chce się uzyskać (w moim przypadku był nim PYC, czyli skompilowany moduł CPython) - widać to również na podlinkowanym GIFie, w którym autor ostatecznie uzyskuje kod C ze "śmieciami przed nim".

Przypomniałem sobie jednak, że kiedyś mi coś mignęło o tym, że Python może wczytywać moduły z archiwów ZIP - był to o tyle obiecujący świetny trop, że ZIP ma "nagłówek" od którego zaczyna się przetwarzanie (parsing) pliku na końcu pliku, podczas gdy BMP ma na początku. To oznacza, że bez problemu można zrobić plik, który jest zarówno poprawnym BMP jak i poprawnym ZIPem. Zacząłem więc od skompilowania modułu z funkcją hello do .pyc (tj. po prostu uruchomiłem skrypt, który wczytywał ten moduł - CPython 2.7 domyślnie generuje .pyc dla modułów wtedy), skompresowania go ZIPem, zmiany rozszerzenia archiwum na .raw, wczytaniu go jako "surowe dane bitmapy" w moim ulubionym IfranView (patrz opcje poniżej), i zapisania jako 24-bitowy BMP.


W samych opcjach odczytu jako RAW istotne było, aby wziąć pod uwagę wszystkie cechy BMP, które wpłyną na to jak ułożą się bajty po zapisaniu pliku (zarówno przez IrfanView, jak i docelowo przez MS Paint) - konkretniej, musiały się ułożyć dokładnie w taki sam sposób w jakim były w pliku RAW (ZIP). Z istotnych ustawień:
  • 24 BPP - nie byłem przekonany czy MS Paint daje pełną kontrolę nad 8-bitową paletą, więc jedynym rozsądnym wyborem był 24-bitowy RGB
  • 4 x 32 - plik RAW/ZIP miał 379 bajtów, co przy trzech bajtach na piksel daje 127 pikseli; co więcej, wielkość każdego wiersza w bajtach musiała być podzielna przez 4 - inaczej BMP wymaga dopełnienia, co wprowadziłoby dodatkowe, niechciane bajty; uznałem, że 4 piksele na wiersz (a więc wiersz 12 bajtowy) będzie OK, co dało 32 piksele na wysokość (4 * 32 = 128)
  • Vertical flip - domyślnie wiersze (scanline) w BMP są zapisywane "do góry nogami", tj. w pionowym odbiciu lustrzanym (nie mam pojęcia czemu akurat tak zrobiono - anyone? dodam, że jeśli poda się wysokość bitmapy jako liczbę ujemną, to wtedy wiersze są zapisywane "normalnie"); aby temu przeciwdziałać, musiałem sam też wczytać dane jako pionowe odbicie lustrzane - dzięki temu przy zapisie do BMP wiersze zostały ponownie odwrócone w pionie, a więc wróciły do pierwotnej postaci
  • BGR - w 24-bitowym taka jest BMP kolejność barw (niebieska, zielona, czerwona)
Po zapisie RAW do BMP uzyskałem w zasadzie ten sam plik RAW, tyle że z nagłówkiem BMP na początku, oraz 5cioma bajtami zerowymi na końcu (RAW miał 379 bajtów, podczas gdy bitmapa 24-bit 4 x 32 zajmuje 384 bajty - stąd zostały dodane te dodatkowe bajty). W tym momencie przestał to być jednak poprawny ZIP, ponieważ pozmieniały się offsety w pliku (wszystko przesunęło się "o nagłówek BMP" do przodu), należało więc hexedytorem dokonać stosownych poprawek w nagłówkach ZIPowych, a konkretniej:
  • End of central directory record →  CentralDirectoryOffset - dodać wielkość nagłówka BMP w bajtach;
  • End of central directory record → ZipCommentLength - na 5, tak żeby komentarz objął te 5 dodanych bajtów;
  • Central directory structure → RelativeOffsetOfLocalHeader - dodać wielkość nagłówka BMP w bajtach;

Tak powstały BMP+ZIP był poprawny i doskonale obsługiwany przez Total Commandera jak i moduł zipfile w Pythonie... ale już nie przez samą dyrektywę import. Po krótkim śledztwie okazało się, że CPython ma dodatkowy wbudowany moduł nazwany zipimporter, który jest odpowiedzialny za parsing plików ZIP w celu zaimportowania z nich modułów. W nim za rozpoczęcie parsingu ZIP odpowiada funkcja read_directory (Modules/zipimport.c w źródłach CPython), która wczytuje ZIP w następujący sposób:

fp = fopen(archive, "rb");
...
if (fseek(fp, -22, SEEK_END) == -1) {
...
if (fread(endof_central_dir, 1, 22, fp) != 22) {
...
if (get_long((unsigned char *)endof_central_dir) != 0x06054B50) {
       /* Bad: End of Central Dir signature */
       fclose(fp);
       PyErr_Format(ZipImportError, "not a Zip file: "
                    "'%.200s'", archive);
       return NULL;
   }

I teraz: nie chodzi o to co jest w powyższym kodzie, ale o to czego tam nie ma - a nie ma tam obsługi istnienia jakiegokolwiek komentarza - istnienie komentarza odsuwa "nagłówek" ZIPa od końca pliku o wielkość komentarza (dlatego interpretacje ZIPów mogą być niejednoznaczne - patrz moja prelekcja na SEConference 2013). A ja podczas "naprawy" ZIP zadeklarowałem komentarz o wielkości 5ciu bajtów.

Rozwiązanie tego było oczywiście dość proste - należało wrócić do hexedytora, wrzucić 5 zerowych bajtów na początek danych bitmapy, usunąć je z końca (z "komentarza"), i ponownie poprawić wielkości pól (komentarz tym razem na zero) - po tym kroku otrzymałem plik BMP+ZIP z którym Python radził sobie doskonale (aby zaimportować moduł z pliku ZIP należy owy ZIP dodać do listy ścieżek w sys.path):

>>> import sys
>>> sys.path.append("mymod.bmp")
>>> import hello
>>> hello.hello("asdf")
asdf

Pozostała więc jedynie kwestia "interaktywnego stworzenia pliku BMP" na potrzeby wideo, do czego ponownie przydał się AutoIt3 oraz skrypt w Pythonie który wziął plik BMP (a raczej plik RAW który sobie z tego BMP wygenerowałem, żeby pozbyć się chwilowo nagłówków BMP) i wygenerował z niego zestaw poleceń dla AutoIt3. Fragment skryptu wyglądał następująco:

Local $p = WinGetPos("[REGEXPTITLE:.* - Paint]")
Local $px = $p[0]
Local $py = $p[1]

Opt("SendKeyDelay", 150)

MouseMove($px + 500, $py + 500)
MouseClick("main")

#include "../test/go.au3"
; Ten plik zawierał polecenia rysowania pikseli
; korzystajac z funkcji ponizej. Np.:
;
;  SetPal(55, 0, 1)
;  DrawDot(0, 0)
;  SetPal(0, 0, 0)
;  DrawDot(1, 0)
;

Func DrawDot($x, $y)
 Local $xx = $px + 30 + $x * 8 + 4 + Random(-2, 2, 1)
 Local $yy = $py + 172 + $y * 8 + 4 + Random(-2, 2, 1)

 MouseMove($xx, $yy, Random(10, 20))
 MouseClick("main")
EndFunc

Func SetPal($r, $g, $b)
 MouseMove($px + 999 + Random(-7, 7, 1), $py + 75 + Random(-7, 7, 1), Random(10, 20))
 MouseClick("main")

 WinWaitActive("[TITLE:Edit Colors]")
 Local $e = WinGetPos("[TITLE:Edit Colors]")
 Local $ex = $e[0]
 Local $ey = $e[1]

 MouseMove($px + 885 + Random(-20, 20, 1), $py + 403 + Random(-20, 20, 1), 5)
 MouseMove($ex + 421 + Random(-7, 7, 1), $ey + 242 + Random(-1, 1, 1), Random(10, 20))
 MouseClick("main")
 MouseClick("main")
 Send($r)
 Send("{TAB}")
 Send($g)
 Send("{TAB}")
 Send($b)
 
 Send("{ENTER}")
 WinWaitClose("[TITLE:Edit Colors]")
EndFunc

Pozostało więc jedynie odpalić skrypt podczas wideo i coś tam mówić, żeby odwrócić uwagę m.in. od tego jak szybko to wszystko idzie. Dodam, że cała narracja podczas wideo była oczywiście zmyślona - osoby zaznajomione z BMP zapewne się zorientowały, że mówię o "nagłówku PYC" podczas gdy [skrypt] rysuje piksele na górze bitmapy - jak wspomniałem wcześniej, BMP zapisuje dane w odbiciu pionowym, a więc nagłówek powinien trafić na sam dół pliku. Tak więc narracja podczas rysowania była jedynie bajką :)

Pod koniec wideo jeszcze trochę się zamieszałem z tym który plik import hello zaimportował - wszystko przez to, że miałem plik hello.py w tym samym katalogu. Niemniej jednak po kilkudziesięciu sekundach się zorientowałem co jest nie tak, i zmieniłem nazwę hello.py na h.py pokazując, że jednak faktycznie ten narysowany moduł działa poprawnie.

I to by było chyba na tylę, jeśli chodzi o konstrukcję mojego Prima Aprilisowego żartu. Wyszło chyba całkiem nieźle, jak na pomysł, który wpadł mi do głowy rano 1 kwietnia ;)

Na koniec dodam, że o kilku mniej lub bardziej losowych trickach związanych z Pythonem mówiłem też na PyCon 2015 na mojej prelekcji pt. "Python in a hacker's toolbox" (wideo) - zachęcam do rzucenia okiem. Część slajdów tam pochodziła z innych moich wystąpień, m.in. wspólnych z j00ru, tj. CONFidence 2014: " On the battlefield with the Dragons" (wideo) oraz Insomni'hack 2015: "Pwning (sometimes) with style – Dragons’ notes on CTFs" (slajdy). Jeśli chodzi o ZIPy, to dość dokładnie przerabiałem je na SEConference 2013: "Dziesięć tysięcy pułapek: ZIP, RAR, etc." (wideo), a o BMP pisałem już m.in. w mojej książce (rozdział 12) oraz w artykule "Format BMP okiem hakera", który można znaleźć na sieci.

P.S. Niestety, samo tworzenie przeze mnie kursu Pythona dla początkujących również było częścią kawału - póki co takich planów nie mam. Byłem dość zaskoczony, że całkiem spora liczba osób stwierdziła, że "szkoda" - tj. nie jestem pewien czy potrzebny jest kolejny kurs Pythona dla początkujących - tego w sieci chyba jest od groma... right?

Comments:

2016-04-04 10:24:08 = Norbitor
{
Muszę przyznać, że ten żart był jednym z lepszych jaki widziałem 1.04. Ja też należę do tych, co spisek wykryli w momencie, gdy odpaliłeś Painta. Pisanie funkcji "niskopoziomowo" mnie nie zaskoczyło, a nawet mi się spodobało.
Jednak automatykę w pisaniu kodu, jak i klikaniu w Paint'cie zauważyłem od razu, przede wszystkim interwały pomiędzy poszczególnymi klawiszami były zbyt idealne. A w Paint było widać, że schemat ruchów myszki był cały czas ten sam. Nie mniej dobra robota!
}
2016-04-04 11:55:14 = masakra
{
Pozdrawiam Mateusza Kalete
}
2016-04-04 21:14:25 = Ezo
{
Eh, a ja myślałem że żartem było samo tworzenie tutoriala o takich prostych rzeczach i nie chciało mi się oglądać :D Dobrze że trafiłem na tego posta
}
2016-04-06 14:00:42 = Tomipnh
{
Żart świetny, miałem inną teorie jak oglądałem to na początku :-)

Patrząc na 'kod' w paincie przypomina mi się http://www.dangermouse.net/esoteric/piet/samples.html
}
2016-04-06 18:12:51 = nn
{
Ile faktycznie czasu Ci to zajęło? Pamiętam Twoją prezentację o zipach, więc domyślam się, że temat już znałeś, jednak gdybym ja to robił to pewnie bym się zaplątał kilka razy i potrzebował na to naście dni.
}
2016-04-06 21:24:53 = QrA
{
Świetne wykonanie i pomysł. Do tego pomysłowość. Było to naprawdę zabawne i podziwiam za utrzymanie powagi :)

PS. Irfan View, nie Ifran ;)
}

Add a comment:

Nick:
URL (optional):
Math captcha: 9 ∗ 9 + 7 =