Có nhiều cách để hiển thị đầu ra của một chương trình; dữ liệu có thể được in ra dưới dạng dễ đọc hoặc ghi vào tệp để sử dụng sau này. Chương này sẽ thảo luận một số khả năng có sẵn.
7.1. Định dạng Đầu ra Nâng cao (Fancier Output Formatting)
Cho đến nay, chúng ta đã thấy hai cách để ghi giá trị: sử dụng câu lệnh biểu thức (expression statements) và hàm print()
. (Một cách thứ ba là sử dụng phương thức write()
của các đối tượng tệp; tệp đầu ra tiêu chuẩn có thể được tham chiếu là sys.stdout
. Xem Thư viện Tham chiếu để biết thêm thông tin về điều này.)
Thông thường, bạn sẽ muốn kiểm soát nhiều hơn về cách định dạng đầu ra của mình thay vì chỉ in các giá trị cách nhau bằng dấu cách. Có một số cách để định dạng đầu ra.
Để sử dụng chuỗi định dạng f (formatted string literals), hãy bắt đầu một chuỗi bằng f
hoặc F
trước dấu nháy mở hoặc dấu nháy ba. Bên trong chuỗi này, bạn có thể viết một biểu thức Python giữa {
và }
có thể tham chiếu đến các biến hoặc giá trị cố định.
>>> year = 2016
>>> event = 'Trưng cầu dân ý'
>>> f'Kết quả của {year} {event}'
'Kết quả của 2016 Trưng cầu dân ý'
Phương thức str.format()
của chuỗi yêu cầu nhiều thao tác thủ công hơn. Bạn vẫn sẽ sử dụng {
và }
để đánh dấu vị trí thay thế giá trị, nhưng bạn cũng cần cung cấp thông tin để định dạng. Ví dụ sau minh họa cách định dạng biến:
>>> yes_votes = 42_572_654
>>> total_votes = 85_705_149
>>> percentage = yes_votes / total_votes
>>> '{:-9} phiếu YES {:2.2%}'.format(yes_votes, percentage)
' 42572654 phiếu YES 49.67%'
Lưu ý cách yes_votes
được lấp đầy bằng dấu cách và có dấu trừ chỉ cho số âm. Ví dụ cũng hiển thị percentage
được nhân với 100, có hai chữ số thập phân và có dấu %
ở cuối.
Cuối cùng, bạn có thể tự thực hiện tất cả các thao tác xử lý chuỗi bằng cách sử dụng cắt chuỗi (string slicing) và nối chuỗi (concatenation) để tạo bố cục tùy chỉnh. Kiểu dữ liệu chuỗi có một số phương thức hữu ích để đệm chuỗi tới một độ rộng cột nhất định.
Khi bạn không cần định dạng đầu ra phức tạp nhưng chỉ muốn hiển thị nhanh một số biến để gỡ lỗi, bạn có thể chuyển đổi bất kỳ giá trị nào thành chuỗi bằng các hàm repr()
hoặc str()
.
Hàm str()
được thiết kế để trả về các biểu diễn của giá trị sao cho dễ đọc đối với con người, trong khi repr()
nhằm tạo ra các biểu diễn có thể được đọc bởi trình thông dịch Python (hoặc sẽ gây lỗi SyntaxError
nếu không có cú pháp tương đương). Với các đối tượng không có biểu diễn cụ thể dành cho con người, str()
sẽ trả về cùng một giá trị như repr()
. Nhiều giá trị, chẳng hạn như số hoặc cấu trúc như danh sách và từ điển, có cùng biểu diễn khi sử dụng cả hai hàm này.
Ví dụ:
>>> s = 'Xin chào, thế giới.'
>>> str(s)
'Xin chào, thế giới.'
>>> repr(s)
"'Xin chào, thế giới.'"
>>> str(1/7)
'0.14285714285714285'
>>> x = 10 * 3.25
>>> y = 200 * 200
>>> s = 'Giá trị của x là ' + repr(x) + ', và y là ' + repr(y) + '...'
>>> print(s)
Giá trị của x là 32.5, và y là 40000...
>>> # Hàm repr() cho chuỗi sẽ thêm dấu nháy và ký tự thoát:
>>> hello = 'xin chào, thế giới\n'
>>> hellos = repr(hello)
>>> print(hellos)
"xin chào, thế giới\n"
>>> # Tham số truyền vào repr() có thể là bất kỳ đối tượng Python nào:
>>> repr((x, y, ('spam', 'eggs')))
"(32.5, 40000, ('spam', 'eggs'))"
Mô-đun string
chứa một lớp Template
, cung cấp một cách khác để thay thế giá trị vào chuỗi bằng cách sử dụng các ký hiệu như $x
và thay thế chúng bằng giá trị từ một từ điển, nhưng cung cấp ít quyền kiểm soát hơn đối với định dạng.
7.1.1. Chuỗi Định Dạng (Formatted String Literals)
Chuỗi định dạng (formatted string literals), hay còn gọi là f-strings, cho phép bạn nhúng giá trị của các biểu thức Python vào trong một chuỗi bằng cách thêm tiền tố f
hoặc F
trước dấu nháy mở và viết biểu thức dưới dạng {biểu_thức}
.
Một bộ định dạng tùy chọn có thể theo sau biểu thức, cho phép kiểm soát tốt hơn cách giá trị được định dạng. Ví dụ sau làm tròn giá trị số pi đến ba chữ số thập phân:
>>> import math
>>> print(f'Giá trị của pi xấp xỉ {math.pi:.3f}.')
Giá trị của pi xấp xỉ 3.142.
Khi truyền một số nguyên sau dấu ':'
, bạn có thể đặt độ rộng tối thiểu cho cột, hữu ích để căn chỉnh các cột trong bảng dữ liệu.
>>> table = {'Sjoerd': 4127, 'Jack': 4098, 'Dcab': 7678}
>>> for name, phone in table.items():
... print(f'{name:10} ==> {phone:10d}')
...
Sjoerd ==> 4127
Jack ==> 4098
Dcab ==> 7678
Các bộ điều chỉnh khác có thể được sử dụng để chuyển đổi giá trị trước khi định dạng. !a
áp dụng ascii()
, !s
áp dụng str()
, và !r
áp dụng repr()
:
>>> animals = 'eels'
>>> print(f'Tàu đệm khí của tôi đầy {animals}.')
Tàu đệm khí của tôi đầy eels.
>>> print(f'Tàu đệm khí của tôi đầy {animals!r}.')
Tàu đệm khí của tôi đầy 'eels'.
Bộ định dạng =
có thể được sử dụng để mở rộng một biểu thức thành văn bản của biểu thức, dấu bằng =
, sau đó là giá trị biểu diễn của biểu thức đã đánh giá:
>>> bugs = 'roaches'
>>> count = 13
>>> area = 'phòng khách'
>>> print(f'Gỡ lỗi {bugs=} {count=} {area=}')
Gỡ lỗi bugs='roaches' count=13 area='phòng khách'
Xem thêm Biểu thức tự mô tả (self-documenting expressions) để biết thêm về bộ định dạng =
. Để tham khảo các quy tắc định dạng này, xem tài liệu hướng dẫn về Ngôn ngữ Định dạng Chuỗi Mini (Format Specification Mini-Language).
7.1.2. Phương thức format()
của Chuỗi (The String format()
Method)
Cách sử dụng cơ bản của phương thức str.format()
như sau:
>>> print('Chúng tôi là {} những người nói "{}!"'.format('hiệp sĩ', 'Ni'))
Chúng tôi là hiệp sĩ những người nói "Ni!"
Dấu ngoặc nhọn {}
và các ký tự bên trong chúng (gọi là trường định dạng – format fields) sẽ được thay thế bằng các đối tượng được truyền vào phương thức str.format()
. Một số trong dấu ngoặc có thể được sử dụng để chỉ định vị trí của đối tượng được truyền vào:
>>> print('{0} và {1}'.format('spam', 'trứng'))
spam và trứng
>>> print('{1} và {0}'.format('spam', 'trứng'))
trứng và spam
Nếu sử dụng các đối số có tên (keyword arguments) trong phương thức str.format()
, giá trị của chúng có thể được tham chiếu bằng tên:
>>> print('Món {food} này {adjective}.'.format(
... food='spam', adjective='hoàn toàn kinh khủng'))
Món spam này hoàn toàn kinh khủng.
Cả đối số vị trí và đối số có tên có thể được kết hợp tùy ý:
>>> print('Câu chuyện về {0}, {1}, và {other}.'.format('Bill', 'Manfred',
... other='Georg'))
Câu chuyện về Bill, Manfred, và Georg.
Nếu bạn có một chuỗi định dạng dài và không muốn chia nhỏ nó, có thể tham chiếu các biến bằng tên thay vì vị trí bằng cách truyền vào một từ điển và sử dụng dấu []
để truy cập khóa:
>>> table = {'Sjoerd': 4127, 'Jack': 4098, 'Dcab': 8637678}
>>> print('Jack: {0[Jack]:d}; Sjoerd: {0[Sjoerd]:d}; '
... 'Dcab: {0[Dcab]:d}'.format(table))
Jack: 4098; Sjoerd: 4127; Dcab: 8637678
Cách này cũng có thể được thực hiện bằng cách truyền từ điển table
dưới dạng đối số có tên với ký hiệu **
:
>>> table = {'Sjoerd': 4127, 'Jack': 4098, 'Dcab': 8637678}
>>> print('Jack: {Jack:d}; Sjoerd: {Sjoerd:d}; Dcab: {Dcab:d}'.format(**table))
Jack: 4098; Sjoerd: 4127; Dcab: 8637678
Cách này đặc biệt hữu ích khi kết hợp với hàm tích hợp vars()
, trả về một từ điển chứa tất cả các biến cục bộ:
>>> table = {k: str(v) for k, v in vars().items()}
>>> message = " ".join([f'{k}: ' + '{' + k +'};' for k in table.keys()])
>>> print(message.format(**table))
__name__: __main__; __doc__: None; __package__: None; __loader__: ...
Ví dụ sau tạo một bảng căn chỉnh gọn gàng, hiển thị các số nguyên và bình phương, lập phương của chúng:
>>> for x in range(1, 11):
... print('{0:2d} {1:3d} {2:4d}'.format(x, x*x, x*x*x))
...
1 1 1
2 4 8
3 9 27
4 16 64
5 25 125
6 36 216
7 49 343
8 64 512
9 81 729
10 100 1000
Để có cái nhìn tổng quan đầy đủ về định dạng chuỗi với str.format()
, hãy xem Cú pháp Định dạng Chuỗi (Format String Syntax).
7.1.3. Định dạng Chuỗi Thủ Công (Manual String Formatting)
Dưới đây là cách trình bày bảng số bình phương và lập phương bằng cách định dạng thủ công:
>>> for x in range(1, 11):
... print(repr(x).rjust(2), repr(x*x).rjust(3), end=' ')
... print(repr(x*x*x).rjust(4))
...
1 1 1
2 4 8
3 9 27
4 16 64
5 25 125
6 36 216
7 49 343
8 64 512
9 81 729
10 100 1000
Lưu ý rằng khoảng cách giữa các cột được thêm vào bởi cách hoạt động của print()
, vốn tự động chèn dấu cách giữa các đối số.
Phương thức str.rjust()
của đối tượng chuỗi giúp căn phải một chuỗi trong một trường có độ rộng cố định bằng cách chèn dấu cách ở bên trái. Ngoài ra, còn có các phương thức tương tự như str.ljust()
(căn trái) và str.center()
(căn giữa). Những phương thức này không ghi trực tiếp vào đầu ra mà chỉ trả về một chuỗi mới. Nếu chuỗi đầu vào quá dài, nó sẽ không bị cắt bớt mà vẫn giữ nguyên.
Một phương thức khác là str.zfill()
, giúp điền số 0 vào bên trái của một chuỗi số. Nó cũng hiểu dấu +
và -
:
>>> '12'.zfill(5)
'00012'
>>> '-3.14'.zfill(7)
'-003.14'
>>> '3.14159265359'.zfill(5)
'3.14159265359'
7.1.4. Định dạng Chuỗi Kiểu Cũ (Old String Formatting)
Toán tử %
(modulo) cũng có thể được sử dụng để định dạng chuỗi. Cú pháp là format % values
, trong đó format
là một chuỗi chứa các ký tự thay thế (%
), và values
là các giá trị cần điền vào.
>>> import math
>>> print('Giá trị của pi xấp xỉ %5.3f.' % math.pi)
Giá trị của pi xấp xỉ 3.142.
Xem thêm chi tiết trong phần Định dạng Chuỗi kiểu printf
(printf-style String Formatting).
7.2. Đọc và Ghi Tệp (Reading and Writing Files)
Hàm open()
trả về một đối tượng tệp và thường được sử dụng với hai đối số vị trí và một đối số từ khóa:
>>> f = open('workfile', 'w', encoding="utf-8")
Đối số đầu tiên là tên tệp. Đối số thứ hai là chuỗi chứa chế độ mở tệp:
'r'
: Chỉ đọc.'w'
: Chỉ ghi (nếu tệp tồn tại, nội dung sẽ bị xóa).'a'
: Ghi tiếp vào cuối tệp.'r+'
: Đọc và ghi.
Nếu không chỉ định mode
, mặc định sẽ là 'r'
.
Mặc định, tệp được mở ở chế độ văn bản (text mode), tức là bạn đọc và ghi chuỗi vào tệp. Nếu không chỉ định encoding
, hệ thống sẽ sử dụng giá trị mặc định của nền tảng. Vì UTF-8 là chuẩn phổ biến, nên nên sử dụng encoding="utf-8"
trừ khi bạn cần một mã hóa khác.
Nếu thêm 'b'
vào chế độ ('rb'
, 'wb'
), tệp sẽ được mở ở chế độ nhị phân (binary mode), đọc và ghi dữ liệu dưới dạng đối tượng bytes
. Khi mở tệp ở chế độ nhị phân, không thể chỉ định encoding
.
Trong chế độ văn bản, khi đọc, Python tự động chuyển đổi kết thúc dòng theo chuẩn hệ thống (\n
trên Unix, \r\n
trên Windows). Khi ghi, nó chuyển đổi \n
về định dạng phù hợp với hệ điều hành. Điều này không nên dùng với tệp nhị phân (JPEG, EXE), vì sẽ gây lỗi.
Để đảm bảo tệp luôn được đóng đúng cách sau khi sử dụng, bạn nên dùng từ khóa with
:
>>> with open('workfile', encoding="utf-8") as f:
... read_data = f.read()
>>> f.closed # Kiểm tra xem tệp đã đóng chưa
True
Nếu không dùng with
, bạn phải gọi f.close()
thủ công:
>>> f = open('workfile', 'r', encoding="utf-8")
>>> data = f.read()
>>> f.close()
Cảnh báo:
Nếu bạn gọi f.write()
mà không đóng tệp (f.close()
), dữ liệu có thể không được ghi đầy đủ xuống đĩa, ngay cả khi chương trình kết thúc thành công.
Sau khi một đối tượng tệp bị đóng (f.close()
), mọi thao tác trên nó sẽ gây lỗi:
>>> f.close()
>>> f.read()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: I/O operation on closed file.
7.2.1. Các Phương Thức của Đối Tượng Tệp (Methods of File Objects)
Các ví dụ còn lại trong phần này sẽ giả định rằng một đối tượng tệp có tên f
đã được tạo.
Đọc nội dung tệp
Để đọc nội dung của một tệp, gọi f.read(size)
, phương thức này đọc một lượng dữ liệu nhất định và trả về nó dưới dạng một chuỗi (trong chế độ văn bản) hoặc một đối tượng bytes
(trong chế độ nhị phân). Tham số size
là tùy chọn.
- Nếu
size
bị bỏ qua hoặc là số âm, toàn bộ nội dung của tệp sẽ được đọc và trả về. Tuy nhiên, nếu tệp lớn hơn bộ nhớ máy, có thể gặp vấn đề về hiệu suất. - Nếu
size
được chỉ định, tối đasize
ký tự (trong chế độ văn bản) hoặcsize
byte (trong chế độ nhị phân) sẽ được đọc. - Nếu đã đến cuối tệp,
f.read()
sẽ trả về một chuỗi rỗng (''
).
Ví dụ:
>>> f.read()
'This is the entire file.\n'
>>> f.read()
''
Đọc từng dòng
Phương thức f.readline()
đọc một dòng duy nhất từ tệp. Ký tự xuống dòng (\n
) được giữ lại ở cuối chuỗi, trừ khi đó là dòng cuối cùng của tệp mà không kết thúc bằng \n
. Điều này giúp phân biệt rõ giữa dòng trống ('\n'
) và khi đã đọc hết tệp (''
).
Ví dụ:
>>> f.readline()
'This is the first line of the file.\n'
>>> f.readline()
'Second line of the file\n'
>>> f.readline()
''
Duyệt qua tệp theo dòng
Để đọc toàn bộ nội dung tệp theo từng dòng, có thể lặp qua đối tượng tệp trực tiếp bằng vòng lặp for
. Cách này hiệu quả về bộ nhớ và dễ đọc:
>>> for line in f:
... print(line, end='')
...
This is the first line of the file.
Second line of the file
Nếu muốn đọc tất cả dòng của tệp vào một danh sách, có thể dùng list(f)
hoặc f.readlines()
.
Ghi vào tệp
Phương thức f.write(string)
ghi nội dung của string
vào tệp, trả về số ký tự đã ghi.
>>> f.write('This is a test\n')
15
Các đối tượng không phải chuỗi cần được chuyển đổi sang chuỗi (trong chế độ văn bản) hoặc đối tượng bytes
(trong chế độ nhị phân) trước khi ghi vào tệp.
Ví dụ:
>>> value = ('the answer', 42)
>>> s = str(value) # Chuyển tuple thành chuỗi
>>> f.write(s)
18
Vị trí trong tệp
Phương thức f.tell()
trả về một số nguyên biểu thị vị trí hiện tại trong tệp. Trong chế độ nhị phân, nó được tính bằng số byte từ đầu tệp. Trong chế độ văn bản, giá trị này không phải lúc nào cũng là số byte mà có thể là một giá trị trừu tượng.
Để thay đổi vị trí đọc/ghi trong tệp, sử dụng f.seek(offset, whence)
, trong đó:
offset
là số byte di chuyển.whence
xác định điểm bắt đầu:0
: Tính từ đầu tệp.1
: Tính từ vị trí hiện tại.2
: Tính từ cuối tệp.- Nếu
whence
bị bỏ qua, mặc định là0
.
Ví dụ:
>>> f = open('workfile', 'rb+')
>>> f.write(b'0123456789abcdef')
16
>>> f.seek(5) # Đến byte thứ 6 trong tệp
5
>>> f.read(1)
b'5'
>>> f.seek(-3, 2) # Đến byte thứ 3 trước cuối tệp
13
>>> f.read(1)
b'd'
Trong tệp văn bản (mở mà không có 'b'
trong chế độ), chỉ có thể sử dụng seek()
với điểm tham chiếu là đầu tệp (whence=0
) hoặc cuối tệp (seek(0, 2)
). Giá trị offset
chỉ có thể là 0
hoặc giá trị trả về từ f.tell()
. Bất kỳ giá trị nào khác sẽ gây ra hành vi không xác định.
Các phương thức bổ sung
Ngoài các phương thức trên, đối tượng tệp còn có một số phương thức ít được sử dụng hơn, chẳng hạn như:
f.isatty()
: Kiểm tra xem tệp có được kết nối với thiết bị đầu cuối không.f.truncate(size)
: Cắt tệp về kích thướcsize
(mặc định là vị trí hiện tại).
Xem Tham chiếu Thư viện Python để biết hướng dẫn chi tiết về các phương thức của đối tượng tệp.
7.2.2. Lưu Trữ Dữ Liệu Có Cấu Trúc bằng JSON (Saving Structured Data with JSON)
Chuỗi có thể dễ dàng được ghi vào và đọc từ một tệp. Đối với số, cần thêm một bước xử lý vì phương thức read()
chỉ trả về chuỗi, và bạn cần truyền nó vào một hàm như int()
để chuyển đổi thành số nguyên, ví dụ: int('123')
trả về giá trị số 123
. Khi bạn muốn lưu trữ các kiểu dữ liệu phức tạp hơn như danh sách lồng nhau và từ điển, việc phân tích cú pháp và tuần tự hóa bằng tay trở nên phức tạp.
Thay vì yêu cầu lập trình viên phải tự viết và gỡ lỗi mã để lưu trữ dữ liệu phức tạp vào tệp, Python cung cấp một cách đơn giản hơn thông qua định dạng trao đổi dữ liệu phổ biến JSON (JavaScript Object Notation). Mô-đun tiêu chuẩn json
có thể chuyển đổi cấu trúc dữ liệu Python thành chuỗi JSON, quá trình này gọi là tuần tự hóa (serializing). Ngược lại, chuyển đổi từ chuỗi JSON thành đối tượng Python gọi là giải tuần tự (deserializing). Trong quá trình tuần tự hóa và giải tuần tự hóa, chuỗi JSON có thể được lưu trữ trong tệp hoặc được gửi qua mạng đến một máy khác.
Ghi chú
Định dạng JSON được sử dụng rộng rãi trong các ứng dụng hiện đại để trao đổi dữ liệu. Nhiều lập trình viên đã quen thuộc với JSON, khiến nó trở thành một lựa chọn tốt cho khả năng tương thích giữa các hệ thống.
Nếu bạn có một đối tượng x
, bạn có thể xem biểu diễn JSON của nó bằng dòng mã đơn giản sau:
>>> import json
>>> x = [1, 'simple', 'list']
>>> json.dumps(x)
'[1, "simple", "list"]'
Một biến thể khác của hàm dumps()
là dump()
, giúp tuần tự hóa đối tượng trực tiếp vào tệp văn bản. Nếu f
là một đối tượng tệp văn bản mở để ghi (open('file.json', 'w')
), ta có thể làm như sau:
json.dump(x, f)
Để giải mã lại đối tượng từ JSON, nếu f
là tệp nhị phân hoặc tệp văn bản mở để đọc (open('file.json', 'r')
):
x = json.load(f)
Ghi chú
Các tệp JSON phải được mã hóa bằng UTF-8. Khi mở tệp JSON để đọc hoặc ghi, hãy sử dụng encoding="utf-8"
để đảm bảo tính nhất quán.
Kỹ thuật tuần tự hóa đơn giản này có thể xử lý danh sách và từ điển, nhưng nếu muốn tuần tự hóa các đối tượng của lớp tùy chỉnh, cần thêm một số bước bổ sung. Tham chiếu tài liệu của mô-đun json
để biết thêm chi tiết.
Xem thêm
- Mô-đun
pickle
– Khác với JSON,pickle
là một giao thức cho phép tuần tự hóa các đối tượng Python phức tạp tùy ý. Do đó, nó chỉ có thể được sử dụng trong Python và không thể giao tiếp với ứng dụng viết bằng ngôn ngữ khác. Hơn nữa,pickle
không an toàn theo mặc định: nếu dữ liệu được tạo ra bởi một kẻ tấn công có kỹ năng, việc giải tuần tự hóapickle
có thể thực thi mã độc hại.