# Hàm trong Python: def, return, tham số, *args và **kwargs, lambda và chú thích kiểu

URL: https://skillaria.top/vi/guides/python/ham-python

Hàm trong Python là khối code có tên, khai báo bằng từ khóa def, được gọi theo tên kèm dấu ngoặc và trả kết quả qua return; không có return thì hàm trả về None. Đối số truyền theo thứ tự hoặc theo tên, *args gom các giá trị vị trí thừa thành tuple, **kwargs gom các tham số có tên thành dict, còn lambda tạo hàm ẩn danh ngắn từ một biểu thức.

Hàm trong Python là một đoạn code có tên, có thể gọi nhiều lần với dữ liệu đầu vào khác nhau. Hàm được khai báo bằng từ khóa `def`, nhận giá trị qua các tham số và trả kết quả bằng `return`. Hàm là cách chính để biến một đoạn script dài thành các bước rõ ràng, dễ kiểm tra.

## Cách khai báo hàm: def trong Python

Khai báo hàm bắt đầu bằng `def`, sau đó là tên, danh sách tham số trong ngoặc và dấu hai chấm. Thân hàm được viết thụt vào bốn khoảng trắng — thụt đầu dòng trong Python không phải trang trí, mà là một phần của cú pháp.

```
def greet(name):
    """Trả về lời chào cho người dùng."""
    return f"Xin chào, {name}!"

print(greet("Lan"))   # Xin chào, Lan!
```

Chuỗi nhiều dòng trong ba dấu nháy ngay sau dòng tiêu đề là docstring — tài liệu của hàm. Lệnh `help(greet)` và gợi ý của trình soạn thảo sẽ hiển thị nó. Theo quy ước PEP 8, tên hàm viết dạng snake_case: `add_expense`, `get_rate`.

Dòng `def greet(name):` được gọi là chữ ký (signature) của hàm: tên cộng với danh sách tham số (và chú thích kiểu nếu có). Nhìn vào chữ ký, người đọc hiểu cách gọi hàm mà không cần xem thân hàm.

## return trong Python làm gì

`return` kết thúc hàm và trả giá trị về cho nơi đã gọi nó. Code phía sau một `return` đã thực thi sẽ không chạy. Nếu không có `return`, hoặc có nhưng không kèm giá trị, hàm trả về `None`.

```
def area(w, h):
    w * h          # kết quả được tính rồi bị bỏ đi

print(area(3, 4))  # None

def area(w, h):
    return w * h

print(area(3, 4))  # 12
```

Quên `return` là một trong những lỗi phổ biến nhất của người mới: hàm vẫn «chạy», nhưng trả về `None`, và lỗi chỉ lộ ra ở dòng sau. Hàm cũng có thể trả về nhiều giá trị — thực chất đó là một tuple, và có thể giải nén dễ dàng:

```
def min_max(numbers):
    return min(numbers), max(numbers)

low, high = min_max([4, 1, 9])
print(low, high)   # 1 9
```

Dùng `return` sớm giúp tránh lồng nhau quá sâu: xử lý các trường hợp đặc biệt trước, rồi viết logic chính.

```
def discount(price, is_member):
    if price  limit    # đọc biến toàn cục — được phép

def raise_limit():
    global limit
    limit = 2000             # không có global thì sẽ tạo biến cục bộ mới
```

Trong thực tế nên tránh `global`: hàm thay đổi trạng thái bên ngoài thì khó kiểm thử. Cách tốt hơn là nhận giá trị qua tham số và trả kết quả về.

## Hàm lồng nhau và closure

Có thể khai báo hàm bên trong một hàm khác. Hàm con nhìn thấy các biến của hàm cha ngay cả khi hàm cha đã kết thúc — đó chính là closure. Để sửa một biến như vậy, dùng `nonlocal`.

```
def make_counter():
    count = 0
    def counter():
        nonlocal count
        count += 1
        return count
    return counter

c = make_counter()
print(c(), c(), c())   # 1 2 3
```

Closure là nền tảng của decorator và các hàm tạo hàm.

## Hàm cũng là một đối tượng

Trong Python, hàm là một đối tượng thông thường: có thể gán vào biến, truyền vào hàm khác, trả về từ hàm, lưu trong dict. Hàm có các thuộc tính như `__name__`, `__doc__`, `__defaults__`, `__annotations__`.

```
def double(x):
    return x * 2

f = double
print(f(5), f.__name__)                # 10 double
print(list(map(double, [1, 2, 3])))   # [2, 4, 6]
```

## Lambda trong Python: hàm ẩn danh

Lambda là một hàm ngắn không có tên, thân chỉ gồm một biểu thức. Kết quả của biểu thức được trả về tự động, không cần viết `return`.

```
square = lambda x: x ** 2
print(square(4))   # 16

expenses = [("Cà phê", 250), ("Taxi", 600), ("Cơm trưa", 450)]
expenses.sort(key=lambda e: e[1], reverse=True)
print(expenses)  # [('Taxi', 600), ('Cơm trưa', 450), ('Cà phê', 250)]

print(list(filter(lambda n: n % 2 == 0, range(10))))  # [0, 2, 4, 6, 8]
```

Hàm lambda phù hợp làm đối số ngắn cho `sorted`, `max`, `map`, `filter`. PEP 8 không khuyến khích gán lambda vào biến như `square` ở trên: nếu hàm cần có tên, hãy viết `def` — khi đó nó có `__name__` rõ ràng trong traceback và có chỗ cho docstring.

|  | def | lambda |
| --- | --- | --- |

| Tên | có | không (`<lambda>`) |

| Thân | bất kỳ số lệnh nào | một biểu thức |

| return | viết rõ ràng | ngầm định |

| Docstring, chú thích | có | không |

| Phù hợp khi | mọi trường hợp | key hoặc callback ngắn |

## Chú thích kiểu: gợi ý, không phải kiểm tra

Chú thích mô tả kiểu tham số và kết quả mong đợi. Bản thân Python không kiểm tra chúng khi chạy: hàm với `x: int` vẫn nhận chuỗi mà không báo lỗi. Việc kiểm tra do các công cụ bên ngoài đảm nhiệm — mypy, pyright, ty — và trình soạn thảo, nơi lỗi được gạch chân ngay trước khi chạy.

```
def add_expense(title: str, amount: float, category: str = "khác") -> dict[str, object]:
    return {"title": title, "amount": amount, "category": category}

def area(w: float, h: float) -> float:
    w * h   # mypy: Missing return statement
```

Chạy kiểm tra mà không cần cài đặt có thể dùng `uvx mypy .`. Ví dụ trên cho thấy lợi ích của chú thích: khi quên `return` với `-> float`, trình phân tích báo lỗi ngay.

Từ Python 3.12 (PEP 695), hàm generic được viết không cần `TypeVar`: tham số kiểu đặt trong ngoặc vuông, còn bí danh kiểu khai báo bằng từ khóa `type`.

```
def first[T](xs: list[T]) -> T:
    return xs[0]

type Money = int | float
```

Trong Python 3.14 (PEP 649), chú thích trở nên lười: chúng được tính khi lần đầu truy cập, nên có thể tham chiếu một class khai báo phía dưới mà không cần dấu nháy, và không còn cần `from __future__ import annotations`.

## «Không truyền» hay «truyền None»: sentinel trong 3.15

Giá trị mặc định `None` không cho phép phân biệt «đối số không được truyền» và «cố ý truyền None». Trước đây người ta giải quyết bằng một giá trị đánh dấu tự tạo:

```
_MISSING = object()

def get_rate(currency, default=_MISSING):
    rates = {"USD": 90.5}
    if currency in rates:
        return rates[currency]
    if default is _MISSING:
        raise KeyError(currency)
    return default
```

Python 3.15 bổ sung hàm tích hợp `sentinel` (PEP 661): `MISSING = sentinel("MISSING")` tạo ra một marker duy nhất có repr dễ hiểu, khi sao chép vẫn giữ nguyên là chính nó, được pickle và dùng được trong chú thích kiểu như `float | MISSING`. Trên các phiên bản cũ, cách dùng `object()` vẫn hoạt động.

## Lỗi thường gặp với hàm

- **Quên return** — hàm trả về `None`.
- **Quên dấu ngoặc khi gọi** — `print(greet)` in ra đối tượng hàm, chứ không phải kết quả.
- **Giá trị mặc định có thể thay đổi** — một list dùng chung cho mọi lần gọi.
- **Sai số lượng đối số** — `TypeError: greet() missing 1 required positional argument: 'name'`.
- **Đặt tên trùng với hàm tích hợp** — hàm tên `list` hoặc `sum` sẽ che mất hàm gốc.

## Tổng kết

Hàm trong Python được khai báo bằng `def`, trả kết quả bằng `return` và nhận đối số theo thứ tự hoặc theo tên. `*args` và `**kwargs` mang lại sự linh hoạt, `/` và `*` giúp lời gọi chặt chẽ, lambda phù hợp cho hàm dùng một lần và ngắn gọn, còn chú thích kiểu cùng mypy bắt lỗi trước khi chạy. Trong khóa học Python, chủ đề này được thực hành qua trình theo dõi chi tiêu: chương trình được chia thành các hàm có chú thích và được kiểm tra bằng mypy.

## FAQ

**Tham số khác đối số ở điểm nào?**

Tham số là tên trong khai báo hàm, còn đối số là giá trị cụ thể truyền khi gọi. Trong def greet(name), name là tham số; trong greet("Lan"), chuỗi "Lan" là đối số.

**Có thể gọi hàm trước khi khai báo nó không?**

Ở cấp module thì không: lệnh def phải chạy trước lời gọi, nếu không sẽ có NameError. Nhưng bên trong một hàm khác, bạn có thể gọi một hàm được khai báo phía dưới, miễn là lời gọi xảy ra sau khi khai báo đã thực thi.

**Một hàm có thể trả về nhiều giá trị không?**

Về hình thức là một giá trị, nhưng giá trị đó có thể là tuple: return a, b trả về một tuple hai phần tử, và có thể giải nén bằng x, y = f().

**Có bắt buộc viết chú thích kiểu không?**

Không, Python chạy được mà không cần chúng. Nhưng với code do nhiều người đọc, chú thích giúp bảo trì dễ hơn và cho phép mypy hoặc pyright tìm lỗi trước khi chạy.

**Trong lambda có thể dùng if không?**

Chỉ được dùng biểu thức điều kiện: lambda x: "chẵn" if x % 2 == 0 else "lẻ". Lệnh, vòng lặp và phép gán bên trong lambda không được phép.


---
Source: Skillaria.Top (https://skillaria.top)
