Tóm tắt buổi trước và nội dung kỳ này
Bài trước chúng ta đã làm quen với khái niệm cơ bản trong Python về Các quy tắc; Các kiểu dữ liệu cơ bản và Các toán tử – từ khóa điều kiện và vòng lặp.
Hôm nay chúng ta sẽ thảo luận nhiều hơn về những thành phần và chức năng có sẵn trong Python được sử dụng thường xuyên. Mặc dù dân data tụi mình sử dụng các thư viện pandas và Numpy với các cấu trúc dữ liệu tương ứng như DataFrame và Numpy array để tận dụng những chức năng tính toán nâng cao khi thao tác với các bộ dữ liệu khủng.
Nhưng, ta sẽ không tránh khỏi việc chạm mặt và thân thiết với các câu trúc dữ liệu cơ bản của Python với các công cụ đi theo tụi nó.
- tuples
- lists
- dictionaries
- sets
Trước hết hãy làm quen với 4 “ông kẹ” này cùng với mình nhé! Sau đó cùng tìm hiểu cách để tự xây dựng một hàm xử lý trong Python. Sau khi đã vững nhưng thao tác “thao túng” tụi nó thì ta sẽ đi vào phần thú vị nhất là cách để làm việc với bộ nhớ máy tính của bạn, cụ thể là các dữ liệu thật tế – Mình bổ sung thêm các phần Cloud vào cho hấp dẫn luôn nè.
Nội dung của chủ đề này quan trọng mình nói khá kỹ vì vậy kéo dài qua tận 2 video, các bạn chịu khó coi qua nhé (mình cũng đánh timeline rồi nên cũng tiện cho các bạn theo dõi hơn)
🔴 Buổi live ngày 6: timeline từ 54:00
🔴 Buổi live ngày 7
Data Structures – Sequences
Trước khi bắt đầu đi sâu vào 4 ông kẹ trên thì một điểm lưu ý nhỏ là
Sequence – Cấu trúc dữ liệu chuỗi, vốn thật ra chính là kiểu dữ liệu của tuples, lists, dictionaries, sets và string (bất ngờ ha – Kiểu dữ liệu của kiểu dữ liệu
strlàsequencesVà sequence type có 2 loại là mutable và immutable – Bài trước mình có đề cập rùi á.
🍀 Tuple
Thật là mình cũng cố tìm hiểu thử chữ Tuple có nghĩa là gì nhưng hầu như cả có thông tin gì về kiến thức rãnh rỗi này.
🔥 Syntax tuple – Cú pháp
Một tuple là một cấu trúc chuỗi bất biến (immutable sequence) nên có có độ dài cố định một khi đã được gán giá trị. Khởi tạo rất đơn giản :
In [2]: tup = (4, 5, 6)
In [3]: tup
Out[3]: (4, 5, 6)Trong nhiều ngữ cảnh, dấu ngoặc tròn có thể bỏ qua mà vẫn đúng cú pháp:
In [4]: tup = 4, 5, 6
In [5]: tup
Out[5]: (4, 5, 6)Mặc dù với tuple ta không cần đóng ngoặc nhưng nếu khai báo các tuple con bên trong một tuple cha, ta buộc phải đóng ngoặc để phân biệt từng tuple con. Ví dụ:
In [10]: nested_tup = (4, 5, 6), (7, 8)
In [11]: nested_tup
Out[11]: ((4, 5, 6), (7, 8))
In [12]: nested_tup[0]
Out[12]: (4, 5, 6)
In [13]: nested_tup[1]
Out[13]: (7, 8)🔥 Convert to tuple – Chuyển đổi
Điều thú vị là bạn có thể convert qua lại giữa các đối tượng sequences với nhau, ở đây là chuyển hết về tuple bằng hàm tuple()
In [6]: tuple([4, 0, 2])
Out[6]: (4, 0, 2)
In [7]: tup = tuple('string')
In [8]: tup
Out[8]: ('s', 't', 'r', 'i', 'n', 'g')Tính năng này hay ở chỗ bạn có thể chuyển mutable objects thành immutable objects
Ý nghĩa
Tương tự với các sequence types khác thì để truy cập một phần tử xác định trong tuple ta dùng dấu [] bao quanh vị trí – index của nó.
In [9]: tup[0]
Out[9]: 's'Có thể bạn chưa biết 0-indexed là đặc điểm kế thừa từ ngôn ngữ C,C++ (đúng hơn là tiền thân của nó BCPL ) và hầu như tất cả ngôn ngữ bậc cao sau này đều kế thừa nguyên lý đó.
Fun fact
🔥 Modify tuple – Sửa đổi
Ngoài list ra thì cho dù các phần tử trong tuple là mutable objects nhưng một khi tuple được tạo thì chúng ta vẫn không thể sửa đổi các phần tử đó
In [14]: tup = tuple(['foo', [1, 2], True])
In [15]: tup[2] = False
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-15-b89d0c4ae599> in <module>
----> 1 tup[2] = False
TypeError: 'tuple' object does not support item assignmentNhưng
In [16]: tup[1].append(3)
In [17]: tup
Out[17]: ('foo', [1, 2, 3], True)
In[18]: tup[1][0] = 2
In[19]: tup
Out[19]: ('foo', [2, 2, 3], True)🔥 Concatenate tuples – Kết hợp
Ta có thể ghép nối các tuples với nhau bằng toán tử +
In [18]: (4, None, 'foo') + (6, 0) + ('bar',)
Out[18]: (4, None, 'foo', 6, 0, 'bar')Hoặc có thể duplicate n lần bằng cách dùng toán tử *
In [19]: ('foo', 'bar') * 4
Out[19]: ('foo', 'bar', 'foo', 'bar', 'foo', 'bar', 'foo', 'bar')Lưu ý là bản thân việc sao chép không phải Python đang copy paste tuple trên ra các vùng nhớ mới mà vốn dĩ chỉ có 1 tuple
Fun fact('foo', 'bar')tham chiếu chính nó và hiển thị lên màn hình
🔥 Unpacking tuples – Tháo gỡ
Ông kẹ tuple này mặc dù không cho người khác chỉnh sửa mình khi biên dịch rồi nhưng lại rất tốt bụng khi cho phép tự do gán từng phần tử cho từng biến. Nói cách khác theo ngôn ngữ toán học là tự do “khai căn ngoặc”.
In [20]: tup = (4, 5, 6)
In [21]: a, b, c = tup
In [22]: b
Out[22]: 5Và thậm chí là các đối tượng sequences của tuple đó
In [23]: tup = 4, 5, (6, 7) #tup = 4, 5, [6, 7]
In [24]: a, b, (c, d) = tup
In [25]: d
Out[25]: 7Nhưng lưu ý là phải gán đủ số lượng biến nhen, không là nó quăng lỗi này vào mặt:
ValueError: too many values to unpack
Trên thực tế thì ta sẽ dùng vòng for để duyệt và gán từng phần tử của tuple
In [32]: seq = [(1, 2, 3), (4, 5, 6), (7, 8, 9)]
In [33]: for a, b, c in seq:
....: print(f'a={a}, b={b}, c={c}')
a=1, b=2, c=3
a=4, b=5, c=6
a=7, b=8, c=9Vậy trong trường hợp ta chỉ quan tâm một số phần tử nhất định và muốn bỏ qua phần còn lại thì sao nhỉ?
In [34]: values = 1, 2, 3, 4, 5
In [35]: a, b, *rest = values
In [36]: a
Out[36]: 1
In [37]: b
Out[37]: 2
In [38]: rest
Out[38]: [3, 4, 5]Ở trên ta dễ dàng thấy wildcard * (mình có đề cập ở phần trước) đặt trước biến rest và nó “ôm” luôn phần còn lại của tuples
Trên thật tế vì ta chẳng quan tâm phần còn lại nên ta sẽ dùng
_để đại diện thay vì đặt một cái tên tường minh nhưrestBest practiseIn [39]: a, b, *_ = values
🔥 Swap variables – Đổi biến
Nói nghe ghê gớm nhưng nó y chang tính chất bắt cầu thui hà
tmp = a
a = b
b = tmpNhưng ta vẫn có thể làm nó phức tạp hơn xíu
In [26]: a, b = 1, 2
In [27]: a
Out[27]: 1
In [28]: b
Out[28]: 2
In [29]: b, a = a, b
In [30]: a
Out[30]: 2
In [31]: b
Out[31]: 1🔥 Tuple methods – Các phương thức của Tuple
Buồn cái là tuple nó không cho ta sửa đổi nên có rất ít phương thức thực thể (instance method). Có lẽ chỉ có duy nhất count là hàm hữu dụng nhất – và nó cũng có mặt bên ông kẹ list
In [40]: a = (1, 2, 2, 2, 3, 4, 2)
In [41]: a.count(2)
Out[41]: 4Ở trên mình có nhắc tới instance method nhưng với Python cơ bản thì ta cũng không cần hiểu sâu lắm. Cơ mà sau này khi học về OOP trong Python thì ta nên biết trong Python có một số loại methods sau:
- Static method
- Instance method
- Class method
- Magic method
🍀 List
Trái ngược với tuple thì list có độ dài thay đổi tùy thuộc vào số lượng biến nó nhận được, cũng tương đồng việc nó có thể được chỉnh sửa.
🔥 Syntax
Khai báo list bằng [] hoặc ta có thể dùng hàm list
In [42]: a_list = [2, 3, 7, None]
In [43]: tup = ("foo", "bar", "baz")
In [44]: b_list = list(tup)
In [45]: b_list
Out[45]: ['foo', 'bar', 'baz']
In [46]: b_list[1] = "peekaboo"
In [47]: b_list
Out[47]: ['foo', 'peekaboo', 'baz']🔥 Tuple vs List
Về mặt ngữ nghĩa thì 2 tên này như nhau (mặc dù tuple không cho sửa đổi) và chúng thường được dùng luân phiên nhau trong nhiều trường hợp
In [48]: gen = range(10)
In [49]: gen
Out[49]: range(0, 10)
In [50]: list(gen)
Out[50]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]🔥 Add elements
Để thêm phần tử mới vào list ta dùng hàm append() , lưu ý là thêm vào cuối nhen
In [51]: b_list.append("dwarf")
In [52]: b_list
Out[52]: ['foo', 'peekaboo', 'baz', 'dwarf']Nhưng nếu bạn muốn thêm vào vị trí cụ thể thì có thể dùng insert()
In [53]: b_list.insert(1, "red")
In [54]: b_list
Out[54]: ['foo', 'red', 'peekaboo', 'baz', 'dwarf']
insertcần tính toán phức tạp hơn so vớiappend, vì khi bạn chèn một phần tử mới vào một chuỗi, Python phải dịch chuyển tất cả các phần tử sau phần tử mới sang bên phải để tạo chỗ cho phần tử mới. Điều này có thể tốn thời gian, đặc biệt nếu chuỗi có kích thước lớn.Nếu bạn cần chèn các phần tử ở cả đầu và cuối của một chuỗi, bạn nên dùng
collections.dequefrom collections import deque # initializing deque de = collections.deque([1, 2, 3]) print("deque: ", de) # using append() to insert element at right end # inserts 4 at the end of deque de.append(4) # printing modified deque print("\nThe deque after appending at right is : ") print(de) # using appendleft() to insert element at left end # inserts 6 at the beginning of deque de.appendleft(6) # printing modified deque print("\nThe deque after appending at left is : ") print(de)
🔥 Remove elements
Trái ngược với insert sẽ là pop vì đều cùng dùng index
In [55]: b_list.pop(2)
Out[55]: 'peekaboo'
In [56]: b_list
Out[56]: ['foo', 'red', 'baz', 'dwarf']Khoan dừng lại khoảng chừng 5s, vậy làm sao xác định index của phần tử? Có lun
list_numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] element = 3 list_numbers.index(element)
Ngoài ra còn hàm remove nếu bạn không biết nó nằm ở vị trí nào
In [57]: b_list.append("foo")
In [58]: b_list
Out[58]: ['foo', 'red', 'baz', 'dwarf', 'foo']
In [59]: b_list.remove("foo")
In [60]: b_list
Out[60]: ['red', 'baz', 'dwarf', 'foo']Nếu bạn chẳng care về performance thì sử dụng append và remove bạn có thể giả lập list như một set (Kiểu cấu trúc dữ liệu set sẽ được đề cập sau)
🔥 in or not in
Cách để kiểm tra nhanh xem có phần tử ta tìm kiếm trong list không
In [61]: "dwarf" in b_list
Out[61]: TrueHoặc muốn xác nhận không có:
In [62]: "dwarf" not in b_list
Out[62]: FalseKiểm tra xem
Fun factlistcó chứa một giá trị hay không chậm hơn nhiều so vớidictionariesvàsetsvì nó phải quét tuyến tính (linear scan) qua các giá trị của danh sách, trong khi có thể kiểm tra các giá trị trong bảng băm của nó.
🎍SPEED TEST:
Okay nói lý thuyết suông thì sao mà tin được phải hem. Thử trải nghiệm liền.
# LIST TEST RUN
# 1 µicrosecond = 10^-6
# 1 nanosecond = 10^-9 second
# Define a function to find a number in a list.
def find_number_in_list(lst, number):
if number in lst:
return True
else:
return False
# Create a long list and a short list to compare the lookup speed.
short_list = list(range(100))
long_list = list(range(10000000))
# Call the function and measure time with timeit.
%timeit find_number_in_list(short_list, 99)
%timeit find_number_in_list(long_list, 9999999)
# DICTIONARY TEST RUN
def find_number_in_dict(dct, number):
if number in dct.keys():
return True
else:
return False
short_dict = {x:x*5 for x in range(1,100)}
long_dict = {x:x*5 for x in range(1,10000000)}
%timeit find_number_in_dict(short_dict, 99)
%timeit find_number_in_dict(short_dict, 9999999)Cho bạn nào thắc mắc hàm timeit là gì thì link đây
Tùy vào cấu hình máy của bạn thì ta thấy rằng:
- Chỉ riêng với việc tìm kiếm trên 100 phần tử thì mình đã cho ra kết quả chạy nhanh hơn 6.6 lần.
- Còn 10,000,000 phần tử thì con số này là gấp 585,714 lần – Khủng khiếp thật.
- Lưu ý là mặc dù Dictionary chạy nhanh hơn nhưng nó cũng cần dùng nhiều bộ nhớ hơn – Đây là bộ nhớ đệm trên CPU của bạn/RAM không phải ổ cứng nhé.
🔥 Concatenate and Combine
Tương tự như tuples, ta có thể ghép các list khác nhau lại bằng toán tử +
In [63]: [4, None, "foo"] + [7, 8, (2, 3)]
Out[63]: [4, None, 'foo', 7, 8, (2, 3)]Nếu hàm append là để thêm phần tử thì hàm extend dùng để thêm một mảng mới =
In [64]: x = [4, None, "foo"]
In [65]: x.extend([7, 8, (2, 3)])
In [66]: x
Out[66]: [4, None, 'foo', 7, 8, (2, 3)]Về mặt performance:
+sẽ tạo một mảng mới và copy các mảng trong biểu thức ghép nối vào mảng mới. Bới vậy nênextendđược ưa dùng hơn nếu ta tạo ra một mảng lớn:NHƯNG liệu có thật sự như vậy?
everything = [] for chunk in list_of_lists: everything.extend(chunk)Let test iteverything = [] for chunk in list_of_lists: everything = everything + chunk
🔥 Sorting
Bạn có thể xắp sếp các phần tử của mảng theo thứ tự tăng hoặc giảm bằng hàm sort mà không cần tạo một đối tượng mới. Mặc định thì sẽ theo chiều ASC (nhỏ tới lớn):
In [67]: a = [7, 2, 5, 1, 3]
In [68]: a.sort()
In [69]: a
Out[69]: [1, 2, 3, 5, 7]Tương tự cho mảng chưa phần tử là chuỗi ký tự (Sắp xếp theo Alpha-Beta):
# a list of strings
programming_languages = ["Python", "Swift","Java", "C++", "Go", "Rust"]
programming_languages.sort()
print(programming_languages)
# ['C++', 'Go', 'Java', 'Python', 'Rust', 'Swift']Vậy nếu muốn đổi chiều thì sao? Ta dùng thêm tham số reverse = True
# a list of numbers
my_numbers = [10, 8, 3, 22, 33, 7, 11, 100, 54]
#sort list in-place in descending order
my_numbers.sort(reverse=True)Hàm sort cũng vô cùng đa zi năng khi có thể nhận thêm tham số để linh hoạt trong cách sắp xếp như:
In [70]: b = ["saw", "small", "He", "foxes", "six"]
In [71]: b.sort(key=len)
In [72]: b
Out[72]: ['He', 'saw', 'six', 'small', 'foxes']Trong ví dụ trên nó nhận biến key=len tương ứng sẽ xắp sếp tập hợp các chuỗi trong list bằng độ dài của chúng
Không chỉ list có hàm sort mà một số cấu trúc dữ liệu khác cũng có, mình sẽ quay lại hàm này sau.
🔥 Slicing
Nhớ hồi học “thái lát” đối tượng kiểu str ở tập trước chứ? Bên list mình cũng có thể “thái lát” được giống vậy bằng ký hiệu (slice notation) – cơ bản thì là dùng toán tử [] bọc lại 2 vị trí start:stop
In [73]: seq = [7, 2, 3, 7, 5, 6, 0, 1]
In [74]: seq[1:5]
Out[74]: [2, 3, 7, 5]Quy tắc để biết nó “chặt” khúc nào là chặt sau vị trí
Mánh để nhớstartvà lấystop - startphần tử.
Ta còn có thể thay thế phần tử của mảng bằng 1 sequence khác
In [75]: seq[3:5] = [6, 3]
In [76]: seq
Out[76]: [7, 2, 3, 6, 3, 6, 0, 1] Ở trên 6,3 sẽ thay thế 7,5 ở mảng cũ tương ứng vị trí [3:5] . Hơi khó hiểu nhỉ, giờ thử test bằng một vài cách khác xem thử xem có gì thú vị:
In [77]: seq[1:7] = [6, 3]
In [78]: seq
Out[78]: [7, 6, 3, 1]▶ Như vậy hiểu đơn giản là nó sẽ remove các phần tử thuộc slicing notation start:stop và thay bằng mảng bên phải.
Bản thân start hay stop đều có thể bỏ qua được, trong trường hợp mặc định thì nó sẽ lấy từ đầu chuỗi cho tới cuối
In [77]: seq[:5]
Out[77]: [7, 2, 3, 6, 3]
In [78]: seq[3:]
Out[78]: [6, 3, 6, 0, 1]Ngoài ra thì ta có thể để số âm vào đại diện cho việc đếm từ đằng sau, tất nhiên mánh để nhớ ở trên cũng ngược lại xíu
# seq = [7, 2, 3, 7, 5, 6, 0, 1]
In [79]: seq[-4:]
Out[79]: [3, 6, 0, 1]
In [80]: seq[-6:-2]
Out[80]: [3, 6, 5, 6]
In [81]: seq[-6:-2]
Out[81]: [3, 7, 5, 6]
In [82]: seq[-6:-2]
Out[82]: []Ngoài ra ta còn có cú pháp [::step] với step là bước nhảy (nhảy step bước và lấy thằng đằng sau)
In [81]: seq[::2]
Out[81]: [7, 3, 5, 0]Có cái mẹo thú vị là dùng
-1để đổi chiều củalisthaytupleIn [82]: seq[::-1] Out[82]: [1, 0, 6, 5, 6, 3, 2, 7]
🍀 Dictionary
Ký hiệu là dict có lẽ là cấu trúc dữ liệu có sẵn quan trọng nhất của Python. Một số ngôn ngữ khác thì dictionaries còn có tên gọi khác là hash maps hay associative array. Một dict lưu tập hợp các cặp key-value, lưu ý là key và value đều được xem là Python objects (everything is object mà).
Mỗi key được liên kết với một value, đây chính là yếu tố nổi bật giúp dict có thể dễ dàng truy xuất, thêm mới, sửa đổi hay xóa những key nhất định được chỉ điểm.
🔥 Syntax
Để tạo dict ta dùng dấu ngoặc nhọn {} và dấu hai chấm : để phân cách key và value.
In [83]: empty_dict = {}
In [84]: d1 = {"a": "some value", "b": [1, 2, 3, 4]}
In [85]: d1
Out[85]: {'a': 'some value', 'b': [1, 2, 3, 4]}🔥 Fetch value by key
Bạn có thể truy cập, thêm hoặc gán phần tử bằng cú pháp tương tự khi truy cập một phẩn tử trong list hay tuple
In [86]: d1[7] = "an integer"
In [87]: d1
Out[87]: {'a': 'some value', 'b': [1, 2, 3, 4], 7: 'an integer'}
In [88]: d1["b"]
Out[88]: [1, 2, 3, 4]Ngoài ra từ khóa in cũng có thể dùng ở đây
In [89]: "b" in d1
Out[89]: True🔥 Delete key-value pair | del vs pop()
Ta có thể dùng từ khóa del hoặc hàm pop để giá trị mong muốn (hàm này thực hiện đồng thời việc trả về giá trị kèm xóa đi key)
Trong ví dụ dưới đây ta sẽ thấy được cách vừa tạo vừa xóa một key dummy tạo ngẫu nhiên bất kỳ:
In [90]: d1[5] = "some value"
In [91]: d1
Out[91]:
{'a': 'some value',
'b': [1, 2, 3, 4],
7: 'an integer',
5: 'some value'}
In [92]: d1["dummy"] = "another value"
In [93]: d1
Out[93]:
{'a': 'some value',
'b': [1, 2, 3, 4],
7: 'an integer',
5: 'some value',
'dummy': 'another value'}
In [94]: del d1[5]
In [95]: d1
Out[95]:
{'a': 'some value',
'b': [1, 2, 3, 4],
7: 'an integer',
'dummy': 'another value'}
In [96]: ret = d1.pop("dummy")
In [97]: ret
Out[97]: 'another value'
In [98]: d1
Out[98]: {'a': 'some value', 'b': [1, 2, 3, 4], 7: 'an integer'}🔥 Iterators for key and value
Ta có 2 phương thức keys() và values() để duyệt qua từng loại tương ứng. Thứ tự của keys phụ thuộc vào thứ tự đúng được thêm vào ban đầu và values cũng dựa trên thứ tự của keys
In [99]: list(d1.keys())
Out[99]: ['a', 'b', 7]
In [100]: list(d1.values())
Out[100]: ['some value', [1, 2, 3, 4], 'an integer']Nếu muốn duyệt qua có đủ cả 2 anh tài này thì ta dùng hàm items()
In [101]: list(d1.items())
Out[101]: [('a', 'some value'), ('b', [1, 2, 3, 4]), (7, 'an integer')]🔥Merge
Để ghép hai dict với nhau ta dùng hàm update()
In [102]: d1.update({"b": "foo", "c": 12})
In [103]: d1
Out[103]: {'a': 'some value', 'b': 'foo', 7: 'an integer', 'c': 12}Note
update()sẽ thay thế bất kỳ khóa đã tồn tại nào bằng khóa mới ngay tại vị trí cũ của nó
🔥 Convert sequences to dict
Gỉa sử ta có 2 sequences và giờ muốn hợp nhất thành 1 dict thì cách đơn giản nhất ta làm như sau:
mapping = {}
for key, value in zip(key_list, value_list):
mapping[key] = valueThật ra về cơ bản thì dict là tập hợp của 2 tuples, nên ta có thể dùng zip() kết hợp với hàm dict() như sau:
In [104]: tuples = zip(range(5), reversed(range(5)))
In [105]: tuples
Out[105]: <zip at 0x17d604d00>
In [106]: mapping = dict(tuples)
In [107]: mapping
Out[107]: {0: 4, 1: 3, 2: 2, 3: 1, 4: 0}Ở phần sau ta sẽ làm quen với dictionary comprehensions
🔥 Default values – elif | setdefault | defaultdict
Ví dụ mình nói câu này bạn sẽ viết một pseudo code như thế nào: “Nếu không có key cần tìm trong dict thì trả về giá trị mặc định, ngược lại thì lấy cặp key-value đó ra”?
Well dummy yêu cầu trên ra tao có thể viết như sau (lưu ý là Pseudocode nên nó chỉ có mục đích mô tả logic lập trinh chứ không đúng cú pháp nhen)
# Pseudocode
if key in some_dict:
value = some_dict[key]
else:
value = default_valueViệc lấy phần tử của dict ta có thể dùng get() hoặc pop() nên ta có thể viết lại hoàn chỉnh như sau:
value = some_dict.get(key, default_value)get() mặc định sẽ trả về None nếu không tìm thấy key trong khi pop() sẽ trả về exception. Bằng cách tùy biến linh hoạt ta có thể tùy chỉnh phần values của dict thành list.
Ví dụ sau có mục tiêu phân nhóm danh sách các từ dựa trên chữ cái đầu tiên thành 1 dict chưa các danh sách phân loại đó:
- B1: duyệt qua từng phần tử trong words
lst - B2: Lấy chữ cái đầu tiên của phần tử chuỗi đó
- B3: Kiểm tra xem có trong
by_letterdictkhông - B4: Không có thì thêm dạng
key_valuevào vớivaluelàlstchứa phần tử đầu tiên làword. Có rồi thì append vào list value củaletterlàkeytương tứng
In [108]: words = ["apple", "bat", "bar", "atom", "book"]
In [109]: by_letter = {}
In [110]: for word in words:
.....: letter = word[0]
.....: if letter not in by_letter:
.....: by_letter[letter] = [word]
.....: else:
.....: by_letter[letter].append(word)
.....:
In [111]: by_letter
Out[111]: {'a': ['apple', 'atom'], 'b': ['bat', 'bar', 'book']}Qua ví dụ trên ta ôn lại một chút về các cú pháp gán, tạo
dictđể tránh tẩu hỏa nhập ma. Vớiby_letter = {}
by_letter['<key>'] = ['abc', 123]: khởi tạo phần tử đầu tiên của dictionary này.by_letter['<key>'].append((3,4,5)): mutuable object nên thêm xóa sửa thậm chí overwrite value của key đều được.- Các hàm lộ diện tới giờ:
delpop().keys().values().items().update()dict(zip())
Ai cũng lười nên là ta có thể viết gọn nhẹ lại bằng hàm setdefault() sau đây:
In [112]: by_letter = {}
In [113]: for word in words:
.....: letter = word[0]
.....: by_letter.setdefault(letter, []).append(word)
.....:
In [114]: by_letter
Out[114]: {'a': ['apple', 'atom'], 'b': ['bat', 'bar', 'book']}Thậm chí ta còn có mô-đun collections cho ta một class hữu ích defaultdict vốn khiến mọi chuyện còn dễ dàng hơn nữa.
Cách dùng hàm này là ta sẽ truyền vào type hoặc function dùng để generate ra giá trị mặc định cho từng slot của dictionary.
In [115]: from collections import defaultdict
In [116]: by_letter = defaultdict(list)
In [117]: for word in words:
.....: by_letter[word[0]].append(word)🔥 immutable key by immutable type
Trong dictionary có 2 thành phần: values vốn là mutable object nhưng ngược lại key lại là immutable objects như các kiểu dữ liệu int, float, stringhoặc tuples (tất nhiên là các phần tử của tuples cũng phải là immutable).
Từ chuyên môn để gọi tính chất của
keylàhashability. Bạn có thể kiểm tra xem một đối tượng cóhashablekhông – Nói cách khác là có thể dùng làm key trong dictionary không bằng hàmhash
In [118]: hash("string")
Out[118]: 4022908869268713487
In [119]: hash((1, 2, (2, 3)))
Out[119]: -9209053662355515447
In [120]: hash((1, 2, [2, 3])) # fails because lists are mutable
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-120-473c35a62c0b> in <module>
----> 1 hash((1, 2, [2, 3])) # fails because lists are mutable
TypeError: unhashable type: 'list'Lưu ý: Giá trị hàm hash() trả về tùy vào phiên bản Python mà có thể khác nhau giữa máy tính các bạn và mình.
Lỡ mà bạn có một danh sách muốn làm key (đôi khi trong thực tế bạn có một composite key đặc tả bằng list) thì muốn tạo nó làm key cho dictionary thì dễ ẹc, hãy dùng hàm
tuple()In [121]: d = {} In [122]: d[tuple([1, 2, 3])] = 5 In [123]: d Out[123]: {(1, 2, 3): 5}
🍀Set
Set là một tập hợp các phần tử độc nhất và tự sắp xếp theo thứ tự.
🔥 Syntax
Có hai cách để tạo set:
- Dùng hàm
set() - Dùng dấu ngoặc nhọn
{}
In [124]: set([2, 2, 2, 1, 3, 3])
Out[124]: {1, 2, 3}
In [125]: {2, 2, 2, 1, 3, 3}
Out[125]: {1, 2, 3}🔥 Operations – Các phép tính cơ bản
Set hỗ trợ các phép toán học cơ bản như union (hợp); intersection (giao); difference (hiệu) ; symmetric difference = complement (bù)
In [126]: a = {1, 2, 3, 4, 5}
In [127]: b = {3, 4, 5, 6, 7, 8}🎯 Khi Union lại thì hàm union() hoặc toán tử nhị phân | tự động distinct – chống trùng lặp, cho ta
In [128]: a.union(b)
Out[128]: {1, 2, 3, 4, 5, 6, 7, 8}
In [129]: a | b
Out[129]: {1, 2, 3, 4, 5, 6, 7, 8}🎯 intersection() là hàm lấy phần giao giữa hai sets. Ta có thể dùng toán tử & để viết gọn hơn
In [130]: a.intersection(b)
Out[130]: {3, 4, 5}
In [131]: a & b
Out[131]: {3, 4, 5}Dưới đây là danh sách các phương thức dùng phổ biến của set

Nếu truyền tham số không phải là
setvàounion()hayintersection()thì nó sẽ tự động “đồng hóa” tụi nó vềsettrước khi thực hiện phép toán.Còn trong các trường hợp khác khi sử dụng toán tử nhị phân thì tụi nó bắt buộc phải là
setstrước.
🔥 Hàm copy()
Thật ra thì cũng tương tự như lst thì set cũng có phương thức copy để tạo ra 1 shallow copy – Ý chỉ bản copy độc lập không ảnh hưởng tới bản gốc/không phải copy địa chỉ.
Khi ta thực hiện tính chất bắt cầu như sau
In [132]: c = a.copy()
In [133]: c |= b
In [134]: c
Out[134]: {1, 2, 3, 4, 5, 6, 7, 8}
# OR
In [135]: d = a.copy()
In [136]: d &= b
In [137]: d
Out[137]: {3, 4, 5}Thì ta có một thuật ngữ mổ tả set operations này là in-place counterparts
Ý nghĩa rất lớn khi ta làm việc với bộ dữ liệu lớn vì ta có thể tạo ra một “thế nhân” mà không ảnh hưởng tới “bản thể gốc”. Có thể dùng hàm
id()để kiểm tra địa chỉ bộ nhớ nhenMột bài viết khá hay các bạn có thể coi qua: link
🔥 Immutable object
set là immutable object – Dễ nhận ra vì nó không cung cấp bất kì hàm nào thêm phần tử mới hay sửa đổi phần tử tại index cụ thể!
Chính vì vậy các phần tử của set phải là immutable object luôn (giống như dict key, lưu ý là key nha vì dict value lại là mutable object) và bản thân tụi nó hashable (dùng hàm hash không quăng ra exception).
In [138]: my_data = [1, 2, 3, 4]
In [139]: my_set = {tuple(my_data)}
In [140]: my_set
Out[140]: {(1, 2, 3, 4)}Ví dụ trên cho ta lưu lst vào set bằng cách convert nó sang tuples
🔥 subset or superset
Bạn có thể kiểm tra xem 1 set có phải là subset (chứa trong set kiểm tra) hay superset (chứa tất cả phần tử của set được kiểm tra) bằng cách
In [141]: a_set = {1, 2, 3, 4, 5}
In [142]: {1, 2, 3}.issubset(a_set)
Out[142]: True
In [143]: a_set.issuperset({1, 2, 3})
Out[143]: True🔥 Sets are equal when?
Chỉ khi cả hai có cùng phần tử (không cần theo thứ tự) thì mới giống nhau
In [144]: {1, 2, 3} == {3, 2, 1}
Out[144]: True🍀Built-In Sequence Functions
Những hàm sau đây dùng khá thường xuyên khi bạn làm việc với các cấu trúc sequences như trên, vậy nên hay tận dụng mỗi khi có thể nhen
🔥 emumerate
Ví dụ ta có yêu cầu vừa duyệt qua một sequence vừa muốn lưu lại index khi duyệt từng phần tử thì ta làm sao? Cơ bản nhất ta sẽ:
index = 0
for value in collection:
# do something with value
index += 1Nhưng hãy thử cách sau:
for index, value in enumerate(collection):
# do something with valueenumerate() trả về dạng tuple (i, value)
🔥 sorted
Hàm này vô cùng tiện dụng, cái tên nói lên tất cả và bất cứ sequences nào cũng có sẵn hàm này
In [145]: sorted([7, 1, 2, 6, 0, 3, 2])
Out[145]: [0, 1, 2, 2, 3, 6, 7]
In [146]: sorted("horse race")
Out[146]: [' ', 'a', 'c', 'e', 'e', 'h', 'o', 'r', 'r', 's']Lưu ý là hàm sorted() dễ bị lộn với sort() của lists nhưng tụi nó cũng có chung danh sách tham số thôi.
🔥 zip
Hàm zip() sẽ ghép từng cặp đôi một các phần tử của lists, typles hoặc một sequences nào đó thành danh sách các tuples:
In [147]: seq1 = ["foo", "bar", "baz"]
In [148]: seq2 = ["one", "two", "three"]
In [149]: zipped = zip(seq1, seq2)
In [150]: list(zipped)
Out[150]: [('foo', 'one'), ('bar', 'two'), ('baz', 'three')]Nếu sẽ lấy số lượng phần tử sequence ít nhất làm số lượng tuples tạo thành
In [151]: seq3 = [False, True]
In [152]: list(zip(seq1, seq2, seq3))
Out[152]: [('foo', 'one', False), ('bar', 'two', True)]Ngoài ra thì ta còn có thể dùng nó trong vòng lặp, điều thú vị là ta có thể kết hợp với enumerate()
In [153]: for index, (a, b) in enumerate(zip(seq1, seq2)):
.....: print(f"{index}: {a}, {b}")
.....:
0: foo, one
1: bar, two
2: baz, three🔥 reversed
Hàm này trước hết sẽ duyệt qua các phần tử và trả về sequence các phần tử đó nhưng theo chiều ngược lại
In [154]: list(reversed(range(10)))
Out[154]: [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]Bản thân hàm
reversed()không tạo ra một đối tượng reversed (trừ khi nó kèm theo các hàm materialized – thực thể hóa? nhưlisthoặcfor)for i in reversed_list: ... print(i)
🍀List, Set, and Dictionary Comprehensions
Điểm hay là nó có thể kết hợp đồng thời 3 nhiệm vụ:
List comprehensions (Có thể dịch là “Biểu thức tạo danh sách”) là một tính năng tiện lợi và được sử dụng rộng rãi của Python.
- Kiểm tra và lọc các phần tử phù hợp với điều kiện cho trước
- Duyệt qua các phần tử của 1 sequence
- Trả về định dạng mong muốn
🔥 List comprehensions
Dạng cơ bản của list comprehensions là:
[expr for value in collection if condition]Nó khá là tương tự với for
In [155]: strings = ["a", "as", "bat", "car", "dove", "python"]
In [156]: [x.upper() for x in strings if len(x) > 2]
Out[156]: ['BAT', 'CAR', 'DOVE', 'PYTHON']Ví dụ cho lst
In [155]: strings = ["a", "as", "bat", "car", "dove", "python"]
In [156]: [x.upper() for x in strings if len(x) > 2]
Out[156]: ['BAT', 'CAR', 'DOVE', 'PYTHON']🎍 NESTED LIST comprehensions
Giả sử ta có một danh sách các danh sách (list of lists) chứa các tên tiếng anh và tiếng TBN:
In [162]: all_data = [["John", "Emily", "Michael", "Mary", "Steven"],
.....: ["Maria", "Juan", "Javier", "Natalia", "Pilar"]]Giờ ta được giao nhiệm vụ tạo một danh sách đơn chưa các tên có từ hai ký tự “a” trở trên. Ta hoàn toàn có thể làm việc này bằng for
In [163]: names_of_interest = []
In [164]: for names in all_data:
.....: enough_as = [name for name in names if name.count("a") >= 2]
.....: names_of_interest.extend(enough_as)
.....:
In [165]: names_of_interest
Out[165]: ['Maria', 'Natalia']Nhưng sẽ ra sao khi cả đống code trên có thể gộp lại trong 1 dòng duy nhất?
In [166]: result = [name for names in all_data for name in names
.....: if name.count("a") >= 2]
In [167]: result
Out[167]: ['Maria', 'Natalia']🎯 Hola la, thật tuyệt vời, mặc dù ban đầu đọc vô hơi rối xíu. Công việc ta làm ở trên còn được gọi là flatten data
🎍 FLATTEN DATA:
In [168]: some_tuples = [(1, 2, 3), (4, 5, 6), (7, 8, 9)]
In [169]: flattened = [x for tup in some_tuples for x in tup]
In [170]: flattened
Out[170]: [1, 2, 3, 4, 5, 6, 7, 8, 9]Với ví dụ ở trên thì ta có thể viết dạng tường minh như sau:
flattened = []
for tup in some_tuples:
for x in tup:
flattened.append(x)Bạn có thể có vô số cấp lồng nhau, tuy nhiên nếu bạn có nhiều hơn hai hoặc ba cấp lồng nhau, bạn có thể nên bắt đầu đặt câu hỏi liệu ngay từ đầu việc thiết kế này có tuân theo quy tắc readability (dễ đọc) hay không?
Dưới đây là một sự kết hợp mới (sáng tạo nhưng miễn hợp lệ là được) list comprehension bên trong list comprehension:
# some_tuples = [(1, 2, 3), (4, 5, 6), (7, 8, 9)]
In [172]: [[x for x in tup] for tup in some_tuples]
Out[172]: [[1, 2, 3], [4, 5, 6], [7, 8, 9]]🔥 Dict comprehension
Format có dạng:
dict_comp = {key-expr: value-expr for value in collection if condition}Một ví dụ đơn giản là ta sẽ tạo một dict gồm vị trí của các phần tử trong biến strings ở ví dụ trên
In [160]: loc_mapping = {value: index for index, value in enumerate(strings)}
In [161]: loc_mapping
Out[161]: {'a': 0, 'as': 1, 'bat': 2, 'car': 3, 'dove': 4, 'python': 5}🔥 Set comprehension
Format có dạng:
set_comp = {expr for value in collection if condition}Quay trở lại ví dụ ở list, bây giờ ta muôn ghi nhận độ dài của từng phần tử trong danh sách strings ở trên thì sao?
In [157]: unique_lengths = {len(x) for x in strings}
In [158]: unique_lengths
Out[158]: {1, 2, 3, 4, 6}Với cách này ta có thể thống kê được các độ dài của các phần tử (distinct). Thật ra ta có thể dùng một hàm khác cũng lợi hại không kém map()
In [159]: set(map(len, strings))
Out[159]: {1, 2, 3, 4, 6}Lời kết
Do phần nội dung các kiến trúc dữ liệu cơ bản quan trọng nên các kiến thức mình tổng hợp có hơi dài và phải trải đều qua tận 2 Video. Nhưng mình tin là các bạn sẽ có được “căn cơ” tốt nhất để chuẩn bị cho những kiến thức khó hơn.
Hẹn gặp các bạn và buổi Livestream kế tiếp nhé (8:00 PM – T3 và CN hằng tuần)
Các kênh cộng đồng của mình
- Facebook: https://www.facebook.com/bocuoilamdata
- Youtube: https://www.youtube.com/@CarrY4U_VN
- Website: https://carry4u.org/
© Bản quyền thuộc về CarrY4U và Bò Cười Làm Data
© Copyright by CarrY4U and bocuoilamdata ☞ Do not Reup”
