Python: Xóa phần tử trùng lặp khỏi List (Năm cách)
Để xóa phần tử trùng lặp khỏi một list trong Python mà vẫn giữ nguyên thứ tự, hãy tạo một dictionary từ list rồi trích xuất các key của nó thành list mới: list(dict.fromkeys(my_list)).
Trong Python,list là một dãy có thứ tự và có thể chứa các giá trị trùng lặp. Một số ứng dụng Python có thể yêu cầu list chỉ gồm các phần tử duy nhất. Có nhiều kỹ thuật để xóa phần tử trùng lặp khỏi list trong Python.
Giải pháp phù hợp phụ thuộc vào việc ứng dụng có cần giữ nguyên thứ tự các phần tử sau khi loại bỏ trùng lặp hay không. Các giải pháp trong hướng dẫn này cũng khác nhau về độ dễ đọc và độ phức tạp, và một số dựa vào hàm trong các mô-đun bên thứ ba.
Giá trị trùng lặp là gì?
Trước khi trình bày cách xóa khỏi list, sẽ hữu ích nếu làm rõ ý nghĩa của giá trị trùng lặp. Hai đối tượng thường được coi là trùng lặp khi giá trị của chúng bằng nhau. Trong Python, toán tử so sánh bằng == trả về True khi hai đối tượng có cùng giá trị. Các đối tượng thuộc kiểu dữ liệu khác nhau vẫn có thể bằng nhau:
Số thực 10.0 và số nguyên 10 bằng nhau, và số nguyên 1 và Boolean True cũng bằng nhau. Trong hầu hết các ngữ cảnh, những giá trị này được xem là trùng lặp.
Một số cấu trúc dữ liệu Python bắt buộc giá trị là duy nhất. Set là một tập hợp chỉ chứa các phần tử duy nhất:
print({10,10.0})print({10.0,10})print({1,True,1.0})Được hỗ trợ bởi
Đoạn mã này tạo ba set bằng cú pháp ngoặc nhọn {}. Mỗi set chứa các đối tượng bằng nhau. Nếu thêm vào set các đối tượng thuộc kiểu khác nhau nhưng có giá trị bằng nhau, chỉ đối tượng đầu tiên được giữ lại.
Python cũng yêu cầu khóa của dictionary phải là duy nhất:
print({10:"Integer 10",10.0:"Float 10"})print({True:"Boolean True",1:"Integer 1"})Được hỗ trợ bởi
Hai dictionary trong đoạn mã này chứa các khóa bằng nhau. Chỉ khóa đầu tiên được giữ lại. Tuy nhiên, giá trị của khóa đó bị thay thế bằng giá trị được thêm sau cùng ứng với một khóa bằng nhau. Do đó, ở ví dụ đầu, khóa được giữ là số nguyên 10, nhưng giá trị của nó là chuỗi "Float 10". Ở ví dụ thứ hai, khóa True (bằng với 1) được gán giá trị "Integer 1".
Set và dictionary đóng vai trò trong một số giải pháp ở hướng dẫn này vì chúng bắt buộc phần tử hoặc khóa là duy nhất.
Xóa trùng lặp khỏi List bằng Set
Cách đơn giản nhất để xóa phần tử trùng lặp khỏi một list là ép kiểu list sang set rồi chuyển lại về list. Set chỉ có thể chứa giá trị duy nhất. Vì vậy, khi thêm vào set, các phần tử trùng sẽ bị loại bỏ:
names =["James","Bob","James","Mark","Kate","Sarah","Kate"]print(list(set(names)))Được hỗ trợ bởi
Hãy xem các bước cần thiết để xóa trùng lặp khỏi list bằng kỹ thuật này:
Ép list sang set, thao tác này loại bỏ toàn bộ phần tử trùng
Ép set trở lại thành một list mới chỉ chứa các giá trị duy nhất
James và Kate xuất hiện hai lần trong list gốc nhưng chỉ một lần trong list cuối.
Tuy nhiên, set là tập hợp không có thứ tự. Thứ tự phần tử trong set không được duy trì. Hãy để ý các tên trong list cuối không theo cùng thứ tự như trong list ban đầu. Cùng một đoạn mã có thể cho ra thứ tự khác khi chạy lại hoặc khi dùng trình thông dịch Python khác vì không có bảo đảm về thứ tự phần tử trong set.
Giải pháp này lý tưởng khi việc mất thông tin về thứ tự phần tử trong list là chấp nhận được. Nếu cần giữ nguyên thứ tự, ta cần các giải pháp khác.
Xóa trùng lặp khỏi List và giữ nguyên thứ tự
Các khóa của dictionary tương tự như set vì chúng phải là duy nhất. Một dictionary có thể có giá trị trùng lặp nhưng không thể có khóa trùng lặp.
Trước Python 3.6, dictionary không duy trì thứ tự phần tử. Tuy nhiên, như một hệ quả từ thay đổi trong cách cài đặt dictionary ở Python 3.6, dictionary hiện duy trì thứ tự chèn phần tử. Điều này chỉ là chi tiết cài đặt ở Python 3.6 nhưng đã trở thành một phần chính thức của ngôn ngữ ở Python 3.7.
Hàm dict.fromkeys() tạo một dictionary từ một iterable. Các phần tử của iterable trở thành các khóa của dictionary mới:
names =["James","Bob","James","Mark","Kate","Sarah","Kate"]print(dict.fromkeys(names))Được hỗ trợ bởi
Vì khóa trong dictionary là duy nhất, các giá trị trùng lặp bị loại khi tạo dictionary. Nếu dùng Python 3.7 trở lên, thứ tự các khóa được đảm bảo giống với thứ tự các phần tử trong list. Mặc định tất cả khóa có giá trị None. Tuy nhiên, ta không cần các giá trị này vì có thể ép dictionary trở lại list:
names =["James","Bob","James","Mark","Kate","Sarah","Kate"]print(list(dict.fromkeys(names)))Được hỗ trợ bởi
Khi ép dictionary thành list, chỉ các khóa được sử dụng. Kỹ thuật này chuyển list gốc thành một list mới không có trùng lặp và vẫn giữ nguyên thứ tự phần tử ban đầu.
Các lựa chọn khác để xóa trùng lặp khỏi List
Các lựa chọn ở phần đầu hướng dẫn này bao quát hai kịch bản khi cần xóa trùng lặp khỏi list:
Dùng set khi không cần thứ tự phần tử.
Dùng dict.fromkeys() để giữ nguyên thứ tự phần tử.
Trong hầu hết tình huống, hai lựa chọn này có khả năng là giải pháp tốt nhất. Tuy nhiên, vẫn còn những kỹ thuật khác để xóa trùng lặp khỏi list.
Dùng vòng lặp để điền vào list mới
Một cách trực diện là duyệt qua list gốc và thêm phần tử mới vào một list mới:
names =["James","Bob","James","Mark","Kate","Sarah","Kate"]
unique_names =[]for name in names:if name notin unique_names:
unique_names.append(name)print(unique_names)Được hỗ trợ bởi
List mới unique_names được khởi tạo rỗng trước vòng lặp for. Một phần tử được thêm vào list mới nếu nó chưa có trong đó. Phiên bản này giữ nguyên thứ tự của các phần tử trong list gốc vì vòng lặp for duyệt theo thứ tự.
Giải pháp này có thể kém hiệu quả với các list lớn. Hãy xét một list dài gồm các số ngẫu nhiên. Ta có thể so sánh hiệu năng của giải pháp này với phiên bản dùng dict.fromkeys():
import random
import timeit
data =[random.randint(0,100)for _ inrange(100)]
use_fromkeys ="unique_data = list(dict.fromkeys(data))"
use_for_loop ="""unique_data = []
for item in data:
if item not in unique_data:
unique_data.append(item)"""
from_keys_time = timeit.timeit(use_fromkeys,globals=globals())
for_loop_time = timeit.timeit(use_for_loop,globals=globals())print(f"Time for 'dict.fromkeys()' version: {from_keys_time:0.2f}s")print(f"Time for loop version: {for_loop_time:0.2f}s")Được hỗ trợ bởi
Time for 'dict.fromkeys()' version: 2.26s
Time for loop version: 17.88sĐược hỗ trợ bởi
Hàm timeit() trong mô-đun cùng tên đo thời gian thực thi một câu lệnh. Các câu lệnh được truyền cho timeit.timeit() dạng chuỗi. Kết quả cho thấy phiên bản dùng vòng lặp for chậm hơn đáng kể so với dùng dict.fromkeys(). Thời gian thực thi sẽ khác nhau tùy máy và môi trường, nhưng lựa chọn dict.fromkeys() gần như luôn nhanh hơn đáng kể.
Dùng list comprehension với set
Vòng lặp for ở phần trước không thể chuyển trực tiếp thành list comprehension vì câu lệnh if kiểm tra xem phần tử đã được thêm vào list mới hay chưa. Cần một cấu trúc dữ liệu riêng để dùng list comprehension. Có thể dùng set để theo dõi những phần tử đã được thêm vào list:
names =["James","Bob","James","Mark","Kate","Sarah","Kate"]
items =set()
unique_names =[name for name in names ifnot(name in items or items.add(name))]print(unique_names)Được hỗ trợ bởi
Hướng dẫn này trình bày giải pháp này để đầy đủ, nhưng lựa chọn này kém dễ đọc và không mang lại cải thiện hiệu năng so với dùng dict.fromkeys().
Nếu tên đã có trong set items, biểu thức trong ngoặc đơn trong list comprehension đánh giá là True. Biểu thức sau từ khóa or sẽ không được đánh giá khi toán hạng đầu tiên là True vì or rút gọn (short-circuit). Do biểu thức trong ngoặc là True, mệnh đề if là False, và tên đó không được thêm vào unique_names.
Khi tên chưa có trong set items, biểu thức sau từ khóa or được đánh giá và thêm tên vào set. Tuy nhiên, phương thức .add() trả về None, một giá trị falsy. Lúc này mệnh đề if là True, và tên cũng được thêm vào list unique_names.
Dùng thư viện bên thứ ba
Nhiều mô-đun bên thứ ba có công cụ để xóa trùng lặp. Thư viện bên thứ bamore_itertools có hàm unique_everseen(), trả về mộtiterator tạo ra các phần tử duy nhất đồng thời giữ nguyên thứ tự của chúng.
Có thể cài đặt thư viện more_itertools trong terminal bằng pip hoặc các trình quản lý gói khác:
Hàm unique() của NumPy trả về một mảng NumPy gồm các phần tử duy nhất từ đối số của nó. Mảng NumPy này có thể chuyển thành list bằng phương thức .tolist() của NumPy:
import numpy as np
names =["James","Bob","James","Mark","Kate","Sarah","Kate"]
unique_names = np.unique(names).tolist()print(unique_names)Được hỗ trợ bởi
Tuy nhiên, lưu ý rằng unique() của NumPy không giữ nguyên thứ tự phần tử. Thay vào đó, nó trả về các phần tử đã được sắp xếp. Một thư viện bên thứ ba phổ biến khác,Pandas, cũng có hàm unique() hoạt động tương tự nhưng giữ nguyên thứ tự phần tử. Các giải pháp dùng NumPy và Pandas rất phù hợp khi những thư viện này đã được dùng trong codebase và không cần cài đặt, import chỉ để xóa trùng lặp khỏi list.
dict.fromkeys() có tốt hơn dùng Set không?
Trong đa số trường hợp, hai lựa chọn tốt nhất là hai cách được trình bày đầu tiên trong hướng dẫn này:
Ép list sang set, rồi ép set trở lại list.
Tạo dictionary từ list bằng dict.fromkeys() rồi ép trở lại list.
Dùng dictionary sẽ giữ nguyên thứ tự phần tử. Tuy nhiên, dùng set hiệu quả hơn và có thể mang lại cải thiện hiệu năng nếu không cần thứ tự phần tử.
Có thể định lượng mức cải thiện hiệu năng bằng timeit:
import random
import timeit
data =[random.randint(0,100)for _ inrange(100)]
use_fromkeys ="unique_data = list(dict.fromkeys(data))"
use_set ="unique_data = list(set(data))"
from_keys_time = timeit.timeit(use_fromkeys,globals=globals())
set_time = timeit.timeit(use_set,globals=globals())print(f"Time for 'dict.fromkeys()' version: {from_keys_time:0.2f}s")print(f"Time for set version: {set_time:0.2f}s")Được hỗ trợ bởi
Time for 'dict.fromkeys()' version: 2.08s
Time for set version: 1.05sĐược hỗ trợ bởi
Xóa trùng lặp khỏi list bằng set nhanh hơn khoảng gấp đôi so với dùng dictionary.
Kết luận
Python và các thư viện bên thứ ba cung cấp nhiều lựa chọn để xóa phần tử trùng lặp khỏi list.
Bạn có thể tiếp tục học Python với các bài viết sau:
Tôi học Vật lý và Toán ở bậc đại học tại Đại học Malta. Sau đó, tôi chuyển đến London và lấy bằng Tiến sĩ Vật lý tại Imperial College. Tôi nghiên cứu các kỹ thuật quang học mới để chụp ảnh võng mạc người. Hiện nay, tôi tập trung vào việc viết về Python, truyền đạt về Python và giảng dạy Python.
Trong Python, list là một dãy có thứ tự và có thể chứa các giá trị trùng lặp. Một số ứng dụng Python có thể yêu cầu list chỉ gồm các phần tử duy nhất. Có nhiều kỹ thuật để xóa phần tử trùng lặp khỏi list trong Python.
Giải pháp phù hợp phụ thuộc vào việc ứng dụng có cần giữ nguyên thứ tự các phần tử sau khi loại bỏ trùng lặp hay không. Các giải pháp trong hướng dẫn này cũng khác nhau về độ dễ đọc và độ phức tạp, và một số dựa vào hàm trong các mô-đun bên thứ ba.
Giá trị trùng lặp là gì?
Trước khi trình bày cách xóa khỏi list, sẽ hữu ích nếu làm rõ ý nghĩa của giá trị trùng lặp. Hai đối tượng thường được coi là trùng lặp khi giá trị của chúng bằng nhau. Trong Python, toán tử so sánh bằng
==trả vềTruekhi hai đối tượng có cùng giá trị. Các đối tượng thuộc kiểu dữ liệu khác nhau vẫn có thể bằng nhau:Số thực
10.0và số nguyên10bằng nhau, và số nguyên1và BooleanTruecũng bằng nhau. Trong hầu hết các ngữ cảnh, những giá trị này được xem là trùng lặp.Một số cấu trúc dữ liệu Python bắt buộc giá trị là duy nhất. Set là một tập hợp chỉ chứa các phần tử duy nhất:
Đoạn mã này tạo ba set bằng cú pháp ngoặc nhọn
{}. Mỗi set chứa các đối tượng bằng nhau. Nếu thêm vào set các đối tượng thuộc kiểu khác nhau nhưng có giá trị bằng nhau, chỉ đối tượng đầu tiên được giữ lại.Python cũng yêu cầu khóa của dictionary phải là duy nhất:
Hai dictionary trong đoạn mã này chứa các khóa bằng nhau. Chỉ khóa đầu tiên được giữ lại. Tuy nhiên, giá trị của khóa đó bị thay thế bằng giá trị được thêm sau cùng ứng với một khóa bằng nhau. Do đó, ở ví dụ đầu, khóa được giữ là số nguyên
10, nhưng giá trị của nó là chuỗi"Float 10". Ở ví dụ thứ hai, khóaTrue(bằng với1) được gán giá trị"Integer 1".Set và dictionary đóng vai trò trong một số giải pháp ở hướng dẫn này vì chúng bắt buộc phần tử hoặc khóa là duy nhất.
Xóa trùng lặp khỏi List bằng Set
Cách đơn giản nhất để xóa phần tử trùng lặp khỏi một list là ép kiểu list sang set rồi chuyển lại về list. Set chỉ có thể chứa giá trị duy nhất. Vì vậy, khi thêm vào set, các phần tử trùng sẽ bị loại bỏ:
Hãy xem các bước cần thiết để xóa trùng lặp khỏi list bằng kỹ thuật này:
James và Kate xuất hiện hai lần trong list gốc nhưng chỉ một lần trong list cuối.
Tuy nhiên, set là tập hợp không có thứ tự. Thứ tự phần tử trong set không được duy trì. Hãy để ý các tên trong list cuối không theo cùng thứ tự như trong list ban đầu. Cùng một đoạn mã có thể cho ra thứ tự khác khi chạy lại hoặc khi dùng trình thông dịch Python khác vì không có bảo đảm về thứ tự phần tử trong set.
Giải pháp này lý tưởng khi việc mất thông tin về thứ tự phần tử trong list là chấp nhận được. Nếu cần giữ nguyên thứ tự, ta cần các giải pháp khác.
Xóa trùng lặp khỏi List và giữ nguyên thứ tự
Các khóa của dictionary tương tự như set vì chúng phải là duy nhất. Một dictionary có thể có giá trị trùng lặp nhưng không thể có khóa trùng lặp.
Trước Python 3.6, dictionary không duy trì thứ tự phần tử. Tuy nhiên, như một hệ quả từ thay đổi trong cách cài đặt dictionary ở Python 3.6, dictionary hiện duy trì thứ tự chèn phần tử. Điều này chỉ là chi tiết cài đặt ở Python 3.6 nhưng đã trở thành một phần chính thức của ngôn ngữ ở Python 3.7.
Hàm
dict.fromkeys()tạo một dictionary từ một iterable. Các phần tử của iterable trở thành các khóa của dictionary mới:Vì khóa trong dictionary là duy nhất, các giá trị trùng lặp bị loại khi tạo dictionary. Nếu dùng Python 3.7 trở lên, thứ tự các khóa được đảm bảo giống với thứ tự các phần tử trong list. Mặc định tất cả khóa có giá trị
None. Tuy nhiên, ta không cần các giá trị này vì có thể ép dictionary trở lại list:Khi ép dictionary thành list, chỉ các khóa được sử dụng. Kỹ thuật này chuyển list gốc thành một list mới không có trùng lặp và vẫn giữ nguyên thứ tự phần tử ban đầu.
Các lựa chọn khác để xóa trùng lặp khỏi List
Các lựa chọn ở phần đầu hướng dẫn này bao quát hai kịch bản khi cần xóa trùng lặp khỏi list:
dict.fromkeys()để giữ nguyên thứ tự phần tử.Trong hầu hết tình huống, hai lựa chọn này có khả năng là giải pháp tốt nhất. Tuy nhiên, vẫn còn những kỹ thuật khác để xóa trùng lặp khỏi list.
Dùng vòng lặp để điền vào list mới
Một cách trực diện là duyệt qua list gốc và thêm phần tử mới vào một list mới:
List mới
unique_namesđược khởi tạo rỗng trước vòng lặpfor. Một phần tử được thêm vào list mới nếu nó chưa có trong đó. Phiên bản này giữ nguyên thứ tự của các phần tử trong list gốc vì vòng lặpforduyệt theo thứ tự.Giải pháp này có thể kém hiệu quả với các list lớn. Hãy xét một list dài gồm các số ngẫu nhiên. Ta có thể so sánh hiệu năng của giải pháp này với phiên bản dùng
dict.fromkeys():Hàm
timeit()trong mô-đun cùng tên đo thời gian thực thi một câu lệnh. Các câu lệnh được truyền chotimeit.timeit()dạng chuỗi. Kết quả cho thấy phiên bản dùng vòng lặpforchậm hơn đáng kể so với dùngdict.fromkeys(). Thời gian thực thi sẽ khác nhau tùy máy và môi trường, nhưng lựa chọndict.fromkeys()gần như luôn nhanh hơn đáng kể.Dùng list comprehension với set
Vòng lặp
forở phần trước không thể chuyển trực tiếp thành list comprehension vì câu lệnhifkiểm tra xem phần tử đã được thêm vào list mới hay chưa. Cần một cấu trúc dữ liệu riêng để dùng list comprehension. Có thể dùng set để theo dõi những phần tử đã được thêm vào list:Hướng dẫn này trình bày giải pháp này để đầy đủ, nhưng lựa chọn này kém dễ đọc và không mang lại cải thiện hiệu năng so với dùng
dict.fromkeys().Nếu tên đã có trong set
items, biểu thức trong ngoặc đơn trong list comprehension đánh giá làTrue. Biểu thức sau từ khóaorsẽ không được đánh giá khi toán hạng đầu tiên làTruevìorrút gọn (short-circuit). Do biểu thức trong ngoặc làTrue, mệnh đềiflàFalse, và tên đó không được thêm vàounique_names.Khi tên chưa có trong set
items, biểu thức sau từ khóaorđược đánh giá và thêm tên vào set. Tuy nhiên, phương thức.add()trả vềNone, một giá trị falsy. Lúc này mệnh đềiflàTrue, và tên cũng được thêm vào listunique_names.Dùng thư viện bên thứ ba
Nhiều mô-đun bên thứ ba có công cụ để xóa trùng lặp. Thư viện bên thứ ba
more_itertoolscó hàmunique_everseen(), trả về một iterator tạo ra các phần tử duy nhất đồng thời giữ nguyên thứ tự của chúng.Có thể cài đặt thư viện
more_itertoolstrong terminal bằngpiphoặc các trình quản lý gói khác:Giờ đây ta có thể dùng
more_itertools.unique_everseen()để xóa trùng lặp khỏi list:Vì
unique_everseen()trả về một iterator, kết quả của nó được ép thành list.Một thư viện bên thứ ba phổ biến khác là NumPy, cũng cung cấp giải pháp để xóa trùng lặp. Có thể cài NumPy bằng
piphoặc các trình quản lý gói khác:Hàm
unique()của NumPy trả về một mảng NumPy gồm các phần tử duy nhất từ đối số của nó. Mảng NumPy này có thể chuyển thành list bằng phương thức.tolist()của NumPy:Tuy nhiên, lưu ý rằng
unique()của NumPy không giữ nguyên thứ tự phần tử. Thay vào đó, nó trả về các phần tử đã được sắp xếp. Một thư viện bên thứ ba phổ biến khác, Pandas, cũng có hàmunique()hoạt động tương tự nhưng giữ nguyên thứ tự phần tử. Các giải pháp dùng NumPy và Pandas rất phù hợp khi những thư viện này đã được dùng trong codebase và không cần cài đặt, import chỉ để xóa trùng lặp khỏi list.dict.fromkeys()có tốt hơn dùng Set không?Trong đa số trường hợp, hai lựa chọn tốt nhất là hai cách được trình bày đầu tiên trong hướng dẫn này:
dict.fromkeys()rồi ép trở lại list.Dùng dictionary sẽ giữ nguyên thứ tự phần tử. Tuy nhiên, dùng set hiệu quả hơn và có thể mang lại cải thiện hiệu năng nếu không cần thứ tự phần tử.
Có thể định lượng mức cải thiện hiệu năng bằng
timeit:Xóa trùng lặp khỏi list bằng set nhanh hơn khoảng gấp đôi so với dùng dictionary.
Kết luận
Python và các thư viện bên thứ ba cung cấp nhiều lựa chọn để xóa phần tử trùng lặp khỏi list.
Bạn có thể tiếp tục học Python với các bài viết sau: