Python: Python eval(): Đánh giá biểu thức động

Các khóa học qua video:
Python SQL Server PHP C# Lập trình C Java HTML5-CSS3-JavaScript
Học trên YouTube <76K/tháng. Đăng ký Hội viên
Viết nhanh hơn - Học tốt hơn
Giải phóng thời gian, khai phóng năng lực
Python eval(): Đánh giá biểu thức động

Mục lục bài viết:


Python eval() cho phép bạn đánh giá các biểu thức Python tùy ý từ đầu vào dựa trên chuỗi hoặc dựa trên mã biên dịch. Hàm này có thể hữu ích khi bạn đang cố gắng đánh giá động các biểu thức Python từ bất kỳ đầu vào nào có dạng chuỗi hoặc đối tượng mã đã biên dịch.

Mặc dù Python eval() là một công cụ cực kỳ hữu ích, nhưng hàm này có một số hàm ý bảo mật quan trọng mà bạn nên cân nhắc trước khi sử dụng nó. Trong hướng dẫn này, bạn sẽ tìm hiểu cách eval() hoạt động và cách sử dụng nó một cách an toàn và hiệu quả trong các chương trình Python của bạn.

Trong hướng dẫn này, bạn sẽ học:

  • Cách hoạt động của eval()
  • Cách sử dụng eval() để đánh giá động đầu vào dựa trên chuỗi hoặc dựa trên mã đã biên dịch tùy ý
  • Cách eval() làm cho mã của bạn không an toàn và cách giảm thiểu các rủi ro bảo mật liên quan.

Ngoài ra, bạn sẽ học cách sử dụng eval() để viết mã một ứng dụng đánh giá tương tác các biểu thức toán học. Với ví dụ này, bạn sẽ áp dụng mọi thứ bạn đã học được từ eval() vào một vấn đề trong thế giới thực.

Hiểu về eval()

Bạn có thể sử dụng Python tích hợp eval() để đánh giá động các biểu thức từ đầu vào dựa trên chuỗi hoặc dựa trên mã đã biên dịch. Nếu bạn chuyển một chuỗi vào eval(), thì hàm sẽ phân tích cú pháp nó, biên dịch nó thành bytecode và đánh giá nó như một biểu thức Python. Nhưng nếu bạn gọi eval() với một đối tượng mã đã biên dịch, thì hàm chỉ thực hiện bước đánh giá, điều này khá thuận tiện nếu bạn gọi eval() nhiều lần với cùng một đầu vào.

Cú pháp của eval() như sau:

eval(expression[, globals[, locals]])

Hàm nhận một đối số đầu tiên là expression, chứa biểu thức mà bạn cần đánh giá. eval() cũng có hai đối số tùy chọn nữa:

  1. globals
  2. locals

Trong ba phần tiếp theo, bạn sẽ tìm hiểu những đối số này là gì và cách eval() sử dụng chúng để đánh giá nhanh các biểu thức Python.

Lưu ý: Bạn cũng có thể sử dụng exec() để thực thi động mã Python. Sự khác biệt chính giữa eval() và exec() là eval() chỉ có thể thực thi hoặc đánh giá các biểu thức, trong khi exec() có thể thực thi bất kỳ đoạn mã Python nào.

Đối số đầu tiên: expression

Đây là đối số bắt buộc chứa đầu vào dựa trên chuỗi hoặc mã đã biên dịch cho hàm. Khi bạn gọi eval(), nội dung của expression được đánh giá là một biểu thức Python. Kiểm tra các ví dụ sau sử dụng đầu vào dựa trên chuỗi:

>>> eval("2 ** 8")
256
>>> eval("1024 + 1024")
2048
>>> eval("sum([8, 16, 32])")
56
>>> x = 100
>>> eval("x * 2")
200

Khi bạn gọi eval() với một chuỗi làm đối số, hàm sẽ trả về giá trị là kết quả từ việc đánh giá chuỗi đầu vào. Theo mặc định, eval() có quyền truy cập vào các tên chung như x trong ví dụ trên.

Để đánh giá một chuỗi dựa trên expression, Python eval() chạy các bước sau:

  1. Phân tích cú pháp expression
  2. Biên dịch nó thành bytecode
  3. Đánh giá nó như một biểu thức Python
  4. Trả kết quả đánh giá

Tên expression của đối số đầu tiên để eval() làm nổi bật rằng hàm chỉ hoạt động với các biểu thức chứ không phải với các câu lệnh ghépTài liệu Python định nghĩa expression như sau:

expression

Một đoạn cú pháp có thể được đánh giá thành một số giá trị. Nói cách khác, một biểu thức là một tập hợp các phần tử biểu thức như chữ, tên, quyền truy cập thuộc tính, toán tử hoặc lệnh gọi hàm mà tất cả đều trả về một giá trị. Ngược lại với nhiều ngôn ngữ khác, không phải tất cả các cấu trúc ngôn ngữ đều là biểu thức. Cũng có những câu lệnh không thể được sử dụng làm biểu thức, chẳng hạn như while. Phép gán cũng là câu lệnh, không phải là biểu thức. (Nguồn)

Mặt khác, một câu lệnh Python có định nghĩa sau:

câu lệnh

Một câu lệnh là một phần của bộ (một “khối” mã). Câu lệnh là một biểu thức hoặc một trong số các cấu trúc với từ khóa, chẳng hạn như ifwhile hoặc for. (Nguồn)

Nếu bạn cố gắng chuyển một câu lệnh ghép vào eval(), thì bạn sẽ nhận được một SyntaxError. Hãy xem ví dụ sau, trong đó bạn cố gắng thực thi một câu lệnh if bằng cách sử dụng eval():

>>> x = 100
>>> eval("if x: print(x)")
  File "<string>", line 1
    if x: print(x)
    ^
SyntaxError: invalid syntax

Nếu bạn cố gắng để đánh giá một câu lệnh ghép sử dụng Python eval(), sau đó bạn sẽ nhận được một SyntaxErrornhư ở trên traceback. Đó là bởi vì eval()chỉ chấp nhận các biểu thức. Bất kỳ lệnh nào, chẳng hạn như ifforwhiledef, hay class, thì sẽ đều phát sinh lỗi.

Lưu ý: Một vòng lặp for là một câu lệnh ghép, nhưng từ khóa for cũng có thể được sử dụng trong comprehensions, được coi là biểu thức. Bạn có thể sử dụng eval() để đánh giá mức độ hiểu ngay cả khi chúng sử dụng từ khóa for.

Hoạt động chuyển nhượng không được phép với eval():

>>> eval("pi = 3.1416")
  File "<string>", line 1
    pi = 3.1416
       ^
SyntaxError: invalid syntax

Nếu bạn cố gắng chuyển một thao tác gán làm đối số cho Python eval(), thì bạn sẽ nhận được một SyntaxError. Các phép toán gán là các câu lệnh chứ không phải là các biểu thức và các câu lệnh không được phép với eval().

Bạn cũng sẽ nhận được SyntaxErrorbất kỳ lúc nào trình phân tích cú pháp không hiểu biểu thức đầu vào. Hãy xem ví dụ sau, trong đó bạn cố gắng đánh giá một biểu thức vi phạm cú pháp Python:

>>> # Incomplete expression
>>> eval("5 + 7 *")
  File "<string>", line 1
    5 + 7 *
          ^
SyntaxError: unexpected EOF while parsing

Bạn không thể truyền một biểu thức eval() vi phạm cú pháp Python. Trong ví dụ trên, bạn cố gắng đánh giá một biểu thức không đầy đủ ("5 + 7 *") và nhận được một SyntaxError vì trình phân tích cú pháp không hiểu cú pháp của biểu thức.

Bạn cũng có thể truyền các đối tượng mã đã biên dịch sang eval(). Để biên dịch mã mà bạn sẽ chuyển sang eval(), bạn có thể sử dụng compile(). Đây là một hàm tích hợp có thể biên dịch một chuỗi đầu vào thành một đối tượng mã hoặc một đối tượng AST để bạn có thể đánh giá nó với eval().

Chi tiết về cách sử dụng compile() nằm ngoài phạm vi của hướng dẫn này, nhưng dưới đây là tóm tắt nhanh về ba đối số bắt buộc đầu tiên của nó:

  1. source giữ mã nguồn mà bạn muốn biên dịch. Đối số này chấp nhận các chuỗi bình thường, chuỗi byte và các đối tượng AST.
  2. filename cung cấp cho tệp mà từ đó mã đã được đọc. Nếu bạn định sử dụng đầu vào dựa trên chuỗi, thì giá trị cho đối số này phải là "<string>".
  3. mode chỉ định loại mã đã biên dịch mà bạn muốn lấy. Nếu bạn muốn xử lý mã đã biên dịch với eval(), thì đối số này phải được đặt thành "eval".

Lưu ý: Để biết thêm thông tin compile(), hãy xem tài liệu chính thức.

Bạn có thể sử dụng compile() để cung cấp các đối tượng mã eval() thay vì các chuỗi bình thường. Kiểm tra các ví dụ sau:

>>> # Arithmetic operations
>>> code = compile("5 + 4", "<string>", "eval")
>>> eval(code)
9
>>> code = compile("(5 + 7) * 2", "<string>", "eval")
>>> eval(code)
24
>>> import math
>>> # Volume of a sphere
>>> code = compile("4 / 3 * math.pi * math.pow(25, 3)", "<string>", "eval")
>>> eval(code)
65449.84694978735

Nếu bạn sử dụng compile() để biên dịch các biểu thức mà bạn sẽ chuyển sang eval(), hãy thực hiện eval() theo các bước sau:

  1. Đánh giá mã đã biên dịch
  2. Trả kết quả đánh giá

Nếu bạn gọi eval() bằng cách sử dụng đầu vào dựa trên mã đã biên dịch, thì hàm sẽ thực hiện bước đánh giá và ngay lập tức trả về kết quả. Điều này có thể hữu ích khi bạn cần đánh giá cùng một biểu thức nhiều lần. Trong trường hợp này, tốt nhất bạn nên biên dịch trước biểu thức và sử dụng lại mã bytecode kết quả trong các lần gọi eval() tiếp theo.

Nếu bạn biên dịch trước biểu thức đầu vào, thì các lệnh gọi liên tiếp tới eval() sẽ chạy nhanh hơn vì bạn sẽ không lặp lại các bước phân tích cú pháp và biên dịch. Việc lặp lại không cần thiết có thể dẫn đến thời gian CPU cao và tiêu thụ quá nhiều bộ nhớ nếu bạn đang đánh giá các biểu thức phức tạp.

Đối sô thứ hai: globals

Đối số thứ hai của eval() được gọi là globals. Nó là tùy chọn và chứa một từ điển cung cấp không gian tên chung cho eval(). Với globals, bạn có thể cho eval() biết những tên chung nào cần sử dụng trong khi đánh giá expression.

Tên chung là tất cả những tên có sẵn trong phạm vi toàn cầu hoặc không gian tên hiện tại của bạn. Bạn có thể truy cập chúng từ bất kỳ đâu trong mã của mình.

Tất cả các tên được chuyển đến globals trong từ điển sẽ có sẵn eval() tại thời điểm thực thi. Hãy xem ví dụ sau, ví dụ này cho thấy cách sử dụng từ điển tùy chỉnh để cung cấp không gian tên chung cho eval():

>>> x = 100  # A global variable
>>> eval("x + 100", {"x": x})
200
>>> y = 200  # Another global variable
>>> eval("x + y", {"x": x})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 1, in <module>
NameError: name 'y' is not defined

Nếu bạn cung cấp một từ điển tùy chỉnh cho đối số globals của eval(), thì eval() sẽ chỉ lấy những tên đó làm các giá trị có phạm vi toàn cục. Mọi tên chung được xác định bên ngoài từ điển tùy chỉnh này sẽ không thể truy cập được từ bên trong eval(). Đó là lý do tại sao Python phát sinh một lỗi NameError khi bạn cố gắng truy cập y trong đoạn code trên: Từ điển được chuyển đến globals không bao gồm y.

Bạn có thể chèn tên vào globals bằng cách liệt kê chúng trong từ điển của mình và sau đó những tên đó sẽ có sẵn trong quá trình đánh giá. Ví dụ: nếu bạn chèn y vào globals, thì đánh giá "x + y" trong ví dụ trên sẽ hoạt động như mong đợi:

>>> eval("x + y", {"x": x, "y": y})
300

Vì bạn thêm y vào từ điển globals tùy chỉnh của mình, nên việc đánh giá "x + y" sẽ thành công và bạn nhận được giá trị trả về mong đợi là 300.

Bạn cũng có thể cung cấp các tên không tồn tại trong phạm vi toàn cục hiện tại của bạn. Để điều này hoạt động, bạn cần cung cấp một giá trị cụ thể cho mỗi một tên. eval() sẽ diễn giải những tên này là tên chung khi chạy:

>>> eval("x + y + z", {"x": x, "y": y, "z": 300})
600
>>> z
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'z' is not defined

Mặc dù z không được xác định trong phạm vi toàn cục hiện tại của bạn, nhưng biến vẫn hiện diện trong globals với giá trị là 300. Trong trường hợp này, eval() có quyền truy cập z như thể nó là một biến toàn cục.

Cơ chế đằng sau globals khá linh hoạt. Bạn có thể truyền bất kỳ biến hiển thị nào (toàn cục, cục bộ hoặc phi cục bộ) đến globals. Bạn cũng có thể truyền các cặp khóa-giá trị tùy chỉnh như "z": 300 trong ví dụ trên. eval() sẽ coi tất cả chúng là biến toàn cục.

Một điểm quan trọng liên quan đến globals là nếu bạn cung cấp một từ điển tùy chỉnh cho nó mà không chứa giá trị cho khóa "__builtins__", thì một tham chiếu đến từ điển của builtins sẽ được tự động chèn vào "__builtins__" trước khi expression được phân tích cú pháp. Điều này đảm bảo rằng eval() sẽ có toàn quyền truy cập vào tất cả các tên được tích hợp sẵn của Python khi đánh giá expression.

Các ví dụ sau đây cho thấy rằng ngay cả khi bạn cung cấp một từ điển trống globals, thì lệnh gọi tới eval() vẫn có quyền truy cập vào các tên có sẵn của Python:

>>> eval("sum([2, 2, 2])", {})
6
>>> eval("min([1, 2, 3])", {})
1
>>> eval("pow(10, 2)", {})
100

Trong đoạn mã trên, bạn cung cấp một từ điển trống ({}) cho globals. Vì từ điển đó không chứa một khóa gọi là "__builtins__", nên Python sẽ tự động chèn một khóa có tham chiếu đến các tên trong builtins. Bằng cách này, eval() có toàn quyền truy cập vào tất cả các tên có sẵn của Python khi nó phân tích cú pháp expression.

Nếu bạn gọi eval() mà không chuyển từ điển tùy chỉnh đến globals, thì đối số sẽ mặc định là từ điển được trả về globals() trong môi trường eval() được gọi là:

>>> x = 100  # A global variable
>>> y = 200  # Another global variable
>>> eval("x + y")  # Access both global variables
300

Khi bạn gọi eval()mà không cung cấp đối số globals, thì hàm sẽ đánh giá expression bằng cách sử dụng từ điển được trả về bởi globals() dưới dạng không gian tên chung của nó. Vì vậy, trong ví dụ trên, bạn có thể tự do truy cập x và y vì chúng là các biến toàn cục được đưa vào phạm vi toàn cục hiện tại của bạn.

Đối số thứ ba: locals

eval() sử dụng một đối số thứ ba được gọi locals. Đây là một đối số tùy chọn khác chứa từ điển. Trong trường hợp này, từ điển chứa các biến được eval() sử dụng làm tên cục bộ khi đánh giá expression.

Tên cục bộ là những tên (biếnhàmlớp, v.v.) mà bạn tạo bên trong một hàm nhất định. Tên cục bộ chỉ hiển thị từ bên trong hàm bao quanh. Bạn tạo những loại tên này khi bạn viết một hàm.

Vì eval() đã được viết sẵn, nên bạn không thể thêm tên cục bộ vào mã hoặc phạm vi cục bộ của nó. Tuy nhiên, bạn có thể truyền từ điển vào locals và eval() sẽ coi những tên đó là tên cục bộ:

>>> eval("x + 100", {}, {"x": 100})
200
>>> eval("x + y", {}, {"x": 100})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 1, in <module>
NameError: name 'y' is not defined

Từ điển thứ hai trong lần gọi đầu tiên để cho eval() giữ biến x. Biến này được eval() hiểu là một biến cục bộ. Nói cách khác, nó được xem như một biến được định nghĩa trong phần thân của eval().

Bạn có thể sử dụng x trong expression và eval() sẽ có quyền truy cập vào nó. Ngược lại, nếu bạn cố gắng sử dụng y, thì bạn sẽ nhận được NameError bởi vì y không được tạo ra cả trong không gian tên globals lẫn không gian tên locals.

Giống như với globals, bạn có thể truyền bất kỳ biến hiển thị nào (toàn cục, cục bộ hoặc không cục bộ) đến locals. Bạn cũng có thể truyền các cặp khóa-giá trị tùy chỉnh như "x": 100 trong ví dụ trên. eval() sẽ coi tất cả chúng là biến cục bộ.

Lưu ý rằng để cung cấp từ điển cho locals, trước tiên bạn cần cung cấp từ điển cho globals. Không thể sử dụng các đối số từ khóa với eval():

>>> eval("x + 100", locals={"x": 100})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: eval() takes no keyword arguments

Nếu bạn cố gắng sử dụng các đối số từ khóa khi gọi eval(), thì bạn sẽ nhận được lỗi TypeError với giải thích rằng eval() không có đối số từ khóa. Vì vậy, bạn cần cung cấp từ điển globals trước khi có thể cung cấp từ điển locals.

Nếu bạn không chuyển một từ điển đến locals, thì nó sẽ mặc định là từ điển được chuyển đến globals. Đây là một ví dụ trong đó bạn chuyển một từ điển trống globalsvà không có gì vào locals:

>>> x = 100
>>> eval("x + 100", {})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 1, in <module>
NameError: name 'x' is not defined

Giả sử rằng bạn không cung cấp từ điển tùy chỉnh locals, đối số sẽ mặc định được chuyển đến từ điển globals. Trong trường hợp này, eval() không có quyền truy cập vào x vì globals có một từ điển trống.

Sự khác biệt thực tế chính giữa globals và locals là Python sẽ tự động chèn một từ khóa "__builtins__" vào globals nếu khóa đó chưa tồn tại. Điều này xảy ra cho dù bạn có cung cấp từ điển tùy chỉnh cho globals hay không. Mặt khác, nếu bạn cung cấp một từ điển tùy chỉnh cho locals, thì từ điển đó sẽ không thay đổi trong quá trình thực thi eval().

Đánh giá biểu thức bằng eval()

Bạn có thể sử dụng eval() để đánh giá bất kỳ loại biểu thức Python nào nhưng không phải là các câu lệnh Python, chẳng hạn như câu lệnh ghép dựa trên từ khóa hoặc câu lệnh gán.

eval() có thể hữu ích khi bạn cần đánh giá động các biểu thức và sử dụng các kỹ thuật hoặc công cụ Python khác sẽ làm tăng đáng kể thời gian và nỗ lực phát triển của bạn. Trong phần này, bạn sẽ học cách sử dụng eval() để đánh giá Boolean, toán học và các biểu thức Python có mục đích chung.

Biểu thức Boolean

Biểu thức Boolean là các biểu thức Python trả về giá trị chân lý (True hoặc False) khi trình thông dịch đánh giá chúng. Chúng thường được sử dụng trong các câu lệnh if để kiểm tra xem một số điều kiện là đúng hay sai. Vì biểu thức Boolean không phải là câu lệnh ghép nên bạn có thể sử dụng eval()để đánh giá chúng:

>>> x = 100
>>> y = 100
>>> eval("x != y")
False
>>> eval("x < 200 and y > 100")
False
>>> eval("x is y")
True
>>> eval("x in {50, 100, 150, 200}")
True

Bạn có thể sử dụng eval() với các biểu thức Boolean sử dụng bất kỳ loại phép toán Python nào sau đây:

  • Phép toán so sánh: <,>,<=,>=,==,!=
  • Phép toán logic: andornot
  • Phép toán kiểm tra thành viên: innot in
  • Phép toán nhận diện: is, is not

Trong mọi trường hợp, hàm sẽ đều trả về giá trị chân lý của biểu thức mà bạn đang đánh giá.

Bây giờ, bạn có thể đang nghĩ, tại sao tôi nên sử dụng eval() thay vì sử dụng trực tiếp biểu thức Boolean? Vâng, giả sử bạn cần triển khai một câu lệnh có điều kiện, nhưng bạn muốn thay đổi điều kiện một cách nhanh chóng như sau:

>>> def func(a, b, condition):
...     if eval(condition):
...         return a + b
...     return a - b
...
>>> func(2, 4, "a > b")
-2
>>> func(2, 4, "a < b")
6
>>> func(2, 2, "a is b")
4

Thì bên trong func(), bạn sử dụng eval() để đánh giá sản phẩm được cung cấp condition và trả về a + b hoặc a - b theo kết quả đánh giá. Bạn chỉ sử dụng một vài điều kiện khác nhau trong ví dụ trên, nhưng bạn có thể sử dụng bất kỳ số điều kiện nào khác với điều kiện bạn phải tuân theo với các tên a và b mà bạn đã định nghĩa trong func().

Bây giờ hãy tưởng tượng bạn sẽ triển khai thứ gì đó như thế này mà không cần sử dụng eval(). Điều đó có tốn ít code và thời gian hơn không? Chắc chắn là không!

Biểu thức toán học

Một trường hợp sử dụng phổ biến nữa của eval() là đánh giá các biểu thức toán học từ đầu vào dựa trên chuỗi. Ví dụ: nếu bạn muốn tạo một máy tính Python, thì bạn có thể sử dụng eval() để đánh giá đầu vào của người dùng và trả về kết quả của các phép tính.

Các ví dụ sau đây cho thấy cách bạn có thể sử dụng eval() cùng với math để thực hiện các phép toán:

>>> # Arithmetic operations
>>> eval("5 + 7")
12
>>> eval("5 * 7")
35
>>> eval("5 ** 7")
78125
>>> eval("(5 + 7) / 2")
6.0
>>> import math
>>> # Area of a circle
>>> eval("math.pi * pow(25, 2)")
1963.4954084936207
>>> # Volume of a sphere
>>> eval("4 / 3 * math.pi * math.pow(25, 3)")
65449.84694978735
>>> # Hypotenuse of a right triangle
>>> eval("math.sqrt(math.pow(10, 2) + math.pow(15, 2))")
18.027756377319946

Khi bạn sử dụng eval() để đánh giá các biểu thức toán học, bạn có thể chuyển các biểu thức thuộc bất kỳ loại hoặc độ phức tạp nào. eval() sẽ phân tích cú pháp, đánh giá chúng và nếu mọi thứ đều ổn, sẽ cho bạn kết quả như mong đợi.

Biểu thức Mục đích Chung

Đến đây thì bạn đã học cách sử dụng eval() với Boolean và các biểu thức toán học. Tuy nhiên, bạn có thể sử dụng eval() với các biểu thức Python phức tạp hơn kết hợp các lệnh gọi hàm, tạo đối tượng, truy cập thuộc tính, comprehension, v.v.

Ví dụ: bạn có thể gọi một hàm tích hợp sẵn hoặc một hàm mà bạn đã nhập bằng mô-đun chuẩn hoặc bên thứ ba:

>>> # Run the echo command
>>> import subprocess
>>> eval("subprocess.getoutput('echo Hello, World')")
'Hello, World'
>>> # Launch Firefox (if available)
>>> eval("subprocess.getoutput('firefox')")
''

Trong ví dụ này, bạn sử dụng eval() để thực hiện một số lệnh hệ thống. Như bạn có thể tưởng tượng, bạn có thể làm rất nhiều điều hữu ích với tính năng này. Tuy nhiên, eval() cũng có thể khiến bạn gặp phải những rủi ro bảo mật nghiêm trọng, chẳng hạn như cho phép người dùng độc hại chạy các lệnh hệ thống hoặc bất kỳ đoạn mã tùy ý nào trong máy của bạn.

Trong phần tiếp theo, bạn sẽ xem xét các cách để giải quyết một số rủi ro bảo mật liên quan đến eval().

Giảm thiểu các vấn đề bảo mật của eval()

Mặc dù nó có số lượng sử dụng gần như không giới hạn, nhưng eval() cũng có ý nghĩa bảo mật quan trọng. eval() được coi là không an toàn vì nó cho phép bạn (hoặc người dùng của bạn) thực thi động mã Python tùy ý.

Đây được coi là thực tế lập trình tồi vì mã bạn đang đọc (hoặc viết) không phải là mã bạn sẽ thực thi. Nếu bạn định sử dụng eval() để đánh giá đầu vào từ người dùng hoặc bất kỳ nguồn bên ngoài nào khác, thì bạn sẽ không biết chắc chắn mã nào sẽ được thực thi. Đó là một rủi ro bảo mật nghiêm trọng nếu ứng dụng của bạn chạy không đúng người.

Vì lý do này, các phương pháp lập trình tốt thường khuyên bạn không nên sử dụng eval(). Nhưng nếu bạn vẫn chọn sử dụng hàm, thì nguyên tắc chung là không bao giờ  sử dụng nó với đầu vào không đáng tin cậy. Phần khó của quy tắc này là tìm ra loại đầu vào nào bạn có thể tin tưởng.

Ví dụ về cách sử dụng eval() thiếu trách nhiệm có thể làm cho mã của bạn không an toàn, giả sử bạn muốn xây dựng một dịch vụ trực tuyến để đánh giá các biểu thức Python tùy ý. Người dùng của bạn sẽ giới thiệu các biểu thức và sau đó nhấp vào nút Run. Ứng dụng sẽ lấy thông tin đầu vào của người dùng và chuyển nó đến eval() để đánh giá.

Ứng dụng này sẽ chạy trên máy chủ cá nhân của bạn, nơi bạn có tất cả các tệp có giá trị đó. Nếu bạn đang chạy một box Linux và quy trình của ứng dụng có quyền phù hợp, thì người dùng độc hại có thể đưa ra một chuỗi nguy hiểm như sau:

"__import__('subprocess').getoutput('rm –rf *')"

Đoạn mã trên sẽ xóa tất cả các tệp trong thư mục hiện tại của ứng dụng. Điều đó sẽ thật kinh khủng, phải không?

Lưu ý: __import__() là một hàm dựng sẵn lấy tên mô-đun làm chuỗi và trả về một tham chiếu đến đối tượng mô-đun. __import__() là một hàm, hoàn toàn khác với câu lệnh import. Bạn không thể đánh giá một lệnh import bằng cách sử dụng eval().

Khi đầu vào không đáng tin cậy, không có cách nào hoàn toàn hiệu quả để tránh các rủi ro bảo mật liên quan đến eval(). Tuy nhiên, bạn có thể giảm thiểu rủi ro của mình bằng cách hạn chế môi trường thực thi của eval(). Bạn sẽ học một số kỹ thuật để làm như vậy trong các phần sau.

Hạn chế của globals và locals

Bạn có thể hạn chế môi trường thực thi eval() bằng cách truyền các từ điển tùy chỉnh đến các đối số globals và locals. Ví dụ: bạn có thể truyền các từ điển trống cho cả hai đối số để ngăn việc eval() truy cập các tên trong phạm vi hoặc không gian tên hiện tại của người gọi:

>>> # Avoid access to names in the caller's current scope
>>> x = 100
>>> eval("x * 5", {}, {})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 1, in <module>
NameError: name 'x' is not defined

Nếu bạn truyền từ điển trống ({}) đến globals và locals, thì eval() sẽ không tìm thấy tên x trong không gian tên chung hoặc không gian tên cục bộ của nó khi đánh giá chuỗi "x * 5". Kết quả là, eval() sẽ ném một NameError.

Thật không may, việc hạn chế các đối số globals và locals như thế này không loại bỏ tất cả các rủi ro bảo mật liên quan đến việc sử dụng eval(), vì bạn vẫn có thể truy cập vào tất cả các tên có sẵn của Python.

Hạn chế sử dụng tên có sẵn

Như bạn đã thấy trước đó, eval() sẽ tự động chèn một tham chiếu đến từ điển của builtins vào globals trước khi phân tích cú pháp expression. Người dùng độc hại có thể khai thác hành vi này bằng cách sử dụng hàm có sẵn __import__() để có quyền truy cập vào thư viện chuẩn và bất kỳ mô-đun bên thứ ba nào mà bạn đã cài đặt trên hệ thống của mình.

Các ví dụ sau cho thấy rằng bạn có thể sử dụng bất kỳ hàm tích hợp nào và bất kỳ mô-đun tiêu chuẩn nào như math hoặc subprocess thậm chí sau khi bạn đã hạn chế globals và locals:

>>> eval("sum([5, 5, 5])", {}, {})
15
>>> eval("__import__('math').sqrt(25)", {}, {})
5.0
>>> eval("__import__('subprocess').getoutput('echo Hello, World')", {}, {})
'Hello, World'

Mặc dù bạn hạn chế globals và locals sử dụng các từ điển trống, bạn vẫn có thể sử dụng bất kỳ chức năng tích hợp nào giống như bạn đã làm với sum() và __import__() trong đoạn mã trên.

Bạn có thể sử dụng __import__() để nhập bất kỳ mô-đun tiêu chuẩn hoặc bên thứ ba nào giống như bạn đã làm ở trên với math và subprocess. Với kỹ thuật này, bạn có thể truy cập vào bất kỳ hàm hoặc lớp được định nghĩa nào trong mathsubprocess hoặc bất kỳ thành phần khác. Bây giờ hãy tưởng tượng những gì một người dùng độc hại có thể làm với hệ thống của bạn bằng cách sử dụng subprocess hoặc bất kỳ mô-đun mạnh mẽ nào khác trong thư viện chuẩn.

Để giảm thiểu rủi ro này, bạn có thể hạn chế quyền truy cập vào các chức năng tích hợp của Python bằng cách ghi đè khóa "__builtins__" trong globals. Thực tiễn tốt khuyên bạn nên sử dụng từ điển tùy chỉnh có chứa cặp khóa-giá trị "__builtins__": {}. Kiểm tra ví dụ sau:

>>> eval("__import__('math').sqrt(25)", {"__builtins__": {}}, {})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 1, in <module>
NameError: name '__import__' is not defined

Nếu bạn truyền một từ điển có chứa cặp khóa-giá trị "__builtins__": {} vào globals, thì eval() sẽ không có quyền truy cập trực tiếp vào các hàm tích hợp sẵn của Python như __import__(). Tuy nhiên, như bạn sẽ thấy trong phần tiếp theo, cách tiếp cận này vẫn không đảm bảo eval() hoàn toàn an toàn.

Hạn chế tên khi input dữ liệu

Mặc dù bạn có thể hạn chế môi trường thực thi của Python eval() bằng cách sử dụng tùy chỉnh các từ điển globals và locals, nhưng eval() vẫn sẽ dễ bị tấn công bởi một vài thủ thuật lạ. Ví dụ, bạn có thể truy cập vào lớp object sử dụng một loại Literal như ""[]{}, hoặc () cùng với một số thuộc tính đặc biệt như sau:

>>> "".__class__.__base__
<class 'object'>
>>> [].__class__.__base__
<class 'object'>
>>> {}.__class__.__base__
<class 'object'>
>>> ().__class__.__base__
<class 'object'>

Khi bạn có quyền truy cập object, bạn có thể sử dụng phương thức đặc biệt là .__subclasses__() để truy cập vào tất cả các lớp kế thừa từ lớp object. Đây là cách nó hoạt động:

>>> for sub_class in ().__class__.__base__.__subclasses__():
...     print(sub_class.__name__)
...
type
weakref
weakcallableproxy
weakproxy
int
...

Mã này sẽ in một danh sách lớn các lớp ra màn hình của bạn. Một số lớp này khá mạnh và có thể cực kỳ nguy hiểm khi rơi vào tay kẻ xấu. Điều này mở ra một lỗ hổng bảo mật quan trọng khác mà bạn không thể đóng bằng cách hạn chế môi trường thực thi của eval():

>>> input_string = """[
...     c for c in ().__class__.__base__.__subclasses__()
...     if c.__name__ == "range"
... ][0](10)"""
>>> list(eval(input_string, {"__builtins__": {}}, {}))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Sự hiểu danh sách trong đoạn mã trên lọc các lớp kế thừa object để trả về một lớp list chứa range. Chỉ mục đầu tiên ([0]) trả về lớp range. Khi bạn có quyền truy cập range, bạn gọi nó để tạo một đối tượng range. Sau đó, bạn gọi list() trên đối tượng range để tạo ra một danh sách gồm 10 số nguyên.

Trong ví dụ này, bạn sử dụng range để minh họa một lỗ hổng bảo mật eval(). Bây giờ hãy tưởng tượng những gì một người dùng độc hại có thể làm nếu hệ thống của bạn tiếp xúc với các lớp như subprocess.Popen.

Lưu ý: Để tìm hiểu sâu hơn về các lỗ hổng của eval(), hãy xem bài viết của Ned Batchelder, Eval thực sự rất nguy hiểm.

Một giải pháp khả thi cho lỗ hổng này là hạn chế việc sử dụng tên trong đầu vào, với một loạt các tên an toàn hoặc không có tên nào cả. Để thực hiện kỹ thuật này, bạn cần trải qua các bước sau:

  1. Tạo một từ điển chứa các tên mà bạn muốn sử dụng với eval().
  2. Biên dịch chuỗi đầu vào thành bytecode bằng cách sử dụng compile() trong chế độ "eval".
  3. Kiểm tra .co_names trên đối tượng bytecode để đảm bảo rằng nó chỉ chứa các tên được phép.
  4. Kích hoạt NameError nếu người dùng cố gắng nhập tên không được phép.

Hãy xem hàm sau đây:

>>> def eval_expression(input_string):
...     # Step 1
...     allowed_names = {"sum": sum}
...     # Step 2
...     code = compile(input_string, "<string>", "eval")
...     # Step 3
...     for name in code.co_names:
...         if name not in allowed_names:
...             # Step 4
...             raise NameError(f"Use of {name} not allowed")
...     return eval(code, {"__builtins__": {}}, allowed_names)

Trong eval_expression(), bạn thực hiện tất cả các bước. Hàm này sẽ hạn chế các tên mà bạn có thể sử dụng eval() chỉ với những tên đó trong từ điển allowed_names. Để làm điều này, hàm sử dụng .co_names, là một thuộc tính của một đối tượng mã trả về một bộ giá trị chứa các tên trong đối tượng mã.

Các ví dụ sau đây cho thấy cách eval_expression() hoạt động trong thực tế:

>>> eval_expression("3 + 4 * 5 + 25 / 2")
35.5
>>> eval_expression("sum([1, 2, 3])")
6
>>> eval_expression("len([1, 2, 3])")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 10, in eval_expression
NameError: Use of len not allowed
>>> eval_expression("pow(10, 2)")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 10, in eval_expression
NameError: Use of pow not allowed

Nếu bạn gọi eval_expression() để đánh giá các phép toán số học hoặc nếu bạn sử dụng các biểu thức bao gồm các tên được phép, thì bạn sẽ nhận được kết quả mong đợi. Nếu không, bạn sẽ nhận được một NameError. Trong các ví dụ trên, tên duy nhất bạn cho phép là sum(). Các tên khác như len() và pow() sẽ không được phép, vì vậy hàm sẽ phát sinh lỗi NameError khi bạn cố gắng sử dụng chúng.

Nếu bạn muốn hoàn toàn không cho phép sử dụng tên, bạn có thể viết lại eval_expression() như sau:

>>> def eval_expression(input_string):
...     code = compile(input_string, "<string>", "eval")
...     if code.co_names:
...         raise NameError(f"Use of names not allowed")
...     return eval(code, {"__builtins__": {}}, {})
...
>>> eval_expression("3 + 4 * 5 + 25 / 2")
35.5
>>> eval_expression("sum([1, 2, 3])")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in eval_expression
NameError: Use of names not allowed

Bây giờ hàm của bạn sẽ không cho phép bất kỳ tên nào trong chuỗi đầu vào. Để thực hiện điều này, bạn kiểm tra tên trong .co_names và kích hoạt một NameError nếu tìm thấy. Nếu không, bạn đánh giá input_string và trả về kết quả đánh giá. Trong trường hợp này, bạn cũng sử dụng một từ điển trống để hạn chế locals.

Bạn có thể sử dụng kỹ thuật này để giảm thiểu các vấn đề bảo mật eval() và củng cố khả năng bảo vệ của mình trước các cuộc tấn công độc hại.

Hạn chế input dữ liệu là các hằng

Một trường hợp sử dụng phổ biến cho eval() là đánh giá các chuỗi có chứa các ký tự Python tiêu chuẩn và biến chúng thành các đối tượng cụ thể.

Thư viện tiêu chuẩn cung cấp một hàm được gọi là literal_eval() có thể giúp đạt được mục tiêu này. Hàm không hỗ trợ toán tử, nhưng nó hỗ trợ list, tuple, số, chuỗi, v.v.

>>> from ast import literal_eval
>>> # Evaluating literals
>>> literal_eval("15.02")
15.02
>>> literal_eval("[1, 15]")
[1, 15]
>>> literal_eval("(1, 15)")
(1, 15)
>>> literal_eval("{'one': 1, 'two': 2}")
{'one': 1, 'two': 2}
>>> # Trying to evaluate an expression
>>> literal_eval("sum([1, 15]) + 5 + 8 * 2")
Traceback (most recent call last):
  ...
ValueError: malformed node or string: <_ast.BinOp object at 0x7faedecd7668>

Lưu ý rằng literal_eval() chỉ hoạt động với các ký tự kiểu tiêu chuẩn. Nó không hỗ trợ việc sử dụng các toán tử hoặc tên. Nếu bạn cố gắng cung cấp một biểu thức cho literal_eval(), thì bạn sẽ nhận được một lỗi ValueError. Hàm này cũng có thể giúp bạn giảm thiểu rủi ro bảo mật liên quan đến việc sử dụng eval().

Sử dụng eval() với input()

Trong Python 3.x có tích hợp sẵn input() dùng để đọc dữ liệu đầu vào của người dùng tại dòng lệnh, chuyển đổi nó thành một chuỗi, tách dòng mới ở cuối và trả về kết quả cho người gọi. Vì kết quả của input() là một chuỗi, bạn có thể cung cấp eval() và đánh giá nó như một biểu thức Python:

>>> eval(input("Enter a math expression: "))
Enter a math expression: 15 * 2
30
>>> eval(input("Enter a math expression: "))
Enter a math expression: 5 + 8
13

Bạn có thể đặt eval() ngoài input() để tự động đánh giá đầu vào của người dùng. Đây là một trường hợp sử dụng phổ biến của eval() vì nó mô phỏng hành vi của input() trong Python 2.x, trong đó input() đánh giá đầu vào của người dùng dưới dạng biểu thức Python và trả về kết quả.

Hành vi này của input() trong Python 2.x đã được thay đổi trong Python 3.x vì các tác động bảo mật của nó.

Xây dựng một trình đánh giá biểu thức toán học

Đến đây thì bạn đã biết cách eval() hoạt động và cách sử dụng nó trong thực tế. Bạn cũng đã biết rằng eval() có thể gây ra lỗ hổng bảo mật nghiêm trọng. Tuy nhiên, bên cạnh đó eval() có thể giúp bạn tiết kiệm rất nhiều thời gian và công sức.

Trong phần này, bạn sẽ viết mã một ứng dụng để đánh giá nhanh các biểu thức toán học. Nếu bạn muốn giải quyết vấn đề này mà không cần sử dụng eval(), thì bạn cần thực hiện các bước sau:

  1. Phân tích cú pháp biểu thức đầu vào.
  2. Thay đổi các thành phần của biểu thức thành các đối tượng Python (số, toán tử, hàm, v.v.).
  3. Kết hợp mọi thứ thành một biểu thức.
  4. Xác nhận rằng biểu thức hợp lệ trong Python.
  5. Đánh giá biểu thức cuối cùng và trả về kết quả.

Đó sẽ là rất nhiều công việc xem xét nhiều loại biểu thức có thể có mà Python có thể xử lý và đánh giá. May mắn thay, bạn có thể sử dụng eval() để giải quyết vấn đề này và bạn đã học được một số kỹ thuật để giảm các rủi ro bảo mật liên quan.

Đầu tiên bạn tạo một file tên mathrepl.py, sau đó thêm mã sau:

 1import math
 2
 3__version__ = "1.0"
 4
 5ALLOWED_NAMES = {
 6    k: v for k, v in math.__dict__.items() if not k.startswith("__")
 7}
 8
 9PS1 = "mr>>"
10
11WELCOME = f"""
12MathREPL {__version__}, your Python math expressions evaluator!
13Enter a valid math expression after the prompt "{PS1}".
14Type "help" for more information.
15Type "quit" or "exit" to exit.
16"""
17
18USAGE = f"""
19Usage:
20Build math expressions using numeric values and operators.
21Use any of the following functions and constants:
22
23{', '.join(ALLOWED_NAMES.keys())}
24"""
25

Trong đoạn mã trên, trước tiên bạn import mô-đun math của Python. Mô-đun này sẽ cho phép bạn thực hiện các phép toán bằng cách sử dụng các hàm và hằng số được xác định trước. Hằng ALLOWED_NAMES chứa một từ điển chứa các tên không đặc biệt trong math. Bằng cách này, bạn sẽ có thể sử dụng chúng với eval().

Bạn cũng tạo thêm ba hằng số chuỗi. Bạn sẽ sử dụng chúng làm giao diện người dùng cho tập lệnh của mình và bạn sẽ in chúng ra màn hình nếu cần.

Bây giờ bạn đã sẵn sàng để viết mã chức năng cốt lõi của ứng dụng của mình. Trong trường hợp này, bạn muốn viết mã một hàm nhận các biểu thức toán học làm đầu vào và trả về kết quả của chúng. Để làm điều này, bạn viết một hàm có tên evaluate():

26def evaluate(expression):
27    """Evaluate a math expression."""
28    # Compile the expression
29    code = compile(expression, "<string>", "eval")
30
31    # Validate allowed names
32    for name in code.co_names:
33        if name not in ALLOWED_NAMES:
34            raise NameError(f"The use of '{name}' is not allowed")
35
36    return eval(code, {"__builtins__": {}}, ALLOWED_NAMES)

Đây là cách hoạt động của hàm:

  1. Dòng 26, bạn định nghĩa hàm evaluate(). Hàm này có chuỗi expression làm tham số và hàm trả về kiểu float đại diện là kết quả của việc đánh giá chuỗi như một biểu thức toán học.
  2. Dòng 29, bạn sử dụng compile() để biến chuỗi đầu vào expression thành mã Python đã biên dịch. Thao tác biên dịch sẽ phát sinh một lỗi SyntaxError nếu người dùng nhập một biểu thức không hợp lệ.
  3. Dòng 32, bạn bắt đầu một vòng lặp for để kiểm tra các tên có trong expression và xác nhận rằng chúng có thể được sử dụng trong biểu thức cuối cùng. Nếu người dùng cung cấp tên không có trong danh sách tên được phép thì ta sẽ nhận được một lỗi NameError.
  4. Dòng 36, bạn thực hiện đánh giá thực tế của biểu thức toán học. Lưu ý rằng bạn chuyển các từ điển tùy chỉnh đến globals và locals như phương pháp hay được khuyến nghị. ALLOWED_NAMES giữ các hàm và hằng số được định nghĩa trong math.

Lưu ý: Vì ứng dụng này sử dụng các hàm được định nghĩa trong math, bạn cần phải xem xét rằng một số hàm này sẽ phát sinh một lỗi ValueError khi bạn gọi chúng với giá trị đầu vào không hợp lệ.

Ví dụ, math.sqrt(-10) sẽ phát sinh lỗi vì căn bậc hai của -10 không được xác định. Sau đó, bạn sẽ thấy cách bắt lỗi này trong mã khách hàng của mình.

Việc sử dụng các giá trị tùy chỉnh cho các tham số globals và locals cùng với việc kiểm tra các tên trong dòng 33 sẽ cho phép bạn giảm thiểu các rủi ro bảo mật liên quan đến việc sử dụng eval().

Trình đánh giá biểu thức toán học của bạn sẽ hoàn thành khi bạn viết mã khách hàng của nó vào main(). Trong hàm này, bạn sẽ tạo vòng lặp chính của chương trình và đóng chu kỳ đọc và đánh giá các biểu thức mà người dùng của bạn nhập vào dòng lệnh.

Đối với ví dụ này, ứng dụng sẽ:

  1. In thông báo chào mừng tới người dùng
  2. Hiển thị lời nhắc sẵn sàng đọc thông tin đầu vào của người dùng
  3. Cung cấp các tùy chọn để nhận hướng dẫn sử dụng và chấm dứt ứng dụng
  4. Đọc biểu thức toán học của người dùng
  5. Đánh giá biểu thức toán học của người dùng
  6. In kết quả đánh giá ra màn hình

Kiểm tra việc triển khai sau đây của main():

38def main():
39    """Main loop: Read and evaluate user's input."""
40    print(WELCOME)
41    while True:
42        # Read user's input
43        try:
44            expression = input(f"{PS1} ")
45        except (KeyboardInterrupt, EOFError):
46            raise SystemExit()
47
48        # Handle special commands
49        if expression.lower() == "help":
50            print(USAGE)
51            continue
52        if expression.lower() in {"quit", "exit"}:
53            raise SystemExit()
54
55        # Evaluate the expression and handle errors
56        try:
57            result = evaluate(expression)
58        except SyntaxError:
59            # If the user enters an invalid expression
60            print("Invalid input expression syntax")
61            continue
62        except (NameError, ValueError) as err:
63            # If the user tries to use a name that isn't allowed
64            # or an invalid value for a given math function
65            print(err)
66            continue
67
68        # Print the result if no error occurs
69        print(f"The result is: {result}")
70
71if __name__ == "__main__":
72    main()

Bên trong main(), trước tiên bạn in thông điệp WELCOME. Sau đó, bạn đọc đầu vào của người dùng trong một câu lệnh try để bắt các ngoại lệ KeyboardInterrupt và EOFError. Nếu một trong hai trường hợp ngoại lệ này xảy ra, thì bạn chấm dứt ứng dụng.

Nếu người dùng nhập tùy chọn help, thì ứng dụng sẽ hiển thị hướng dẫn USAGE. Tương tự như vậy, nếu người dùng nhập quit hoặc exit, thì ứng dụng sẽ kết thúc.

Cuối cùng, bạn sử dụng evaluate() để đánh giá biểu thức toán học của người dùng, và sau đó bạn in kết quả ra màn hình. Điều quan trọng cần lưu ý là một lời gọi đến evaluate() có thể đưa ra các trường hợp ngoại lệ sau:

  • SyntaxError: Điều này xảy ra khi người dùng nhập một biểu thức không tuân theo cú pháp Python.
  • NameError: Điều này xảy ra khi người dùng cố gắng sử dụng tên (hàm, lớp hoặc thuộc tính) không được phép.
  • ValueError: Điều này xảy ra khi người dùng cố gắng sử dụng một giá trị không được phép làm đầu vào cho một hàm nhất định trong math.

Lưu ý rằng trong main(), bạn nắm bắt tất cả các ngoại lệ này và in thông báo cho người dùng biết lỗi tương ứng là gì. Điều này sẽ cho phép người dùng xem lại biểu thức, khắc phục sự cố và chạy lại chương trình.

Như vậy đến đây bạn đã xây dựng xong một trình đánh giá biểu thức toán học trong khoảng bảy mươi dòng mã bằng cách sử dụng eval(). Dưới đây là chương trình hoàn chỉnh:

import math

__version__ = "1.0"

ALLOWED_NAMES = {
  k: v for k, v in math.__dict__.items() if not k.startswith("__")
}

PS1 = "mr>>"

WELCOME = f"""
MathREPL {__version__}, trình đánh giá biểu thức toán học Python!
Nhập vào một biểu thức toán học sau dòng nhắc "{PS1}".
Soạn "help" rồi Enter để biết thêm thông tin.
Soạn "quit" hoặc "exit" để thoát.
"""

USAGE = f"""
Usage:
Xây dựng biểu thức toán học sử dụng các phép toán và các con số.
Sử dụng bất kỳ hằng và hàm nào dưới đây:

{', '.join(ALLOWED_NAMES.keys())}
"""


def evaluate(expression):
  """Đánh giá biểu thức toán học."""
  # Biên dịch biểu thức
  code = compile(expression, "<string>", "eval")

  # Xác nhận tên được phép hay không:
  for name in code.co_names:
    if name not in ALLOWED_NAMES:
      raise NameError(f"Việc sử dụng tên '{name}' là không được phép")

  return eval(code, {"__builtins__": {}}, ALLOWED_NAMES)


def main():
  """Vòng lặp main: Đọc và đánh giá các giá trị nhập vào từ người dùng."""
  print(WELCOME)
  while True:
    # Đọc dữ liệu đầu vào từ người dùng
    try:
      expression = input(f"{PS1} ")
    except (KeyboardInterrupt, EOFError):
      raise SystemExit()

    # Xử lý các lệnh đặc biệt
    if expression.lower() == "help":
      print(USAGE)
      continue
    if expression.lower() in {"quit", "exit"}:
      raise SystemExit()

    # Đánh giá biểu thức và xử lý lỗi
    try:
      result = evaluate(expression)
    except SyntaxError:
      # Nếu người dùng đưa vào biểu thức sai:
      print("Cú pháp biểu thức đưa vào không hợp lệ")
      continue
    except (NameError, ValueError) as err:
      # Nếu người dùng cố tình sử dụng tên không cho phép
      # hoặc một giá trị không hợp lệ cho một hàm toán học đã cho:
      print(err)
      continue

    # In ra kết quả nếu không có lỗi xảy ra
    print(f"Kết quả: {result}")


if __name__ == "__main__":
  main()

Bạn chạy chương trình sẽ hiện ra kết quả thế này:

MathREPL 1.0, trình đánh giá biểu thức toán học Python!
Nhập vào một biểu thức toán học sau dòng nhắc "mr>>".
Soạn "help" rồi Enter để biết thêm thông tin.
Soạn "quit" hoặc "exit" để thoát.

mr>> 

 

Bạn thực hiện một số ví dụ như sau xem sao:

mr>> 9*5
Kết quả: 45
mr>> sqrt(9)
Kết quả: 3.0
mr>> sqrt(-9)
math domain error
mr>> sqrt(81
Cú pháp biểu thức đưa vào không hợp lệ
mr>> 

Phần kết luận

Bạn có thể sử dụng eval() để đánh giá các biểu thức Python từ đầu vào dựa trên chuỗi hoặc dựa trên mã. Hàm tích hợp này có thể hữu ích khi bạn đang cố gắng đánh giá nhanh các biểu thức Python và bạn muốn tránh rắc rối khi tạo trình đánh giá biểu thức của riêng mình từ đầu.

Trong hướng dẫn này, bạn đã tìm hiểu cách eval() hoạt động và cách sử dụng nó một cách an toàn và hiệu quả để đánh giá các biểu thức Python tùy ý.

Bây giờ bạn có thể:

  • Sử dụng Python eval() để đánh giá động các biểu thức Python cơ bản
  • Chạy các câu lệnh phức tạp hơn như lệnh gọi hàm, tạo đối tượng và truy cập thuộc tính bằng cách sử dụng eval()
  • Giảm thiểu rủi ro bảo mật liên quan đến việc sử dụng eval()
» Tiếp: Dự án Python cho người mới bắt đầu: Thông báo giá Bitcoin
« Trước: Xây dựng ứng dụng di động với framework Python Kivy
Các khóa học qua video:
Python SQL Server PHP C# Lập trình C Java HTML5-CSS3-JavaScript
Học trên YouTube <76K/tháng. Đăng ký Hội viên
Viết nhanh hơn - Học tốt hơn
Giải phóng thời gian, khai phóng năng lực
Copied !!!