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 ==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.Decimalfractions.Fraction.
  • Thành thạo str Unicode + slicing + methods + f-string với format spec.
  • Hiểu bool là subclass của intNone 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. bool — subclass của int

Đây là chi tiết "kì cục nhưng tiện" của Python: TrueFalse 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

6.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ì")

7. 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: ...

8. 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]

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

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",
}

9. 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.

9.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]]

10. == 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 ==.

11. 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

11.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
    ...

12. 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: ...

13. 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 9 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 == 1False == 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

[] == [][] 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

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 pointc, 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 9 đà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 9 (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.