Mục lục
- Mục tiêu bài học
- Vì sao cần hàm?
- Cú pháp def
- return và giá trị None
- Tham số (parameter) vs đối số (argument)
- Positional vs keyword argument
- Default parameter value
- Trả nhiều giá trị qua tuple unpacking
- Docstring
- Type hints cơ bản
- Scope: biến local vs global
- Pitfall: mutable default argument
- Bài tập
- Tóm tắt
Mục tiêu bài học
Sau bài này bạn sẽ:
- Viết được hàm Python với
def, biết khi nào nên tách logic thành hàm. - Phân biệt parameter (trong định nghĩa) và argument (lúc gọi).
- Dùng được positional argument, keyword argument và default value.
- Trả nhiều giá trị bằng tuple unpacking.
- Biết viết docstring và type hints cơ bản.
- Hiểu sơ scope local / global và tránh pitfall mutable default argument.
Bài chạy trên Python 3.8 trở lên.
Vì sao cần hàm?
Khi một đoạn logic được dùng ở nhiều chỗ, copy-paste nhiều lần sẽ dẫn đến 3 vấn đề:
- Trùng lặp: sửa 1 chỗ thì các chỗ khác vẫn còn bug.
- Khó đọc: chương trình dài lê thê, người đọc phải lội từng dòng.
- Khó test: không có ranh giới rõ ràng để chạy thử từng phần.
Hàm là cách đặt tên cho một đoạn logic, dùng lại bằng cách gọi tên đó. Nguyên tắc này gọi là DRY — Don't Repeat Yourself.
Ví dụ tính BMI (chỉ số khối cơ thể) cho 3 người mà không có hàm:
# Không có hàm — code trùng lặp
bmi_1 = 70 / (1.75 ** 2)
bmi_2 = 60 / (1.65 ** 2)
bmi_3 = 80 / (1.80 ** 2)
print(bmi_1, bmi_2, bmi_3)
Khi đổi công thức (ví dụ chuyển sang đơn vị Mỹ) phải sửa 3 chỗ. Có hàm thì chỉ sửa 1 chỗ:
def bmi(weight_kg, height_m):
return weight_kg / (height_m ** 2)
print(bmi(70, 1.75))
print(bmi(60, 1.65))
print(bmi(80, 1.80))
Cú pháp def
Hàm khai báo bằng từ khoá def:
def function_name(param1, param2):
# body — phần thân hàm, thụt vào 4 spaces
result = param1 + param2
return result
Các thành phần:
def— từ khoá khai báo.function_name— tên hàm, viết snake_case theo PEP 8.(param1, param2)— danh sách tham số trong dấu ngoặc đơn (có thể rỗng).:— dấu hai chấm bắt buộc, kết thúc dòng khai báo.- Body thụt vào (4 spaces) — là phần code chạy khi hàm được gọi.
Đặt tên hàm nên là động từ mô tả việc hàm làm:
# Tốt — động từ rõ nghĩa
def calculate_bmi(weight, height): ...
def load_dataset(path): ...
def is_valid_email(text): ...
# Tệ — danh từ chung chung, không rõ làm gì
def data(x): ...
def thing(): ...
Hàm chỉ được thực thi khi bị gọi:
def greet():
print("Xin chào")
# Đến đây chưa in gì cả — chỉ mới định nghĩa
greet() # giờ mới chạy → in "Xin chào"
return và giá trị None
return trả một giá trị về cho nơi gọi hàm. Sau khi return chạy, hàm dừng ngay — các dòng sau không được thực thi.
def square(x):
return x * x
print("Dòng này không bao giờ chạy") # sau return → bị bỏ qua
result = square(5)
print(result) # 25
Nếu hàm không có return (hoặc return không kèm giá trị), Python ngầm trả về None:
def shout(text):
print(text.upper()) # in ra, nhưng không trả về gì
value = shout("hello")
print(value) # None
print(type(value)) # <class 'NoneType'>
Phân biệt 2 việc khác nhau:
print(x)— hiển thị ra màn hình, dành cho người đọc.return x— truyền giá trị ra ngoài hàm, dành cho code tiếp tục xử lý.
Có thể có nhiều return trong cùng một hàm (early return):
def absolute(x):
if x >= 0:
return x
return -x
print(absolute(-7)) # 7
Tham số (parameter) vs đối số (argument)
Hai từ này thường bị dùng lẫn nhưng có ý nghĩa khác:
- Parameter (tham số) — tên trong dấu ngoặc của
def. Đây là biến chỉ tồn tại bên trong hàm. - Argument (đối số) — giá trị thực được truyền vào khi gọi hàm.
def add(a, b): # a, b là parameter
return a + b
x = 3
y = 5
add(x, y) # x, y (giá trị 3 và 5) là argument
add(10, 20) # 10, 20 cũng là argument
Nói cách khác: khi định nghĩa hàm, bạn dùng parameter; khi gọi hàm, bạn truyền argument.
Positional vs keyword argument
Python cho phép truyền argument theo 2 cách:
Positional argument — truyền theo đúng thứ tự parameter trong định nghĩa:
def divide(a, b):
return a / b
divide(10, 2) # a=10, b=2 → 5.0
divide(2, 10) # a=2, b=10 → 0.2 (thứ tự quan trọng!)
Keyword argument — gọi tên parameter rõ ràng, thứ tự không quan trọng:
divide(a=10, b=2) # 5.0
divide(b=2, a=10) # 5.0 — đổi thứ tự vẫn đúng
Kết hợp được 2 kiểu, nhưng positional phải đứng trước keyword:
divide(10, b=2) # OK
# divide(a=10, 2) # SyntaxError: positional argument follows keyword argument
Keyword argument thường dùng khi hàm có nhiều tham số — đọc rõ ý hơn:
# Khó đọc — không rõ 4 số này là gì
train_model(0.001, 32, 100, True)
# Rõ ràng hơn nhiều
train_model(learning_rate=0.001, batch_size=32, epochs=100, verbose=True)
Default parameter value
Có thể cho parameter một giá trị mặc định. Khi gọi hàm, argument đó trở thành tuỳ chọn:
def greet(name="World"):
return f"Hello, {name}!"
print(greet()) # Hello, World!
print(greet("Alice")) # Hello, Alice!
print(greet(name="Bob")) # Hello, Bob!
Quy tắc thứ tự: parameter có default phải đứng sau parameter không có default:
# OK
def power(base, exponent=2):
return base ** exponent
# SyntaxError — non-default argument follows default argument
# def power(base=2, exponent):
# return base ** exponent
Default value rất phổ biến trong ML framework: train(epochs=10, lr=0.001, optimizer="adam") — người dùng chỉ cần truyền tham số nào thực sự muốn đổi.
Trả nhiều giá trị qua tuple unpacking
Hàm Python về kỹ thuật chỉ trả về một giá trị. Nhưng giá trị đó có thể là tuple chứa nhiều phần — và bạn có thể unpack ra nhiều biến ngay lúc nhận:
def stats(numbers):
mean = sum(numbers) / len(numbers)
variance = sum((x - mean) ** 2 for x in numbers) / len(numbers)
std = variance ** 0.5
return mean, std # về bản chất trả tuple (mean, std)
avg, sd = stats([1, 2, 3, 4, 5])
print(avg) # 3.0
print(sd) # 1.4142135623730951
Cũng có thể bỏ unpack, nhận nguyên tuple:
result = stats([1, 2, 3, 4, 5])
print(result) # (3.0, 1.4142135623730951)
print(type(result)) # <class 'tuple'>
Khi không quan tâm một giá trị nào đó, dùng dấu gạch dưới _:
avg, _ = stats([1, 2, 3, 4, 5]) # chỉ lấy mean
print(avg)
Pattern này hay gặp trong scikit-learn / PyTorch khi hàm trả về nhiều phần (loss, accuracy), hoặc khi split dữ liệu: X_train, X_test, y_train, y_test = train_test_split(...).
Docstring
Docstring là chuỗi tài liệu đặt ngay dưới dòng def, viết trong nháy ba """...""". Nó giải thích hàm làm gì, nhận tham số nào, trả gì.
def bmi(weight_kg, height_m):
"""Tính BMI (Body Mass Index).
Args:
weight_kg: cân nặng tính bằng kg.
height_m: chiều cao tính bằng mét.
Returns:
BMI dạng float, theo công thức weight / height^2.
"""
return weight_kg / (height_m ** 2)
Khác với comment #, docstring được lưu vào thuộc tính __doc__ và là cái mà help() đọc ra:
print(bmi.__doc__)
help(bmi)
IDE (VS Code, PyCharm) và Jupyter / Colab cũng hiển thị docstring khi bạn hover hoặc gõ ? sau tên hàm. Viết docstring là cách rẻ nhất để chính bạn 3 tháng sau hiểu được code của mình.
Type hints cơ bản
Từ Python 3.5, có thể ghi chú kiểu cho parameter và giá trị trả về — gọi là type hints:
def add(x: int, y: int) -> int:
return x + y
def bmi(weight_kg: float, height_m: float) -> float:
return weight_kg / (height_m ** 2)
def greet(name: str = "World") -> str:
return f"Hello, {name}!"
Cú pháp:
param: Type— kiểu của parameter.-> Typesau dấu ngoặc — kiểu trả về.
Quan trọng: Python không enforce type hints lúc runtime. Bạn có thể truyền add("1", "2") và nó vẫn chạy ra "12" (string concatenation). Type hints chỉ phục vụ:
- Người đọc — biết hàm mong đợi kiểu nào.
- IDE — autocomplete tốt hơn, gạch đỏ khi truyền sai kiểu.
- Static type checker như
mypy,pyright— kiểm tra trước khi chạy.
Trong code AI / ML, type hints rất hữu ích vì hàm thường nhận tensor, ndarray, DataFrame với shape phức tạp. Các bài sau sẽ gặp các kiểu phức tạp hơn như list[int], dict[str, float], Optional[int].
Scope: biến local vs global
Biến tạo bên trong hàm là local — chỉ tồn tại khi hàm đang chạy:
def compute():
result = 42 # biến local
return result
compute()
# print(result) # NameError: name 'result' is not defined
Biến tạo ở ngoài (top-level của file) là global — đọc được từ trong hàm:
PI = 3.14159 # global
def area(radius):
return PI * radius ** 2 # đọc PI từ scope ngoài
print(area(2)) # 12.56636
Lưu ý: gán trong hàm tạo ra biến local mới, không sửa biến global trùng tên:
count = 0
def increment():
count = count + 1 # UnboundLocalError — Python coi count là local nhưng chưa có giá trị
Đây mới chỉ là phần đầu. Quy tắc đầy đủ (LEGB: Local → Enclosing → Global → Built-in) và từ khoá global / nonlocal sẽ học ở bài về scope sau. Lúc đầu chỉ cần nhớ: cố gắng không sửa biến global từ trong hàm, thay vào đó nhận input qua parameter và trả output qua return.
Pitfall: mutable default argument
Đây là một trong những bẫy hay gặp nhất của Python. Đoạn code sau không chạy như bạn nghĩ:
def append_item(item, items=[]):
items.append(item)
return items
print(append_item(1)) # [1]
print(append_item(2)) # [1, 2] — KHÔNG phải [2]!
print(append_item(3)) # [1, 2, 3]
Nguyên nhân: default value [] được tạo MỘT lần tại lúc định nghĩa hàm, không phải mỗi lần gọi. Do đó tất cả lời gọi không truyền items đều chia sẻ cùng một list. Quy tắc này áp dụng cho mọi mutable object (list, dict, set).
Cách an toàn: dùng None làm sentinel, tạo list mới bên trong:
def append_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
print(append_item(1)) # [1]
print(append_item(2)) # [2] — đúng như mong đợi
print(append_item(3)) # [3]
Với immutable default value (int, float, str, tuple, None, bool) thì không gặp lỗi này — chúng không thể bị sửa tại chỗ.
Bài tập
Bài 1: Hàm tính BMI và phân loại.
Viết hàm bmi(weight_kg, height_m) trả về chỉ số BMI. Sau đó viết hàm bmi_category(bmi_value) trả về chuỗi phân loại theo chuẩn WHO:
- BMI < 18.5 →
"underweight" - 18.5 ≤ BMI < 25 →
"normal" - 25 ≤ BMI < 30 →
"overweight" - BMI ≥ 30 →
"obese"
Yêu cầu: có docstring, có type hints. Test với (70, 1.75) và (90, 1.70).
Bài 2: Chuyển đổi nhiệt độ C ↔ F.
Viết 2 hàm:
c_to_f(celsius)— công thứcF = C * 9/5 + 32.f_to_c(fahrenheit)— công thứcC = (F - 32) * 5/9.
Sau đó viết hàm convert(value, to="F") — nếu to="F" thì coi value là Celsius và chuyển sang Fahrenheit, nếu to="C" thì ngược lại. Test:
print(convert(100)) # 212.0
print(convert(32, to="C")) # 0.0
print(convert(0)) # 32.0
Bài 3 (mở rộng): viết hàm min_max(numbers) trả về tuple (min, max) của list số mà không dùng built-in min() / max(). Gợi ý: duyệt list bằng vòng for và cập nhật 2 biến.
Tóm tắt
- Hàm tách logic dùng lại, theo nguyên tắc DRY; khai báo bằng
def name(params):, tên snake_case. returntrả giá trị và kết thúc hàm; không córeturn→ hàm trảNone.- Parameter là tên trong định nghĩa; argument là giá trị truyền vào lúc gọi.
- Có thể gọi bằng positional hoặc keyword argument — positional phải đứng trước keyword.
- Default value giúp argument đó trở thành tuỳ chọn; parameter có default phải đặt sau parameter không có default.
- Trả nhiều giá trị thực chất là trả một tuple, kết hợp với unpacking
a, b = f(). - Docstring
"""..."""ngay saudefgiúphelp(), IDE hiển thị tài liệu. - Type hints
def add(x: int, y: int) -> intkhông enforce runtime nhưng tốt cho IDE vàmypy. - Biến trong hàm là local; tránh sửa biến global, ưu tiên truyền qua parameter và trả qua
return. - Không dùng mutable default argument (
items=[]) — dùngNonerồi tạo mới bên trong hàm.
Bài tiếp theo sẽ học list — kiểu dữ liệu chứa nhiều phần tử, nền tảng cho hầu hết thao tác dữ liệu trong Python.
