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 strsequences

Và 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 assignment

Như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 ('foo', 'bar') tham chiếu chính nó và hiển thị lên màn hình

Fun fact

🔥 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]: 5

Và 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]: 7

Như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=9

Vậ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ư rest

In [39]: a, b, *_ = values
Best practise

🔥 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 = tmp

Như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']

insert cần tính toán phức tạp hơn so với append, 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.deque

from 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 appendremove 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]: True

Hoặc muốn xác nhận không có:

In [62]: "dwarf" not in b_list
Out[62]: False

Kiểm tra xem list có chứa một giá trị hay không chậm hơn nhiều so với dictionariessets vì 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ó.

Fun fact

🎍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ên extend đượ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)
everything = []
for chunk in list_of_lists:
    everything = everything + chunk
Let test it

🔥 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í start và lấy stop - start phần tử.

Mánh để nhớ

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ủa list hay tuple

In [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()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}

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ó

Note

🔥 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] = value

Thậ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_value

Việ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_letter dict không
  • B4: Không có thì thêm dạng key_value vào với valuelst chứa phần tử đầu tiên là word. Có rồi thì append vào list value của letterkeytươ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ới by_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ờ: del pop() .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 keyhashability. Bạn có thể kiểm tra xem một đối tượng có hashable không – Nói cách khác là có thể dùng làm key trong dictionary không bằng hàm hash

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

Bảng 3.1: Python set operations

Nếu truyền tham số không phải là set vào union() hay intersection() thì nó sẽ tự động “đồng hóa” tụi nó về set trướ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à sets trướ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ớ nhen

Mộ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 += 1

Nhưng hãy thử cách sau:

for index, value in enumerate(collection):
   # do something with value

enumerate() 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ặc for)

 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”