Można dodać nowe informacje (takie jak print
, raise
, with
) do składni Pythona?
Powiedz, aby pozwolić ...
mystatement "Something"
Lub,
new_if True:
print "example"
Nie tak bardzo, jeśli powinieneś , ale raczej jeśli to możliwe (bez modyfikowania kodu interpretera języka Python)
Odpowiedzi:
Może ci się to przydać - wewnętrzne elementy Pythona: dodawanie nowej instrukcji do Pythona , cytowane tutaj:
Ten artykuł jest próbą lepszego zrozumienia, jak działa front-end Pythona. Samo przeczytanie dokumentacji i kodu źródłowego może być trochę nudne, więc przyjmuję podejście praktyczne: zamierzam dodać
until
instrukcję do Pythona.Całe kodowanie tego artykułu zostało wykonane w najnowocześniejszej gałęzi Py3k w lustrze repozytorium Python Mercurial .
until
oświadczenieNiektóre języki, takie jak Ruby, mają
until
instrukcję, która jest uzupełnieniemwhile
(until num == 0
jest odpowiednikiemwhile num != 0
). W Rubim mogę napisać:I wydrukuje:
Chcę więc dodać podobną możliwość do Pythona. Oznacza to, że można pisać:
Dygresja na rzecz języka
Ten artykuł nie jest próbą sugerowania dodania
until
instrukcji do Pythona. Chociaż myślę, że takie stwierdzenie uczyniłoby jakiś kod bardziej przejrzystym, a ten artykuł pokazuje, jak łatwo jest go dodać, całkowicie szanuję filozofię minimalizmu Pythona. Jedyne, co próbuję tutaj zrobić, to uzyskać wgląd w wewnętrzne działanie Pythona.Modyfikacja gramatyki
Python używa niestandardowego generatora parserów o nazwie
pgen
. To jest parser LL (1), który konwertuje kod źródłowy Pythona na drzewo parsowania. Dane wejściowe do generatora parsera to plikGrammar/Grammar
[1] . To jest prosty plik tekstowy, który określa gramatykę języka Python.[1] : Odtąd odniesienia do plików w źródle Pythona są podawane względnie do katalogu głównego drzewa źródłowego, czyli katalogu, w którym uruchamiasz configure i make, aby zbudować Python.
W pliku gramatyki należy wprowadzić dwie modyfikacje. Pierwszą jest dodanie definicji
until
instrukcji. Znalazłem, gdziewhile
stwierdzenie zostało zdefiniowane (while_stmt
) i dodaneuntil_stmt
poniżej [2] :[2] : To pokazuje powszechną technikę, której używam podczas modyfikowania kodu źródłowego, którego nie znam: praca według podobieństwa . Ta zasada nie rozwiąże wszystkich problemów, ale zdecydowanie może ułatwić proces. Ponieważ wszystko, co trzeba zrobić,
while
również musi zostać zrobioneuntil
, służy to jako całkiem dobra wskazówka.Zauważ, że zdecydowałem się wykluczyć
else
klauzulę z mojej definicjiuntil
, tylko po to, aby była trochę inna (i ponieważ szczerze mówiąc nie podoba mi sięelse
klauzula pętli i nie sądzę, aby dobrze pasowała do Zen w Pythonie).Druga zmiana polega na zmodyfikowaniu reguły w
compound_stmt
celu uwzględnieniauntil_stmt
, jak widać w powyższym fragmencie. Towhile_stmt
znowu zaraz potem .Po uruchomieniu
make
po zmodyfikowaniuGrammar/Grammar
, informacja, żepgen
program jest uruchomiony do ponownego generowaniaInclude/graminit.h
iPython/graminit.c
, a następnie kilka plików uzyskać ponownie skompilowany.Modyfikacja kodu generacji AST
Po utworzeniu przez parsera Pythona drzewa parsowania, drzewo to jest konwertowane na AST, ponieważ AST jest znacznie prostszy w pracy na kolejnych etapach procesu kompilacji.
Więc zamierzamy odwiedzić,
Parser/Python.asdl
który definiuje strukturę AST Pythona i dodać węzeł AST dla naszej nowejuntil
instrukcji, ponownie tuż podwhile
:Jeśli teraz uruchomisz
make
, zwróć uwagę, że przed skompilowaniem wielu plikówParser/asdl_c.py
jest uruchamiany w celu wygenerowania kodu C z pliku definicji AST. To (podobnieGrammar/Grammar
) jest kolejnym przykładem kodu źródłowego Pythona używającego minijęzyka (innymi słowy DSL) w celu uproszczenia programowania. Zauważ również, że ponieważParser/asdl_c.py
jest to skrypt w Pythonie, jest to rodzaj ładowania początkowego - aby zbudować Pythona od podstaw, Python musi już być dostępny.Podczas
Parser/asdl_c.py
generowania kodu do zarządzania naszym nowo zdefiniowanym węzłem AST (w plikachInclude/Python-ast.h
iPython/Python-ast.c
), nadal musimy ręcznie napisać kod, który konwertuje do niego odpowiedni węzeł drzewa parsowania. Odbywa się to w plikuPython/ast.c
. Tam funkcja o nazwieast_for_stmt
konwertuje węzły drzewa analizy instrukcji na węzły AST. Ponownie, kierując się naszym starym przyjacielemwhile
, wskakujemy od razu do tematuswitch
obsługi instrukcji złożonych i dodajemy klauzulę dlauntil_stmt
:Teraz powinniśmy wdrożyć
ast_for_until_stmt
. Oto ona:Ponownie, zostało to zakodowane podczas dokładnego przyglądania się odpowiednikowi
ast_for_while_stmt
, z tą różnicą,until
że zdecydowałem się nie popieraćelse
klauzuli. Zgodnie z oczekiwaniami, AST jest tworzony rekurencyjnie, przy użyciu innych funkcji tworzących AST, takich jakast_for_expr
wyrażenie warunku iast_for_suite
treśćuntil
instrukcji. Na koniecUntil
zwracany jest nowy węzeł o nazwie .Zauważ, że uzyskujemy dostęp do węzła drzewa parsowania
n
za pomocą niektórych makr, takich jakNCH
iCHILD
. Warto je zrozumieć - ich kod jest wInclude/node.h
.Dygresja: skład AST
Zdecydowałem się utworzyć nowy typ AST dla
until
instrukcji, ale w rzeczywistości nie jest to konieczne. Mogłem zaoszczędzić trochę pracy i zaimplementować nową funkcjonalność przy użyciu kompozycji istniejących węzłów AST, ponieważ:Jest funkcjonalnie równoważne z:
Zamiast tworzyć
Until
węzeł wast_for_until_stmt
, mogłem utworzyćNot
węzeł zWhile
węzłem jako dziecko. Ponieważ kompilator AST już wie, jak obsługiwać te węzły, można pominąć kolejne kroki procesu.Kompilowanie AST do kodu bajtowego
Następnym krokiem jest skompilowanie AST do kodu bajtowego Pythona. Kompilacja ma pośredni wynik, którym jest CFG (Control Flow Graph), ale ponieważ obsługuje go ten sam kod, na razie zignoruję ten szczegół i zostawię go w innym artykule.
Kod, któremu przyjrzymy się dalej, to
Python/compile.c
. Idąc za tropemwhile
, znajdujemy funkcjęcompiler_visit_stmt
, która jest odpowiedzialna za kompilowanie instrukcji do kodu bajtowego. Dodajemy klauzulę naUntil
:Jeśli zastanawiasz się, co to
Until_kind
jest, jest to stała (właściwie wartość_stmt_kind
wyliczenia) automatycznie generowana z pliku definicji AST doInclude/Python-ast.h
. W każdym razie nazywamy to,compiler_until
co oczywiście nadal nie istnieje. Zaraz do tego dojdę.Jeśli jesteś ciekawy jak ja, zauważysz, że
compiler_visit_stmt
jest to dziwne. Żadna ilośćgrep
-ping drzewa źródłowego nie ujawnia, gdzie jest wywołane. W takim przypadku pozostaje tylko jedna opcja - C makro-fu. Rzeczywiście, krótkie śledztwo prowadzi nas doVISIT
makra zdefiniowanego wPython/compile.c
:Jest używany do wywoływania
compiler_visit_stmt
wcompiler_body
. Wracając jednak do naszej działalności ...Zgodnie z obietnicą, oto
compiler_until
:Muszę się przyznać: ten kod nie został napisany w oparciu o głębokie zrozumienie kodu bajtowego Pythona. Podobnie jak reszta artykułu, zrobiono to naśladując funkcję rodziny
compiler_while
. Czytając go uważnie, pamiętając jednak, że maszyna wirtualna Pythona jest oparta na stosie, i zaglądając do dokumentacjidis
modułu, która zawiera listę kodów bajtowych Pythona wraz z opisami, można zrozumieć, co się dzieje.To wszystko, skończyliśmy ... prawda?
Po wprowadzeniu wszystkich zmian i uruchomieniu
make
możemy uruchomić nowo skompilowany Python i wypróbować naszą nowąuntil
instrukcję:Voila, to działa! Zobaczmy kod bajtowy utworzony dla nowej instrukcji za pomocą
dis
modułu w następujący sposób:Oto wynik:
Najciekawszą operacją jest numer 12: jeśli warunek jest prawdziwy, przeskakujemy za pętlą. To jest poprawna semantyka dla
until
. Jeśli skok nie zostanie wykonany, treść pętli działa, dopóki nie wróci do stanu z operacji 35.Czując się dobrze po zmianie, spróbowałem uruchomić funkcję (wykonać
myfoo(3)
) zamiast wyświetlać jej kod bajtowy. Wynik był mniej niż zachęcający:Whoa ... to nie może być dobre. Więc co poszło nie tak?
Przypadek brakującej tablicy symboli
Jednym z kroków, które kompilator Pythona wykonuje podczas kompilowania AST, jest utworzenie tablicy symboli dla kompilowanego kodu. Wywołanie
PySymtable_Build
inPyAST_Compile
wywołuje moduł tablicy symboli (Python/symtable.c
), który porusza się po AST w sposób podobny do funkcji generowania kodu. Posiadanie tabeli symboli dla każdego zakresu pomaga kompilatorowi w ustaleniu niektórych kluczowych informacji, takich jak, które zmienne są globalne, a które lokalne.Aby rozwiązać problem, musimy zmodyfikować
symtable_visit_stmt
funkcję wPython/symtable.c
, dodając kod do obsługiuntil
instrukcji, po podobnym kodzie dlawhile
instrukcji [3] :[3] : Nawiasem mówiąc, bez tego kodu jest ostrzeżenie kompilatora dla
Python/symtable.c
. Kompilator zauważa, żeUntil_kind
wartość wyliczenia nie jest obsługiwana w instrukcji switchsymtable_visit_stmt
i narzeka. Zawsze ważne jest, aby sprawdzić ostrzeżenia kompilatora!A teraz naprawdę skończyliśmy. Kompilowanie źródła po tej zmianie powoduje wykonanie
myfoo(3)
pracy zgodnie z oczekiwaniami.Wniosek
W tym artykule pokazałem, jak dodać nową instrukcję do Pythona. Choć wymagała sporo majsterkowania w kodzie kompilatora Pythona, zmiana nie była trudna do zaimplementowania, ponieważ jako wskazówkę użyłem podobnej i istniejącej instrukcji.
Kompilator Pythona to wyrafinowany kawałek oprogramowania i nie twierdzę, że jestem w nim ekspertem. Jednak naprawdę interesują mnie wewnętrzne elementy Pythona, a zwłaszcza jego interfejs. Dlatego uważam, że to ćwiczenie jest bardzo przydatnym towarzyszem teoretycznego badania zasad kompilatora i kodu źródłowego. Posłuży jako podstawa dla przyszłych artykułów, które zagłębią się w kompilator.
Bibliografia
Przy konstrukcji tego artykułu wykorzystałem kilka doskonałych odniesień. Oto one, w przypadkowej kolejności:
Pierwotnym źródłem
źródło
until
jestisa
/isan
jak wif something isa dict:
orif something isan int:
Jednym ze sposobów na wykonanie takich czynności jest wstępne przetworzenie źródła i zmodyfikowanie go, przetłumaczenie dodanej instrukcji na język Python. Jest wiele problemów, które przyniesie to podejście i nie polecałbym go do ogólnego użytku, ale do eksperymentowania z językiem lub metaprogramowaniem w określonym celu może się czasami przydać.
Na przykład, powiedzmy, że chcemy wprowadzić instrukcję „myprint”, która zamiast drukować na ekranie, loguje się do określonego pliku. to znaczy:
byłaby równoważna
Istnieją różne opcje, jak wykonać zamianę, od podstawiania wyrażeń regularnych do generowania AST, po napisanie własnego parsera w zależności od tego, jak blisko składnia pasuje do istniejącego języka Python. Dobrym podejściem pośrednim jest użycie modułu tokenizera. Powinno to umożliwić dodawanie nowych słów kluczowych, struktur kontrolnych itp. Podczas interpretowania źródła podobnie do interpretera Pythona, unikając w ten sposób załamań, które spowodowałyby proste rozwiązania regex. Dla powyższego „myprint” możesz napisać następujący kod transformacji:
(To sprawia, że myprint skutecznie staje się słowem kluczowym, więc użycie go jako zmiennej w innym miejscu prawdopodobnie spowoduje problemy)
Problem polega więc na tym, jak go użyć, aby kod był użyteczny w Pythonie. Jednym ze sposobów byłoby po prostu napisanie własnej funkcji importu i użycie jej do załadowania kodu napisanego w Twoim niestandardowym języku. to znaczy:
Wymaga to jednak obsługi dostosowanego kodu inaczej niż normalne moduły Pythona. tj. „
some_mod = myimport("some_mod.py")
” zamiast „import some_mod
”Innym dość zgrabnym (aczkolwiek hackerskim) rozwiązaniem jest utworzenie niestandardowego kodowania (patrz PEP 263 ), jak pokazuje ten przepis. Możesz to zaimplementować jako:
Teraz, po uruchomieniu tego kodu (np. Możesz umieścić go w swoim .pythonrc lub site.py), każdy kod zaczynający się od komentarza „# coding: mylang” zostanie automatycznie przetłumaczony w powyższym kroku wstępnego przetwarzania. na przykład.
Ostrzeżenia:
Istnieją problemy z podejściem preprocesorowym, co prawdopodobnie będziesz znać, jeśli pracowałeś z preprocesorem C. Głównym jest debugowanie. Wszystko, co widzi Python, to wstępnie przetworzony plik, co oznacza, że tekst wydrukowany w śladzie stosu itp. Będzie się do niego odnosił. Jeśli wykonałeś znaczące tłumaczenie, może się to bardzo różnić od tekstu źródłowego. Powyższy przykład nie zmienia numerów linii itp., Więc nie będzie zbyt różny, ale im bardziej go zmienisz, tym trudniej będzie to rozgryźć.
źródło
myimport
na module, który po prostu zawiera,print 1
ponieważ jest to tylko wiersz kodu=1 ... SyntaxError: invalid syntax
b=myimport("b.py")
” i b.py zawierające tylko „print 1
”. Czy jest coś więcej do błędu (ślad stosu etc)?import
używa wbudowanego__import__
, więc jeśli go nadpiszesz ( przed zaimportowaniem modułu, który wymaga zmodyfikowanego importu), nie potrzebujesz osobnegomyimport
Tak, do pewnego stopnia jest to możliwe. Istnieje moduł , który używa
sys.settrace()
do implementacjigoto
icomefrom
„słów kluczowych”:źródło
Oprócz zmiany i ponownej kompilacji kodu źródłowego (co jest możliwe w przypadku oprogramowania open source), zmiana języka podstawowego nie jest tak naprawdę możliwa.
Nawet jeśli ponownie skompilujesz źródło, nie będzie to Python, tylko twoja zhakowana zmieniona wersja, do której musisz bardzo uważać, aby nie wprowadzać błędów.
Jednak nie jestem pewien, dlaczego chcesz. Zorientowane obiektowo funkcje Pythona sprawiają, że osiągnięcie podobnych wyników w obecnym języku jest całkiem proste.
źródło
Ogólna odpowiedź: musisz wstępnie przetworzyć swoje pliki źródłowe.
Bardziej konkretna odpowiedź: zainstaluj EasyExtend i wykonaj następujące kroki
i) Utwórz nowy langlet (język rozszerzenia)
Bez dodatkowej specyfikacji zostanie utworzona paczka plików w EasyExtend / langlets / mystmts /.
ii) Otwórz mystmts / parsedef / Grammar.ext i dodaj następujące wiersze
To wystarczy, aby zdefiniować składnię nowej instrukcji. Niedoterminal small_stmt jest częścią gramatyki Pythona i jest miejscem, w którym jest przechwytywana nowa instrukcja. Parser rozpozna teraz nową instrukcję, tj. Plik źródłowy zawierający ją zostanie przeanalizowany. Kompilator odrzuci go jednak, ponieważ nadal musi zostać przekształcony w prawidłowy Python.
iii) Teraz należy dodać semantykę zdania. W tym celu należy edytować plik msytmts / langlet.py i dodać odwiedzającego węzeł my_stmt.
iv) cd do langlets / mystmts i wpisz
Teraz należy rozpocząć sesję i skorzystać z nowo zdefiniowanego zestawienia:
Sporo kroków, aby dojść do trywialnego stwierdzenia, prawda? Nie ma jeszcze API, które pozwala definiować proste rzeczy bez przejmowania się gramatyką. Ale EE jest bardzo niezawodny, modulo kilka błędów. Więc to tylko kwestia czasu, kiedy pojawi się API, które pozwoli programistom definiować wygodne rzeczy, takie jak operatory wrostków lub małe instrukcje przy użyciu wygodnego programowania obiektowego. W przypadku bardziej złożonych rzeczy, takich jak osadzanie całych języków w Pythonie za pomocą budowania języka, nie ma możliwości obejścia pełnego podejścia gramatycznego.
źródło
Oto bardzo prosty, ale kiepski sposób dodawania nowych stwierdzeń, tylko w trybie interpretacyjnym . Używam go do małych 1-literowych poleceń do edycji adnotacji genów przy użyciu tylko sys.displayhook, ale żeby móc odpowiedzieć na to pytanie, dodałem również sys.excepthook dla błędów składniowych. To ostatnie jest naprawdę brzydkie, pobiera surowy kod z bufora readline. Zaletą jest to, że w ten sposób dodawanie nowych instrukcji jest banalnie proste.
źródło
Znalazłem poradnik dotyczący dodawania nowych wyciągów:
https://troeger.eu/files/teaching/pythonvm08lab.pdf
Zasadniczo, aby dodać nowe wyciągi, musisz edytować
Python/ast.c
(między innymi) i ponownie skompilować plik binarny Pythona.Chociaż jest to możliwe, nie rób tego. Możesz osiągnąć prawie wszystko za pomocą funkcji i klas (które nie będą wymagały od ludzi przekompilowywania Pythona tylko po to, aby uruchomić skrypt ..)
źródło
Można to zrobić za pomocą EasyExtend :
źródło
Nie chodzi o dodawanie nowych instrukcji do składni języka, ale makra są potężnym narzędziem: https://github.com/lihaoyi/macropy
źródło
Nie bez modyfikacji tłumacza. Wiem, że wiele języków w ciągu ostatnich kilku lat zostało opisanych jako „rozszerzalne”, ale nie w taki sposób, w jaki to opisujesz. Rozszerzasz Python, dodając funkcje i klasy.
źródło
Istnieje język oparty na Pythonie o nazwie Logix, za pomocą którego MOŻESZ robić takie rzeczy. Od jakiegoś czasu nie był w fazie rozwoju, ale funkcje, o które prosiłeś , działają z najnowszą wersją.
źródło
Niektóre rzeczy można zrobić z dekoratorami. Załóżmy np., Że Python nie miał
with
instrukcji. Moglibyśmy wtedy zaimplementować podobne zachowanie:Jest to jednak dość nieczyste rozwiązanie, jak tutaj zrobiono. Szczególnie zachowanie, w którym dekorator wywołuje funkcję i ustawia
_
ją,None
jest nieoczekiwane. Dla wyjaśnienia: ten dekorator jest równoznaczny z pisaniema dekoratorzy zwykle mają modyfikować, a nie wykonywać funkcje.
Z takiej metody korzystałem już wcześniej w skrypcie, w którym musiałem tymczasowo ustawić katalog roboczy dla kilku funkcji.
źródło
Dziesięć lat temu nie mogłeś i wątpię, żeby to się zmieniło. Jednak modyfikacja składni nie była wtedy taka trudna, jeśli byłeś przygotowany do ponownej kompilacji Pythona, i wątpię, czy to się zmieniło.
źródło