Đối số vị trí được ánh xạ tuần tự từ trái sang, nên a nhận 1, b nhận 3. b có giá trị mặc định là 2, nhưng vì đã có giá trị được truyền nên giá trị đó được ưu tiên. 4 và 5 không được ánh xạ hết sẽ được gom vào args (dấu hoa thị đơn) dưới dạng tuple, thành (4, 5). Vì không truyền đối số từ khóa nào cả nên kwargs là từ điển rỗng {}, không phải None. Nếu nghĩ b vẫn giữ giá trị mặc định 2 thì sẽ ra (1, 2, (3, 4, 5), {}), nhưng đối số vị trí luôn được ưu tiên hơn giá trị mặc định. Vì thứ nằm trong args là tuple chứ không phải danh sách nên cũng không hiển thị bằng dấu ngoặc vuông.
Câu 2 | Chỉ định từ khóa
Về lệnh gọi sau, truyền một tên không có trong định nghĩa bằng từ khóa, nội dung nào được hiển thị?
Vì chỉ có 1 đối số vị trí nên a nhận 1, còn b không được truyền nên vẫn giữ giá trị mặc định là 2. Vì tham số hình thức có tên x không tồn tại trong định nghĩa, nên đối số từ khóa x=9 được đưa vào kwargs (dấu hoa thị đôi) dưới dạng {'x': 9}. Vì không còn dư đối số vị trí nào nên args là tuple rỗng (). Nếu nghĩ x=9 sẽ vào b thì sẽ ra (1, 9, (), {}), nhưng đối số từ khóa chỉ vào tham số hình thức có tên trùng khớp. Nếu nghĩ 9 sẽ vào args thì sẽ ra (1, 2, (9,), {}), nhưng giá trị đã được truyền kèm tên thì không bị chuyển sang phần dư đối số vị trí. Nếu nhầm lẫn dấu hoa thị đơn nhận từ điển còn dấu hoa thị đôi nhận tuple thì sẽ trông có vẻ như args chứa {'x': 9} còn kwargs rỗng.
Câu 3 | Danh sách rỗng mặc định
Gọi 3 lần hàm sau, vốn viết giá trị mặc định là một danh sách rỗng. 3 dòng nào được hiển thị?
Hiển thị lần lượt None / None / None từ trên xuống
Hiển thị lần lượt [1] / [1, 2] / [1, 2, 3] từ trên xuống
Hiển thị lần lượt [1] / [2] / [3] từ trên xuống
Hiển thị lần lượt [1] / [1, 2] / [2, 3] từ trên xuống
Đáp ánB. Hiển thị lần lượt [1] / [1, 2] / [1, 2, 3] từ trên xuống
Giá trị mặc định [] chỉ được tạo ra đúng 1 lần khi dòng def được thực thi, và cùng một danh sách đó được lưu lại trong đối tượng hàm. Vì không phải mỗi lần gọi lại tạo ra một danh sách rỗng mới, nên kết quả của append được giữ lại cho lần gọi sau, và cứ thế kéo dài thành [1], [1, 2], [1, 2, 3]. Nếu nghĩ mỗi lần gọi sẽ có một danh sách mới thì sẽ ra [1] / [2] / [3], nhưng đó là hành vi khi đặt mặc định là lst=None rồi tạo lại danh sách bên trong thân hàm. Nếu viết nhầm thân hàm thành return lst.append(x), thì giá trị trả về của append là None sẽ xếp liền 3 lần. Ngoài ra, nếu không tách 3 dòng mà viết gộp trên 1 dòng như print(add(1), add(2), add(3)), vì cả 3 lệnh gọi đều trỏ đến cùng một danh sách, nên [1, 2, 3] sau khi mọi thứ đã được đánh giá xong sẽ xuất hiện liền 3 lần.
Câu 4 | Chia sẻ giá trị mặc định
Sau khi gọi hàm 3 lần, hiển thị thuộc tính của đối tượng hàm. Nội dung nào được hiển thị?
Đối tượng hàm giữ giá trị mặc định trong một thuộc tính tên là __defaults__ dưới dạng tuple. Trong các tham số hình thức, chỉ có lst có giá trị mặc định, nên đó là một tuple có 1 phần tử, hiển thị kèm dấu phẩy ở cuối. Nội dung bên trong chính là danh sách được tạo đúng 1 lần ở dòng def, và kết quả append qua 3 lần gọi vẫn còn nguyên đó, nên trở thành ([1, 2, 3],). Nếu chưa gọi lần nào thì sẽ là ([],), nhưng việc ảnh hưởng của các lần gọi hiện ra ở đây chính là điểm đáng sợ của đối số mặc định khả biến. Bản thân __defaults__ là tuple nên không hiển thị bằng dấu ngoặc vuông, và danh sách bên trong cũng không biến thành tuple.
Câu 5 | Đánh giá giá trị mặc định
Sau khi viết một biến làm giá trị mặc định, biến đó bị ghi đè giá trị khác. 2 dòng nào được hiển thị?
i = 5def h(x=i): return xi = 10print(h())print(h(i))
5 rồi 5
5 rồi 10
10 rồi 10
10 rồi 5
Đáp ánB. 5 rồi 10
Biểu thức giá trị mặc định không được đánh giá lại mỗi lần gọi hàm, mà chỉ được đánh giá đúng 1 lần tại thời điểm dòng def được thực thi. Lúc đó i là 5, nên giá trị 5 được lưu vào đối tượng hàm. Dù sau đó ghi đè i thành 10, giá trị mặc định đã lưu cũng không đổi, nên h() trả về 5. Trong khi đó h(i) đọc i tại thời điểm gọi rồi truyền vào, nên trả về giá trị lúc đó là 10. Nếu nghĩ giá trị mặc định được đánh giá lại mỗi lần gọi thì sẽ ra cả 2 dòng đều là 10; nếu nghĩ giá trị truyền qua đối số cũng bị thay bằng giá trị mặc định thì sẽ ra cả 2 dòng đều là 5. Cái bẫy của đối số mặc định khả biến cũng phát sinh từ chính quy tắc này.
Câu 6 | Thứ tự đối số
Lưu đoạn mã sau vào tệp rồi chạy bằng python3. Điều gì sẽ xảy ra?
def g(a=1, b): return a + bprint(g(2, 3))
Xảy ra SyntaxError tại thời điểm định nghĩa, không có dòng nào được thực thi
Hiển thị 4, thực thi thành công trót lọt cho đến hết
Xảy ra TypeError ngay tại thời điểm gọi, thực thi dừng lại ở đó
Hiển thị 5, thực thi thành công trót lọt cho đến hết
Đáp ánA. Xảy ra SyntaxError tại thời điểm định nghĩa, không có dòng nào được thực thi
Không được phép đặt một tham số hình thức không có giá trị mặc định ngay sau một tham số hình thức có giá trị mặc định. Quy tắc này được kiểm tra ở giai đoạn cú pháp, nên khi chạy tệp sẽ trước tiên xảy ra SyntaxError với thông báo non-default argument follows default argument. Vì việc phân tích cú pháp được thực hiện cho toàn bộ tệp trước khi chạy, nên kể cả dòng print cũng không có dòng nào được thực thi. Đây hoàn toàn khác giai đoạn phát hiện so với TypeError phát sinh do gọi sai. Bản thân g(2, 3) khớp với số lượng tham số hình thức, nên nếu định nghĩa đúng thì lẽ ra sẽ hiển thị 5, nhưng không đi đến được bước đó. Nếu muốn viết đúng thì cần đưa tham số có giá trị mặc định ra sau như def g(b, a=1).
Câu 7 | Giá trị trả về của hàm
Gọi 2 hàm khác nhau về việc có hay không có return theo cách sau. Nội dung nào được hiển thị?
one kết thúc mà không đi qua return lần nào, nên giá trị trả về là None. Không phải là trả về tuple rỗng hay chuỗi rỗng. two xếp 1 và 2 phân cách bằng dấu phẩy sau return, nhưng đây không phải trả về 2 giá trị, mà là trả về 1 tuple duy nhất (1, 2). Do đó type(two()).__name__ là tuple. Nếu nghĩ nhiều giá trị trả về sẽ được gom thành danh sách thì sẽ ra list, nhưng biểu thức xếp bằng dấu phẩy tạo ra tuple. Việc vấp phải khi định gán và dùng giá trị trả về của một hàm chỉ hiển thị, phần lớn nguyên nhân nằm ở None này.
Câu 8 | Gọi hàm với unpack
Về đoạn mã sau, gọi hàm bằng cách đặt dấu hoa thị trước một danh sách, nội dung nào được hiển thị?
Khi đặt một dấu hoa thị trước một danh sách ở phía gọi hàm, nội dung của nó được phân phối từng cái một như các đối số vị trí. Do đó a nhận 1, b nhận 2, còn số 3 dư ra được gom vào args (dấu hoa thị đơn) dưới dạng tuple, thành (3,). Vì chỉ có 1 phần tử nên có dấu phẩy ở cuối. Vì không truyền đối số từ khóa nào nên kwargs là từ điển rỗng {}, không phải None. Nếu quên đặt dấu hoa thị mà viết f(vals), thì bản thân danh sách sẽ vào tham số hình thức đầu tiên, cho ra ([1, 2, 3], 2, (), {}); không có lỗi xảy ra nhưng chỉ kết quả bị thay đổi. Vì thứ vào args là tuple nên cũng không hiển thị bằng dấu ngoặc vuông.
Câu 9 | Cách xử lý chú thích kiểu
Gọi hàm sau với một giá trị khác kiểu đã viết trong chú thích kiểu (type annotation). Điều gì sẽ xảy ra?
Dòng 1 hiện ra chuỗi ký tự, dòng 2 hiện ra <class 'int'>
Xảy ra TypeError ở dòng 1, được thông báo rằng không thể truyền kiểu khác với chú thích
Dòng 1 hiện ra chuỗi ký tự, dòng 2 hiện ra int
Xảy ra ValueError ở dòng 1, được thông báo rằng không thể chuyển đổi sang int
Đáp ánA. Dòng 1 hiện ra chuỗi ký tự, dòng 2 hiện ra <class 'int'>
Chú thích kiểu chỉ được ghi lại trong một từ điển tên là __annotations__ của đối tượng hàm, kiểu không hề được kiểm tra khi thực thi. Dù a được ghi là int vẫn có thể truyền chuỗi, dù b được ghi là str vẫn có thể truyền số nguyên. Dù giá trị trả về được ghi là bool, thực tế trả về chính là chuỗi đã nhận vào. Do đó dòng 1 hiển thị nguyên vẹn chuỗi ký tự đã truyền vào. Cái nằm trong __annotations__['a'] ở dòng 2 là đối tượng kiểu int, nên khi print sẽ hiển thị dưới dạng <class 'int'>. Không phải chỉ hiện ra 3 ký tự int. Khi muốn thực sự kiểm tra kiểu, cần dùng công cụ kiểm tra riêng, hoặc tự kiểm tra trong thân hàm.
Câu 10 | Cách sửa giá trị mặc định
Về đoạn mã sau, viết lại với giá trị mặc định là None. 3 dòng nào được hiển thị?
def add2(x, lst=None): if lst is None: lst = [] lst.append(x) return lstprint(add2(1))print(add2(2))print(add2(3, [9]))
Hiển thị lần lượt [1] / [1, 2] / [9, 3] từ trên xuống
Hiển thị lần lượt [1] / [2] / [9, 3] từ trên xuống
Hiển thị lần lượt [1] / [2] / [3, 9] từ trên xuống
Hiển thị lần lượt [1] / [2] / [3] từ trên xuống
Đáp ánB. Hiển thị lần lượt [1] / [2] / [9, 3] từ trên xuống
Cách làm chuẩn là đặt giá trị mặc định là None, rồi ở đầu thân hàm kiểm tra có phải None hay không để tạo một danh sách rỗng mới. Vì None là giá trị không thể thay đổi, nên dù chỉ được đánh giá 1 lần cũng không xảy ra vấn đề dùng chung. Lần thứ nhất và lần thứ hai bỏ qua lst, mỗi lần đều tạo ra một danh sách rỗng riêng biệt nên trở thành [1] và [2], phần tử của lần gọi trước không còn sót lại. Lần thứ ba phía gọi truyền [9], nên lst is None là sai, số 3 được thêm vào cuối danh sách đã truyền, thành [9, 3]. Nếu vẫn giữ lst=[] như trước khi sửa, thì lần thứ hai khi bỏ qua sẽ còn sót phần tử của lần trước, thành [1, 2]. Nếu bỏ qua danh sách mà phía gọi truyền vào và luôn tạo danh sách mới thì sẽ ra [1] / [2] / [3], làm mất đi lợi ích của cách làm chuẩn này là "khi được truyền thì dùng chính nó". Nếu thay vì append ở cuối mà dùng insert(0, x) để chèn vào đầu, thì lần thứ ba sẽ ra [3, 9].
Câu 11 | Thứ tự tìm kiếm tên
Đâu là thứ tự đúng của LEGB mà Python dùng để tìm tên biến?
Tìm theo thứ tự: hàm đang thực thi, đỉnh mô-đun, hàm bên ngoài, có sẵn (built-in)
Tìm theo thứ tự: hàm đang thực thi, hàm bên ngoài, đỉnh mô-đun, có sẵn (built-in)
Tìm theo thứ tự: đỉnh mô-đun, có sẵn (built-in), hàm đang thực thi, hàm bên ngoài
Tìm theo thứ tự: có sẵn (built-in), đỉnh mô-đun, hàm bên ngoài, hàm đang thực thi
Đáp ánB. Tìm theo thứ tự: hàm đang thực thi, hàm bên ngoài, đỉnh mô-đun, có sẵn (built-in)
LEGB là viết tắt của Local, Enclosing, Global, Built-in, biểu thị việc tìm kiếm từ trong cùng ra ngoài cùng. Trước tiên nhìn vào bên trong hàm đang thực thi, tiếp theo là bên trong hàm bên ngoài bao quanh nó, sau đó là đỉnh của mô-đun, cuối cùng là các hàm có sẵn như len hay print. Việc tìm kiếm kết thúc ngay khi tìm thấy, nên nếu dùng cùng tên ở bên trong thì tên ở bên ngoài sẽ bị che khuất. Nếu đảo thành thứ tự từ ngoài vào trong, thì biến cục bộ tạo trong hàm sẽ thua biến cùng tên ở đỉnh mô-đun, điều này không khớp với hành vi thực tế. Việc nhầm lẫn thứ tự giữa Global và Enclosing cũng khá phổ biến, nhưng hàm bên ngoài nằm ở vị trí trong hơn so với đỉnh mô-đun.
Câu 12 | Gán cục bộ
Về đoạn mã sau, gán vào một tên trùng với tên bên ngoài ở bên trong hàm. 2 dòng nào được hiển thị?
x = 'global'def f(): x = 'local' print(x)f()print(x)
local rồi local
local rồi global
global rồi global
global rồi local
Đáp ánB. local rồi global
Khi gán vào một tên bên trong hàm, tên đó được tạo mới như một biến cục bộ, và biến cùng tên ở đỉnh mô-đun hoàn toàn không bị ảnh hưởng. print(x) bên trong hàm đọc x cục bộ nên hiện ra local, còn print(x) sau khi thoát khỏi hàm đọc x ở đỉnh mô-đun nên hiện ra global. Nếu nghĩ việc gán bên trong hàm ảnh hưởng ra cả bên ngoài thì sẽ ra cả 2 dòng đều là local, nhưng muốn vậy cần khai báo global x ở đầu hàm. Ngược lại, nếu nghĩ việc gán bên trong hàm bị bỏ qua và giá trị bên ngoài được đọc thì sẽ ra cả 2 dòng đều là global, nhưng vì đã gán nên chắc chắn một biến cục bộ đã được tạo ra. Sự bất đối xứng "đọc thì ảnh hưởng ra ngoài được, nhưng ghi thì không" này chính là nền tảng của phạm vi (scope) trong Python.
Câu 13 | nonlocal
Với đoạn mã sau, cố ghi đè biến bên ngoài từ hàm bên trong, nội dung nào được hiển thị?
def outer(): n = 1 def inner(): nonlocal n n = 2 inner() return ndef outer2(): n = 1 def inner(): n = 2 inner() return nprint(outer(), outer2())
1 2
2 2
2 1
1 1
Đáp ánC. 2 1
inner của outer có viết nonlocal n, nên việc gán vào n sẽ ảnh hưởng đến n của hàm bên ngoài liền kề là outer. Sau khi gọi inner, n mà outer trả về là 2. inner của outer2 không có khai báo này, nên n = 2 chỉ tạo mới một biến cục bộ của inner mà thôi, n của outer2 vẫn giữ nguyên là 1. Do đó 2 1 được hiển thị. Nếu nghĩ cả hai đều bị ảnh hưởng thì sẽ ra 2 2, nếu nghĩ cả hai đều không bị ảnh hưởng thì sẽ ra 1 1, nhưng chỉ riêng dòng nonlocal đã tạo ra sự khác biệt này. Ngoài ra, nonlocal không thể dùng nếu hàm bên ngoài không có sẵn biến cùng tên, và cũng không thể trỏ đến biến ở đỉnh mô-đun. Khi muốn ghi đè biến ở đỉnh mô-đun thì dùng global.
Câu 14 | Lỗi chưa gán
Đọc một tên có phép gán ở dòng phía sau, nhưng đọc ở dòng trước đó. Khi thực thi đoạn mã sau, điều gì sẽ xảy ra?
z = 100def bad(): print(z) z = 1bad()
Xảy ra UnboundLocalError, không hiển thị gì và dừng lại
Hiển thị 100, sau đó thực thi thành công trót lọt cho đến hết
Hiển thị None, sau đó thực thi thành công trót lọt cho đến hết
Hiển thị 1, sau đó thực thi thành công trót lọt cho đến hết
Đáp ánA. Xảy ra UnboundLocalError, không hiển thị gì và dừng lại
Python, vào lúc biên dịch thân hàm, đã quyết định trước rằng mọi tên có phép gán bên trong hàm đó đều là biến cục bộ. Việc phán đoán này được thực hiện cho toàn bộ hàm, không liên quan đến thứ tự các dòng. Trong hàm bad này, vì có phép gán z = 1 ở dòng phía sau, nên z được coi là biến cục bộ của hàm này. Do đó tại thời điểm print(z), giá trị chưa được gán nên xảy ra UnboundLocalError. Câu chữ trong thông báo của ngoại lệ này thay đổi tùy phiên bản Python, nhưng tên loại là UnboundLocalError thì không đổi. Dù có z = 100 ở đỉnh mô-đun cũng không được tham chiếu đến. Nếu nghĩ rằng vì nằm trước dòng gán nên biến bên ngoài sẽ được đọc thì sẽ ra 100, nhưng Python không hoạt động như vậy. Cũng không có chuyện biến cục bộ chưa có giá trị lại trở thành None.
Câu 15 | Khai báo global
Về đoạn mã sau, xếp một hàm có khai báo global và một hàm không có. Nội dung nào được hiển thị?
n = 10def f(): global n n = 20 return ndef g(): n = 30 return nprint(f(), g(), n)
20 30 30
20 30 20
20 30 10
10 30 10
Đáp ánB. 20 30 20
Trong f có viết global n, nên n = 20 ghi đè n ở đỉnh mô-đun. Bản thân f trả về 20, và n ở đỉnh mô-đun cũng trở thành 20. Trong g không có khai báo, nên n = 30 chỉ tạo mới một biến cục bộ của g, không ảnh hưởng đến n ở đỉnh mô-đun. g trả về giá trị cục bộ của riêng nó là 30. n mà print cuối cùng đọc là n ở đỉnh mô-đun, và vì đã bị f ghi đè nên là 20. Nếu nghĩ phép gán của g cũng ảnh hưởng ra ngoài thì sẽ ra 20 30 30; nếu nghĩ global không có tác dụng thì sẽ ra 20 30 10. Cũng cần lưu ý khai báo global là chỉ thị "đưa việc gán vào tên này lên đỉnh mô-đun", còn nếu chỉ đọc giá trị thì không cần khai báo.
Câu 16 | Hiệu lực của __all__
Chuẩn bị tệp mymod.py sau, rồi ở một tệp khác viết from mymod import *. Những tên nào được đưa vào phía gọi?
Đáp ánD. Chỉ pub được đưa vào, other và hello thì không
Việc lấy vào bằng dấu hoa thị thông thường sẽ đưa vào tất cả các tên không bắt đầu bằng dấu gạch dưới, nhưng nếu phía mô-đun có một danh sách chuỗi tên là __all__, thì chỉ những tên được liệt kê ở đó mới được chọn. Trong ví dụ này, __all__ chỉ ghi pub nên cả other lẫn hello đều không được đưa vào. other tuy không bắt đầu bằng dấu gạch dưới, nhưng vì đã có __all__ nên danh sách đó được ưu tiên. Việc là hàm cũng không được đối xử đặc biệt, nên hello cũng vẫn không được đưa vào. __all__ chỉ có hiệu lực khi lấy vào bằng dấu hoa thị; cách lấy vào chỉ định tên như from mymod import hello, hay cách viết import mymod rồi gọi mymod.hello(), đều không bị cản trở.
Câu 17 | Giá trị của __name__
Về mymod2.py sau, tổ hợp giá trị __name__ khi chạy trực tiếp bằng python3 mymod2.py và khi import mymod2 từ một tệp khác là gì?
Khi chạy trực tiếp là mymod2, khi import là __main__
Cả khi chạy trực tiếp lẫn khi import đều là __main__
Khi chạy trực tiếp là __main__, khi import là mymod2
Cả khi chạy trực tiếp lẫn khi import đều là mymod2
Đáp ánC. Khi chạy trực tiếp là __main__, khi import là mymod2
Khi chạy trực tiếp tệp đó bằng python3, __name__ sẽ mang giá trị '__main__'. Khi được import từ một tệp khác, __name__ sẽ mang tên tệp đã bỏ phần mở rộng, tức là 'mymod2'. Do đó khi chạy trực tiếp sẽ hiện ra 2 dòng mymod2: __main__ và chạy trực tiếp, còn khi import thì chỉ hiện ra 1 dòng mymod2: mymod2. if __name__ == '__main__': là cách viết chuẩn tận dụng sự khác biệt này để tách riêng "xử lý chỉ muốn chạy khi được chạy trực tiếp". Nếu đảo ngược tổ hợp này, thì xử lý dùng để kiểm tra hoạt động sẽ chạy ngay cả khi bị import, điều này không phù hợp với mục đích của cách viết này. Nếu cả hai trường hợp đều cho ra cùng một giá trị thì ngay từ đầu đã không thể tách riêng được.
Câu 18 | sys.argv
Chạy tệp args.py sau bằng lệnh python3 args.py foo bar, 2 dòng nào được hiển thị?
sys.argv là một danh sách xếp các giá trị được truyền từ dòng lệnh, và ở đầu danh sách chứa chính tên tập lệnh. Trong ví dụ này nó trở thành một danh sách 3 phần tử như ['args.py', 'foo', 'bar'], nên len là 3. sys.argv[1:] cắt lấy từ chỉ số 1 trở đi, thành ['foo', 'bar'] đã loại bỏ tên tập lệnh. Nếu nghĩ số lượng đối số đã truyền chính là độ dài thì sẽ ra 2, nhưng đã quên đếm tên tập lệnh ở đầu. Việc nghĩ sys.argv[1:] là ['args.py', 'foo'] là do đọc nhầm vị trí bắt đầu của slice. Nếu chạy mà không truyền đối số nào thì độ dài là 1, và sys.argv[1:] là một danh sách rỗng.
Câu 19 | Nạp lại mô-đun
Đâu là hành vi đúng khi import cùng một mô-đun hai lần trong một chương trình?
Lần thứ hai thân mô-đun cũng được đọc lại từ đầu và chạy lại lần nữa
Lần thứ hai một đối tượng khác được tạo ra, trạng thái không còn được chia sẻ
Lần thứ hai bộ nhớ đệm được dùng, thân mô-đun không được chạy lại
Lần thứ hai xảy ra ImportError, cùng một tên chỉ có thể dùng một lần
Đáp ánC. Lần thứ hai bộ nhớ đệm được dùng, thân mô-đun không được chạy lại
Mô-đun đã được nạp một lần sẽ được lưu vào bộ nhớ đệm trong một từ điển tên là sys.modules. Lần import thứ hai chỉ đơn giản là gắn lại tên với đúng đối tượng mô-đun đã đăng ký ở đó, nên không xảy ra việc đọc lại tệp hay chạy lại thân mô-đun. Có thể kiểm chứng điều này bằng cách viết một print ở đỉnh mô-đun, thì dù import hai lần cũng chỉ hiện ra một lần. Dù đặt bí danh khác khi import thì đối tượng được trỏ đến vẫn là cùng một cái. Do đó, nếu ghi đè một biến của một mô-đun nào đó, thì ở những nơi khác đang import mô-đun đó cũng sẽ thấy giá trị đã bị ghi đè. Việc import cùng một tên hai lần không gây ra lỗi; khi đang phát triển và muốn đọc lại thân mô-đun thì cần dùng rõ ràng chức năng của importlib.
Câu 20 | Xác định môi trường ảo
Đâu là cách phù hợp để xác định xem Python đang chạy có nằm trong môi trường ảo được tạo bằng venv hay không?
Nếu sys.prefix chưa được định nghĩa, có thể xác định đang ở trong môi trường ảo
Nếu sys.prefix và sys.base_prefix trùng nhau, có thể xác định đang ở trong môi trường ảo
Nếu sys.base_prefix đã được định nghĩa, có thể xác định đang ở trong môi trường ảo
Nếu sys.prefix và sys.base_prefix khác nhau, có thể xác định đang ở trong môi trường ảo
Đáp ánD. Nếu sys.prefix và sys.base_prefix khác nhau, có thể xác định đang ở trong môi trường ảo
sys.prefix chỉ nơi Python đang chạy được cài đặt, còn sys.base_prefix chỉ nơi của Python gốc đã tạo ra nó. Bên ngoài môi trường ảo thì cả hai có cùng giá trị, nhưng bên trong môi trường được tạo bằng python3 -m venv, sys.prefix sẽ trỏ đến thư mục môi trường đã tạo, còn sys.base_prefix vẫn trỏ đến Python gốc, nên hai giá trị khác nhau. Sự khác nhau này chính là manh mối để xác định. Việc trùng nhau lại là đang ở trong môi trường ảo là điều kiện ngược lại. sys.base_prefix luôn luôn được định nghĩa kể cả bên ngoài môi trường ảo, nên không thể dùng việc có định nghĩa hay không để xác định. sys.prefix cũng luôn được định nghĩa. Chuỗi đường dẫn cụ thể hay phiên bản pip khác nhau tùy môi trường, nên chỉ cần nhớ quan hệ này là đủ.
Câu 21 | Giá trị trả về của write
Về đoạn mã sau, nhận giá trị trả về của phương thức ghi (write). Nội dung nào được hiển thị?
with open('/tmp/sample.txt', 'w') as f: n = f.write("one\ntwo\nthree\n")print(n)
Hiển thị None
Hiển thị 3
Hiển thị 14
Hiển thị 11
Đáp ánC. Hiển thị 14
write trả về số ký tự đã ghi làm giá trị trả về. "one\ntwo\nthree\n" có one 3 ký tự, two 3 ký tự, three 5 ký tự, tổng cộng 11 ký tự, cộng thêm 3 ký tự xuống dòng nên tổng là 14 ký tự. Nếu quên đếm ký tự xuống dòng thì sẽ ra 11. Nếu nghĩ số dòng đã ghi được trả về thì sẽ ra 3, nhưng cái mà write đếm chỉ là số ký tự. Việc nghĩ không trả về gì rồi chọn None cũng là lỗi thường gặp, dễ nhầm với các phương thức trả về None như append hay sort, nhưng write trả về một con số. Lưu ý write không tự động thêm ký tự xuống dòng, nên khi muốn phân tách dòng thì phải tự viết vào trong chuỗi.
Câu 22 | readlines
Đọc một tệp có 3 dòng bằng readlines. Nội dung nào được hiển thị?
with open('/tmp/sample.txt', 'w') as f: f.write("one\ntwo\nthree\n")with open('/tmp/sample.txt') as f: print(f.readlines())
Hiển thị ['one\ntwo\nthree\n']
Hiển thị 'one\ntwo\nthree\n'
Hiển thị ['one\n', 'two\n', 'three\n']
Hiển thị ['one', 'two', 'three']
Đáp ánC. Hiển thị ['one\n', 'two\n', 'three\n']
readlines trả về toàn bộ các dòng dưới dạng danh sách, nhưng ký tự xuống dòng ở cuối mỗi phần tử vẫn được giữ nguyên. Do đó kết quả là ['one\n', 'two\n', 'three\n']. Nếu nghĩ ký tự xuống dòng bị loại bỏ thì sẽ ra ['one', 'two', 'three'], nhưng muốn có kết quả đó thì phải áp dụng rstrip cho từng phần tử. Việc trả về dưới dạng một chuỗi duy nhất không tách rời là hàm read, còn readlines trả về một danh sách. Nếu nghĩ rằng ký tự xuống dòng không được xử lý như dấu phân cách mà toàn bộ tệp chỉ được bọc thành 1 phần tử duy nhất thì sẽ ra ['one\ntwo\nthree\n'], nhưng readlines tách phần tử tại mỗi lần xuống dòng. Khi viết for line in f để lấy ra từng dòng một, các dòng được đưa đến cũng có ký tự xuống dòng đính kèm tương tự.
Câu 23 | read lần thứ hai
Đọc liên tiếp 2 lần từ cùng một đối tượng tệp. Nội dung nào được hiển thị?
with open('/tmp/sample.txt') as f: a = f.read() b = f.read()print(len(a), b == '')
Hiển thị 14 False
Hiển thị 14 True
Hiển thị 0 False
Hiển thị 0 True
Đáp ánB. Hiển thị 14 True
Đối tượng tệp ghi nhớ vị trí hiện tại. Lệnh read đầu tiên đọc hết từ đầu đến cuối, nên a chứa 14 ký tự bao gồm cả xuống dòng, và vị trí chuyển đến cuối tệp. Lệnh read lần thứ hai cố đọc từ vị trí cuối tệp nên không còn gì và trả về chuỗi rỗng. Do đó len(a) là 14, còn b == '' là True. Nếu nghĩ lần thứ hai cũng trả về cùng nội dung thì b == '' sẽ là False, nhưng muốn đọc lại cùng nội dung cần dùng f.seek(0) để đưa vị trí về đầu. len(a) bằng 0 xảy ra khi tệp rỗng, hoặc khi vị trí đã ở cuối từ trước. Đây là trường hợp dễ hiểu nhầm rằng việc đọc thất bại vì không hiện ra gì, nhưng nguyên nhân là vị trí đang ở cuối tệp.
Câu 24 | with và close
Kiểm tra thuộc tính closed cả bên trong và bên ngoài khối with. 2 dòng nào được hiển thị?
with open('/tmp/sample.txt') as f: print(f.closed)print(f.closed)
False rồi False
False rồi True
True rồi False
True rồi True
Đáp ánB. False rồi True
Bên trong khối with, tệp vẫn đang mở nên closed là False. Khi thoát khỏi khối thì tệp tự động được đóng lại, nên closed sau đó trở thành True. Do đó False được hiển thị trước, rồi True. Mục đích của việc dùng with là để đảm bảo thao tác đóng này luôn được thực hiện dù có quên viết, ngay cả khi xảy ra ngoại lệ giữa chừng trong khối và thoát ra sớm thì tệp vẫn được đóng. Cũng có thể dùng dấu phẩy để gộp mở nhiều tệp trong cùng một câu lệnh with, và trường hợp đó tất cả đều được đóng theo cùng cách. Cố đọc từ một đối tượng tệp đã đóng sẽ gây ra ValueError, nên không thể đọc lại ở bên ngoài with.
Câu 25 | Chế độ ghi
Mở cùng một tệp 3 lần để ghi rồi đọc ra. Nội dung nào được hiển thị?
p = '/tmp/sample.txt'with open(p, 'w') as f: f.write('AAA')with open(p, 'a') as f: f.write('BBB')with open(p, 'w') as f: f.write('C')with open(p) as f: print(f.read())
Nội dung tệp trở thành AAABBB
Nội dung tệp trở thành AAABBBC
Nội dung tệp trở thành C
Nội dung tệp trở thành CAABBB
Đáp ánC. Nội dung tệp trở thành C
Khi mở bằng 'w', tệp sẽ bị cắt về độ dài 0 tại thời điểm đó bất kể đã có nội dung hay chưa. Theo dõi tuần tự: lần mở 'w' đầu tiên ghi AAA vào, lần tiếp theo mở 'a' là ghi thêm nên trở thành AAABBB. Nhưng ngay khi mở lần thứ ba bằng 'w' thì AAABBB bị xóa sạch, rồi C được ghi vào đó, nên cuối cùng chỉ còn lại C. Nếu nghĩ cả 3 lần đều mở bằng 'a' thì sẽ ra AAABBBC; nếu nghĩ lần ghi cuối cùng bị bỏ qua thì sẽ ra AAABBB. Việc ghi đè từ đầu mà không cắt bớt là hành vi của 'r+', trường hợp đó chỉ ký tự đầu tiên bị thay thế, cho ra dạng CAABBB. Việc cắt bớt xảy ra ngay tại thời điểm mở chứ không phải tại thời điểm ghi, nên nếu chỉ mở bằng 'w' mà không ghi gì thì tệp sẽ trống. Nếu định ghi thêm thì dùng 'a', nếu định tạo lại thì dùng 'w', thể hiện ý định qua chế độ mở.
Câu 26 | Thao tác sau khi đóng
Cố đọc từ một đối tượng tệp sau khi đã close, điều gì sẽ xảy ra?
f = open('/tmp/sample.txt')f.close()print(f.read())
Xảy ra ValueError, được thông báo là thao tác trên tệp đã đóng
Hiển thị chuỗi rỗng, thực thi thành công trót lọt
Tự động mở lại, toàn bộ nội dung tệp được hiển thị
Xảy ra OSError, được thông báo là không thể mở lại tệp
Đáp ánA. Xảy ra ValueError, được thông báo là thao tác trên tệp đã đóng
Nếu gọi read trên một đối tượng tệp đã close, sẽ xảy ra ValueError với thông báo I/O operation on closed file. Vì bản thân đối tượng ghi nhớ rằng nó đã bị đóng, nên dù có cần thiết cũng không tự động mở lại. Cũng không phải trả về chuỗi rỗng với ý nghĩa là không còn gì để đọc, đó là hành vi của read sau khi đã đọc hết đến cuối. Các lỗi như FileNotFoundError khi không tìm thấy tệp thuộc nhóm OSError, nhưng ngoại lệ khi thao tác trên một tệp đã đóng lại khác, đó là ValueError. Nếu dùng câu lệnh with thì về mặt cấu trúc sẽ tránh được việc cố đọc sau khi đã thoát khỏi khối.
Câu 27 | Chỉ đọc
Cố ghi vào một tệp được mở mà không chỉ định chế độ, điều gì sẽ xảy ra?
with open('/tmp/sample.txt', 'w') as f: f.write('data')with open('/tmp/sample.txt') as f: f.write('more')
Xảy ra io.UnsupportedOperation, được thông báo là không thể ghi
Xảy ra ValueError, được thông báo là chỉ định chế độ không đúng
more được thêm vào cuối nội dung, thực thi thành công trót lọt
Nội dung bị thay bằng more, thực thi thành công trót lọt
Đáp ánA. Xảy ra io.UnsupportedOperation, được thông báo là không thể ghi
Nếu bỏ qua đối số thứ hai của open, chế độ sẽ mặc định là chỉ đọc 'r'. Nếu gọi write trên một đối tượng tệp được mở ở chế độ chỉ đọc, sẽ xảy ra io.UnsupportedOperation kèm thông báo not writable. Vì đây là cách viết vẫn hợp lệ về cú pháp, nên chỉ khi chạy mới nhận ra là đã nhầm chế độ. Nếu muốn ghi đè thì dùng 'w', nếu muốn thêm vào cuối thì dùng 'a', nếu muốn vừa đọc vừa ghi thì cần mở lại bằng 'r+'. Ngược lại, nếu gọi read trên một tệp được mở bằng 'w' hay 'a', cũng sẽ nhận cùng io.UnsupportedOperation nhưng với thông báo not readable. Có thể coi chế độ là nơi tuyên bố những gì được phép làm với đối tượng tệp đó.
Câu 28 | seek và tell
Về đoạn mã sau, chỉ đọc 5 ký tự từ đầu rồi kiểm tra vị trí. Nội dung nào được hiển thị?
with open('/tmp/sample.txt') as f: head = f.read(5) pos = f.tell() f.seek(0) print(repr(head), pos, f.tell())
'one\ntwo\nthree\n' 14 0
'one\nt' 5 0
'one\nt' 5 5
'one\nt' 0 0
Đáp ánB. 'one\nt' 5 0
Khi truyền một số cho read, chỉ đọc đúng số ký tự đó. 5 ký tự từ đầu là 3 ký tự của one, 1 ký tự xuống dòng, và t của two, thành 'one\nt'. Vì vị trí tiến lên đúng số đã đọc, nên tell ngay sau đó trả về 5. seek(0) tiếp theo đưa vị trí về đầu, nên tell cuối cùng là 0. Nếu nghĩ read không làm thay đổi vị trí thì tell sẽ ra 0 cả hai lần; nếu nghĩ seek không có hiệu lực thì tell sẽ ra 5 cả hai lần. Nếu nhầm rằng đối số của read là số dòng chứ không phải số ký tự, thì sẽ nhầm là đọc đến dòng thứ 5 và chọn nhầm toàn bộ tệp 'one\ntwo\nthree\n' và 14. Nếu dùng seek(0), có thể đọc lại từ đầu với cùng một đối tượng tệp.
Câu 29 | JSON và tuple
Về đoạn mã sau, chuyển một từ điển có chứa tuple thành JSON rồi đọc lại. 2 dòng nào được hiển thị?
JSON chỉ có mảng (array), không tồn tại kiểu gọi là tuple. Vì vậy tại thời điểm dumps, tuple được chuyển thành mảng, kết quả đầu ra là {"t": [1, 2]}. Khi đọc lại, mảng cũng được khôi phục thành danh sách (list), nên kiểu của back['t'] là list, không quay lại thành tuple. Vì không còn lưu lại thông tin ban đầu là tuple ở bất cứ đâu, nên khi qua lại một vòng thì kiểu bị thay đổi. Nếu muốn xử lý như tuple, cần tự áp dụng tuple() sau khi đọc lại. Trong đầu ra JSON không thể xuất hiện dấu ngoặc tròn, và tuple cũng không bị chuyển thành chuỗi. Vì cùng lý do, nếu dùng số nguyên làm khóa của từ điển thì khi dumps sẽ bị chuyển thành chuỗi, và khi đọc lại cũng không trở lại thành số nguyên.
Câu 30 | Khóa của JSON
Thử chuyển một từ điển có khóa số nguyên và một tập hợp thành JSON, điều gì sẽ xảy ra?
Dòng 1 xảy ra TypeError, dòng 2 không được thực thi
Dòng 1 hiện ra {"1": "a"}, dòng 2 xảy ra TypeError
Dòng 1 hiện ra {"1": "a"}, dòng 2 hiện ra [1, 2]
Đáp ánC. Dòng 1 hiện ra {"1": "a"}, dòng 2 xảy ra TypeError
Khóa của đối tượng JSON bắt buộc phải là chuỗi, nên khi dumps một từ điển có khóa số nguyên, khóa sẽ được chuyển thành chuỗi và xuất ra {"1": "a"}. Đây là xử lý thành công bình thường, không phát sinh ngoại lệ ở bước này. Tuy nhiên cần lưu ý là khi đọc lại thì khóa vẫn ở dạng chuỗi, không trở lại thành số nguyên. Tập hợp ở dòng 2 không có kiểu tương ứng trong JSON, nên khi cố dumps sẽ xảy ra TypeError với thông báo Object of type set is not JSON serializable. Nếu nghĩ tập hợp sẽ được ghi ra như một mảng thì sẽ ra [1, 2], nhưng muốn có kết quả chuyển đổi như vậy thì phải tự áp dụng list() trước khi truyền vào. Đối với các giá trị có chứa tiếng Nhật (hay ký tự ngoài ASCII nói chung), theo mặc định sẽ được xuất ra dưới dạng thoát ký tự (escape), còn nếu truyền ensure_ascii là sai thì sẽ xuất ra nguyên dạng ký tự đó.
Luyện tập: giải các câu hỏi trên trang này
Đây là công cụ luyện tập với câu hỏi ngẫu nhiên (hoạt động khi JavaScript được bật). Bạn vẫn có thể đọc toàn bộ câu hỏi và phần giải thích ở trên.
* Lời giải là thông tin phục vụ việc học. Phạm vi ra đề và chế độ thi có thể thay đổi theo từng năm, vì vậy hãy luôn kiểm tra thông báo chính thức của đơn vị tổ chức thi.
Trang này là bản dịch từ nguyên bản tiếng Nhật. Nếu nội dung bản dịch và nguyên bản khác nhau, bản tiếng Nhật sẽ được ưu tiên. Xem nguyên bản tiếng Nhật