7 Sai Lầm Chí Mạng Khi Tự Học Python - Đừng Làm Những Điều Này
Deep Dive Python I/O: Vượt xa “Hello, World!” và Hiểu về Buffering

Mọi hành trình lập trình đều bắt đầu bằng dòng lệnh kinh điển: print("Hello, World!"). Tuy nhiên, đối với các lập trình viên Python hướng tới sự chuyên nghiệp, việc dừng lại ở mức độ hiểu biết cơ bản này là không đủ. Đằng sau sự đơn giản của hàm print() là cả một hệ thống phức tạp bao gồm các lời gọi hệ thống (System Calls), quản lý bộ đệm (Buffering), mã hóa ký tự (Encoding) và kiến trúc ngăn xếp I/O (I/O Stack) của Python.

Bạn xem thêm:

Bài viết này sẽ phân tích chuyên sâu bài toán xuất dữ liệu ra màn hình, không chỉ để hoàn thành nhiệm vụ “Hello, World!”, mà còn để nắm vững các khái niệm I/O cốt lõi, giúp bạn tối ưu hóa hiệu suất và gỡ lỗi các vấn đề liên quan đến xuất dữ liệu hiệu quả.

1. Phân tích Chuyên sâu Bài toán

Bài toán đặt ra là: Xuất chuỗi ký tự “Hello, World!” ra thiết bị đầu ra tiêu chuẩn (thường là màn hình console hoặc terminal).

Phân loại Bài toán

Đây là bài toán thuộc lĩnh vực Input/Output (I/O) OperationsSystem Interaction. Mục tiêu là giao tiếp giữa chương trình Python (chạy trong user space) và Hệ điều hành (kernel space) để hiển thị dữ liệu. Đây không phải là bài toán yêu cầu thuật toán phức tạp, mà là bài toán về việc sử dụng hiệu quả các API và hiểu rõ cơ chế hoạt động bên dưới.

Phân tích Input/Output và Bối cảnh ban đầu

  • Input: Một đối tượng Python, cụ thể là một chuỗi ký tự (string literal) "Hello, World!". Trong Python 3, đây là chuỗi Unicode.
  • Output: Sự hiển thị của chuỗi ký tự này trên Standard Output (stdout).
  • Bối cảnh: Chương trình Python cần một cơ chế để chuyển đổi đối tượng nội bộ thành một định dạng mà Hệ điều hành có thể hiểu (bytes) và hiển thị.

Phân tích Ràng buộc (Constraints)

Trong các bài toán I/O, các ràng buộc không nằm ở độ phức tạp thời gian (Big O), mà nằm ở các khía cạnh hệ thống:

  1. Độ trễ I/O (I/O Latency) và Hiệu suất: Các hoạt động I/O (ghi đĩa, mạng, console) chậm hơn đáng kể so với các hoạt động tính toán trên CPU. Nếu một ứng dụng thực hiện hàng triệu thao tác I/O riêng lẻ, độ trễ này sẽ trở thành nút thắt cổ chai. Điều này đòi hỏi cơ chế Buffering hiệu quả để giảm thiểu số lần tương tác với Hệ điều hành.
  2. Mã hóa (Encoding): Python 3 sử dụng Unicode. Hệ điều hành cần nhận dữ liệu dưới dạng bytes. Ràng buộc đặt ra là quá trình chuyển đổi từ str sang bytes phải chính xác (thường là UTF-8) và phù hợp với cấu hình encoding của môi trường đích. Nếu không, UnicodeEncodeError có thể xảy ra.
  3. Tính đồng thời (Concurrency): Trong các ứng dụng đa luồng, việc ghi đồng thời vào stdout cần được xử lý an toàn (thread-safe) để tránh dữ liệu bị xen kẽ. Hàm print() của Python đảm bảo tính nguyên tử (atomic) cơ bản cho mỗi lần gọi.

Các Trường hợp biên (Edge Cases) quan trọng

  1. Chuỗi chứa ký tự Unicode (Non-ASCII): print("Xin chào 🌍"). Hệ thống phải xử lý encoding chính xác.
  2. Redirected Stdout (Chuyển hướng Stdout): Khi output được chuyển hướng đến một file (python script.py > output.txt) hoặc pipe (python script.py | grep World). Cơ chế buffering mặc định của Python sẽ thay đổi, có thể dẫn đến việc dữ liệu không xuất hiện ngay lập tức.
  3. Xuất dữ liệu nhị phân (Binary Data): Cố gắng in dữ liệu bytes trực tiếp bằng các phương thức văn bản tiêu chuẩn có thể gây lỗi hoặc hỏng dữ liệu.
  4. Môi trường không có stdout (Headless/Detached): Các tiến trình chạy ngầm (daemons) có thể không được gắn với một terminal hợp lệ. Việc ghi vào stdout có thể gây ra lỗi nếu không được xử lý.

2. Nền tảng Lý thuyết và Công cụ Python

Deep Dive Python I/O: Vượt xa "Hello, World!" và Hiểu về Buffering
Deep Dive Python I/O: Vượt xa “Hello, World!” và Hiểu về Buffering

Để làm chủ việc xuất dữ liệu trong Python, chúng ta cần nắm vững các khái niệm về luồng dữ liệu và kiến trúc I/O của Python.

2.1. Các khái niệm Computer Science cốt lõi

Standard Streams và File Descriptors

Trong các hệ điều hành hiện đại, khi một tiến trình được khởi tạo, nó thường được cấp ba luồng tiêu chuẩn, được định danh bằng các số nguyên gọi là File Descriptors (FD):

  1. STDIN (Standard Input – FD 0): Dữ liệu đầu vào.
  2. STDOUT (Standard Output – FD 1): Dữ liệu đầu ra chính.
  3. STDERR (Standard Error – FD 2): Thông báo lỗi và chẩn đoán.

Khi bạn gọi print(), mặc định bạn đang ghi dữ liệu vào STDOUT (FD 1).

System Calls và sự cần thiết của Buffering

Để thực hiện I/O, chương trình Python phải yêu cầu Hệ điều hành thực hiện thay thông qua các System Calls (ví dụ: lời gọi write() trên Linux). Việc thực hiện System Call tốn kém về mặt hiệu năng vì nó yêu cầu chuyển đổi ngữ cảnh từ User Mode sang Kernel Mode.

Đây là lý do cốt lõi cho sự tồn tại của Buffering. Buffering là kỹ thuật lưu trữ tạm thời dữ liệu trong bộ nhớ. Thay vì thực hiện một System Call cho mỗi dòng được in ra, dữ liệu được tích lũy trong buffer. Khi buffer đầy, hoặc khi một điều kiện cụ thể được thỏa mãn, toàn bộ dữ liệu mới được “xả” (flushed) đến Hệ điều hành thông qua một System Call duy nhất.

Các chế độ Buffering

Có ba chế độ buffering chính:

  1. Unbuffered (Không đệm): Dữ liệu được gửi đi ngay lập tức. Hiệu suất thấp nhất nhưng đảm bảo độ trễ thấp nhất. STDERR thường là unbuffered.
  2. Line-buffered (Đệm theo dòng): Dữ liệu được lưu trữ cho đến khi gặp ký tự xuống dòng (\n) hoặc khi buffer đầy. STDOUT thường là line-buffered khi nó được kết nối với một terminal tương tác (interactive terminal).
  3. Block-buffered (Đệm theo khối) / Fully-buffered: Dữ liệu được lưu trữ cho đến khi buffer đạt đến một kích thước nhất định (ví dụ: 8KB). Đây là chế độ hiệu quả nhất và thường là mặc định khi STDOUT được redirect sang file hoặc pipe.

2.2. Kiến trúc Ngăn xếp I/O của Python (The Python I/O Stack)

Deep Dive Python I/O: Vượt xa "Hello, World!" và Hiểu về Buffering
Deep Dive Python I/O: Vượt xa “Hello, World!” và Hiểu về Buffering

Python cung cấp một lớp trừu tượng hóa mạnh mẽ trên các luồng I/O thông qua module io. Khi bạn truy cập sys.stdout, bạn không làm việc trực tiếp với FD 1. Thay vào đó, bạn đang làm việc với một đối tượng Python phức tạp được xây dựng theo từng lớp:

  1. io.TextIOWrapper (Lớp Văn bản): Đây là lớp trên cùng và là thứ mà hàm print() giao tiếp. Nhiệm vụ chính của nó là xử lý encoding (chuyển đổi str thành bytes), decoding, xử lý ký tự xuống dòng (universal newlines), và quản lý buffering văn bản (bao gồm line-buffering).
  2. io.BufferedWriter / io.BufferedReader (Lớp Nhị phân được đệm): Lớp giữa. Nó nhận bytes từ TextIOWrapper và lưu trữ chúng trong bộ đệm nhị phân để tăng hiệu suất (thực hiện block-buffering).
  3. io.FileIO (Lớp Nhị phân thô): Lớp dưới cùng. Nó giao tiếp trực tiếp với Hệ điều hành thông qua các File Descriptors thô và các System Calls.

Hiểu kiến trúc này là chìa khóa để nắm vững cách dữ liệu di chuyển từ mã Python của bạn ra thế giới bên ngoài.

2.3. Công cụ Python chuyên biệt: Hàm print()

Trong Python 3, print là một hàm mạnh mẽ với các tham số linh hoạt:

print(*objects, sep=' ', end='\n', file=sys.stdout, flush=False)
  • *objects: Các đối tượng cần in. print() sẽ gọi __str__() để chuyển chúng thành chuỗi.
  • sep: Ký tự phân cách (mặc định là khoảng trắng).
  • end: Ký tự kết thúc (mặc định là xuống dòng \n).
  • file: Đích đến của đầu ra. Mặc định là sys.stdout.
  • flush: Nếu là True, buộc xả buffer ngay lập tức. Đây là tham số quan trọng để kiểm soát hành vi buffering.

3. Phát triển Tư duy Thuật toán

7 Sai Lầm Chí Mạng Khi Tự Học Python - Đừng Làm Những Điều Này
Deep Dive Python I/O: Vượt xa “Hello, World!” và Hiểu về Buffering

Đối với một tác vụ đơn giản như “Hello, World!”, chúng ta không tập trung vào tối ưu hóa thuật toán, mà tập trung vào việc so sánh các phương pháp triển khai khác nhau ở các mức độ trừu tượng khác nhau.

3.1. Hướng tiếp cận Cấp thấp (Tương đương Brute Force)

Hướng tiếp cận “thô sơ” nhất là tương tác trực tiếp với Hệ điều hành, bỏ qua ngăn xếp I/O của Python.

Ý tưởng chi tiết: Sử dụng os.write()

Chúng ta sử dụng module os để ghi trực tiếp vào File Descriptor.

import os
# 1 là file descriptor của stdout
# Chúng ta PHẢI truyền bytes, không phải str
os.write(1, b"Hello, World!\n")

Lý do tại sao KHÔNG hiệu quả (trong ngữ cảnh Python)

  1. Phức tạp và Thủ công: Yêu cầu lập trình viên tự quản lý encoding (str sang bytes). Điều này dễ gây lỗi với các ký tự Unicode.
  2. Bỏ qua Buffering của Python: os.write() bỏ qua hệ thống buffering tinh vi của Python. Điều này có thể dẫn đến hiệu suất kém hơn khi thực hiện nhiều thao tác ghi nhỏ lẻ, vì mỗi lần gọi đều có thể dẫn đến một System Call.
  3. Rủi ro về thứ tự: Việc trộn lẫn I/O cấp cao (như print()) và os.write() có thể dẫn đến thứ tự đầu ra không mong muốn, do dữ liệu của print() bị kẹt trong buffer trong khi os.write() đi thẳng đến OS.
  4. Không Pythonic: Đi ngược lại triết lý trừu tượng hóa của Python.

3.2. Tối ưu hóa và Hướng tiếp cận Hiệu quả

Hướng tiếp cận hiệu quả là tận dụng ngăn xếp I/O được tối ưu hóa của Python.

Lựa chọn 1: Sử dụng sys.stdout.write()

Một phương pháp thay thế là ghi trực tiếp vào đối tượng luồng sys.stdout.

import sys
sys.stdout.write("Hello, World!\n")

Phân tích:

Phương pháp này vẫn sử dụng toàn bộ ngăn xếp I/O của Python (bao gồm TextIOWrapper để encoding và buffering). Nó nhanh hơn print() một chút vì nó loại bỏ overhead của việc xử lý các tham số phức tạp (sep, end, chuyển đổi kiểu dữ liệu). Nó cung cấp kiểm soát chi tiết hơn (không tự động thêm \n), nhưng chỉ chấp nhận chuỗi làm đầu vào.

Lựa chọn 2 (Tối ưu và Pythonic): Sử dụng hàm print()

Giải pháp chuẩn mực (Canonical) trong Python để xuất dữ liệu văn bản ra STDOUT là sử dụng hàm print().

print("Hello, World!")

Giải thích LÝ DO lựa chọn:

  1. Tính Đơn giản và Dễ đọc (Readability): Đây là cách diễn đạt rõ ràng và ngắn gọn nhất.
  2. Tự động hóa: Nó tự động xử lý việc chuyển đổi đối tượng sang chuỗi và thêm ký tự kết thúc dòng.
  3. Quản lý I/O Tối ưu: Nó tận dụng io.TextIOWrapper, tự động xử lý encoding và sử dụng cơ chế buffering hiệu quả (tự động chọn line-buffered hoặc block-buffered tùy thuộc vào môi trường).

Trong hầu hết các trường hợp, sự cân bằng giữa hiệu năng và tính dễ đọc của print() khiến nó trở thành lựa chọn tốt nhất.

4. Minh họa Thuật toán Tối ưu (Algorithm Walkthrough)

9 Bước Học Lập Trình Giao Diện Python Với tkinter: Xây Dựng Máy Tính Đơn Giản
Deep Dive Python I/O: Vượt xa “Hello, World!” và Hiểu về Buffering

Để thực sự hiểu sức mạnh và sự phức tạp tiềm ẩn của print(), chúng ta cần xem xét cách hoạt động của buffering trong một kịch bản thực tế.

Ví dụ: Chúng ta muốn in ra một thanh tiến trình, cập nhật trạng thái theo thời gian.

import time

print("Đang xử lý: ", end="")
for i in range(3):
    print(f"[{i+1}/3]", end=" ")
    time.sleep(1)
print("Hoàn tất.")

Chúng ta mong đợi chương trình sẽ in ra từng phần một sau mỗi giây. Tuy nhiên, hành vi thực tế có thể gây ngạc nhiên.

Kịch bản 1: Môi trường Buffered (Phổ biến)

Hãy phân tích luồng thực thi, giả sử sys.stdout đang sử dụng buffering (Line-buffered hoặc Block-buffered).

  • Bước 1: print("Đang xử lý: ", end="") được gọi.
    • Chuỗi “Đang xử lý: ” được ghi vào buffer của sys.stdout (trong TextIOWrapper).
    • end"" (không phải \n).
    • Vì không có \n (nếu là line-buffered) và buffer chưa đầy (nếu là block-buffered), buffer KHÔNG được xả.
    • Trạng thái màn hình: Trống.
    • Trạng thái Buffer: ["Đang xử lý: "]
  • Bước 2 (Lặp i=0): print(f"[1/3]", end=" "). time.sleep(1).
    • Chuỗi “[1/3] ” được ghi vào buffer.
    • Buffer không được xả.
    • Trạng thái màn hình: Trống.
    • Trạng thái Buffer: ["Đang xử lý: ", "[1/3] "]
  • Bước 3, 4 (Lặp i=1, i=2): Tương tự. Chương trình tạm dừng, nhưng không có gì xuất hiện trên màn hình.
  • Bước 5: print("Hoàn tất.") được gọi.
    • Chuỗi “Hoàn tất.” được ghi vào buffer.
    • end mặc định là \n.
    • Việc gặp \n (nếu là line-buffered) hoặc việc chương trình kết thúc (trong mọi trường hợp) sẽ kích hoạt việc xả buffer.
    • System Call được thực hiện với toàn bộ nội dung buffer.
    • Trạng thái màn hình: Đang xử lý: [1/3] [2/3] [3/3] Hoàn tất. (Tất cả xuất hiện cùng một lúc sau 3 giây).

Dữ liệu bị kẹt trong buffer.

Kịch bản 2: Ép buộc xả buffer (Forced Flushing)

Để đạt được hành vi mong muốn (cập nhật theo thời gian thực), chúng ta sử dụng tham số flush=True.

# ...
for i in range(3):
    # Ép buộc xả buffer ngay lập tức
    print(f"[{i+1}/3]", end=" ", flush=True) 
    time.sleep(1)
# ...
  • Bước 2 (Sửa đổi, Lặp i=0): print(f"[1/3]", end=" ", flush=True).
    • Chuỗi “[1/3] ” được ghi vào buffer.
    • flush=True ép buộc TextIOWrapper xả buffer ngay lập tức, đi xuống các lớp BufferedWriterFileIO, dẫn đến System Call.
    • Trạng thái màn hình: Đang xử lý: [1/3]
    • Trạng thái Buffer: []

Minh họa này cho thấy tầm quan trọng sống còn của việc hiểu rõ cơ chế buffering khi làm việc với I/O tương tác trong Python.

5. Phân tích So sánh và Các Hướng Tiếp cận Khác

Xử Lý List & Chuỗi Trong Python - 10 Bài Tập Thực Hành
Deep Dive Python I/O: Vượt xa “Hello, World!” và Hiểu về Buffering

Việc lựa chọn công cụ I/O và cách thức thực hiện ảnh hưởng lớn đến hiệu năng và khả năng bảo trì của ứng dụng.

5.1. Xử lý Dữ liệu Nhị phân (Binary I/O)

Một hạn chế quan trọng của print()sys.stdout.write() là chúng hoạt động ở chế độ văn bản (text mode). Chúng mong đợi chuỗi Unicode và sẽ thực hiện mã hóa.

Nếu bạn cần xuất dữ liệu nhị phân thô (ví dụ: bytes của một hình ảnh) ra stdout, bạn không thể sử dụng chúng trực tiếp. Thay vào đó, bạn phải truy cập vào luồng nhị phân bên dưới thông qua thuộc tính buffer.

import sys
binary_data = b'\x48\x65\x6C\x6C\x6F' # "Hello" in bytes
# sys.stdout.buffer.write(binary_data)

sys.stdout.buffer cho phép truy cập vào lớp io.BufferedWriter bên dưới TextIOWrapper, bỏ qua quá trình mã hóa văn bản. Đây là kỹ thuật bắt buộc khi làm việc với dữ liệu nhị phân.

5.2. Đánh đổi: print() vs logging

Trong các ứng dụng chuyên nghiệp, việc sử dụng print() để xuất thông tin trạng thái hoặc gỡ lỗi là một anti-pattern. Module logging tiêu chuẩn của Python cung cấp một giải pháp mạnh mẽ hơn nhiều.

Tính năng print() logging
Mục đích Xuất kết quả chính (Output) Ghi lại sự kiện, chẩn đoán (Log)
Đích đến mặc định sys.stdout sys.stderr (thường là vậy)
Mức độ ưu tiên Không có DEBUG, INFO, WARNING, ERROR…
Thông tin ngữ cảnh Không có Timestamp, Tên module, Số dòng…
Cấu hình đích đến Thủ công (tham số file) Linh hoạt (File, Syslog, Email…)

Trade-offs: logging có overhead hiệu suất lớn hơn print(). Tuy nhiên, lợi ích về khả năng bảo trì và chẩn đoán trong các ứng dụng lớn là vượt trội. Sử dụng print() cho kết quả đầu ra chính của chương trình và logging cho mọi thứ khác.

5.3. Tối ưu hóa Hiệu suất I/O trong Vòng lặp

Khi cần xuất một lượng lớn dữ liệu, hiệu suất của I/O trở nên quan trọng. Đây là sự đánh đổi giữa Thời gian và Không gian (Space-Time Trade-off).

Vấn đề (Tối ưu Không gian, Kém về Thời gian):

# Chậm do overhead của I/O và system calls lặp đi lặp lại
for item in large_dataset:
    print(item)

Mỗi lần gọi print() đều phải đi qua ngăn xếp I/O và có thể kích hoạt một system call (do line-buffering).

Giải pháp tối ưu (Đánh đổi Không gian lấy Thời gian):

Xây dựng một danh sách các chuỗi trong bộ nhớ và thực hiện I/O một lần duy nhất.

# Nhanh hơn đáng kể
output_buffer = []
for item in large_dataset:
    # Sử dụng F-strings để định dạng nhanh nhất
    output_buffer.append(f"{item}")

# Thực hiện một system call lớn thay vì nhiều system call nhỏ
print("\n".join(output_buffer))

Phương pháp này giảm thiểu số lượng lời gọi hệ thống và tận dụng tối đa hiệu quả của block-buffering, dẫn đến throughput cao hơn đáng kể.

6. Triển khai Code Python Hoàn chỉnh và Kiểm thử

Tại sao Python là lựa chọn số 1 cho người mới
Deep Dive Python I/O: Vượt xa “Hello, World!” và Hiểu về Buffering

Dưới đây là cách triển khai chuẩn mực cho việc hiển thị lời chào, tuân thủ các tiêu chuẩn mã hóa Python hiện đại (PEP 8, Type Hinting, Docstrings), đồng thời minh họa các kỹ thuật đã thảo luận.

import sys
import io
import time
from typing import Optional, TextIO

def display_greeting(message: str = "Hello, World!", stream: Optional[TextIO] = None) -> None:
    """Displays a greeting message to the specified output stream.

    This function utilizes the built-in print() function for robust and Pythonic
    output handling. It automatically manages line endings, encoding, and buffering
    behavior based on the characteristics of the output stream.

    Args:
        message: The string message to display. Defaults to "Hello, World!".
        stream: The file-like object (TextIO) where the message will be
                written. If None, defaults to sys.stdout.
    """
    # Việc truyền None vào tham số file là hợp lệ, print() sẽ dùng sys.stdout hiện tại.
    try:
        print(message, file=stream)
    except (IOError, ValueError) as e:
        # Xử lý trường hợp stream bị đóng (ValueError) hoặc lỗi I/O khác (ví dụ: BrokenPipeError).
        # Trong thực tế, thường ghi log lỗi này vào stderr.
        print(f"Error writing message to stream: {e}", file=sys.stderr)

def demonstrate_buffering_with_flush() -> None:
    """Demonstrates the effect of I/O buffering and how to control it with flush=True."""
    print("\n--- Test Case 6: Demonstrating Buffering and Flush ---")
    print("Processing (updates should appear immediately due to flush=True): ", end="", flush=True)
    
    for i in range(3):
        # In a real application, time.sleep simulates work being done.
        # flush=True ensures the update is visible immediately, overriding buffering.
        print(f"[{i+1}/3]", end=" ", flush=True)
        # Sử dụng một khoảng thời gian nhỏ để minh họa
        time.sleep(0.1) 
    print("Done.")

# Khối kiểm thử và minh họa cách sử dụng
if __name__ == "__main__":

    print("--- Running Test Cases ---")

    # 1. Trường hợp tiêu chuẩn: In ra stdout
    print("\nTest Case 1: Standard Output (Console)")
    display_greeting()

    # 2. Trường hợp biên: Ký tự Unicode
    print("\nTest Case 2: Unicode Characters")
    # Điều này kiểm tra khả năng xử lý encoding của môi trường.
    display_greeting(message="Xin chào thế giới! 🌍")

    # 3. Trường hợp ghi vào stderr
    print("\nTest Case 3: Writing to stderr")
    # Sử dụng stderr cho thông báo chẩn đoán.
    # Chúng ta phải chỉ định sys.stderr ở đây vì mặc định của hàm là None (trỏ tới stdout)
    display_greeting(message="Thông báo chẩn đoán (stderr).", stream=sys.stderr)

    # 4. Kiểm thử tự động với Redirection (Sử dụng io.StringIO)
    # Đây là kỹ thuật quan trọng để viết unit test cho các hàm có I/O.
    print("\nTest Case 4: Automated Redirection Test (In-memory buffer)")
    
    # Tạo một stream giả lập trong bộ nhớ
    mock_stream = io.StringIO()
    test_message = "Testing redirection."
    
    # Gọi hàm và ghi vào stream giả lập
    display_greeting(message=test_message, stream=mock_stream)

    # Lấy giá trị đã được ghi vào buffer
    # .strip() để loại bỏ ký tự xuống dòng mặc định do print() thêm vào để so sánh
    output_value = mock_stream.getvalue().strip() 

    assert output_value == test_message
    print(f"Captured output: '{output_value}'")
    print("Assertion passed.")

    # 5. Trường hợp biên: Stream bị đóng
    print("\nTest Case 5: Closed Stream (Edge Case)")
    closed_stream = io.StringIO()
    closed_stream.close() # Đóng stream trước khi ghi
    print("Attempting to write to a closed stream (Expect error on stderr):")
    # Hàm sẽ bắt lỗi ValueError và in ra thông báo lỗi trên stderr
    display_greeting(message="This should fail.", stream=closed_stream)
    
    # 6. Minh họa Buffering
    demonstrate_buffering_with_flush()
    print("\n--- All Test Cases Completed ---")

Các khóa học liên quan:



Để lại một bình luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *