Tóm tắt buổi trước và nội dung kỳ này

Trong buổi trước mình đã đi qua về cụ thể cách cài đặt và một số kiến thức cơ bản về IPython cũng như một vài cú pháp trong Jupyter.

Mặc dù đầu buổi Livestream thứ 4 này mình vẫn phải back lại nói bổ sung thêm mục cuối phần “Nội suy” và cho các bạn biết thêm cách để tạo một đối tượng như thế nào nhưng mình tin vững nền tảng thì mới đi xa được nè

🔴 Buổi live ngày 4:

À ngoài ra mình muốn bổ sung cách cài đặt packages trực tiếp trong Jupyter Notebook

# Install a pip package in the current Jupyter kernel
import sys
!conda install --yes --prefix {sys.prefix} numpy
# !{sys.executable} -m pip install numpy

Language Semantics – ngữ nghĩa (quy tắc)

Python là ngôn ngữ lập trình được thiết kế với trọng tâm là khả năng đọc, đơn giản và rõ ràng. Đơn giản nhất là hãy so sánh một ngôn ngữ bậc cao và nổi tiếng khác như C# bằng một hàm đơn giản là print chữ “Hello World” trong console thì sẽ khác nhau như thế nào?

Đây là code C#

// Hello World! program
using System;
 
namespace HelloWorld
{
    class Hello {         
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}

Còn đây là Python

print("Hello World")

Rõ ràng chẳng cần phải hiểu code viết như thế nào nhưng rõ ràng nhìn vào code Python rõ ràng đỡ … “nản” hơn nhiều rồi.

🔥 indentation – no braces, EOL rules

Điểm khác biệt giữa Python và hầu hết những ngôn ngữ lập trình bậc cao khác là nó sử dụng khoảng trắng (tabs hoặc spaces) để tổ chức code thay vì ngoặc. Ví dụ đoạn code sau:

for x in array:
    if x < pivot:
        less.append(x)
    else:
        greater.append(x)

Trong ví dụ trên:

Dấu hai chấm : biểu thị sự bắt đầu của một khối mã thụt lề, sau đó tất cả mã phải được thụt lề bằng nhau cho đến khi kết thúc khối mã.

Theo quy ước cộng đồng thì bạn nên thụt vào 4 khoảng trắng làm mặc dịnh, có thể thay thế bằng 1 lần tab.

Giống IPython và Jupyter notebooks thì những IDE hiện đại khác đều có hỗ trợ indentation sẵn

Thêm một quy tắc quan trong nữa là dấu chấm phẩy-semicolon ; bị bắt buộc điền vào cuối mỗi dòng của mỗi câu lệnh để đánh dấu kết thúc câu lệnh đó. Còn trong Python thì nó chỉ được dùng để phân tách các câu lệnh trong cùng 1 dòng như ví dụ sau:

a = 5; b = 6; c = 7

Cơ mà lời khuyên của mình đừng làm vậy vì code bạn sẽ khó đọc.

🔥 Everything is an object, isn’t it?

Một đặc điểm quan trọng đó là các tổ chức dữ liệu trong Python theo mô hình đối tượng object models.

Mọi số, chuỗi, cấu trúc dữ liệu, hàm, lớp, mô-đun, v.v. đều tồn tại trong trình thông dịch Python đều là các đối tượng (objects). Mà đối tượng thì phải chứa dữ liệu trên các trường/biến (gọi là thuộc tính) và kèm theo các hàm xử lý (gọi là phương thức).

Việc tạo đối tượng thông qua các lớp (classes) là một khuôn mẫu cho từng đối tượng. Ta định nghĩa thuộc tính và thương thức khi khai báo lớp. Ví dụ:

Thật ra một ngôn ngữ có thể tuân theo quy tắc của nhiều mô hình khác nhau trong đó một mô hình vô cùng phổ biến đó là Mô hình hướng đối tượng (OOP-Object Oriented Programming)

Dưới đây là một số ví dụ về các mô hình trong ngôn ngữ lập trình:

  • Mô hình hướng đối tượng: Python, Java, C#
  • Mô hình mệnh lệnh (Imperative models): C, C++, Fortran
  • Mô hình mệnh đề: Prolog, Lisp, Scheme

🔥 Comments, Docstring – Chú thích

Bất kỳ đoạn hoặc dòng code nào được bắt đầu bằng dấu thăng # thì trình biên dịch Python sẽ skip qua đoạn hay dòng code đó.

Cú pháp này được dÙng để thêm chú thích mong muốn hoặc đôi khi vì ta đơn giản không muốn chạy nhưng lại không muốn xóa chúng đi.

Trong Jupiter ta có thể dùng tổ hợp phí Ctrl+/

Phím tắt
results = []
for line in file_handle:
    # keep the empty lines for now
    # if len(line) == 0:
    #   continue
    results.append(line.replace("foo", "bar"))

Một quy tắc của mình là nếu bạn cần chú thích cho hàm, đoạn code hoặc phương thức, lớp khai báo thì sẽ để ở trước đoạn code đó. Còn nếu chỉ để chú thích 1 dòng thì đặt đàng sau dòng code đó là được

print("Reached this line")  # Simple status report

Ngoài ra một thumb rule nữa là hãy chịu khó document lại những gì mình viết bằng cách sau (người ta gọi là docstring)

def add_numbers(a, b):
    """
    Add two numbers together

    Returns
    -------
    the_sum : type of arguments
    """
    return a + b

🔥 Function and object method calls

Để gọi hàm thì ta sử dụng dấu ngoặc đơn(parentheses) và thêm tham số vào hàm, nếu có trả về giá trị để dùng lại sau thì nên gán nó vào một biến (variable).

Đôi khi cũng có những hàm không nhận tham số thì ta chỉ cần để trống trong ngoặc:

result = f(x, y, z)
fn_x()

Do là mọi thứ trong Python đều là đối tượng nên hầu hết chúng đề có hàm của chúng (bao gồm các hàm hệ thống khởi tạo mặc định) và việc gọi hàm từ một đối tượng để truy cập các nội dung bên trong nó đơn giản bằng cách sau:

obj.some_method(x, y, z)

Khi gọi hàm ta có thể truyền tham số theo vị trí (index) của biến hoặc tên của biến đã khai báo trong hàm. Ví dụ:

from math import sqrt

def quadratic(a, b, c):
    x1 = -b / (2*a)
    x2 = sqrt(b**2 - 4*a*c) / (2*a)
    return (x1 + x2), (x1 - x2)

Hàm phương trình bậc 2 trên có nhiều cách gọi như:

# Vị trí giá trị truyền vào quan trọng
>>> quadratic(31, 93, 62)
(-1.0, -2.0)
>>> quadratic(62, 93, 31)
(-0.5, -1.0)

# Vị trí không quan trọng
>>> quadratic(a=31, b=93, c=62)
(-1.0, -2.0)
>>> quadratic(c=62, b=93, a=31)
(-1.0, -2.0)

# Và thập chí trộn hầm bà lằng
>>> quadratic(31, 93, c=62)
(-1.0, -2.0)

🔥 Variables and argument passing

Khi gán bất cứ thứ gì cho biến thì đồng nghĩa bạn tạo một reference (liên kết – tham chiếu) tới dối tượng ở bên phải dấu bằng gán biến. Ví dụ sau:

In [8]: a = [1, 2, 3]

In [9]: b = a

In [10]: b
Out[10]: [1, 2, 3]

Một số ngôn ngữ khác thì khi gán b cho đối tượng của a thì nó sẽ copy phần dịa chỉ bộ nhớ của a ra rồi liên kết địa chỉ dữ liệu trên vùng bộ nhớ mới đó cho b. Nhưng với Python bảng chất a và b đều đang trỏ vào cùng 1 địa chỉ

Cách để kiểm tra:

In [11]: a.append(4)

In [12]: b
Out[12]: [1, 2, 3, 4]

DA mình phải nắm được ý nghĩa tham chiếu trong Python – khi, như thế nào và tại sao dữ liệu được sao chép đặc biệt quan trọng khi đang làm việc với các tập dữ liệu lớn hơn trong Python.

Nếu không nắm được chuyện này sẽ dễ dẫn tới các lỗi dirty modifying các tập dữ liệu phân tích khác nhau mà lại không biết.

Gán biến còn được gọi là binding (data) vì chúng ta đang ràng buộc một tên biến với một đối tượng. Tên biến đã được gán đôi khi có thể được gọi là biến bị ràng buộc. (bound variables)

Khi bạn truyền đối tượng vào tham số của một hàm, biến cục bộ mới (local variables) được tạo ra và tham chiếu tới biến gốc ở ngoài hàm. Mọi thay đổi trong hàm đều dẫn tới thay đổi bên ngoài

In [13]: def append_element(some_list, element):
   ....:     some_list.append(element)
In [14]: data = [1, 2, 3]

In [15]: append_element(data, 4)

In [16]: data
Out[16]: [1, 2, 3, 4]

Nhưng nếu tạo một đối tượng cùng tên với biến gốc ở ngoài hàm và gắn nó vói một đối tượng mới thì nó sẽ không ghi đè qua biến gốc ở parent scope cho dù có thay đổi trong phạm vi hàm

def change_list(list1):
    """Changes the list passed as an argument.

    Args:
    list1: A list of numbers.
    """
#     list1.append(6) #This will modify the parent scope
    list1 = [100, 2, 3, 4, 5]
    list1.append(101)

# Create a list
list1 = [1, 2, 3, 4, 5]

# Print the list before calling the function
print("Before call function:",list1)

# Call the function
change_list(list1)

# Print the list after calling the function
print("After call function:",list1)

🔥 Dynamic references, strong types – Tham chiếu động

Các biến trong Python không tham được ép kiểu theo dạng thừa kế như các ngôn ngữ khác mà đơn giản nó tùy thuộc vào kiểu của đối tượng được gán với nó

In [17]: a = 5

In [18]: type(a)
Out[18]: int

In [19]: a = "foo"

In [20]: type(a)
Out[20]: str

Chính điều trên nên nhiều người vội kết luận rằng Python không phải là mộttype language

Type-language là ngôn ngữ lập trình trong đó các biến, biểu thức và câu lệnh được gán một kiểu dữ liệu cụ thể. Điều này có nghĩa là các biến chỉ có thể chứa các giá trị của kiểu dữ liệu đã chỉ định.

Điều này hoàn toàn sai, nhìn ví dụ như dươi đây sẽ hiểu

In [21]: "5" + 5
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-21-7fe5aa79f268> in <module>
----> 1 "5" + 5
TypeError: can only concatenate str (not "int") to str

Một số ngôn ngữ, ‘5’ có thể được chuyển thành số nguyên, do đó kết quả ra 10.

Lại có các ngôn ngữ khác, số nguyên 5 có thể được chuyển đổi thành ký tự, và vậy nên dòng lệnh trên dùng để nối chuỗi thành ’55’.

Trong Python, việc ép kiểu ngầm như vậy không được phép. Nói cho đúng thì Python là strong type language, có nghĩa là mọi đối tượng đều có một loại (hoặc lớp) cụ thể và các chuyển đổi ngầm sẽ chỉ xảy ra trong một số trường hợp được cho phép nhất định, chẳng hạn như:

In [22]: a = 4.5

In [23]: b = 2

# String formatting, to be visited later
In [24]: print(f"a is {type(a)}, b is {type(b)}")
a is <class 'float'>, b is <class 'int'>

In [25]: a / b
Out[25]: 2.25

Như ví dụ trên b là số nguyên nhưng được ép kiểu trở thành số thực trong phép chia.

Việc biết được đối tượng có kiểu dữ liệu nào rất quan trọng vì mỗi kiểu dữ liệu có các xử lý dữ liệu khác nhau và đặc biệt giúp ta kiểm soát đầu vô của các hàm.

Ví dụ có đôi khi

Để kiểm đối tượng có thuộc một kiểu dữ liệu xác định không ta dùng hàm isinstance

In [26]: a = 5

In [27]: isinstance(a, int)
Out[27]: True

Hàm này cũng nhận tuple để kiểm tra kiểu của đối tượng có thuộc một trong những kiểu mình quan tâm (tuple là gì mình sẽ giải thích sau nhen)

In [28]: a = 5; b = 4.5

In [29]: isinstance(a, (int, float))
Out[29]: True

In [30]: isinstance(b, (int, float))
Out[30]: True

Thật ra thì cách làm này mình cũng thấy khá là vô nghĩa vì mình cũng ít dùng 🤔 Bạn nào có biết ý nghĩa của việc này chứ? Bản thân mình thì thường dùng hàm type là chính

🔥 Attributes and methods

Cách truy cập thuộc tính (attributes) hoặc phương thức (methods) giống với việc gọi hàm (function calls) ở trên vậy:

<obj.attribute_name>

Và như mình từng để cập trong phần Magic method thì ta có hàm getattr() cũng được dùng để xác định một hàm nhất định của đối tượng có tồn tại hay không?

In [32]: getattr(a, "split")
Out[32]: <function str.split(sep=None, maxsplit=-1)>

getattr() hay các hàm liên quan như hasattr() và setattr() rất hữu ích trong việc viết các đoạn mã generic dễ tái sử dụng – Bạn sẽ thấy nhiều trong các thư viện

Fun fact

Một ví dụ về generic code cho bạn nào quan tâm

# hàm generic sử dụng getattr() để gọi một phương thức hoặc thuộc tính trên một đối tượng:
def call_sth(obj: Any, method_or_attribute_name: str) -> Any:
  """Gọi một phương thức trên một đối tượng.

  Args:
    obj: Đối tượng mà phương thức sẽ được gọi trên.
    method_name: Tên của phương thức sẽ được gọi.

  Returns:
    Giá trị trả về của phương thức.
  """
  method = getattr(obj, method_or_attribute_name)
  return method()

# Ví dụ sử dụng:

class Person:
  name = "Alice
  def greet(self):
    print("Hello!")

person = Person()

# Gọi phương thức `greet()` trên đối tượng `person` bằng cách sử dụng hàm `call_method()`.
call_sth(person, "greet")
call_sth(person, "name")

>>>> Hello!

🔥 Duck typing 🦆

“Nếu nó đi như con vịt, kêu như con vịt thì nó là con vịt” Còn sao dùng con vịt mà không dùng con khác thì mình cũng chịu 😂

Đôi khi bạn không quan tâm tới kiểu dữ liệu đối tượng mà chỉ vì các hàm và hành vi (behavior – Ví dụ List thì có hàm len và phần tử đầu tiên của nó được đánh index là 0) của kiểu đó nên bạn mới cần xác định

Một (behavior) thú vị mình muốn đề cập là iterable – lặp lại. Bạn có thể xác định một obj có iterable không chỉ khi nó triển khai (implements) iterator protocol hay chưa.

Fact: iterable obj (hầu hết là các Python collection types) sẽ có magic method __iter__ và để check nó có phải hay không ta dùng hàm iter sau:

In [33]: def isiterable(obj):
   ....:     try:
   ....:         iter(obj)
   ....:         return True
   ....:     except TypeError: # not iterable
   ....:         return False
In [34]: isiterable("a string")
Out[34]: True

In [35]: isiterable([1, 2, 3])
Out[35]: True

In [36]: isiterable(5)
Out[36]: False

🔥 Imports

Đây là phần bạn thường dùng nhất – module import. Thật ra các module bản chất là các file .py chưa code mà thôi. Ví dụ ta tạo một “thư viện sau” và đặt tên là some_module.py:

# some_module.py
PI = 3.14159

def f(x):
    return x + 2

def g(a, b):
    return a + b

Để truy cập thư viên đó (phải nằm trên cùng 1 folder) thì ta chỉ dơn giản là:

import some_module
result = some_module.f(5)
pi = some_module.PI

Hoặc

from some_module import g, PI
result = g(5, PI)

Sử dụn keyword alias as để tùy biến thêm cho việc gọi tên thư viện thậm chí là hàm và biến trong thư viện

import some_module as sm
from some_module import PI as pi, g as gf

r1 = sm.f(pi)
r2 = gf(6, pi)

🔥 Binary operators and comparisons – Toán tử nhị phân và các phép so sánh

Phần này liên quan mật thiết tới toán rùi nè. Một số toán tử quan trọng mình để trong hình:

Các ví dụ:

1. Cộng trừ nhân chia

In [37]: 5 - 7
Out[37]: -2

In [38]: 12 + 21.5
Out[38]: 33.5

In [39]: 5 <= 2
Out[39]: False

2. Kiểm tra xem 2 biến có tham chiếu nhau không

In [40]: a = [1, 2, 3]

In [41]: b = a

In [42]: c = list(a)

In [43]: a is b
Out[43]: True

In [44]: a is not c
Out[44]: True

Lưu ý: Hàm list sẽ tạo ra một bản copy mới cho đối tượng. Toán tử so sánh is tương đồng với dùng ==

In [45]: a == c
Out[45]: True

Dân data mình thường dùng is để check xem biến đó có rỗng không

In [46]: a = None

In [47]: a is None
Out[47]: True

Cơ mà khi sử dụng Pandas DataFrame thì ta sẽ có hàm sau

import pandas as pd

# Create a Pandas DataFrame
df = pd.DataFrame([[1, 2, 3], [None, 5, 6]], columns=['A', 'B', 'C'])

# Check for None in the DataFrame
is_null = df.isnull()

# Print the result
print(is_null)

>>>
   A  B  C
0  False  False  False
1   True  False  False

Trong Python, None là một từ khóa đặc biệt được sử dụng để đại diện cho một giá trị rỗng. None không phải là một kiểu dữ liệu riêng biệt, mà là một đối tượng của lớp NoneType.

Null là một thuật ngữ chung được sử dụng trong nhiều ngôn ngữ lập trình để đại diện cho một giá trị rỗng. Trong Python, None là tương đương với null trong các ngôn ngữ khác.

🔥 Mutable and immutable objects – Bất biến?

Nhiều đối tượng trong Python, chẳng hạn như list, dictionary, ndarrayvà hầu hết các kiểu dữ liệu do người dùng định danh (class) , đều là đối tượng có thể thay đổi được. Điều này có nghĩa là đối tượng hoặc giá trị mà chúng chứa có thể được sửa đổi:

In [48]: a_list = ["foo", 2, [4, 5]]

In [49]: a_list[2] = (3, 4)

In [50]: a_list
Out[50]: ['foo', 2, (3, 4)]

Nhưng bên cạnh đó có những loại không thể thay đổi dữ liệu bên trong chúng như string, tuples

In [51]: a_tuple = (3, 5, (4, 5))

In [52]: a_tuple[1] = "four"
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-52-cd2a018a7529> in <module>
----> 1 a_tuple[1] = "four"
TypeError: 'tuple' object does not support item assignment

Hãy nhớ rằng chỉ vì thể thay đổi một đối tượng không có nghĩa là nên làm như vậy. Những hành động như vậy sẽ mang lại hiệu ứng phụ (side effects). Ví dụ, khi viết một hàm, bất kỳ side effects nào cũng nên được thông báo rõ ràng cho người dùng trong tài liệu hoặc chú thích của hàm. Nếu có thể, nên ưu tiên tính bất biến (immutability) để đảm báo tính toàn vẹn dữ liệu.

Lời kết

Thiệt là chương này mình phải ngồi soạn gần 2 ngày rất là phê. Chủ yếu phải vừa soạn vừa test code lại vừa mở rộng vấn dề nên khá mất thời gian nhưng không sao miễn giúp được nhiều bạn là mình thấy vui rồi.

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”