اكتشف قوة وحدة ast في بايثون لمعالجة شجرة بناء الجملة المجردة. تعلم تحليل وتعديل وإنشاء كود بايثون برمجيًا.
وحدة Python Ast: تبسيط معالجة شجرة بناء الجملة المجردة
توفر وحدة ast
الخاصة بـ Python طريقة قوية للتفاعل مع شجرة بناء الجملة المجردة (AST) لكود Python. الـ AST عبارة عن تمثيل شجري للبنية النحوية لكود المصدر، مما يجعل من الممكن تحليل كود Python وتعديله وحتى إنشائه برمجيًا. يفتح هذا الباب أمام تطبيقات مختلفة، بما في ذلك أدوات تحليل التعليمات البرمجية، وإعادة التخصيص الآلي، والتحليل الثابت، وحتى ملحقات اللغة المخصصة. سترشدك هذه المقالة خلال أساسيات وحدة ast
، مع توفير أمثلة عملية ورؤى حول قدراتها.
ما هي شجرة بناء الجملة المجردة (AST)؟
قبل الغوص في وحدة ast
، دعنا نفهم ما هي شجرة بناء الجملة المجردة. عندما يقوم مترجم Python بتنفيذ التعليمات البرمجية الخاصة بك، فإن الخطوة الأولى هي تحليل الكود إلى AST. تمثل بنية الشجرة هذه العناصر النحوية للكود، مثل الدوال والفئات والحلقات والتعبيرات والعوامل، إلى جانب علاقاتها. تتجاهل AST التفاصيل غير ذات الصلة مثل المسافات البيضاء والتعليقات، مع التركيز على المعلومات الهيكلية الأساسية. من خلال تمثيل الكود بهذه الطريقة، يصبح من الممكن للبرامج تحليل التعليمات البرمجية نفسها والتلاعب بها، وهو أمر مفيد للغاية في الكثير من الحالات.
البدء باستخدام وحدة ast
تعد وحدة ast
جزءًا من المكتبة القياسية لـ Python، لذلك لست بحاجة إلى تثبيت أي حزم إضافية. ما عليك سوى استيرادها للبدء في استخدامها:
import ast
الوظيفة الأساسية لوحدة ast
هي ast.parse()
، والتي تأخذ سلسلة من كود Python كمدخلات وتعيد كائن AST.
code = """
def add(x, y):
return x + y
"""
ast_tree = ast.parse(code)
print(ast_tree)
سيؤدي هذا إلى إخراج شيء مثل: <_ast.Module object at 0x...>
. في حين أن هذا الإخراج ليس مفيدًا بشكل خاص، إلا أنه يشير إلى أنه تم تحليل الكود بنجاح إلى AST. يحتوي كائن ast_tree
الآن على الهيكل الكامل للتعليمات البرمجية التي تم تحليلها.
استكشاف AST
لفهم بنية AST، يمكننا استخدام الدالة ast.dump()
. تجتاز هذه الدالة الشجرة بشكل متكرر وتطبع تمثيلاً تفصيليًا لكل عقدة.
code = """
def add(x, y):
return x + y
"""
ast_tree = ast.parse(code)
print(ast.dump(ast_tree, indent=4))
سيكون الإخراج هو:
Module(
body=[
FunctionDef(
name='add',
args=arguments(
posonlyargs=[],
args=[
arg(arg='x', annotation=None, type_comment=None),
arg(arg='y', annotation=None, type_comment=None)
],
kwonlyargs=[],
kw_defaults=[],
defaults=[]
),
body=[
Return(
value=BinOp(
left=Name(id='x', ctx=Load()),
op=Add(),
right=Name(id='y', ctx=Load())
)
)
],
decorator_list=[],
returns=None,
type_comment=None
)
],
type_ignores=[]
)
يوضح هذا الإخراج الهيكل الهرمي للكود. دعونا نقسمه:
Module
: عقدة الجذر التي تمثل الوحدة بأكملها.body
: قائمة العبارات داخل الوحدة.FunctionDef
: يمثل تعريف الدالة. تتضمن سماته ما يلي:name
: اسم الدالة ('add').args
: وسيطات الدالة.arguments
: يحتوي على معلومات حول وسيطات الدالة.arg
: يمثل وسيطة واحدة (مثل 'x', 'y').body
: نص الدالة (قائمة العبارات).Return
: يمثل عبارة الإرجاع.value
: القيمة التي يتم إرجاعها.BinOp
: يمثل عملية ثنائية (مثل x + y).left
: المعامل الأيسر (مثل 'x').op
: المشغل (مثل 'Add').right
: المعامل الأيمن (مثل 'y').
اجتياز AST
توفر وحدة ast
فئة ast.NodeVisitor
لاجتياز AST. عن طريق إنشاء فئة فرعية من ast.NodeVisitor
وتجاوز أساليبها، يمكنك معالجة أنواع عقد معينة عند مواجهتها أثناء الاجتياز. هذا مفيد لتحليل بنية التعليمات البرمجية، وتحديد أنماط معينة، أو استخلاص المعلومات.
import ast
class FunctionNameExtractor(ast.NodeVisitor):
def __init__(self):
self.function_names = []
def visit_FunctionDef(self, node):
self.function_names.append(node.name)
code = """
def add(x, y):
return x + y
def subtract(x, y):
return x - y
"""
ast_tree = ast.parse(code)
extractor = FunctionNameExtractor()
extractor.visit(ast_tree)
print(extractor.function_names) # Output: ['add', 'subtract']
في هذا المثال، يرث FunctionNameExtractor
من ast.NodeVisitor
ويتجاوز أسلوب visit_FunctionDef
. يتم استدعاء هذا الأسلوب لكل عقدة تعريف دالة في AST. يضيف الأسلوب اسم الدالة إلى قائمة function_names
. يبدأ أسلوب visit()
اجتياز AST.
مثال: إيجاد جميع تعيينات المتغيرات
import ast
class VariableAssignmentFinder(ast.NodeVisitor):
def __init__(self):
self.assignments = []
def visit_Assign(self, node):
for target in node.targets:
if isinstance(target, ast.Name):
self.assignments.append(target.id)
code = """
x = 10
y = x + 5
message = "hello"
"""
ast_tree = ast.parse(code)
finder = VariableAssignmentFinder()
finder.visit(ast_tree)
print(finder.assignments) # Output: ['x', 'y', 'message']
يعثر هذا المثال على جميع تعيينات المتغيرات في الكود. يتم استدعاء أسلوب visit_Assign
لكل عبارة تعيين. يقوم بالتكرار خلال أهداف التعيين، وإذا كان الهدف اسمًا بسيطًا (ast.Name
)، فإنه يضيف الاسم إلى قائمة assignments
.
تعديل AST
تتيح لك وحدة ast
أيضًا تعديل AST. يمكنك تغيير العقد الموجودة أو إضافة عقد جديدة أو إزالة العقد تمامًا. لتعديل AST، يمكنك استخدام فئة ast.NodeTransformer
. على غرار ast.NodeVisitor
، يمكنك إنشاء فئة فرعية من ast.NodeTransformer
وتجاوز أساليبها لتعديل أنواع عقد معينة. الفرق الرئيسي هو أن طرق ast.NodeTransformer
يجب أن تُرجع العقدة المعدلة (أو عقدة جديدة لتحل محلها). إذا قام أسلوب بإرجاع None
، تتم إزالة العقدة من AST.
بعد تعديل AST، تحتاج إلى تجميعها مرة أخرى في كود Python قابل للتنفيذ باستخدام الدالة compile()
.
import ast
class AddOneTransformer(ast.NodeTransformer):
def visit_Num(self, node):
return ast.Num(n=node.n + 1)
code = """
x = 10
y = 20
"""
ast_tree = ast.parse(code)
transformer = AddOneTransformer()
new_ast_tree = transformer.visit(ast_tree)
new_code = compile(new_ast_tree, '<string>', 'exec')
# Execute the modified code
exec(new_code)
print(x) # Output: 11
print(y) # Output: 21
في هذا المثال، يرث AddOneTransformer
من ast.NodeTransformer
ويتجاوز أسلوب visit_Num
. يتم استدعاء هذا الأسلوب لكل عقدة حرفية رقمية (ast.Num
). يقوم الأسلوب بإنشاء عقدة ast.Num
جديدة مع الزيادة في القيمة بمقدار 1. يعيد أسلوب visit()
AST المعدل.
تأخذ الدالة compile()
AST المعدل، واسم ملف (<string>
في هذه الحالة، مما يشير إلى أن الكود يأتي من سلسلة)، ووضع التنفيذ ('exec'
لتنفيذ كتلة من التعليمات البرمجية). يعيد كائن التعليمات البرمجية الذي يمكن تنفيذه باستخدام الدالة exec()
.
مثال: استبدال اسم متغير
import ast
class VariableNameReplacer(ast.NodeTransformer):
def __init__(self, old_name, new_name):
self.old_name = old_name
self.new_name = new_name
def visit_Name(self, node):
if node.id == self.old_name:
return ast.Name(id=self.new_name, ctx=node.ctx)
return node
code = """
def multiply_by_two(number):
return number * 2
result = multiply_by_two(5)
print(result)
"""
ast_tree = ast.parse(code)
replacer = VariableNameReplacer('number', 'num')
new_ast_tree = replacer.visit(ast_tree)
new_code = compile(new_ast_tree, '<string>', 'exec')
# Execute the modified code
exec(new_code)
يستبدل هذا المثال جميع تكرارات اسم المتغير 'number'
بـ 'num'
. يأخذ VariableNameReplacer
الأسماء القديمة والجديدة كحجج. يتم استدعاء أسلوب visit_Name
لكل عقدة اسم. إذا تطابق معرف العقدة مع الاسم القديم، فإنه ينشئ عقدة ast.Name
جديدة بالاسم الجديد والسياق نفسه (node.ctx
). يشير السياق إلى كيفية استخدام الاسم (مثل التحميل والتخزين).
توليد التعليمات البرمجية من AST
بينما يتيح لك compile()
تنفيذ التعليمات البرمجية من AST، فإنه لا يوفر طريقة للحصول على التعليمات البرمجية كسلسلة. لتوليد كود Python من AST، يمكنك استخدام مكتبة astunparse
. هذه المكتبة ليست جزءًا من المكتبة القياسية، لذا تحتاج إلى تثبيتها أولاً:
pip install astunparse
بعد ذلك، يمكنك استخدام الدالة astunparse.unparse()
لإنشاء كود من AST.
import ast
import astunparse
code = """
def add(x, y):
return x + y
"""
ast_tree = ast.parse(code)
generated_code = astunparse.unparse(ast_tree)
print(generated_code)
سيكون الإخراج هو:
def add(x, y):
return (x + y)
ملاحظة: تتم إضافة الأقواس المحيطة بـ (x + y)
بواسطة astunparse
لضمان أسبقية المشغل الصحيحة. قد لا تكون هذه الأقواس ضرورية بالضرورة، لكنها تضمن صحة الكود.
مثال: إنشاء فئة بسيطة
import ast
import astunparse
class_name = 'MyClass'
method_name = 'my_method'
# Create the class definition node
class_def = ast.ClassDef(
name=class_name,
bases=[],
keywords=[],
body=[
ast.FunctionDef(
name=method_name,
args=ast.arguments(
posonlyargs=[],
args=[],
kwonlyargs=[],
kw_defaults=[],
defaults=[]
),
body=[
ast.Pass()
],
decorator_list=[],
returns=None,
type_comment=None
)
],
decorator_list=[]
)
# Create the module node containing the class definition
module = ast.Module(body=[class_def], type_ignores=[])
# Generate the code
code = astunparse.unparse(module)
print(code)
ينشئ هذا المثال كود Python التالي:
class MyClass:
def my_method():
pass
يوضح هذا كيفية بناء AST من البداية ثم إنشاء كود منها. هذا النهج قوي لأدوات إنشاء التعليمات البرمجية والبرمجة الفوقية.
التطبيقات العملية لوحدة ast
تحتوي وحدة ast
على العديد من التطبيقات العملية، بما في ذلك:
- تحليل التعليمات البرمجية: تحليل التعليمات البرمجية بحثًا عن انتهاكات الأنماط أو نقاط الضعف الأمنية أو عنق الزجاجة في الأداء. على سبيل المثال، يمكنك كتابة أداة لفرض معايير الترميز عبر مشروع كبير.
- إعادة التخصيص الآلية: أتمتة مهام مثل إعادة تسمية المتغيرات أو استخراج الأساليب أو تحويل الكود لاستخدام ميزات اللغة الأحدث. تستخدم أدوات مثل `rope` ASTs لقدرات إعادة التخصيص القوية.
- التحليل الثابت: تحديد الأخطاء أو الأخطاء المحتملة في التعليمات البرمجية دون تشغيلها فعليًا. تستخدم أدوات مثل `pylint` و`flake8` تحليل AST لاكتشاف المشكلات.
- إنشاء التعليمات البرمجية: إنشاء التعليمات البرمجية تلقائيًا بناءً على القوالب أو المواصفات. هذا مفيد لإنشاء كود متكرر أو إنشاء كود لمنصات مختلفة.
- ملحقات اللغة: إنشاء ملحقات لغة مخصصة أو لغات خاصة بالمجال (DSLs) عن طريق تحويل كود Python إلى تمثيلات مختلفة.
- تدقيق الأمان: تحليل التعليمات البرمجية بحثًا عن الإنشاءات أو الثغرات الأمنية التي يحتمل أن تكون ضارة. يمكن استخدام هذا لتحديد ممارسات الترميز غير الآمنة.
مثال: فرض نمط الترميز
لنفترض أنك تريد فرض أن جميع أسماء الدوال في مشروعك تتبع اصطلاح snake_case (مثل my_function
بدلاً من myFunction
). يمكنك استخدام وحدة ast
للتحقق من الانتهاكات.
import ast
import re
class SnakeCaseChecker(ast.NodeVisitor):
def __init__(self):
self.errors = []
def visit_FunctionDef(self, node):
if not re.match(r'^[a-z]+(_[a-z]+)*$', node.name):
self.errors.append(f"Function name '{node.name}' does not follow snake_case convention")
def check_code(self, code):
ast_tree = ast.parse(code)
self.visit(ast_tree)
return self.errors
# Example usage
code = """
def myFunction(x):
return x * 2
def calculate_area(width, height):
return width * height
"""
checker = SnakeCaseChecker()
errors = checker.check_code(code)
if errors:
for error in errors:
print(error)
else:
print("No style violations found")
يحدد هذا الكود فئة SnakeCaseChecker
التي ترث من ast.NodeVisitor
. يتحقق أسلوب visit_FunctionDef
مما إذا كان اسم الدالة يطابق التعبير النمطي snake_case. إذا لم يكن الأمر كذلك، فإنه يضيف رسالة خطأ إلى قائمة errors
. يقوم أسلوب check_code
بتحليل التعليمات البرمجية واجتياز AST وإرجاع قائمة الأخطاء.
أفضل الممارسات عند العمل مع وحدة ast
- فهم بنية AST: قبل محاولة معالجة AST، خصص بعض الوقت لفهم بنيتها باستخدام
ast.dump()
. سيساعدك هذا في تحديد العقد التي تحتاج إلى العمل معها. - استخدام
ast.NodeVisitor
وast.NodeTransformer
: توفر هذه الفئات طريقة مناسبة لاجتياز AST وتعديلها دون الحاجة إلى التنقل في الشجرة يدويًا. - اختبار شامل: عند تعديل AST، اختبر التعليمات البرمجية الخاصة بك جيدًا للتأكد من أن التغييرات صحيحة ولا تدخل أي أخطاء.
- ضع في اعتبارك
astunparse
لإنشاء التعليمات البرمجية: في حين أنcompile()
مفيد لتنفيذ التعليمات البرمجية المعدلة، فإنastunparse
يوفر طريقة لإنشاء كود Python مقروء من AST. - استخدم تلميحات النوع: يمكن أن تعمل تلميحات النوع على تحسين إمكانية القراءة والصيانة للتعليمات البرمجية الخاصة بك بشكل كبير، خاصة عند العمل مع هياكل AST المعقدة.
- وثّق التعليمات البرمجية الخاصة بك: عند إنشاء زوار أو محولات AST مخصصة، قم بتوثيق التعليمات البرمجية الخاصة بك بوضوح لشرح الغرض من كل أسلوب والتغييرات التي يجريها على AST.
التحديات والاعتبارات
- التعقيد: يمكن أن يكون العمل مع ASTs معقدًا، خاصةً لقواعد التعليمات البرمجية الأكبر. يمكن أن يكون فهم أنواع العقد المختلفة وعلاقاتها أمرًا صعبًا.
- الصيانة: يمكن أن تتغير هياكل AST بين إصدارات Python. تأكد من اختبار الكود الخاص بك بإصدارات Python المختلفة لضمان التوافق.
- الأداء: قد يكون اجتياز ASTs الكبيرة وتعديلها بطيئًا. ضع في اعتبارك تحسين التعليمات البرمجية الخاصة بك لتحسين الأداء. يمكن أن يساعد تخزين العقد التي يتم الوصول إليها بشكل متكرر أو استخدام خوارزميات أكثر كفاءة.
- معالجة الأخطاء: تعامل مع الأخطاء بأناقة عند تحليل أو معالجة AST. قم بتوفير رسائل خطأ إعلامية للمستخدم.
- الأمان: كن حذرًا عند تنفيذ التعليمات البرمجية التي تم إنشاؤها من AST، خاصةً إذا كان AST يعتمد على إدخال المستخدم. قم بتنظيف الإدخال لمنع هجمات حقن التعليمات البرمجية.
الخلاصة
توفر وحدة ast
الخاصة بـ Python طريقة قوية ومرنة للتفاعل مع شجرة بناء الجملة المجردة لكود Python. من خلال فهم بنية AST واستخدام الفئات ast.NodeVisitor
وast.NodeTransformer
، يمكنك تحليل وتعديل وإنشاء كود Python برمجيًا. يفتح هذا الباب أمام مجموعة واسعة من التطبيقات، من أدوات تحليل التعليمات البرمجية إلى إعادة التخصيص الآلية وحتى ملحقات اللغة المخصصة. في حين أن العمل مع ASTs يمكن أن يكون معقدًا، إلا أن فوائد القدرة على معالجة التعليمات البرمجية برمجيًا كبيرة. احتضن قوة وحدة ast
لفتح إمكانيات جديدة في مشاريع Python الخاصة بك.