Danh sách bài viết

Bài 10: Hàm (Function): def, tham số, return

Học cách viết hàm trong Python: cú pháp def, tham số (parameter) vs đối số (argument), return, default value, keyword argument, docstring, type hints, scope cơ bản và pitfall mutable default argument.

24/05/2026
12 phút đọc
0 lượt xem
1

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.

2

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))
3

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"
4

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 xtruyề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
5

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.

6

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)
7

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.

8

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(...).

9

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.

10

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.
  • -> Type sau 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].

11

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.

12

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

13

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)(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ức F = C * 9/5 + 32.
  • f_to_c(fahrenheit) — công thức C = (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.

14

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.
  • return trả 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 sau def giúp help(), IDE hiển thị tài liệu.
  • Type hints def add(x: int, y: int) -> int khô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ùng None rồ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.