Cơ bản

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

Cập nhật: · Skillaria.Top

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 <= 0:
        return 0
    if not is_member:
        return price
    return price * 0.9

Tham số của hàm trong Python: vị trí và có tên

Tham số là các tên trong khai báo hàm, còn đối số là giá trị truyền vào khi gọi. Có thể truyền đối số theo thứ tự (vị trí) hoặc theo tên. Đối số có tên làm lời gọi tự giải thích, và thứ tự không còn quan trọng.

def add_expense(title, amount, category="khác"):
    return {"title": title, "amount": amount, "category": category}

add_expense("Cà phê", 250)                         # theo vị trí
add_expense(amount=250, title="Cà phê")            # theo tên
add_expense("Taxi", 600, category="đi lại")        # kết hợp

Tham số có giá trị mặc định là tùy chọn. Quy tắc: tham số có mặc định phải đứng sau các tham số bắt buộc, còn khi gọi, đối số vị trí phải đứng trước đối số có tên. Nếu không, Python báo SyntaxError.

Cạm bẫy: giá trị mặc định là đối tượng có thể thay đổi

Giá trị mặc định được tính một lần — tại thời điểm chạy dòng def, chứ không phải mỗi lần gọi hàm. Nếu đó là list hoặc dict, nó sẽ được dùng chung cho mọi lần gọi.

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

print(add_item(1))  # [1]
print(add_item(2))  # [1, 2] — list còn nhớ lần gọi trước

Cách làm đúng là dùng None làm mặc định và tạo đối tượng mới bên trong hàm:

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

*args và **kwargs: số lượng đối số tùy ý

Dấu sao trước tham số gom tất cả đối số vị trí thừa thành một tuple (tuple), còn hai dấu sao gom các đối số có tên thừa thành một dict (dict). Tên args và kwargs chỉ là quy ước, quan trọng là dấu sao.

def total(*args):
    return sum(args)

print(total(100, 250, 40))   # 390

def make_user(name, **kwargs):
    user = {"name": name}
    user.update(kwargs)
    return user

print(make_user("Lan", city="Hà Nội", age=30))
# {'name': 'Lan', 'city': 'Hà Nội', 'age': 30}

Các dấu sao cũng hoạt động theo chiều ngược lại — khi gọi hàm, chúng giải nén một dãy hoặc dict thành các đối số:

values = [3, 4]
print(pow(*values))          # 81

opts = {"sep": " | ", "end": "\n"}
print("a", "b", **opts)      # a | b

Tham số chỉ theo vị trí và chỉ theo tên

Dấu gạch chéo / và dấu sao * đứng trong danh sách tham số hoạt động như rào chắn: mọi tham số trước / chỉ truyền được theo thứ tự (cú pháp có từ Python 3.8, PEP 570), còn mọi tham số sau * chỉ truyền được theo tên.

def convert(amount, /, *, rate, currency="USD"):
    return round(amount / rate, 2), currency

convert(100, rate=90.5)          # (1.1, 'USD')
convert(amount=100, rate=90.5)   # TypeError: amount chỉ truyền theo vị trí
convert(100, 90.5)               # TypeError: rate chỉ truyền theo tên

Tham số chỉ theo tên giúp tránh nhầm lẫn kiểu convert(100, 90.5, "EUR"), khi không rõ mỗi giá trị là gì.

Phạm vi biến: quy tắc LEGB

Python tìm tên biến theo chuỗi: Local (trong hàm) → Enclosing (trong hàm bao ngoài) → Global (cấp module) → Built-in (tên tích hợp như len). Trong hàm có thể đọc biến toàn cục, nhưng để gán giá trị mới cho nó cần khai báo global.

limit = 1000

def check(amount):
    return amount > 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.

deflambda
Têncókhông (<lambda>)
Thânbất kỳ số lệnh nàomột biểu thức
returnviết rõ ràngngầm định
Docstring, chú thíchcókhông
Phù hợp khimọi trường hợpkey 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.

Câu hỏi thường gặp

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.

Học chủ đề này qua video

Chủ đề này được giảng chi tiết kèm thực hành trong khóa học “Python 3.15: от новичка до профи”.

Đến khóa học