Czy możesz dodać nowe instrukcje do składni Pythona?

124

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)

dbr
źródło
10
Z nieco pokrewnej uwagi, jeden przypadek użycia, w którym może być przydatne tworzenie nowych instrukcji w locie (w przeciwieństwie do poważnego „rozszerzania” języka), jest przeznaczony dla osób, które używają interaktywnego interpretera jako kalkulatora lub nawet powłoki systemu operacyjnego . Często tworzę małe jednorazowe funkcje w locie, aby zrobić coś, co zamierzam powtórzyć, iw takich sytuacjach fajnie byłoby tworzyć bardzo skrócone polecenia, takie jak makra lub instrukcje, zamiast wpisywać długie nazwy za pomocą składni function (). Oczywiście nie do tego służy Py… ale ludzie spędzają dużo czasu na interaktywnym używaniu go.
Kilo
5
@Kilo warto przyjrzeć się ipythonowi - ma wiele funkcji powłoki, na przykład możesz używać zwykłych poleceń "ls" i "cd", uzupełnianie tabulatorami, wiele funkcji makr itp.
dbr
Niektóre języki są doskonale rozszerzalne, np. Forth i Smalltalk, ale ich paradygmaty językowe są inne niż te używane przez Pythona. W obu przypadkach wszelkie nowe słowa (Forth) lub metody (Smalltalk) stają się integralną, nieodróżnialną częścią języka tej instalacji. Tak więc każda instalacja Forth czy Smalltalk staje się z czasem unikalnym dziełem. Forth jest również oparty na RPN. Ale myśląc po liniach DSL, coś takiego powinno być możliwe do osiągnięcia w Pythonie. Chociaż, jak powiedzieli inni, dlaczego?
1
Jako osoba biegle posługująca się zarówno językiem Python, jak i Forth, a także osoba, która zaimplementowała kilka kompilatorów Forth w przeszłości, mogę wnieść tutaj pewien autorytet. Bez uzyskania surowego dostępu do wewnętrznego parsera Pythona jest to całkowicie niemożliwe. Możesz to sfałszować, przetwarzając wstępnie, co ilustrują (szczerze mówiąc, dość zręczne!) Poniższe odpowiedzi, ale prawdziwa aktualizacja składni i / lub semantyki języka w gorącym interprecie nie jest możliwa. Jest to zarówno przekleństwo Pythona, jak i jego przewaga nad językami podobnymi do Lispa i Forth.
Samuel A. Falvo II

Odpowiedzi:

153

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ć untilinstrukcję do Pythona.

Całe kodowanie tego artykułu zostało wykonane w najnowocześniejszej gałęzi Py3k w lustrze repozytorium Python Mercurial .

untiloświadczenie

Niektóre języki, takie jak Ruby, mają untilinstrukcję, która jest uzupełnieniem while( until num == 0jest odpowiednikiem while num != 0). W Rubim mogę napisać:

num = 3
until num == 0 do
  puts num
  num -= 1
end

I wydrukuje:

3
2
1

Chcę więc dodać podobną możliwość do Pythona. Oznacza to, że można pisać:

num = 3
until num == 0:
  print(num)
  num -= 1

Dygresja na rzecz języka

Ten artykuł nie jest próbą sugerowania dodania untilinstrukcji 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 plik Grammar/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 untilinstrukcji. Znalazłem, gdzie whilestwierdzenie zostało zdefiniowane ( while_stmt) i dodane until_stmtponiżej [2] :

compound_stmt: if_stmt | while_stmt | until_stmt | for_stmt | try_stmt | with_stmt | funcdef | classdef | decorated
if_stmt: 'if' test ':' suite ('elif' test ':' suite)* ['else' ':' suite]
while_stmt: 'while' test ':' suite ['else' ':' suite]
until_stmt: 'until' test ':' suite

[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ć, whilerównież musi zostać zrobione until, służy to jako całkiem dobra wskazówka.

Zauważ, że zdecydowałem się wykluczyć elseklauzulę z mojej definicji until, tylko po to, aby była trochę inna (i ponieważ szczerze mówiąc nie podoba mi się elseklauzula pętli i nie sądzę, aby dobrze pasowała do Zen w Pythonie).

Druga zmiana polega na zmodyfikowaniu reguły w compound_stmtcelu uwzględnienia until_stmt, jak widać w powyższym fragmencie. To while_stmtznowu zaraz potem .

Po uruchomieniu makepo zmodyfikowaniu Grammar/Grammar, informacja, że pgenprogram jest uruchomiony do ponownego generowania Include/graminit.hi Python/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.asdlktóry definiuje strukturę AST Pythona i dodać węzeł AST dla naszej nowej untilinstrukcji, ponownie tuż pod while:

| While(expr test, stmt* body, stmt* orelse)
| Until(expr test, stmt* body)

Jeśli teraz uruchomisz make, zwróć uwagę, że przed skompilowaniem wielu plików Parser/asdl_c.pyjest uruchamiany w celu wygenerowania kodu C z pliku definicji AST. To (podobnie Grammar/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.pyjest 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.pygenerowania kodu do zarządzania naszym nowo zdefiniowanym węzłem AST (w plikach Include/Python-ast.hi Python/Python-ast.c), nadal musimy ręcznie napisać kod, który konwertuje do niego odpowiedni węzeł drzewa parsowania. Odbywa się to w pliku Python/ast.c. Tam funkcja o nazwie ast_for_stmtkonwertuje węzły drzewa analizy instrukcji na węzły AST. Ponownie, kierując się naszym starym przyjacielem while, wskakujemy od razu do tematu switchobsługi instrukcji złożonych i dodajemy klauzulę dla until_stmt:

case while_stmt:
    return ast_for_while_stmt(c, ch);
case until_stmt:
    return ast_for_until_stmt(c, ch);

Teraz powinniśmy wdrożyć ast_for_until_stmt. Oto ona:

static stmt_ty
ast_for_until_stmt(struct compiling *c, const node *n)
{
    /* until_stmt: 'until' test ':' suite */
    REQ(n, until_stmt);

    if (NCH(n) == 4) {
        expr_ty expression;
        asdl_seq *suite_seq;

        expression = ast_for_expr(c, CHILD(n, 1));
        if (!expression)
            return NULL;
        suite_seq = ast_for_suite(c, CHILD(n, 3));
        if (!suite_seq)
            return NULL;
        return Until(expression, suite_seq, LINENO(n), n->n_col_offset, c->c_arena);
    }

    PyErr_Format(PyExc_SystemError,
                 "wrong number of tokens for 'until' statement: %d",
                 NCH(n));
    return NULL;
}

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ć elseklauzuli. Zgodnie z oczekiwaniami, AST jest tworzony rekurencyjnie, przy użyciu innych funkcji tworzących AST, takich jak ast_for_exprwyrażenie warunku i ast_for_suitetreść untilinstrukcji. Na koniec Untilzwracany jest nowy węzeł o nazwie .

Zauważ, że uzyskujemy dostęp do węzła drzewa parsowania nza pomocą niektórych makr, takich jak NCHi CHILD. Warto je zrozumieć - ich kod jest w Include/node.h.

Dygresja: skład AST

Zdecydowałem się utworzyć nowy typ AST dla untilinstrukcji, 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ż:

until condition:
   # do stuff

Jest funkcjonalnie równoważne z:

while not condition:
  # do stuff

Zamiast tworzyć Untilwęzeł w ast_for_until_stmt, mogłem utworzyć Notwęzeł z Whilewę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 tropem while, znajdujemy funkcję compiler_visit_stmt, która jest odpowiedzialna za kompilowanie instrukcji do kodu bajtowego. Dodajemy klauzulę na Until:

case While_kind:
    return compiler_while(c, s);
case Until_kind:
    return compiler_until(c, s);

Jeśli zastanawiasz się, co to Until_kindjest, jest to stała (właściwie wartość _stmt_kindwyliczenia) automatycznie generowana z pliku definicji AST do Include/Python-ast.h. W każdym razie nazywamy to, compiler_untilco oczywiście nadal nie istnieje. Zaraz do tego dojdę.

Jeśli jesteś ciekawy jak ja, zauważysz, że compiler_visit_stmtjest 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 do VISITmakra zdefiniowanego w Python/compile.c:

#define VISIT(C, TYPE, V) {\
    if (!compiler_visit_ ## TYPE((C), (V))) \
        return 0; \

Jest używany do wywoływania compiler_visit_stmtw compiler_body. Wracając jednak do naszej działalności ...

Zgodnie z obietnicą, oto compiler_until:

static int
compiler_until(struct compiler *c, stmt_ty s)
{
    basicblock *loop, *end, *anchor = NULL;
    int constant = expr_constant(s->v.Until.test);

    if (constant == 1) {
        return 1;
    }
    loop = compiler_new_block(c);
    end = compiler_new_block(c);
    if (constant == -1) {
        anchor = compiler_new_block(c);
        if (anchor == NULL)
            return 0;
    }
    if (loop == NULL || end == NULL)
        return 0;

    ADDOP_JREL(c, SETUP_LOOP, end);
    compiler_use_next_block(c, loop);
    if (!compiler_push_fblock(c, LOOP, loop))
        return 0;
    if (constant == -1) {
        VISIT(c, expr, s->v.Until.test);
        ADDOP_JABS(c, POP_JUMP_IF_TRUE, anchor);
    }
    VISIT_SEQ(c, stmt, s->v.Until.body);
    ADDOP_JABS(c, JUMP_ABSOLUTE, loop);

    if (constant == -1) {
        compiler_use_next_block(c, anchor);
        ADDOP(c, POP_BLOCK);
    }
    compiler_pop_fblock(c, LOOP, loop);
    compiler_use_next_block(c, end);

    return 1;
}

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 dokumentacji dismoduł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 makemożemy uruchomić nowo skompilowany Python i wypróbować naszą nową untilinstrukcję:

>>> until num == 0:
...   print(num)
...   num -= 1
...
3
2
1

Voila, to działa! Zobaczmy kod bajtowy utworzony dla nowej instrukcji za pomocą dismodułu w następujący sposób:

import dis

def myfoo(num):
    until num == 0:
        print(num)
        num -= 1

dis.dis(myfoo)

Oto wynik:

4           0 SETUP_LOOP              36 (to 39)
      >>    3 LOAD_FAST                0 (num)
            6 LOAD_CONST               1 (0)
            9 COMPARE_OP               2 (==)
           12 POP_JUMP_IF_TRUE        38

5          15 LOAD_NAME                0 (print)
           18 LOAD_FAST                0 (num)
           21 CALL_FUNCTION            1
           24 POP_TOP

6          25 LOAD_FAST                0 (num)
           28 LOAD_CONST               2 (1)
           31 INPLACE_SUBTRACT
           32 STORE_FAST               0 (num)
           35 JUMP_ABSOLUTE            3
      >>   38 POP_BLOCK
      >>   39 LOAD_CONST               0 (None)
           42 RETURN_VALUE

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:

Traceback (most recent call last):
  File "zy.py", line 9, in
    myfoo(3)
  File "zy.py", line 5, in myfoo
    print(num)
SystemError: no locals when loading 'print'

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_Buildin PyAST_Compilewywoł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_stmtfunkcję w Python/symtable.c, dodając kod do obsługi untilinstrukcji, po podobnym kodzie dla whileinstrukcji [3] :

case While_kind:
    VISIT(st, expr, s->v.While.test);
    VISIT_SEQ(st, stmt, s->v.While.body);
    if (s->v.While.orelse)
        VISIT_SEQ(st, stmt, s->v.While.orelse);
    break;
case Until_kind:
    VISIT(st, expr, s->v.Until.test);
    VISIT_SEQ(st, stmt, s->v.Until.body);
    break;

[3] : Nawiasem mówiąc, bez tego kodu jest ostrzeżenie kompilatora dla Python/symtable.c. Kompilator zauważa, że Until_kindwartość wyliczenia nie jest obsługiwana w instrukcji switch symtable_visit_stmti 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:

  • PEP 339: Projekt kompilatora CPythona - prawdopodobnie najważniejszy i najbardziej wszechstronny fragment oficjalnej dokumentacji kompilatora Pythona. Będąc bardzo krótkim, boleśnie pokazuje niedobór dobrej dokumentacji wewnętrznych elementów Pythona.
  • „Python Compiler Internals” - artykuł Thomasa Lee
  • „Python: Design and Implementation” - prezentacja Guido van Rossuma
  • Maszyna wirtualna Python (2.5), wycieczka z przewodnikiem - prezentacja Petera Trögera

Pierwotnym źródłem

Eli Bendersky
źródło
7
Świetny artykuł (/ blog), dzięki! Akceptuję, ponieważ to doskonale odpowiada na pytanie, a odpowiedzi „nie rób tego” / „kodowanie: mylang” są już wysoko oceniane, więc będą ładnie wyglądać w kolejności \ o /
dbr
1
Ale niestety nie jest to odpowiedź. Połączony artykuł jest, ale nie możesz go głosować za lub akceptować. Odradza się odpowiedzi składające się wyłącznie z linku.
Alfe
6
@Alfe: to zostało opublikowane dwa lata temu, zaakceptowane i dało +1 od 16 czytelników. Zauważ, że prowadzi to do mojego własnego posta na blogu, a kopiowanie dużego artykułu do StackOverflow nie jest czymś, co zamierzam robić. Możesz to zrobić w użytecznej edycji, zamiast grać w policję.
Eli Bendersky
2
@EliBendersky Useful to dość mało powiedziane na ten artykuł. Dziękuję za wyjaśnienie, jak te rzeczy faktycznie działają w Pythonie. To naprawdę pomogło mi zrozumieć AST, które jest istotne dla mojej obecnej pracy. ** również, jeśli jesteś ciekawy, moja wersja untiljest isa/ isanjak w if something isa dict:orif something isan int:
Inversus
5
Tak więc, ta odpowiedź brzmi: „Napisz i skompiluj swój własny język ze źródła, rozwidlony z Pythona”
ThorSummoner
53

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:

myprint "This gets logged to file"

byłaby równoważna

print >>open('/tmp/logfile.txt','a'), "This gets logged to file"

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:

import tokenize

LOGFILE = '/tmp/log.txt'
def translate(readline):
    for type, name,_,_,_ in tokenize.generate_tokens(readline):
        if type ==tokenize.NAME and name =='myprint':
            yield tokenize.NAME, 'print'
            yield tokenize.OP, '>>'
            yield tokenize.NAME, "open"
            yield tokenize.OP, "("
            yield tokenize.STRING, repr(LOGFILE)
            yield tokenize.OP, ","
            yield tokenize.STRING, "'a'"
            yield tokenize.OP, ")"
            yield tokenize.OP, ","
        else:
            yield type,name

(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:

import new
def myimport(filename):
    mod = new.module(filename)
    f=open(filename)
    data = tokenize.untokenize(translate(f.readline))
    exec data in mod.__dict__
    return mod

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:

import codecs, cStringIO, encodings
from encodings import utf_8

class StreamReader(utf_8.StreamReader):
    def __init__(self, *args, **kwargs):
        codecs.StreamReader.__init__(self, *args, **kwargs)
        data = tokenize.untokenize(translate(self.stream.readline))
        self.stream = cStringIO.StringIO(data)

def search_function(s):
    if s!='mylang': return None
    utf8=encodings.search_function('utf8') # Assume utf8 encoding
    return codecs.CodecInfo(
        name='mylang',
        encode = utf8.encode,
        decode = utf8.decode,
        incrementalencoder=utf8.incrementalencoder,
        incrementaldecoder=utf8.incrementaldecoder,
        streamreader=StreamReader,
        streamwriter=utf8.streamwriter)

codecs.register(search_function)

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.

# coding: mylang
myprint "this gets logged to file"
for i in range(10):
    myprint "so does this : ", i, "times"
myprint ("works fine" "with arbitrary" + " syntax" 
  "and line continuations")

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źć.

Brian
źródło
12
Niezłe! Zamiast mówić „nie można się obawiać”, w rzeczywistości dajesz kilka dobrych odpowiedzi (co sprowadza się do „naprawdę nie chcesz tego robić”).
c0m4,
Nie jestem pewien, czy rozumiem, jak działa pierwszy przykład - próba użycia myimportna module, który po prostu zawiera, print 1ponieważ jest to tylko wiersz kodu=1 ... SyntaxError: invalid syntax
olamundo
@noam: nie jestem pewien, co ci się nie podoba - tutaj po prostu wyświetla się „1” zgodnie z oczekiwaniami. (To jest z dwoma blokami rozpoczynającymi się od „import tokenize” i „import new” powyżej, umieszczonymi w pliku a.py, a także „ b=myimport("b.py")” i b.py zawierające tylko „ print 1”. Czy jest coś więcej do błędu (ślad stosu etc)?
Brian
3
Wydaje się, że Python3 nie pozwala na to, chociaż niekoniecznie celowo; Otrzymuję błąd BOM.
Tobu
zwróć uwagę, że importużywa wbudowanego __import__, więc jeśli go nadpiszesz ( przed zaimportowaniem modułu, który wymaga zmodyfikowanego importu), nie potrzebujesz osobnegomyimport
Tobias Kienzler
21

Tak, do pewnego stopnia jest to możliwe. Istnieje moduł , który używa sys.settrace()do implementacji gotoi comefrom„słów kluczowych”:

from goto import goto, label
for i in range(1, 10):
  for j in range(1, 20):
    print i, j
    if j == 3:
      goto .end # breaking out from nested loop
label .end
print "Finished"
Constantin
źródło
4
Nie jest to jednak nowa składnia ... po prostu na to wygląda.
Hans Nowak
3
-1: Podlinkowana strona ma nagłówek: "Moduł 'goto' był żartem primaaprilisowym, opublikowanym 1 kwietnia 2004. Tak, działa, ale mimo wszystko to żart. Proszę nie używać go w prawdziwym kodzie!"
Jim
6
@Jim może ponownie rozważyć -1. podpowiada mechanizm implementacji. fajna rzecz na początek.
n611x007
14

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.

paxdiablo
źródło
2
Nie zgadzam się z jednym punktem. Jeśli dodasz nowe słowa kluczowe, myślę, że nadal będzie to Python. Jeśli zmienisz istniejące słowa kluczowe, jak mówisz, jest to po prostu zhakowane.
Bill the Lizard
9
Jeśli dodasz nowe słowa kluczowe, będzie to język pochodzący z Pythona. Jeśli zmienisz słowa kluczowe, byłby to język niezgodny z Pythonem.
tzot
1
Jeśli dodasz słowa kluczowe, możesz przegapić „prostą, łatwą do nauczenia składnię” i „obszerne biblioteki”. Myślę, że funkcje językowe są prawie zawsze błędne (przykłady to COBOL, Perl i PHP).
S.Lott,
5
Nowe słowa kluczowe złamałyby kod Pythona, który używa ich jako identyfikatorów.
akaihola
12

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)

import EasyExtend
EasyExtend.new_langlet("mystmts", prompt = "my> ", source_ext = "mypy")

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

small_stmt: (expr_stmt | print_stmt  | del_stmt | pass_stmt | flow_stmt |
             import_stmt | global_stmt | exec_stmt | assert_stmt | my_stmt )

my_stmt: 'mystatement' expr

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.

 def call_my_stmt(expression):
     "defines behaviour for my_stmt"
     print "my stmt called with", expression

 class LangletTransformer(Transformer):
       @transform
       def my_stmt(self, node):
           _expr = find_node(node, symbol.expr)
           return any_stmt(CST_CallFunc("call_my_stmt", [_expr]))

 __publish__ = ["call_my_stmt"]

iv) cd do langlets / mystmts i wpisz

python run_mystmts.py

Teraz należy rozpocząć sesję i skorzystać z nowo zdefiniowanego zestawienia:

__________________________________________________________________________________

 mystmts

 On Python 2.5.1 (r251:54863, Apr 18 2007, 08:51:08) [MSC v.1310 32 bit (Intel)]
 __________________________________________________________________________________

 my> mystatement 40+2
 my stmt called with 42

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
11

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.


jcomeau@intrepid:~/$ cat demo.py; ./demo.py
#!/usr/bin/python -i
'load everything needed under "package", such as package.common.normalize()'
import os, sys, readline, traceback
if __name__ == '__main__':
    class t:
        @staticmethod
        def localfunction(*args):
            print 'this is a test'
            if args:
                print 'ignoring %s' % repr(args)

    def displayhook(whatever):
        if hasattr(whatever, 'localfunction'):
            return whatever.localfunction()
        else:
            print whatever

    def excepthook(exctype, value, tb):
        if exctype is SyntaxError:
            index = readline.get_current_history_length()
            item = readline.get_history_item(index)
            command = item.split()
            print 'command:', command
            if len(command[0]) == 1:
                try:
                    eval(command[0]).localfunction(*command[1:])
                except:
                    traceback.print_exception(exctype, value, tb)
        else:
            traceback.print_exception(exctype, value, tb)

    sys.displayhook = displayhook
    sys.excepthook = excepthook
>>> t
this is a test
>>> t t
command: ['t', 't']
this is a test
ignoring ('t',)
>>> ^D
jcomeau_ictx
źródło
4

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 ..)

dbr
źródło
Prawdziwe łącze do pliku PDF - ta „autonwersja” jest zepsuta i została zerwana, bo Bóg wie już od dawna: troeger.eu/files/teaching/pythonvm08lab.pdf
ZXX
3

Można to zrobić za pomocą EasyExtend :

EasyExtend (EE) to generator preprocesorów i framework do metaprogramowania napisany w czystym Pythonie i zintegrowany z CPythonem. Głównym celem EasyExtend jest tworzenie języków rozszerzeń, tj. Dodawanie niestandardowej składni i semantyki do Pythona.

Matthew Trevor
źródło
1
Po tym linku pojawia się teraz strona: „EasyExtend nie żyje. Dla tych, którzy są zainteresowani EE, istnieje kolejny projekt o nazwie Langscape Inna nazwa, całkowite przeprojektowanie, ta sama podróż”. Ponieważ istnieje niebezpieczeństwo, że ta strona informacyjna może się zepsuć, może warto zaktualizować odpowiedź.
celtschk
1

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.

Bill the Lizard
źródło
1

Niektóre rzeczy można zrobić z dekoratorami. Załóżmy np., Że Python nie miał withinstrukcji. Moglibyśmy wtedy zaimplementować podobne zachowanie:

# ====== Implementation of "mywith" decorator ======

def mywith(stream):
    def decorator(function):
        try: function(stream)
        finally: stream.close()
    return decorator

# ====== Using the decorator ======

@mywith(open("test.py","r"))
def _(infile):
    for l in infile.readlines():
        print(">>", l.rstrip())

Jest to jednak dość nieczyste rozwiązanie, jak tutaj zrobiono. Szczególnie zachowanie, w którym dekorator wywołuje funkcję i ustawia _ją, Nonejest nieoczekiwane. Dla wyjaśnienia: ten dekorator jest równoznaczny z pisaniem

def _(infile): ...
_ = mywith(open(...))(_) # mywith returns None.

a 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.

kdb
źródło
0

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.

Alex Coventry
źródło