คุณสามารถเพิ่มคำสั่งใหม่ให้กับไวยากรณ์ของ Python ได้หรือไม่?


124

คุณสามารถเพิ่มงบใหม่ (เช่นprint, raise, with) ไวยากรณ์งูใหญ่?

บอกว่ายอม ..

mystatement "Something"

หรือ,

new_if True:
    print "example"

ไม่มากถ้าคุณควรแต่ถ้าเป็นไปได้ (ย่อมาจากการแก้ไขโค้ดล่าม python)


10
ในบันทึกที่ค่อนข้างเกี่ยวข้องกรณีการใช้งานอย่างหนึ่งที่อาจเป็นประโยชน์ในการสร้างข้อความใหม่ได้ทันที (ตรงข้ามกับการ "ขยาย" ภาษาอย่างจริงจัง) สำหรับผู้ที่ใช้ล่ามแบบโต้ตอบเป็นเครื่องคิดเลขหรือแม้แต่ OS เชลล์ . ฉันมักจะสร้างฟังก์ชั่นการทิ้งเล็ก ๆ น้อย ๆ ในทันทีเพื่อทำบางสิ่งที่ฉันกำลังจะทำซ้ำและในสถานการณ์เหล่านั้นจะเป็นการดีที่จะสร้างคำสั่งย่อ ๆ เช่นมาโครหรือคำสั่งแทนที่จะพิมพ์ชื่อยาวด้วยไวยากรณ์ function () แน่นอนว่านั่นไม่ใช่สิ่งที่ Py มีไว้สำหรับ .. แต่ผู้คนใช้เวลาส่วนใหญ่ในการใช้งานแบบโต้ตอบ
กิโล

5
@Kilo มันอาจจะคุ้มค่าที่จะดู ipython - มันมีคุณสมบัติของเชลล์มากมายเช่นคุณสามารถใช้คำสั่ง "ls" และ "cd" ปกติ, การเติมแท็บ, คุณสมบัติมาโครจำนวนมาก ฯลฯ
dbr

บางภาษาสามารถขยายได้อย่างยอดเยี่ยมเช่น Forth และ Smalltalk แต่กระบวนทัศน์ของภาษาก็แตกต่างจากที่ Python ใช้เช่นกัน ด้วยคำใหม่ทั้งสองคำ (Forth) หรือวิธีการ (Smalltalk) กลายเป็นส่วนหนึ่งของภาษาที่แยกไม่ออกสำหรับการติดตั้งนั้น ดังนั้นการติดตั้ง Forth หรือ Smalltalk แต่ละครั้งจึงกลายเป็นการสร้างที่ไม่เหมือนใครเมื่อเวลาผ่านไป นอกจากนี้ Forth ยังใช้ RPN แต่การคิดตามแนวของ DSL สิ่งนี้ควรทำได้ใน Python แต่อย่างที่คนอื่น ๆ พูดไว้ที่นี่ทำไม?

1
เนื่องจากมีคนที่คล่องแคล่วทั้งใน Python และ Forth และผู้ที่ใช้ Forth คอมไพเลอร์หลายตัวในช่วงหลายปีที่ผ่านมาฉันสามารถมีส่วนร่วมได้ที่นี่ด้วยอำนาจระดับหนึ่ง หากไม่ได้รับการเข้าถึงดิบเพื่อแยกวิเคราะห์ภายในของ Python มันเป็นไปไม่ได้เลย คุณสามารถปลอมได้โดยการประมวลผลล่วงหน้าตามที่ (ตรงไปตรงมาค่อนข้างเนียน!) คำตอบด้านล่างแสดงให้เห็น แต่การอัปเดตไวยากรณ์และ / หรือความหมายของภาษาอย่างแท้จริงในตัวแปลร้อนเป็นไปไม่ได้ นี่เป็นทั้งคำสาปของ Python และข้อได้เปรียบของภาษาที่คล้าย Lisp- และ Forth
Samuel A. Falvo II

คำตอบ:


153

คุณอาจพบว่าสิ่งนี้มีประโยชน์ - Python internals: การเพิ่มคำสั่งใหม่ให้กับ Pythonโดยอ้างถึงที่นี่:


บทความนี้เป็นความพยายามที่จะทำความเข้าใจให้ดีขึ้นว่าส่วนหน้าของ Python ทำงานอย่างไร การอ่านเอกสารและซอร์สโค้ดอาจจะน่าเบื่อไปหน่อยดังนั้นฉันจึงใช้แนวทางปฏิบัติที่นี่: ฉันจะเพิ่มuntilคำสั่งให้กับ Python

ทั้งหมดเข้ารหัสสำหรับบทความนี้ที่ได้กระทำกับการตัดขอบสาขา Py3k ในกระจกที่เก็บหลาม Mercurial

untilคำสั่ง

บางภาษาเช่น Ruby มีuntilคำสั่งซึ่งเป็นส่วนเสริมของwhile( until num == 0เทียบเท่ากับwhile num != 0) ใน Ruby ฉันสามารถเขียน:

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

และจะพิมพ์:

3
2
1

ดังนั้นฉันต้องการเพิ่มความสามารถที่คล้ายกันให้กับ Python นั่นคือความสามารถในการเขียน:

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

การพูดนอกเรื่องผู้สนับสนุนภาษา

บทความนี้ไม่ได้พยายามแนะนำการเพิ่มuntilคำสั่งใน Python แม้ว่าฉันคิดว่าคำสั่งดังกล่าวจะทำให้โค้ดบางส่วนชัดเจนขึ้นและบทความนี้แสดงให้เห็นว่าการเพิ่มนั้นง่ายเพียงใด แต่ฉันก็เคารพปรัชญาความเรียบง่ายของ Python อย่างสมบูรณ์ สิ่งที่ฉันพยายามทำที่นี่จริงๆคือได้รับข้อมูลเชิงลึกเกี่ยวกับการทำงานภายในของ Python

การปรับเปลี่ยนไวยากรณ์

งูหลามใช้เครื่องกำเนิดไฟฟ้า parser pgenที่กำหนดเองที่มีชื่อว่า นี่คือตัวแยกวิเคราะห์ LL (1) ที่แปลงซอร์สโค้ด Python เป็นโครงสร้างแยกวิเคราะห์ อินพุตกำเนิดแยกวิเคราะห์เป็นไฟล์[1]Grammar/Grammar นี่คือไฟล์ข้อความธรรมดาที่ระบุไวยากรณ์ของ Python

[1] : จากนี้ไปการอ้างอิงถึงไฟล์ในซอร์ส Python จะได้รับค่อนข้างตรงกับรูทของแผนผังซอร์สซึ่งเป็นไดเร็กทอรีที่คุณเรียกใช้การกำหนดค่าและสร้างเพื่อสร้าง Python

ต้องทำการแก้ไขสองอย่างกับไฟล์ไวยากรณ์ ประการแรกคือการเพิ่มคำจำกัดความสำหรับuntilคำสั่ง ฉันพบตำแหน่งที่กำหนดwhileคำสั่ง ( while_stmt) และเพิ่มuntil_stmtด้านล่าง[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] : นี่แสดงให้เห็นถึงเทคนิคทั่วไปผมใช้เมื่อแก้ไขรหัสที่มาฉันไม่คุ้นเคยกับการทำงานโดยความคล้ายคลึงกัน หลักการนี้ไม่สามารถแก้ปัญหาของคุณได้ทั้งหมด แต่สามารถทำให้กระบวนการนี้ง่ายขึ้น เนื่องจากทุกสิ่งที่ต้องทำwhileก็ต้องทำเพื่อuntilมันจึงเป็นแนวทางที่ดีทีเดียว

โปรดทราบว่าฉันได้ตัดสินใจที่จะแยกelseประโยคออกจากคำจำกัดความของฉันuntilเพียงเพื่อให้มันแตกต่างกันเล็กน้อย (และเพราะฉันไม่ชอบelseประโยคของลูปและไม่คิดว่ามันเข้ากันได้ดีกับ Zen of Python)

การเปลี่ยนแปลงที่สองคือการแก้ไขกฎสำหรับcompound_stmtรวมuntil_stmtดังที่คุณเห็นในตัวอย่างด้านบน หลังจากwhile_stmtนั้นอีกครั้ง

เมื่อคุณเรียกใช้makeหลังจากแก้ไขGrammar/Grammarแจ้งให้ทราบว่าpgenโปรแกรมจะดำเนินการอีกครั้งสร้างInclude/graminit.hและPython/graminit.cแล้วหลายไฟล์ได้อีกรวบรวม

การแก้ไขรหัสการสร้าง AST

หลังจากตัวแยกวิเคราะห์ Python ได้สร้างแผนผังการแยกวิเคราะห์โครงสร้างนี้จะถูกแปลงเป็น AST เนื่องจาก AST นั้นง่ายกว่ามากในการทำงานในขั้นตอนต่อ ๆ ไปของกระบวนการคอมไพล์

ดังนั้นเราจะไปเยี่ยมชมParser/Python.asdlซึ่งกำหนดโครงสร้างของ AST ของ Python และเพิ่มโหนด AST สำหรับuntilคำสั่งใหม่ของเราอีกครั้งด้านล่างwhile:

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

หากคุณเรียกใช้ตอนนี้makeโปรดสังเกตว่าก่อนที่จะรวบรวมไฟล์จำนวนมากParser/asdl_c.pyให้รันเพื่อสร้างรหัส C จากไฟล์ข้อกำหนด AST นี่ (เช่นGrammar/Grammar) เป็นอีกตัวอย่างหนึ่งของซอร์สโค้ด Python โดยใช้มินิภาษา (หรืออีกนัยหนึ่งคือ DSL) เพื่อลดความซับซ้อนในการเขียนโปรแกรม โปรดทราบว่าเนื่องจากParser/asdl_c.pyเป็นสคริปต์ Python นี่คือการบูตแบบหนึ่ง - ในการสร้าง Python ตั้งแต่เริ่มต้น Python จะต้องพร้อมใช้งานอยู่แล้ว

ในขณะที่Parser/asdl_c.pyสร้างโค้ดเพื่อจัดการโหนด AST ที่กำหนดขึ้นใหม่ของเรา (ลงในไฟล์Include/Python-ast.hและPython/Python-ast.c) เรายังคงต้องเขียนโค้ดที่แปลงโหนดพาร์สทรีที่เกี่ยวข้องลงไปด้วยมือ Python/ast.cนี้จะกระทำในแฟ้ม ที่นั่นฟังก์ชั่นที่มีชื่อว่าast_for_stmtแปลงโหนดต้นไม้แยกวิเคราะห์สำหรับงบเป็นโหนด AST อีกครั้งตามคำแนะนำของเพื่อนเก่าของwhileเราเรากระโดดเข้าสู่กลุ่มใหญ่switchเพื่อจัดการคำสั่งผสมและเพิ่มประโยคสำหรับuntil_stmt:

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

ast_for_until_stmtตอนนี้เราควรใช้ นี่คือ:

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;
}

อีกครั้งสิ่งนี้ถูกเข้ารหัสในขณะที่ดูค่าเทียบเท่าอย่างใกล้ชิดast_for_while_stmtด้วยความแตกต่างที่untilฉันตัดสินใจที่จะไม่สนับสนุนelseอนุประโยค ตามที่คาดไว้ AST ถูกสร้างขึ้นแบบวนซ้ำโดยใช้ฟังก์ชันการสร้าง AST อื่น ๆ เช่นast_for_exprสำหรับนิพจน์เงื่อนไขและast_for_suiteสำหรับเนื้อหาของuntilคำสั่ง ในที่สุดโหนดใหม่ที่ตั้งชื่อUntilจะถูกส่งคืน

โปรดทราบว่าเราเข้าถึงโหนดแยกต้นไม้nใช้มาโครบางคนชอบและNCH CHILDเหล่านี้เป็นความเข้าใจคุ้มค่า - Include/node.hรหัสของพวกเขาอยู่ใน

การย่อย: องค์ประกอบ AST

ฉันเลือกที่จะสร้าง AST ประเภทใหม่สำหรับuntilคำสั่ง แต่จริงๆแล้วมันไม่จำเป็น ฉันสามารถบันทึกงานบางอย่างและใช้ฟังก์ชันใหม่โดยใช้องค์ประกอบของโหนด AST ที่มีอยู่เนื่องจาก:

until condition:
   # do stuff

เทียบเท่ากับการทำงาน:

while not condition:
  # do stuff

แทนที่จะสร้างUntilโหนดในast_for_until_stmtฉันสามารถสร้างNotโหนดที่มีWhileโหนดเป็นชายด์ได้ เนื่องจากคอมไพเลอร์ AST รู้วิธีจัดการกับโหนดเหล่านี้แล้วจึงสามารถข้ามขั้นตอนต่อไปของกระบวนการได้

การรวบรวม AST เป็น bytecode

ขั้นตอนต่อไปคือการรวบรวม AST เป็น Python bytecode การคอมไพล์มีผลลัพธ์ระดับกลางซึ่งเป็น CFG (Control Flow Graph) แต่เนื่องจากโค้ดเดียวกันนี้จัดการได้ฉันจะไม่สนใจรายละเอียดนี้ในตอนนี้และปล่อยไว้สำหรับบทความอื่น

Python/compile.cรหัสเราจะดูที่อยู่ถัดไป ตามการนำไปสู่whileเราจะพบฟังก์ชันcompiler_visit_stmtซึ่งมีหน้าที่รวบรวมคำสั่งเป็น bytecode เราเพิ่มประโยคสำหรับUntil:

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

หากคุณสงสัยว่าอะไรUntil_kindคือค่าคงที่ (จริงๆแล้วคือค่าของการ_stmt_kindแจงนับ) ที่สร้างขึ้นโดยอัตโนมัติจากไฟล์นิยาม AST เป็นไฟล์Include/Python-ast.h. อย่างไรก็ตามเราเรียกว่าcompiler_untilซึ่งแน่นอนว่ายังไม่มีอยู่จริง ฉันจะไปสักครู่

ถ้าคุณอยากรู้อยากเห็นเหมือนฉันคุณจะสังเกตเห็นว่าcompiler_visit_stmtมันแปลก ไม่มีการgrepเปิดเผยจำนวนของต้นไม้ต้นทางที่เรียกว่า ในกรณีนี้จะเหลือเพียงตัวเลือกเดียวคือ C macro-fu อันที่จริงการตรวจสอบสั้น ๆ นำเราไปสู่VISITมาโครที่กำหนดไว้ในPython/compile.c:

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

โดยจะใช้ในการเรียกในcompiler_visit_stmt compiler_bodyกลับมาที่ธุรกิจของเราอย่างไรก็ตาม ...

ตามที่สัญญาไว้นี่คือ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;
}

ฉันมีคำสารภาพที่ต้องทำ: รหัสนี้ไม่ได้เขียนขึ้นโดยอาศัยความเข้าใจอย่างลึกซึ้งเกี่ยวกับ bytecode ของ Python เช่นเดียวกับส่วนที่เหลือของบทความก็ทำเลียนแบบcompiler_whileฟังก์ชันเครือญาติ อย่างไรก็ตามเมื่ออ่านอย่างละเอียดโปรดทราบว่า Python VM เป็นแบบสแต็กและมองเข้าไปในเอกสารประกอบของdisโมดูลซึ่งมีรายการไบต์โค้ดของ Pythonพร้อมคำอธิบายจึงเป็นไปได้ที่จะเข้าใจว่าเกิดอะไรขึ้น

เสร็จแล้ว ... เราไม่ใช่เหรอ?

หลังจากทำการเปลี่ยนแปลงทั้งหมดmakeแล้วเราสามารถรัน Python ที่คอมไพล์ใหม่และลองใช้untilคำสั่งใหม่ของเรา:

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

Voila ได้ผล! มาดู bytecode ที่สร้างขึ้นสำหรับคำสั่งใหม่โดยใช้disโมดูลดังนี้:

import dis

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

dis.dis(myfoo)

นี่คือผลลัพธ์:

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

การดำเนินการที่น่าสนใจที่สุดคือหมายเลข 12: ถ้าเงื่อนไขเป็นจริงเราจะข้ามไปที่หลังลูป untilนี่คือความหมายที่ถูกต้องสำหรับ หากการกระโดดไม่ดำเนินการตัวห่วงจะยังคงทำงานต่อไปจนกว่าจะกระโดดกลับสู่เงื่อนไขที่การดำเนินการ 35

รู้สึกดีกับการเปลี่ยนแปลงของฉันฉันจึงลองเรียกใช้ฟังก์ชัน (เรียกใช้งานmyfoo(3)) แทนการแสดง bytecode ผลลัพธ์น้อยกว่าการให้กำลังใจ:

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'

โอ้โฮ ... ไม่ดีเลย แล้วเกิดอะไรขึ้น?

กรณีของตารางสัญลักษณ์ที่หายไป

หนึ่งในขั้นตอนที่คอมไพเลอร์ Python ดำเนินการเมื่อคอมไพล์ AST คือสร้างตารางสัญลักษณ์สำหรับโค้ดที่คอมไพล์ เรียกร้องให้PySymtable_BuildในPyAST_Compileสายเป็นโมดูลตารางสัญลักษณ์ ( Python/symtable.c) ซึ่งเดิน AST ในลักษณะที่คล้ายคลึงกับฟังก์ชั่นการสร้างรหัสที่ การมีตารางสัญลักษณ์สำหรับแต่ละขอบเขตจะช่วยให้คอมไพลเลอร์สามารถหาข้อมูลสำคัญบางอย่างได้เช่นตัวแปรใดเป็นแบบโกลบอลและตัวแปรใดที่อยู่ในขอบเขต

ในการแก้ไขปัญหาเราต้องแก้ไขsymtable_visit_stmtฟังก์ชันในPython/symtable.cการเพิ่มโค้ดสำหรับการจัดการuntilคำสั่งหลังจากโค้ดที่คล้ายกันสำหรับwhileคำสั่ง[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] : Python/symtable.cโดยวิธีการที่ไม่มีรหัสนี้มีคำเตือนสำหรับคอมไพเลอร์ คอมไพลเลอร์สังเกตว่าUntil_kindค่าการแจงนับไม่ได้รับการจัดการในคำสั่ง switch symtable_visit_stmtและบ่น การตรวจสอบคำเตือนของคอมไพเลอร์เป็นสิ่งสำคัญเสมอ!

และตอนนี้เราทำเสร็จแล้ว การรวบรวมแหล่งที่มาหลังจากการเปลี่ยนแปลงนี้ทำให้การmyfoo(3)ทำงานเป็นไปตามที่คาดไว้

ข้อสรุป

ในบทความนี้ฉันได้สาธิตวิธีการเพิ่มคำสั่งใหม่ให้กับ Python แม้ว่าจะต้องมีการปรับแต่งโค้ดของคอมไพเลอร์ Python แต่การเปลี่ยนแปลงก็ไม่ยากที่จะนำไปใช้เพราะฉันใช้คำสั่งที่คล้ายกันและที่มีอยู่เป็นแนวทาง

คอมไพเลอร์ Python เป็นซอฟต์แวร์ที่ซับซ้อนและฉันไม่ได้อ้างว่าเป็นผู้เชี่ยวชาญ อย่างไรก็ตามฉันสนใจภายในของ Python จริงๆและโดยเฉพาะส่วนหน้า ดังนั้นฉันจึงพบว่าแบบฝึกหัดนี้มีประโยชน์มากสำหรับการศึกษาเชิงทฤษฎีเกี่ยวกับหลักการของคอมไพเลอร์และซอร์สโค้ด มันจะใช้เป็นฐานสำหรับบทความในอนาคตที่จะเจาะลึกลงไปในคอมไพเลอร์

อ้างอิง

ฉันใช้ข้อมูลอ้างอิงที่ยอดเยี่ยมสองสามข้อสำหรับการสร้างบทความนี้ พวกเขาอยู่ที่นี่โดยไม่เรียงลำดับเฉพาะ:

  • PEP 339: การออกแบบคอมไพเลอร์ CPython - อาจเป็นเอกสารทางการที่สำคัญและครอบคลุมที่สุดสำหรับคอมไพเลอร์ Python การที่สั้นมากมันแสดงให้เห็นถึงความขาดแคลนของเอกสารที่ดีเกี่ยวกับภายในของ Python
  • "Python Compiler Internals" - บทความโดย Thomas Lee
  • "Python: Design and Implementation" - การนำเสนอโดย Guido van Rossum
  • Python (2.5) Virtual Machine ทัวร์แนะนำ - การนำเสนอโดย Peter Tröger

แหล่งที่มาเดิม


7
บทความยอดเยี่ยม (/ บล็อก) ขอบคุณ! ยอมรับเนื่องจากสิ่งนี้ตอบคำถามได้อย่างสมบูรณ์และคำตอบ "อย่าทำอย่างนั้น" / "coding: mylang" ได้รับการโหวตสูงแล้วดังนั้นจะปรากฏตามลำดับ \ o /
dbr

1
แต่น่าเสียดายที่นี่ไม่ใช่คำตอบ บทความที่เชื่อมโยงเป็น แต่คุณไม่สามารถโหวตหรือยอมรับได้ คำตอบที่ประกอบด้วยลิงก์ทั้งหมดเป็นสิ่งที่ไม่พึงปรารถนา
Alfe

6
@Alfe: โพสต์เมื่อสองปีที่แล้วได้รับการยอมรับและ +1 โดยผู้อ่าน 16 คน โปรดทราบว่าลิงก์ไปยังบล็อกโพสต์ของฉันเองและการคัดลอกบทความขนาดใหญ่ไปยัง StackOverflow ไม่ใช่สิ่งที่ฉันตั้งใจจะทำ อย่าลังเลที่จะทำสิ่งนั้นในการแก้ไขที่เป็นประโยชน์แทนที่จะเล่นตำรวจ
Eli Bendersky

2
@EliBendersky มีประโยชน์ค่อนข้างจะพูดน้อยสำหรับบทความนั้น ขอบคุณที่อธิบายมากว่าสิ่งเหล่านี้ทำงานอย่างไรใน python สิ่งนี้ช่วยให้ฉันเข้าใจ AST ซึ่งเกี่ยวข้องกับงานปัจจุบันของฉันจริงๆ ** นอกจากนี้ในกรณีที่คุณสงสัยเวอร์ชันของฉันuntilคือisa/ isanเหมือนif something isa dict:หรือif something isan int:
Inversus

5
ซูคำตอบนี้คือ "เขียนและเรียบเรียงภาษาของคุณเองจากแหล่งที่มาซึ่งแยกจาก python"
ธ อสซัมมอนเนอร์

53

วิธีหนึ่งในการทำสิ่งนี้คือการประมวลผลซอร์สล่วงหน้าและแก้ไขโดยแปลคำสั่งที่เพิ่มของคุณเป็น python วิธีนี้จะนำมาซึ่งปัญหาต่าง ๆ และฉันไม่แนะนำให้ใช้สำหรับการใช้งานทั่วไป แต่สำหรับการทดลองกับภาษาหรือการเขียนโปรแกรมแบบเฉพาะเจาะจงอาจมีประโยชน์ในบางครั้ง

ตัวอย่างเช่นสมมติว่าเราต้องการแนะนำคำสั่ง "myprint" ซึ่งแทนที่จะพิมพ์ไปที่หน้าจอแทนที่จะบันทึกไฟล์เฉพาะ เช่น:

myprint "This gets logged to file"

จะเทียบเท่ากับ

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

มีตัวเลือกมากมายในการทำการแทนที่ตั้งแต่การแทนที่ regex ไปจนถึงการสร้าง AST ไปจนถึงการเขียน parser ของคุณเองขึ้นอยู่กับว่าไวยากรณ์ของคุณใกล้เคียงกับ python ที่มีอยู่แค่ไหน แนวทางระดับกลางที่ดีคือการใช้โมดูลโทเค็นไนเซอร์ สิ่งนี้จะช่วยให้คุณสามารถเพิ่มคำหลักใหม่โครงสร้างการควบคุมและอื่น ๆ ในขณะที่ตีความแหล่งที่มาคล้ายกับตัวแปล python ดังนั้นจึงหลีกเลี่ยงไม่ให้โซลูชัน regex หยาบแตก สำหรับ "myprint" ข้างต้นคุณสามารถเขียนรหัสการแปลงต่อไปนี้:

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

(สิ่งนี้ทำให้ myprint เป็นคีย์เวิร์ดอย่างมีประสิทธิภาพดังนั้นการใช้เป็นตัวแปรที่อื่นอาจทำให้เกิดปัญหาได้)

ปัญหาคือจะใช้อย่างไรเพื่อให้โค้ดของคุณใช้งานได้จาก python วิธีหนึ่งก็คือการเขียนฟังก์ชันการนำเข้าของคุณเองและใช้เพื่อโหลดโค้ดที่เขียนด้วยภาษาที่คุณกำหนดเอง เช่น:

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

สิ่งนี้ต้องการให้คุณจัดการโค้ดที่กำหนดเองของคุณแตกต่างจากโมดูล python ทั่วไปอย่างไรก็ตาม เช่น " some_mod = myimport("some_mod.py")" มากกว่า " import some_mod"

อีกวิธีหนึ่งที่ค่อนข้างเรียบร้อย (แม้ว่าจะแฮ็กกี้) คือการสร้างการเข้ารหัสแบบกำหนดเอง (ดูPEP 263 ) ตามที่สูตรนี้แสดงให้เห็น คุณสามารถใช้สิ่งนี้เป็น:

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)

หลังจากรันโค้ดนี้แล้ว (เช่นคุณสามารถวางไว้ใน. pythonrc หรือ site.py) โค้ดใด ๆ ที่ขึ้นต้นด้วยความคิดเห็น "# coding: mylang" จะถูกแปลโดยอัตโนมัติผ่านขั้นตอนก่อนการประมวลผลข้างต้น เช่น.

# 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")

คำเตือน:

มีปัญหากับวิธีการก่อนตัวประมวลผลเนื่องจากคุณอาจคุ้นเคยหากคุณเคยทำงานกับตัวประมวลผลล่วงหน้า C หลักคือการดีบัก python ทั้งหมดที่เห็นคือไฟล์ที่ประมวลผลล่วงหน้าซึ่งหมายความว่าข้อความที่พิมพ์ใน stack trace ฯลฯ จะอ้างถึงสิ่งนั้น หากคุณได้ทำการแปลจำนวนมากสิ่งนี้อาจแตกต่างจากข้อความต้นฉบับของคุณมาก ตัวอย่างด้านบนไม่เปลี่ยนหมายเลขบรรทัด ฯลฯ ดังนั้นจะไม่แตกต่างกันมากเกินไป แต่ยิ่งคุณเปลี่ยนมากเท่าไหร่ก็จะยิ่งเข้าใจยากขึ้นเท่านั้น


12
ทำได้ดีนี่! แทนที่จะพูดว่า 'ไม่สามารถทำได้' คุณให้คำตอบที่ดีสองสามข้อ (ซึ่งทำให้ 'คุณไม่ต้องการทำสิ่งนี้จริงๆ') โหวตคะแนน
c0m4

ฉันไม่แน่ใจว่าฉันเข้าใจว่าตัวอย่างแรกทำงานอย่างไร - พยายามใช้myimportกับโมดูลที่มีprint 1เพียงบรรทัดเดียวของรหัสที่ให้ผล=1 ... SyntaxError: invalid syntax
olamundo

@noam: ไม่แน่ใจว่าอะไรล้มเหลวสำหรับคุณ - ที่นี่ฉันได้รับ "1" พิมพ์ตามที่คาดไว้ (นี่คือ 2 บล็อกที่เริ่มต้น "import tokenize" และ "import new" ด้านบนใส่ในไฟล์ a.py เช่นเดียวกับ " b=myimport("b.py")" และ b.py ที่มี just " print 1" ข้อผิดพลาดมีอะไรเพิ่มเติมหรือไม่ (stack trace ฯลฯ )?
Brian

3
Python3 ดูเหมือนจะไม่อนุญาตสิ่งนี้แม้ว่าจะไม่จำเป็นต้องมีวัตถุประสงค์ ฉันได้รับข้อผิดพลาด BOM
Tobu

โปรดทราบว่าimportใช้ builtin __import__ดังนั้นหากคุณเขียนทับ ( ก่อนที่จะนำเข้าโมดูลที่ต้องนำเข้าที่แก้ไข) คุณไม่จำเป็นต้องแยกต่างหากmyimport
Tobias Kienzler

21

ใช่มันเป็นไปได้ในระดับหนึ่ง มีโมดูลที่ใช้sys.settrace()ในการติดตั้งgotoและcomefrom"คีย์เวิร์ด":

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"

4
นั่นไม่ใช่ไวยากรณ์ใหม่จริงๆ แต่ดูเหมือนมัน
Hans Nowak

3
-1: หน้าที่เชื่อมโยงมีหัวข้อนี้: "โมดูล 'goto' เป็นเรื่องตลกของ April Fool ซึ่งเผยแพร่เมื่อวันที่ 1 เมษายน 2004 ใช่มันใช้งานได้ แต่มันเป็นเรื่องตลกอย่างไรก็ตามโปรดอย่าใช้ในโค้ดจริง!"
จิม

6
@ จิมอาจพิจารณา -1 ใหม่ คำแนะนำคุณเกี่ยวกับกลไกการใช้งาน สิ่งที่ดีที่จะเริ่มต้นด้วย
n611x007

14

ขาดการเปลี่ยนและคอมไพล์ซอร์สโค้ดใหม่ (ซึ่งเป็นไปได้ด้วยโอเพนซอร์ส) การเปลี่ยนภาษาพื้นฐานเป็นไปไม่ได้จริงๆ

แม้ว่าคุณจะทำการคอมไพล์ซอร์สใหม่ แต่ก็ไม่ได้เป็นไพ ธ อน แต่เป็นเวอร์ชันที่ถูกแฮ็กอัปที่เปลี่ยนไปซึ่งคุณต้องระวังอย่างมากที่จะไม่นำข้อบกพร่องเข้ามา

อย่างไรก็ตามฉันไม่แน่ใจว่าทำไมคุณถึงต้องการ คุณสมบัติเชิงวัตถุของ Python ทำให้การบรรลุผลลัพธ์ที่คล้ายกันกับภาษานั้นทำได้ง่ายมาก


2
ฉันไม่เห็นด้วยในประเด็นหนึ่ง หากคุณเพิ่มคำหลักใหม่ฉันคิดว่ามันจะยังคงเป็น Python หากคุณเปลี่ยนคำหลักที่มีอยู่นั่นแสดงว่าเป็นเพียงการแฮ็กอย่างที่คุณพูด
Bill the Lizard

9
หากคุณเพิ่มคำหลักใหม่ก็จะเป็นภาษาที่ได้รับจาก Python หากคุณเปลี่ยนคีย์เวิร์ดอาจเป็นภาษาที่ไม่รองรับ Python
tzot

1
หากคุณเพิ่มคำหลักคุณอาจพลาดประเด็นของ "ไวยากรณ์ที่ง่ายต่อการเรียนรู้" และ "ไลบรารีที่กว้างขวาง" ฉันคิดว่าคุณสมบัติของภาษามักจะผิดพลาด (ตัวอย่างเช่น COBOL, Perl และ PHP)
S.Lott

5
คำหลักใหม่จะทำลายรหัส Python ซึ่งใช้เป็นตัวระบุ
akaihola

12

คำตอบทั่วไป: คุณต้องประมวลผลไฟล์ต้นฉบับของคุณล่วงหน้า

คำตอบที่เฉพาะเจาะจงมากขึ้น: ติดตั้งEasyExtendและทำตามขั้นตอนต่อไปนี้

i) สร้าง langlet ใหม่ (ภาษาส่วนขยาย)

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

หากไม่มีข้อกำหนดเพิ่มเติมจะต้องสร้างไฟล์จำนวนมากภายใต้ EasyExtend / langlets / mystmts /

ii) เปิด mystmts / parsedef / Grammar.ext และเพิ่มบรรทัดต่อไปนี้

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

นี่เพียงพอที่จะกำหนดไวยากรณ์ของคำสั่งใหม่ของคุณ small_stmt ที่ไม่ใช่เทอร์มินัลเป็นส่วนหนึ่งของไวยากรณ์ Python และเป็นสถานที่ที่มีการเชื่อมต่อคำสั่งใหม่ตัวแยกวิเคราะห์จะรับรู้คำสั่งใหม่นั่นคือไฟล์ต้นฉบับที่มีมันจะถูกแยกวิเคราะห์ คอมไพเลอร์จะปฏิเสธเพราะยังต้องเปลี่ยนเป็น Python ที่ถูกต้อง

iii) ตอนนี้เราต้องเพิ่มความหมายของคำสั่ง สำหรับสิ่งนี้ต้องแก้ไข msytmts / langlet.py และเพิ่มผู้เยี่ยมชมโหนด 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 เป็น langlets / mystmts และพิมพ์

python run_mystmts.py

ตอนนี้เซสชันจะเริ่มต้นและสามารถใช้คำสั่งที่กำหนดใหม่ได้:

__________________________________________________________________________________

 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

มีขั้นตอนไม่กี่ขั้นตอนกว่าจะมาถึงคำแถลงการณ์ใช่ไหม? ยังไม่มี API ที่ช่วยให้กำหนดสิ่งง่ายๆโดยไม่ต้องสนใจไวยากรณ์ แต่ EE เป็นโมดูโลที่น่าเชื่อถือมาก ดังนั้นจึงเป็นเพียงเรื่องของเวลาที่ API จะปรากฏขึ้นซึ่งช่วยให้โปรแกรมเมอร์สามารถกำหนดสิ่งที่สะดวกเช่นตัวดำเนินการ infix หรือข้อความขนาดเล็กโดยใช้การเขียนโปรแกรม OO ที่สะดวกสบาย สำหรับสิ่งที่ซับซ้อนมากขึ้นเช่นการฝังทั้งภาษาใน Python โดยการสร้าง langlet จะไม่มีทางใช้ไวยากรณ์แบบเต็ม


11

นี่คือวิธีที่ง่ายมาก แต่เส็งเคร็งที่จะเพิ่มงบใหม่ในโหมดการสื่อความหมายเท่านั้น ฉันใช้มันสำหรับคำสั่งตัวอักษร 1 ตัวเล็ก ๆ สำหรับการแก้ไขคำอธิบายประกอบยีนโดยใช้ sys.displayhook เท่านั้น แต่ฉันก็สามารถตอบคำถามนี้ได้ฉันจึงเพิ่ม sys.excepthook สำหรับข้อผิดพลาดทางไวยากรณ์ด้วย อันหลังน่าเกลียดมากโดยดึงรหัสดิบจากบัฟเฟอร์อ่านไลน์ ข้อดีคือการเพิ่มข้อความใหม่ด้วยวิธีนี้เป็นเรื่องง่ายเล็กน้อย


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

4

ฉันพบคำแนะนำเกี่ยวกับการเพิ่มข้อความใหม่:

https://troeger.eu/files/teaching/pythonvm08lab.pdf

โดยพื้นฐานแล้วในการเพิ่มคำสั่งใหม่คุณต้องแก้ไข Python/ast.c (เหนือสิ่งอื่นใด) และคอมไพล์ไบนารี python ใหม่

แม้ว่าจะเป็นไปได้ แต่อย่า คุณสามารถบรรลุเกือบทุกอย่างผ่านฟังก์ชั่นและคลาส (ซึ่งไม่จำเป็นต้องให้คนคอมไพล์ไพ ธ อนซ้ำเพื่อเรียกใช้สคริปต์ของคุณ .. )


ลิงก์จริงไปยัง PDF - "การแปลงอัตโนมัติ" นั้นเสียและพังไปแล้วสำหรับพระเจ้าที่รู้มานานแล้ว: troeger.eu/files/teaching/pythonvm08lab.pdf
ZXX

3

ทำได้โดยใช้EasyExtend :

EasyExtend (EE) เป็นตัวสร้างพรีโปรเซสเซอร์และเฟรมเวิร์ก metaprogramming ที่เขียนด้วย Python บริสุทธิ์และรวมเข้ากับ CPython จุดประสงค์หลักของ EasyExtend คือการสร้างภาษาส่วนขยายเช่นการเพิ่มไวยากรณ์และความหมายที่กำหนดเองให้กับ Python


1
ต่อไปนี้ลิงก์จะแสดงหน้า: "EasyExtend ตายไปแล้วสำหรับผู้ที่สนใจ EE มีโครงการต่อเนื่องชื่อ Langscape ชื่อที่แตกต่างออกแบบใหม่ทั้งหมดการเดินทางเดียวกัน" เนื่องจากมีอันตรายที่หน้าข้อมูลนี้อาจตายไปอาจเป็นความคิดที่ดีที่จะอัปเดตคำตอบ
celtschk


1

ไม่ใช่โดยไม่ต้องปรับเปลี่ยนล่าม ฉันรู้ว่าหลาย ๆ ภาษาในช่วงหลายปีที่ผ่านมาถูกอธิบายว่า "ขยายได้" แต่ไม่ใช่ในแบบที่คุณอธิบาย คุณขยาย Python โดยการเพิ่มฟังก์ชันและคลาส


1

มีภาษาที่ใช้ python เรียกว่าLogixซึ่งคุณสามารถทำสิ่งนั้นได้ ไม่ได้อยู่ระหว่างการพัฒนามาระยะหนึ่งแล้ว แต่คุณลักษณะที่คุณขอใช้งานได้กับเวอร์ชันล่าสุด


ฟังดูน่าสนใจ แต่ดูเหมือนจะเสียชีวิตในราวปี 2552: web.archive.org/web/20090107014050/http://livelogix.net/logix
Tobias Kienzler

1

บางอย่างสามารถทำได้ด้วยมัณฑนากร สมมติว่า Python ไม่มีwithคำสั่ง จากนั้นเราสามารถใช้พฤติกรรมที่คล้ายกันเช่นนี้:

# ====== 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())

มันเป็นวิธีแก้ปัญหาที่ค่อนข้างไม่สะอาด แต่ทำที่นี่ โดยเฉพาะอย่างยิ่งพฤติกรรมที่มัณฑนากรเรียกฟังก์ชั่นและชุด_ที่จะNoneเป็นที่ไม่คาดคิด เพื่อความกระจ่าง: มัณฑนากรนี้เทียบเท่ากับการเขียน

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

และมัณฑนากรโดยปกติคาดว่าจะแก้ไขไม่ให้ดำเนินการฟังก์ชัน

ฉันเคยใช้วิธีดังกล่าวมาก่อนในสคริปต์ซึ่งฉันต้องตั้งไดเร็กทอรีการทำงานชั่วคราวสำหรับฟังก์ชันต่างๆ


0

สิบปีที่แล้วคุณทำไม่ได้และฉันสงสัยว่ามันเปลี่ยนไป อย่างไรก็ตามมันไม่ยากที่จะแก้ไขไวยากรณ์ในตอนนั้นหากคุณพร้อมที่จะคอมไพล์ python ใหม่และฉันสงสัยว่ามันเปลี่ยนไปเช่นกัน

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.