Chương 02 · Variables, Types & Operators

Biến, kiểu dữ liệu và toán tử

Python là dynamic typed nhưng strongly typed — biến không khai báo type, nhưng runtime sẽ không tự ý ép kiểu lung tung như JavaScript. Chương này đào sâu vào int độ chính xác vô hạn, float theo IEEE 754, chuỗi Unicode, ngữ nghĩa tham chiếu (reference semantics), khác biệt giữa == và is, và những idiom Pythonic như chain comparison hay x is None.

Độ dài: ~900 dòng Bài tập: 5 Quiz: 7 Prerequisites: Chương 1
🎯 Mục tiêu chương
  • Hiểu cách Python khai báo biến (không có var/let) và convention snake_case.
  • Master int arbitrary precision — vì sao 2**100 chạy được mà JS thì không.
  • Biết khi nào dùng float, decimal.Decimal và fractions.Fraction.
  • Thành thạo str Unicode + slicing + methods + f-string với format spec.
  • Hiểu bool là subclass của int và None là singleton.
  • Phân biệt immutable vs mutable, ngữ nghĩa name → object của Python.
  • Biết khi nào dùng ==, khi nào dùng is, khi nào dùng in.
  • Viết chain comparison 0 < x < 10 đúng cách và bắt đầu làm quen type hints.

1. Khai báo biến — không cần var, let hay type

Python không có keyword khai báo biến. Gán giá trị là tạo biến. Không có var, let, const; cũng không cần ghi type. Đây là phong cách "code đọc như văn bản" của Python.

Khai báo biến
x = 5
name = "An"
is_active = True
price = 19.99

print(type(x))         # <class 'int'>
print(type(name))      # <class 'str'>
print(type(is_active)) # <class 'bool'>

# Reassign sang type khác — runtime cho phép
x = "giờ tôi là chuỗi"
print(type(x))         # <class 'str'>
🧠 Mental model — Python "dynamic + strong"

Dynamic: type gắn với giá trị, không gắn với biến. Biến chỉ là cái tên (label) trỏ vào object. Đổi giá trị có thể đổi luôn type.

Strong: nhưng Python không tự ý coerce type. "5" + 3 sẽ raise TypeError — khác hẳn JavaScript trả "53". Đây là điểm an toàn của Python so với JS.

1.1. Convention đặt tên — PEP 8

LoạiConventionVí dụ
Biến, hàmsnake_caseuser_name, calculate_total()
Hằng sốUPPER_SNAKEMAX_RETRY, PI
ClassPascalCaseUserAccount, HttpClient
Module / packagelowercase ngắnos, json, requests
"Private" (convention)_underscore_internal_cache
Tránh va chạm với keywordthêm _ cuốiclass_, type_
Convention snake_case
# ✅ Đúng — snake_case
user_age = 25
first_name = "An"
def calculate_total(items): ...

# ❌ Sai — camelCase (style JS/Java)
userAge = 25
def calculateTotal(items): ...

# ✅ Hằng số — UPPER_CASE (chỉ là convention, Python KHÔNG enforce)
MAX_RETRY = 3
PI = 3.14159
DEFAULT_TIMEOUT = 30
🔥 Gotcha — không có hằng số thật sự

MAX_RETRY = 3 chỉ là convention. Python không có keyword như const (JS) hay final (Dart). Code dưới vẫn chạy:

MAX_RETRY = 3
MAX_RETRY = 99   # không lỗi! chỉ là convention

Python 3.8+ có typing.Final hint cho mypy nhưng vẫn không enforce runtime. Văn hoá Python là "we're all consenting adults" — tin nhau, không khoá cứng.

2. int — số nguyên độ chính xác vô hạn

Đây là điểm khác biệt lớn nhất của Python so với JavaScript và Dart. Trong Python, int không giới hạn số chữ số — nó tự động phình to (arbitrary precision) khi cần.

int không overflow
x = 2 ** 100
print(x)
# 1267650600228229401496703205376

print(type(x))
# <class 'int'>  — vẫn là int, không cần BigInt riêng

# Tính giai thừa 100 — vẫn chính xác từng chữ số
import math
print(math.factorial(100))
# 93326215443944152681699238856266700490715968264381621468592963895217...

Đối chiếu với các ngôn ngữ khác:

Ngôn ngữSố nguyên tối đaVượt thì sao?
Python intVô hạn (chỉ giới hạn RAM)Không overflow
JavaScript Number2^53 - 1 ≈ 9 × 10¹⁵Mất precision (silent)
JavaScript BigIntVô hạnOK nhưng phải dùng hậu tố n
Dart int (VM)64-bit signed (~9.2 × 10¹⁸)Wrap-around (overflow ngầm)
C/C++ int32-bit (~2.1 × 10⁹)Wrap-around (Undefined Behaviour)
💡 Trade-off — chậm hơn fixed-width

Số nhỏ (vừa với 1 word CPU) Python lưu inline, tốc độ gần như C. Số lớn vượt 64-bit thì Python chuyển sang "bignum" lưu thành mảng digit, thao tác chậm hơn ~10–100×. Trong 99% code thường ngày bạn không cảm nhận được, nhưng nếu làm crypto / số học hạng nặng thì cân nhắc dùng numpy.int64 hoặc thư viện chuyên biệt như gmpy2.

2.1. Toán tử số nguyên

Operators trên int
7 + 3     # 10  — cộng
7 - 3     # 4   — trừ
7 * 3     # 21  — nhân
7 / 3     # 2.3333...  — chia (LUÔN trả float, kể cả int / int)
7 // 3    # 2   — chia lấy nguyên (floor division)
7 % 3     # 1   — chia lấy dư
7 ** 3    # 343 — luỹ thừa

# Lưu ý: // và % với số âm — Python floor về phía âm vô cực
-7 // 3   # -3  (KHÔNG phải -2 như C/Java)
-7 % 3    # 2   (luôn cùng dấu với divisor)

3. float — IEEE 754 double và những cạm bẫy

float trong Python = IEEE 754 double-precision 64-bit, giống hệt JavaScript Number và Dart double. Mọi gotcha của số thực binary đều áp dụng.

0.1 + 0.2 chiến tranh nghìn năm
>>> 0.1 + 0.2
0.30000000000000004

>>> 0.1 + 0.2 == 0.3
False

# So sánh đúng — math.isclose
>>> import math
>>> math.isclose(0.1 + 0.2, 0.3)
True

# float đặc biệt
>>> float("inf")
inf
>>> float("nan")
nan
>>> float("nan") == float("nan")
False   # NaN không bằng chính nó — IEEE 754 rule

3.1. decimal.Decimal — khi cần chính xác (tiền)

Không bao giờ tính tiền bằng float. Dùng Decimal — số thập phân chính xác từng chữ số.

Tính tiền với Decimal
from decimal import Decimal, getcontext

# Khởi tạo từ chuỗi để tránh lỗi float lúc gõ literal
price = Decimal("19.99")
tax = Decimal("0.08")
total = price * (1 + tax)
print(total)   # 21.5892 — chính xác từng cent

# So với float — sai số tích luỹ
print(19.99 * 1.08)   # 21.589200000000002 (!!)

# Set precision toàn cục
getcontext().prec = 10
Decimal("1") / Decimal("7")   # Decimal('0.1428571429')

3.2. fractions.Fraction — phân số

Số hữu tỉ tuyệt đối
from fractions import Fraction

a = Fraction(1, 3)
b = Fraction(1, 6)
print(a + b)              # 1/2  — tự rút gọn
print(Fraction("0.1") + Fraction("0.2"))
# 3/10  — tránh hoàn toàn lỗi 0.30000000000000004

4. str — chuỗi Unicode mặc định

Khác Python 2 (lưu byte ASCII), Python 3 str là Unicode. Mọi ký tự — tiếng Việt có dấu, emoji, tiếng Trung — đều được hỗ trợ first-class.

str là Unicode
s = "Xin chào, Việt Nam! 🇻🇳"
print(len(s))    # đếm code point Unicode, không đếm byte
print(type(s))   # <class 'str'>

# Indexing — như array
print(s[0])     # 'X'
print(s[-1])    # '🇻🇳' (đếm từ cuối)

# Slicing — s[start:stop:step], stop loại trừ
print(s[0:3])   # 'Xin'
print(s[:3])    # 'Xin' (start mặc định = 0)
print(s[4:])    # 'chào, Việt Nam! 🇻🇳'
print(s[::-1])  # đảo ngược chuỗi

# IMMUTABLE — không sửa được ký tự
s[0] = "Y"     # TypeError: 'str' object does not support item assignment

4.1. Methods quan trọng của str

MethodTác dụngVí dụ
upper() / lower()Viết hoa / viết thường"abc".upper() → 'ABC'
strip()Bỏ whitespace 2 đầu" hi ".strip() → 'hi'
split(sep)Tách chuỗi thành list"a,b,c".split(",") → ['a','b','c']
join(iterable)Nối list thành chuỗi"-".join(["a","b"]) → 'a-b'
replace(old, new)Thay thế"abc".replace("b","X") → 'aXc'
startswith / endswithCheck prefix / suffix"abc".startswith("a") → True
find / indexTìm vị trí substring"hello".find("l") → 2
count(sub)Đếm số lần xuất hiện"banana".count("a") → 3
isdigit()Toàn ký số?"123".isdigit() → True
Combo split + map + join
csv = "  1, 2 ,3  ,4"
nums = [int(x.strip()) for x in csv.split(",")]
print(nums)    # [1, 2, 3, 4]

# Ngược lại — list → string
print(", ".join(str(n) for n in nums))
# "1, 2, 3, 4"

5. f-string — format string hiện đại

Ra đời từ Python 3.6 (PEP 498), f-string là cách format string hiện đại và nhanh nhất. Thay thế hoàn toàn %-formatting cũ và "{}".format() trong code mới.

f-string cơ bản
name = "An"
age = 25

# Cũ — % formatting (1990s)
"Tôi tên %s, %d tuổi" % (name, age)

# Cũ hơn chút — .format()
"Tôi tên {}, {} tuổi".format(name, age)

# Hiện đại — f-string (Python 3.6+)
print(f"Tôi tên {name}, {age} tuổi")
# Tôi tên An, 25 tuổi

# Expression bên trong
print(f"Năm sau: {age + 1}")
print(f"Tên hoa: {name.upper()}")
print(f"Là người lớn: {age >= 18}")

5.1. Format specification — căn lề, làm tròn, dấu phân cách

Cú pháp: {value:format_spec}. Đây là siêu năng lực của f-string.

Format spec
x = 3.14159
n = 1234567
name = "An"

# Làm tròn số thập phân
print(f"{x:.2f}")        # '3.14'
print(f"{x:.4f}")        # '3.1416'

# Phân cách hàng nghìn
print(f"{n:,}")          # '1,234,567'
print(f"{n:_}")          # '1_234_567' (Python style)

# Căn lề (mặc định 10 ký tự)
print(f"{name:>10}")     # '        An'  (căn phải)
print(f"{name:<10}")     # 'An        '  (căn trái)
print(f"{name:^10}")     # '    An    '  (căn giữa)
print(f"{name:*^10}")    # '****An****' (fill bằng *)

# Phần trăm
ratio = 0.7523
print(f"{ratio:.1%}")    # '75.2%'

# Hex / binary / octal
print(f"{255:x}")        # 'ff'
print(f"{255:#x}")       # '0xff'
print(f"{10:b}")         # '1010'

# Debug self-documenting (Python 3.8+) — in cả tên biến
print(f"{x=}")            # 'x=3.14159'
💡 Python 3.12+ — f-string được nâng cấp lớn

Trước 3.12, f-string có nhiều giới hạn (không nest cùng loại quote, không xuống dòng trong biểu thức, không dùng backslash). Từ Python 3.12 (PEP 701), f-string trở thành một biểu thức Python đầy đủ:

# Trước 3.12 — lỗi syntax
print(f"{"hello".upper()}")

# 3.12+ — OK, nest quote thoải mái, xuống dòng cũng được
print(f"{
    "hello".upper()
    if shout else "hello"
}")

6. Nhập / Xuất dữ liệu — input() và print()

Hai hàm built-in này là kênh giao tiếp cơ bản nhất giữa chương trình và người dùng. Chúng đơn giản, nhưng mỗi hàm đều có vài tham số ít người biết mà lại rất hữu dụng.

6.1. input() — đọc từ bàn phím

input_co_ban.py
# input() nhận một chuỗi nhắc (prompt), in ra rồi CHỜ người dùng gõ Enter
ten = input("Tên bạn là gì? ")
print(f"Xin chào, {ten}!")

# ⚠️ ĐIỀU QUAN TRỌNG NHẤT: input() LUÔN trả về str
tuoi = input("Tuổi: ")      # người dùng gõ 22
print(type(tuoi))            # <class 'str'>  — KHÔNG phải int!
print(tuoi + 1)             # TypeError: can only concatenate str (not "int") to str

# ✅ Phải ép kiểu tường minh
tuoi = int(input("Tuổi: "))
print(f"Sang năm bạn {tuoi + 1} tuổi")
🕳️ Ba cái bẫy của input()
input_bay.py
# ① Luôn là str — quên ép kiểu là lỗi số một của người mới
n = input("Số: ")
if n > 10:                # TypeError: '>' not supported between 'str' and 'int'
    ...

# ② Ép kiểu có thể ném ValueError khi người dùng gõ bậy
tuoi = int(input("Tuổi: "))   # gõ "abc" → ValueError, chương trình sập

# ③ Khoảng trắng thừa không tự bị bỏ
lua_chon = input("y/n: ")     # người dùng gõ " y " (lỡ tay)
if lua_chon == "y":            # False! vì chuỗi là " y "
    ...

# ✅ Cách viết đúng cho cả ba
lua_chon = input("y/n: ").strip().lower()
nhap_an_toan.py — mẫu chuẩn để đọc số
def nhap_so_nguyen(nhac: str, nho_nhat: int = 0, lon_nhat: int = 150) -> int:
    """Hỏi lại cho tới khi người dùng nhập số hợp lệ."""
    while True:
        thô = input(nhac).strip()
        try:
            n = int(thô)
        except ValueError:
            print(f"  '{thô}' không phải số nguyên, thử lại.")
            continue
        if not nho_nhat <= n <= lon_nhat:
            print(f"  Phải trong khoảng [{nho_nhat}, {lon_nhat}], thử lại.")
            continue
        return n

tuoi = nhap_so_nguyen("Tuổi của bạn: ")
📌 input() không dùng được ở mọi nơi

Nó cần một terminal có người ngồi trước. Trong test tự động, trong cron job, trong container không có TTY, hoặc khi đầu vào đã hết, input() ném EOFError hoặc treo vô hạn. Vì vậy chương trình thật lấy tham số qua dòng lệnh (sys.argv, argparse) hoặc biến môi trường — input() chỉ dành cho công cụ tương tác. Chương 14 sẽ dựng CLI đúng cách.

6.2. print() — bốn tham số ít người biết

print_day_du.py
# Chữ ký đầy đủ:
# print(*objects, sep=' ', end='\n', file=sys.stdout, flush=False)

# ① Nhiều giá trị — tự chèn dấu cách giữa chúng
print("a", 1, True, None)        # a 1 True None

# ② sep — đổi dấu phân cách
print("2026", "08", "14", sep="-")    # 2026-08-14
print(*["a", "b", "c"], sep=", ")      # a, b, c   (* để bung list)

# ③ end — đổi ký tự kết thúc (mặc định xuống dòng)
print("Đang tải", end="")
for _ in range(3):
    print(".", end="", flush=True)   # flush: hiện ngay, không đợi đầy bộ đệm
print(" xong!")                     # Đang tải... xong!

# ④ file — ghi ra nơi khác thay vì màn hình
import sys
print("Lỗi: thiếu tham số", file=sys.stderr)   # ra luồng lỗi chuẩn

with open("ket_qua.txt", "w", encoding="utf-8") as f:
    print("Dòng 1", file=f)          # ghi vào tệp, tự thêm \n
💡 stdout và stderr — vì sao phải phân biệt

stdout là kết quả của chương trình; stderr là thông báo cho người dùng. Tách hai luồng cho phép nối lệnh trong shell mà không bị lẫn:

terminal
$ python xu_ly.py > ket_qua.csv     # chỉ kết quả vào tệp
Đang xử lý 100 dòng...              # thông báo vẫn hiện trên màn hình (stderr)

$ python xu_ly.py 2> loi.log        # chỉ thông báo lỗi vào tệp

Nếu bạn in thông báo tiến độ ra stdout, chúng sẽ lẫn vào tệp CSV và phá hỏng dữ liệu. Quy tắc: dữ liệu ra stdout, mọi thứ khác ra stderr.

🔁 Đối chiếu với JS & Dart

JS (Node): đọc bàn phím phải qua module readline và bất đồng bộ — không có hàm chặn nào tương đương input(). console.log tự thêm xuống dòng, không có tham số sep/end.
Dart: stdin.readLineSync() gần nhất với input(), trả String? (có thể null). print() chỉ nhận một tham số.
Python: input() chặn và đơn giản; print() linh hoạt nhất trong ba ngôn ngữ nhờ sep, end, file, flush.

7. bool — subclass của int

Đây là chi tiết "kì cục nhưng tiện" của Python: True và False không chỉ là boolean — chúng thực sự là số. bool là một subclass của int.

bool = int subclass
>>> isinstance(True, int)
True

>>> True == 1
True
>>> False == 0
True

>>> True + True
2
>>> True * 10
10
>>> sum([True, True, False, True])
3   # đếm True trong list — idiom Pythonic
💡 Idiom — đếm số phần tử thoả điều kiện
numbers = [1, 2, 3, 4, 5, 6]
even_count = sum(1 for n in numbers if n % 2 == 0)
# Hoặc gọn hơn nữa, tận dụng True == 1:
even_count = sum(n % 2 == 0 for n in numbers)
print(even_count)   # 3

7.1. Truthy / Falsy

Khi cần boolean (vd: if, while, not), Python xem các giá trị sau là falsy:

  • False
  • None
  • 0, 0.0, 0j (complex)
  • "" (empty string)
  • [], (), {}, set() (empty collection)
  • Object có method __bool__ trả False, hoặc __len__ trả 0

Mọi giá trị khác đều truthy. Khác JavaScript ở chỗ Python coi collection rỗng là falsy:

Falsy collection — khác JS
>>> bool([])     # False ✅ (JS: true)
False
>>> bool({})     # False ✅ (JS: true)
False
>>> bool("0")    # True  (string khác empty)
True

# Pythonic check rỗng — không cần len()
items = []
if not items:
    print("không có gì")

8. None — singleton "không có giá trị"

None tương đương null của các ngôn ngữ khác. Nhưng có 2 điểm đặc biệt:

  1. Singleton — chỉ có duy nhất 1 object None trong toàn bộ runtime.
  2. Type là NoneType — không thể subclass, không thể tạo thêm instance.
None là singleton
>>> type(None)
<class 'NoneType'>

>>> a = None
>>> b = None
>>> a is b
True   # cả 2 đều trỏ vào CÙNG MỘT object None duy nhất

>>> id(a) == id(b)
True
🔥 Quy tắc vàng — luôn dùng is None, không dùng == None

Cả 2 đều "work" cho trường hợp thông thường, nhưng luôn dùng is None vì:

  • is nhanh hơn (chỉ so sánh con trỏ).
  • == gọi __eq__ — một object lập dị có thể override để obj == None trả True ngay cả khi không phải None.
  • Lint tool (ruff, pylint, flake8) sẽ cảnh báo nếu thấy == None.
# ❌ Tránh
if x == None: ...

# ✅ Đúng
if x is None: ...
if x is not None: ...

9. Immutable vs Mutable — chia 2 phe của vũ trụ Python

Mọi object trong Python rơi vào 1 trong 2 phe: immutable (không sửa được) hoặc mutable (sửa được tại chỗ). Phân biệt được hai phe này là chìa khoá để hiểu mọi bug "reference" sau này.

ImmutableMutable
int, float, complex
str
tuple
frozenset
bytes
bool, None
list
dict
set
bytearray
Mọi class do người dùng định nghĩa (mặc định)
str immutable vs list mutable
# str — immutable
s = "hello"
s[0] = "H"          # TypeError
s = s.replace("h", "H") # tạo string MỚI, gán lại tên s

# list — mutable
xs = [1, 2, 3]
xs[0] = 99         # OK — sửa tại chỗ
xs.append(4)         # OK — sửa tại chỗ
print(xs)            # [99, 2, 3, 4]

9.1. Hệ quả: chỉ immutable mới làm key của dict / phần tử của set

Vì hash phải ổn định, mutable không có __hash__.

Hashable hay không?
>>> hash("hello")        # OK
-4378789663243879052
>>> hash((1, 2, 3))      # tuple immutable → OK
2528502973977326415
>>> hash([1, 2, 3])      # list mutable → lỗi
TypeError: unhashable type: 'list'

# Áp dụng — dùng tuple làm key
locations = {
    (21.0285, 105.8542): "Hà Nội",
    (10.7769, 106.7009): "TP.HCM",
}

10. Reference semantics — "tên trỏ vào object"

Đây là model quan trọng nhất cần nắm. Trong Python, biến không "chứa" giá trị — biến là một cái nhãn trỏ vào object trên heap. Gán b = a không copy; nó chỉ làm b trỏ vào cùng object như a.

Aliasing bug kinh điển
a = [1, 2, 3]
b = a               # b trỏ CÙNG list với a, không phải copy!
b.append(4)
print(a)            # [1, 2, 3, 4]  ← a cũng đổi!!
print(a is b)       # True (cùng object)
🧠 Mental model — "Sticky note"

Hãy nghĩ biến như sticky note dán lên object. b = a = dán thêm 1 sticky note b lên đúng object a đang trỏ. Cả 2 note đều "thấy" cùng một object — sửa qua note nào cũng đổi.

Với immutable, "sửa" không đổi object — nó tạo object mới và dán lại note. Đó là lý do x = 5; y = x; x = 6 không ảnh hưởng y.

10.1. copy.copy vs copy.deepcopy

Shallow vs deep copy
import copy

original = [[1, 2], [3, 4]]

# Shallow — copy top-level, nhưng nested object vẫn share
shallow = copy.copy(original)   # hoặc original[:] hoặc list(original)
shallow[0].append(99)
print(original)  # [[1, 2, 99], [3, 4]]  ← bị ảnh hưởng!

# Deep — copy cả nested
deep = copy.deepcopy(original)
deep[0].append(777)
print(original)  # [[1, 2, 99], [3, 4]]  ← không đổi
print(deep)      # [[1, 2, 99, 777], [3, 4]]

11. Toán tử — bảng đầy đủ

Bạn đã gặp rải rác các toán tử ở những mục trước. Mục này gom lại thành bảng tra cứu đầy đủ, kèm những điểm mà Python làm khác các ngôn ngữ khác.

11.1. Toán tử số học

Toán tửTênVí dụKết quảGhi chú
+Cộng7 + 29Cũng nối str, list, tuple
-Trừ7 - 25Cũng là hiệu của set
*Nhân7 * 214"ab" * 3 → 'ababab'
/Chia thực7 / 23.5Luôn trả float, kể cả 4 / 2 → 2.0
//Chia lấy nguyên7 // 23Làm tròn xuống: -7 // 2 → -4
%Chia lấy dư7 % 21Dấu theo số chia: -7 % 3 → 2
**Luỹ thừa2 ** 101024Kết hợp phải sang trái: 2**3**2 = 512
chia_va_du.py
# Python 3: / LUÔN cho float — khác Python 2, khác C/Java
print(6 / 3)      # 2.0  (không phải 2)
print(6 // 3)     # 2

# // làm tròn XUỐNG (floor), không phải cắt về 0
print(7 // 2)     #  3
print(-7 // 2)    # -4   ⚠️ JS/Java cho -3 (cắt về 0)

# % lấy dấu theo SỐ CHIA — rất tiện cho tính toán vòng tròn
print(-7 % 3)     # 2    ⚠️ JS/Java cho -1
print((-1) % 7)   # 6    → lùi 1 bước từ 0 trong vòng 7 phần tử

# divmod: lấy cả thương và dư trong một lần tính
print(divmod(3725, 60))    # (62, 5) → 62 phút 5 giây

# ** kết hợp phải sang trái
print(2 ** 3 ** 2)        # 512 = 2**(3**2), KHÔNG phải (2**3)**2 = 64

11.2. Toán tử gán ghép

Toán tửTương đươngToán tửTương đương
x += 1x = x + 1x &= yx = x & y
x -= 1x = x - 1x |= yx = x | y
x *= 2x = x * 2x ^= yx = x ^ y
x /= 2x = x / 2x <<= 1x = x << 1
x //= 2x = x // 2x >>= 1x = x >> 1
x %= 3x = x % 3x **= 2x = x ** 2
🕳️ += không phải lúc nào cũng bằng x = x + y
iadd.py
a = [1, 2]
b = a
a += [3]          # SỬA TẠI CHỖ list gốc (gọi list.__iadd__)
print(b)           # [1, 2, 3]  ← b thấy thay đổi!

a = [1, 2]
b = a
a = a + [3]      # TẠO list MỚI rồi gán lại cho tên a
print(b)           # [1, 2]     ← b không đổi

# Với int/str/tuple (immutable) thì hai cách luôn cho kết quả như nhau
# vì không có gì để sửa tại chỗ.

Với đối tượng mutable, += gọi __iadd__ — sửa ngay trên object hiện có. Với immutable, nó rơi về __add__ — tạo object mới. Đây là hệ quả trực tiếp của mục 9 (Immutable vs Mutable) và mục 10 (Reference semantics).

11.3. Toán tử so sánh và logic

Toán tửÝ nghĩaVí dụGhi chú
== !=Bằng / khác giá trị[1,2] == [1,2] → TrueGọi __eq__; xem mục 12
< > <= >=So sánh thứ tự"a" < "b" → TrueChuỗi so theo mã Unicode
is is notCùng object hay khôngx is NoneChỉ dùng với None/True/False/sentinel
in not inThuộc về3 in [1,2,3]Xem mục 13
andVàa and bTrả a nếu a falsy, ngược lại trả b
orHoặca or bTrả a nếu a truthy, ngược lại trả b
notPhủ địnhnot xLuôn trả bool
logic_tra_gia_tri.py
# ⭐ and/or KHÔNG trả bool — chúng trả về MỘT TRONG HAI toán hạng
print(0 or "mặc định")      # 'mặc định'   (0 falsy → lấy vế phải)
print("An" or "mặc định")   # 'An'         (truthy → lấy luôn vế trái)
print(1 and 2)              # 2
print(0 and 2)              # 0

# Ứng dụng: giá trị mặc định gọn gàng
ten = nhap_vao or "Khách"       # chuỗi rỗng → dùng 'Khách'

# ⚠️ Cẩn thận: 0 và "" đều falsy nên bị thay mất
so_luong = nhap_so or 10       # nhập 0 → thành 10! Có thể sai ý định.
so_luong = nhap_so if nhap_so is not None else 10   # ✅ chính xác

# Đoản mạch (short-circuit): vế phải KHÔNG chạy nếu không cần
if ds and ds[0] == "x":       # ds rỗng → không đụng ds[0] → không IndexError
    ...

11.4. Toán tử bit

Toán tửTênVí dụ (thập phân)Ví dụ (nhị phân)
&AND12 & 10 → 81100 & 1010 → 1000
|OR12 | 10 → 141100 | 1010 → 1110
^XOR12 ^ 10 → 61100 ^ 1010 → 0110
~NOT (đảo bit)~12 → -13~x == -x - 1
<<Dịch trái3 << 2 → 12Nhân với 2**n
>>Dịch phải12 >> 2 → 3Chia nguyên cho 2**n
📌 & | ^ còn dùng cho set

Python nạp chồng ba toán tử này cho kiểu set: a | b là hợp, a & b là giao, a ^ b là hiệu đối xứng. Từ Python 3.9, | còn dùng để hợp nhất dict: {"a":1} | {"b":2}. Và trong type hint, int | None nghĩa là "int hoặc None" (Chương 12). Cùng một ký hiệu, bốn ngữ cảnh — Python dùng lại toán tử rất nhiều, hãy đọc theo kiểu dữ liệu hai bên.

11.5. Thứ tự ưu tiên

MứcToán tửNhóm
1 (cao nhất)()Ngoặc
2**Luỹ thừa (kết hợp phải sang trái)
3+x -x ~xĐơn ngôi
4* / // %Nhân chia
5+ -Cộng trừ
6<< >>Dịch bit
7& → ^ → |Bit (theo thứ tự này)
8== != < > <= >= is inSo sánh (chain được — xem mục 14)
9notPhủ định
10andVà
11orHoặc
12 (thấp nhất)x if c else y, lambdaBiểu thức điều kiện
💡 Đừng học thuộc — hãy dùng ngoặc
ngoac.py
# Đúng nhưng người đọc phải dừng lại nghĩ
if a and b or c and not d:
    ...

# ✅ Cùng ý nghĩa, đọc một lần là hiểu
if (a and b) or (c and not d):
    ...

# Trong Python, & và | ưu tiên CAO HƠN so sánh (==, <, ...)
if x & 1 == 0:      # Python hiểu là (x & 1) == 0 → chạy đúng
if (x & 1) == 0:    # ✅ vẫn nên viết ngoặc: rõ ý, không phải nhớ bảng ưu tiên
# ⚠️ C / JavaScript thì NGƯỢC lại: x & 1 == 0 là x & (1 == 0) → luôn 0.
#    Thói quen viết ngoặc giúp code đúng ở mọi ngôn ngữ.

12. == vs is — bằng giá trị vs cùng object

Hai toán tử dễ nhầm. Quy tắc:

Toán tửHỏi cái gì?Cơ chế
==Giá trị có bằng nhau?Gọi __eq__, có thể coerce nhẹ
isCó phải cùng 1 object?So sánh id() — không gọi method
== vs is
>>> 5 == 5.0
True          # giá trị toán học bằng nhau
>>> 5 is 5.0
False         # int và float là 2 object khác nhau

>>> a = [1, 2]
>>> b = [1, 2]
>>> a == b
True          # cùng nội dung
>>> a is b
False         # nhưng là 2 list khác nhau trong RAM

>>> c = a
>>> c is a
True          # giờ thì cùng object
💡 Quy tắc — chỉ dùng is với
  • None: if x is None:
  • True / False trong context cần phân biệt (hiếm)
  • Sentinel object: _SENTINEL = object() rồi if x is _SENTINEL:

Mọi trường hợp khác — dùng ==.

13. Toán tử in — kiểm tra thành phần

Một trong những toán tử "đáng tự hào" của Python — đọc gần như tiếng Anh tự nhiên. in hoạt động trên mọi container/iterable.

in muôn nơi
# Substring trong string
"chao" in "xin chao"            # True
"X" in "xin chao"               # False (case-sensitive)

# Phần tử trong list / tuple
3 in [1, 2, 3]                  # True
9 in (1, 2, 3)                  # False

# Key trong dict (KHÔNG phải value)
d = {"a": 1, "b": 2}
"a" in d                          # True
1 in d                            # False (1 là value, không phải key)
1 in d.values()                   # True — check value

# Phần tử trong set
5 in {1, 3, 5}                  # True

# Phủ định — not in
"z" not in "abc"                  # True

13.1. Độ phức tạp — list vs set/dict

ContainerĐộ phức tạp x in containerLý do
list, tupleO(n)Quét tuyến tính từng phần tử
strO(n × m)Substring search
set, frozensetO(1) trung bìnhHash table lookup
dictO(1) trung bìnhHash table lookup theo key
💡 Optimization — đổi list sang set

Nếu bạn check membership nhiều lần trên cùng một collection, đổi sang set sẽ nhanh hơn O(n) → O(1). Với 1 triệu phần tử, đây là sự khác biệt giữa "chạy 5 phút" và "chạy 50ms".

banned_ids = [101, 202, 303, # ... 1M phần tử]
if user.id in banned_ids:  # O(n) — chậm
    ...

banned_ids = set(banned_ids)
if user.id in banned_ids:  # O(1) — nhanh
    ...

14. Chain comparison — đẹp hơn JS/Dart

Python cho phép viết toán tử so sánh liên tiếp như viết bất đẳng thức trong toán học. Đây là điểm Python đẹp hơn hẳn JavaScript/Dart.

Chain comparison
x = 7

# Cách Pythonic — đọc như toán
if 0 < x < 10:
    print("x trong khoảng (0, 10)")

# Tương đương với:
if 0 < x and x < 10:
    ...

# NHƯNG khác ở chỗ x chỉ được evaluate 1 LẦN
# Quan trọng nếu x là expression đắt như f() hoặc có side effect
if 0 < expensive_call() < 100:  # gọi 1 lần
    ...

# Có thể chain nhiều hơn 2
if 0 <= score <= 100:
    grade = "valid"

# Trộn nhiều toán tử cũng OK
a, b, c = 1, 2, 3
print(a < b < c)        # True
print(a < b == 2)       # True (a<b AND b==2)
print(a == 1 < 10 > 3) # True
🔥 Bẫy nhẹ — đừng quá đà

Chain quá phức tạp khó đọc:

# Khó đọc — đừng chain chain kiểu này
if a < b < c < d < e < f: ...

# Dễ đọc hơn — tách
if a < b and b < c < d and d < e < f: ...

15. Type hints — gợi ý kiểu (không bắt buộc)

Từ Python 3.5 (PEP 484), Python hỗ trợ type hints — gợi ý kiểu cho biến và function. Điểm quan trọng: runtime KHÔNG enforce. Type hints chỉ giúp IDE, linter, và type checker như mypy/pyright bắt lỗi trước khi chạy.

Type hints cơ bản
# Biến với hint
name: str = "An"
age: int = 25
prices: list[float] = [19.99, 29.99]

# Function với hint
def greet(name: str, age: int) -> str:
    return f"Xin chào {name}, {age} tuổi"

# Type không bị enforce ở runtime
age: int = "hai-mươi-lăm"   # KHÔNG lỗi! chạy bình thường
print(age)               # "hai-mươi-lăm"

# Nhưng mypy/pyright sẽ bắt:
# error: Incompatible types in assignment (str, expected int)
💡 Khuyến nghị — type hints cho code mới 2026
  • Code thư viện public, code có nhiều người dùng: luôn hint.
  • Script một lần dùng: tuỳ thích.
  • Đặt tool: mypy --strict hoặc pyright trong CI để catch sai sót.
  • Chương 12 sẽ đào sâu: Optional, Union, Generic, TypedDict, Protocol.

Bài tập

Bài 1 — Format tiền theo locale

Viết hàm format_money(amount: float, currency: str) -> str trả về chuỗi tiền tệ theo định dạng locale. Hỗ trợ ít nhất 2 ký hiệu:

  • "USD" → định dạng kiểu Mỹ: $1,234.56
  • "EUR" → định dạng kiểu Châu Âu: 1.234,56 €

Test:

format_money(1234.56, "USD")   # "$1,234.56"
format_money(1234.56, "EUR")   # "1.234,56 €"
format_money(0.5, "USD")       # "$0.50"

Yêu cầu: dùng f-string với format spec, không dùng thư viện ngoài.

Đáp án mẫu
def format_money(amount: float, currency: str) -> str:
    if currency == "USD":
        return f"${amount:,.2f}"
    elif currency == "EUR":
        # Trick: format kiểu US trước, rồi swap , ↔ .
        us = f"{amount:,.2f}"
        eu = us.replace(",", "X").replace(".", ",").replace("X", ".")
        return f"{eu} €"
    else:
        raise ValueError(f"Unsupported currency: {currency}")

Format spec :,.2f tách:

  • , — dùng dấu phẩy phân cách hàng nghìn
  • .2f — 2 chữ số sau dấu thập phân

Bài 2 — Mutable default trap

Đọc code và đoán output trước khi chạy:

def add_item(item, items=[]):
    items.append(item)
    return items

print(add_item("a"))
print(add_item("b"))
print(add_item("c"))

Sau đó: giải thích vì sao có bug, và sửa lại để hàm hoạt động đúng (mỗi lần gọi không tham số → list rỗng mới).

Đáp án + fix

Output thực tế:

['a']
['a', 'b']
['a', 'b', 'c']

Lý do: default argument được evaluate một lần duy nhất khi function được định nghĩa, không phải mỗi lần gọi. Cái [] trở thành một list shared cho mọi lần gọi không pass items.

Fix idiomatic:

def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

Dùng None làm sentinel — immutable singleton, không bị aliasing. Đây là pattern Python chuẩn cho mutable default. Ruff rule B006 sẽ cảnh báo bug này.

Bài 3 — Integer caching

Đoán output (mà không chạy):

a = 256
b = 256
print(a is b)        # ?

a = 257
b = 257
print(a is b)        # ?

# Trong file .py (không phải REPL)
def test():
    a = 257
    b = 257
    return a is b
print(test())

Giải thích kết quả.

Đáp án

Output:

True
False   # (REPL) hoặc True (file đơn lẻ — tuỳ tối ưu peephole)
True    # trong function, compiler có thể intern

Lý do: CPython cache (intern) sẵn các số nguyên trong khoảng [-5, 256] — bất cứ int nào trong range này đều trỏ vào cùng object. 257 trở lên thì không, mỗi lần là object mới.

Bài học: is dùng để check identity, không dùng để check value. Detail này là implementation-specific của CPython và có thể đổi giữa các phiên bản. Luôn dùng == để so sánh giá trị, is chỉ với None / sentinel.

Bài 4 — is_palindrome

Viết hàm is_palindrome(s: str) -> bool trả True nếu chuỗi đối xứng, bỏ qua chữ hoa/thường và ký tự không phải chữ-số.

Test cases:

is_palindrome("radar")                              # True
is_palindrome("A man a plan a canal Panama")       # True
is_palindrome("hello")                              # False
is_palindrome("Race car!")                          # True
is_palindrome("")                                   # True (chuỗi rỗng)
Đáp án + idiomatic
def is_palindrome(s: str) -> bool:
    cleaned = [c.lower() for c in s if c.isalnum()]
    return cleaned == cleaned[::-1]

Giải thích:

  • c.isalnum() — chỉ giữ chữ và số, bỏ space và dấu câu.
  • c.lower() — normalize chữ hoa/thường.
  • cleaned[::-1] — slice với step=-1 = đảo ngược list.
  • == trên list so sánh element-wise.

Phiên bản tiết kiệm memory hơn (không tạo list đảo):

def is_palindrome(s: str) -> bool:
    cleaned = [c.lower() for c in s if c.isalnum()]
    i, j = 0, len(cleaned) - 1
    while i < j:
        if cleaned[i] != cleaned[j]:
            return False
        i += 1
        j -= 1
    return True

Bài 5 — compare với chain comparison

Viết hàm compare(a, b) -> int trả:

  • -1 nếu a < b
  • 0 nếu a == b
  • 1 nếu a > b

Yêu cầu: viết 1 dòng duy nhất (tận dụng chain comparison + bool == int).

compare(3, 5)        # -1
compare(5, 5)        # 0
compare(9, 2)        # 1
Đáp án
def compare(a, b) -> int:
    return (a > b) - (a < b)

Giải thích:

  • a > b trả True (=1) hoặc False (=0).
  • a < b trả True hoặc False.
  • Trừ 2 boolean: kết quả là 1, 0, hoặc -1.

Đây là idiom kinh điển trước Python 3 — module functools còn có cmp_to_key để bridge với key-based sort. Cũng minh hoạ rõ "bool là int".

Quiz

Q1

True + True trong Python trả về gì?

Xem đáp án
✓ Đáp án

2. bool là subclass của int, nên True == 1 và False == 0. True + True = 1 + 1 = 2. Loại trả về là int (không phải bool nữa). Đây là idiom phổ biến để đếm số phần tử thoả điều kiện trong list.

Q2

[] == [] và [] is [] trả gì?

Xem đáp án
✓ Đáp án

[] == [] → True (cùng nội dung, cùng rỗng).
[] is [] → False — mỗi [] tạo một list MỚI trong RAM, hai object khác nhau dù trông giống.

Đây là minh hoạ kinh điển: == hỏi "giá trị có bằng?", is hỏi "có phải cùng 1 object?". Chỉ singleton như None, True, False mới đáng so sánh bằng is.

Q3

"abc" * 3 trả gì? Vì sao?

Xem đáp án
✓ Đáp án

'abcabcabc'. Python overload toán tử * cho string — nó lặp lại chuỗi n lần. Cũng áp dụng cho list: [0] * 5 → [0, 0, 0, 0, 0].

Cẩn thận với [[]] * 3 = [[], [], []] — cả 3 list rỗng đều là cùng object (vấn đề aliasing). Dùng [[] for _ in range(3)] để có 3 list riêng biệt.

Q4

Vì sao x = 0.1 + 0.2; x == 0.3 trả False?

Xem đáp án
✓ Đáp án

Vì float dùng IEEE 754 — không biểu diễn chính xác được 0.1 hay 0.2 trong binary (cả 2 là phân số tuần hoàn vô hạn). Cộng 2 số xấp xỉ → kết quả là 0.30000000000000004, khác 0.3 (cũng xấp xỉ).

Cách so sánh đúng:

  • math.isclose(0.1 + 0.2, 0.3) — dùng tolerance hợp lý.
  • Decimal("0.1") + Decimal("0.2") == Decimal("0.3") — chính xác từng chữ số.
Q5

len("café") trả gì trong Python 3?

Xem đáp án
✓ Đáp án

4. Python 3 str là Unicode, len() đếm code point — c, a, f, é là 4 ký tự.

Trong Python 2, "café" mặc định là bytes UTF-8, len trả 5 (ký tự é chiếm 2 byte). Đây là một trong những lý do lớn nhất khiến cộng đồng migrate sang Python 3.

Gotcha: với emoji hợp thành như "🇻🇳" (flag = 2 regional indicator), len có thể trả 2 — vì đếm theo code point, không phải "ký tự cảm nhận". Đếm grapheme cluster cần thư viện grapheme.

Q6

Biểu thức 0 < x < 10 < y có hợp lệ trong Python không?

Xem đáp án
✓ Đáp án

Có. Python hỗ trợ chain comparison với nhiều mắt xích. Biểu thức tương đương (0 < x) and (x < 10) and (10 < y), nhưng các giá trị giữa (x, 10) chỉ được evaluate 1 lần — quan trọng nếu chúng là function call có side effect.

Đây là điểm Python đẹp hơn JavaScript/Dart — viết bất đẳng thức như trong toán học. Tuy nhiên không nên chain quá 3 mắt vì khó đọc.

Q7

Code x: int = "5" ở runtime Python có gặp lỗi không?

Xem đáp án
✓ Đáp án

Không có lỗi runtime! Type hints (: int) không được Python interpreter enforce. Code chạy bình thường, x mang giá trị chuỗi "5".

Để bắt được lỗi loại này, cần chạy type checker tĩnh như mypy hoặc pyright. Lệnh mypy script.py sẽ báo:

error: Incompatible types in assignment
       (expression has type "str", variable has type "int")

Triết lý Python: type hints là contract giữa dev với dev, không phải bộ phận an toàn runtime. Chương 12 đào sâu typing với Optional, Union, Generic, TypedDict...

Tổng kết

Qua chương 2, bạn đã nắm:

  • Khai báo biến không cần keyword, dùng snake_case, hằng số là convention UPPER_CASE.
  • int độ chính xác vô hạn — siêu năng lực Python; trade-off chậm hơn khi số rất lớn.
  • float IEEE 754 — biết khi nào chuyển sang Decimal (tiền) hoặc Fraction (phân số chính xác).
  • str Unicode mặc định, immutable, slicing mạnh, hàng tá methods.
  • f-string với format spec đầy đủ — vũ khí chính để hiển thị data.
  • bool là subclass int, None là singleton — luôn check is None.
  • Immutable vs mutable — chia 2 phe, ảnh hưởng pass-by-reference, hashable, dict key.
  • Reference semantics — biến là tên trỏ object; copy.copy vs copy.deepcopy khi cần.
  • == vs is — luôn dùng ==, is chỉ với None/sentinel.
  • in đa năng — O(1) trên set/dict, O(n) trên list.
  • Chain comparison — viết bất đẳng thức như toán học.
  • Type hints — không enforce runtime, nhưng cần thiết cho code chuyên nghiệp.

Kết nối

  • Chương 3 (Data Structures) — đào sâu list, tuple, dict, set + comprehensions.
  • Chương 12 (Type Hints) — từ hint cơ bản tới Optional, Generic, Protocol, mypy strict mode.
  • JavaScript Chương 2 — đối chiếu type coercion JS (kẹt rất nhiều "wat") vs strong typing Python.
  • Dart Chương 2 — đối chiếu sound null safety Dart vs None convention của Python.