4. Thêm các công cụ điều khiển luồng dữ liệu

Bên cạnh câu lệnh while vừa được giới thiệu, Python còn sử dụng một số cấu trúc khác mà chúng ta sẽ tìm hiểu trong chương này.

4.1. Câu lệnh if

Có lẽ loại câu lệnh phổ biến nhất là câu lệnh if. Ví dụ:

>>> x = int(input("Vui lòng nhập một số nguyên: "))
Vui lòng nhập một số nguyên: 42
>>> if x < 0:
... x = 0
... print('Số âm đã được chuyển thành số không')
... elif x == 0:
... print('Số không')
... elif x == 1:
... print('Một')
... else:
... print('Nhiều hơn')
...
Nhiều hơn

Có thể có không hoặc nhiều phần elif, và phần else là tùy chọn. Từ khóa 'elif' là viết tắt của 'else if', và có ích trong việc tránh việc thụt đầu dòng quá mức. Một chuỗi if ... elif ... elif ... là sự thay thế cho các câu lệnh switch hoặc case trong các ngôn ngữ khác.

Nếu bạn đang so sánh cùng một giá trị với nhiều hằng số, hoặc kiểm tra các kiểu hay thuộc tính cụ thể, bạn cũng có thể thấy câu lệnh match hữu ích. Để biết thêm chi tiết, hãy xem Câu lệnh match.

4.2. Câu lệnh for

Câu lệnh for trong Python hơi khác một chút so với những gì bạn có thể đã quen thuộc trong C hoặc Pascal. Thay vì luôn lặp qua một cấp số cộng các con số (như trong Pascal), hoặc cho phép người dùng tự định nghĩa cả bước lặp và điều kiện dừng (như C), câu lệnh for của Python lặp qua các phần tử của bất kỳ chuỗi nào (một danh sách hoặc một chuỗi ký tự), theo thứ tự mà chúng xuất hiện trong chuỗi đó. Ví dụ (không có ý chơi chữ):

>>> # Đo chiều dài một số chuỗi:
>>> words = ['cat', 'window', 'defenestrate']
>>> for w in words:
... print(w, len(w))
...
cat 3
window 6
defenestrate 12

Việc viết mã thay đổi một tập hợp trong khi đang lặp qua chính tập hợp đó có thể rất khó để xử lý chính xác. Thay vào đó, thông thường sẽ đơn giản hơn nếu lặp qua một bản sao của tập hợp hoặc tạo một tập hợp mới:

 # Tạo một tập hợp mẫu
users = {'Hans': 'active', 'Éléonore': 'inactive', '景太郎': 'active'}

# Strategy:  Iterate over a copy
for user, status in users.copy().items():
    if status == 'inactive':
        del users[user]

# Chiến lược: Tạo một tập hợp mới
active_users = {}
for user, status in users.items():
    if status == 'active':
        active_users[user] = status

4.3. Hàm range()

Nếu bạn thực sự cần lặp qua một dãy số, hàm tích hợp range() sẽ rất hữu ích. Nó tạo ra các cấp số cộng:

>>> for i in range(5):
...     print(i)
...
0
1
2
3
4

Điểm kết thúc được đưa vào không bao giờ là một phần của chuỗi được tạo ra; range(10) tạo ra 10 giá trị, tương ứng với các chỉ số hợp lệ cho các phần tử của một chuỗi có độ dài 10. Ta cũng có thể cho phép dãy bắt đầu tại một số khác, hoặc chỉ định một bước tăng khác (thậm chí là số âm; đôi khi được gọi là 'bước lặp'):

>>> list(range(5, 10))
[5, 6, 7, 8, 9]

>>> list(range(0, 10, 3))
[0, 3, 6, 9]

>>> list(range(-10, -100, -30))
[-10, -40, -70]

Để lặp qua các chỉ số của một dãy, bạn có thể kết hợp range()len() như sau:

>>> a = ['Mary', 'had', 'a', 'little', 'lamb']
>>> for i in range(len(a)):
...     print(i, a[i])
...
0 Mary
1 had
2 a
3 little
4 lamb

Tuy nhiên, trong hầu hết các trường hợp như vậy, việc sử dụng hàm enumerate() sẽ thuận tiện hơn, xem Looping Techniques.

Một điều kỳ lạ sẽ xảy ra nếu bạn chỉ in một đối tượng range:

>>> range(10)
range(0, 10)

Trong nhiều khía cạnh, đối tượng được trả về bởi range() hoạt động như thể nó là một danh sách, nhưng thực tế không phải vậy. Nó là một đối tượng trả về các phần tử liên tiếp của dãy mong muốn khi bạn lặp qua nó, nhưng nó không thực sự tạo ra danh sách, do đó giúp tiết kiệm bộ nhớ.

Chúng ta gọi một đối tượng như vậy là iterable, nghĩa là nó phù hợp để làm đích cho các hàm và cấu trúc mong đợi một thứ gì đó mà từ đó chúng có thể lấy các phần tử liên tiếp cho đến khi nguồn cung cạn kiệt. Chúng ta đã thấy rằng câu lệnh for là một cấu trúc như vậy, trong khi một ví dụ về hàm nhận một iterable là sum():

>>> sum(range(4))  # 0 + 1 + 2 + 3
6

Sau này chúng ta sẽ thấy thêm nhiều hàm trả về các iterable và nhận iterable làm đối số. Trong chương Data Structures, chúng ta sẽ thảo luận chi tiết hơn về list().

4.4. Câu lệnh breakcontinue

Câu lệnh break thoát ra khỏi vòng lặp for hoặc while trong cùng:

>>> for n in range(2, 10):
...     for x in range(2, n):
...         if n % x == 0:
...             print(f"{n} equals {x} * {n//x}")
...             break
...
4 equals 2 * 2
6 equals 2 * 3
8 equals 2 * 4
9 equals 3 * 3

Câu lệnh continue chuyển sang lần lặp tiếp theo của vòng lặp:

>>> for num in range(2, 10):
... if num % 2 == 0:
... print(f"Tìm thấy một số chẵn {num}")
... continue
... print(f"Tìm thấy một số lẻ {num}")
...
Tìm thấy một số chẵn 2
Tìm thấy một số lẻ 3
Tìm thấy một số chẵn 4
Tìm thấy một số lẻ 5
Tìm thấy một số chẵn 6
Tìm thấy một số lẻ 7
Tìm thấy một số chẵn 8
Tìm thấy một số lẻ 9

4.5. Mệnh đề else trong vòng lặp

Trong một vòng lặp for hoặc while, câu lệnh break có thể được kết hợp với một mệnh đề else. Nếu vòng lặp kết thúc mà không thực thi break, mệnh đề else sẽ được thực thi.

Trong một vòng lặp for, mệnh đề else được thực thi sau khi vòng lặp hoàn thành lần lặp cuối cùng, nghĩa là khi không có lệnh break nào xảy ra.

Trong một vòng lặp while, nó được thực thi sau khi điều kiện của vòng lặp trở thành sai.

Trong cả hai loại vòng lặp, mệnh đề else không được thực thi nếu vòng lặp bị kết thúc bởi break. Tất nhiên, các cách kết thúc vòng lặp sớm khác, chẳng hạn như return hoặc một exception được gây ra, cũng sẽ bỏ qua việc thực thi mệnh đề else.

Điều này được minh họa trong vòng lặp for sau đây, dùng để tìm số nguyên tố:

>>> for n in range(2, 10):
...     for x in range(2, n):
...         if n % x == 0:
...             print(n, 'equals', x, '*', n//x)
...             break
...     else:
...         # loop fell through without finding a factor
...         print(n, 'is a prime number')
...
2 is a prime number
3 is a prime number
4 equals 2 * 2
5 is a prime number
6 equals 2 * 3
7 is a prime number
8 equals 2 * 4
9 equals 3 * 3

(Đúng vậy, đây là mã đúng. Hãy nhìn kỹ: mệnh đề else thuộc về vòng lặp for, không phải câu lệnh if.)

Một cách để nghĩ về mệnh đề else là hãy tưởng tượng nó đi kèm với if bên trong vòng lặp. Khi vòng lặp thực thi, nó sẽ chạy theo thứ tự như if/if/if/else. if ở bên trong vòng lặp, được gặp nhiều lần. Nếu điều kiện bất kỳ lúc nào là true, break sẽ xảy ra. Nếu điều kiện không bao giờ là true, mệnh đề else bên ngoài vòng lặp sẽ được thực thi.

Khi dùng với vòng lặp, mệnh đề else có nhiều điểm chung hơn với mệnh đề else của câu lệnh try so với câu lệnh if: mệnh đề else của câu lệnh try chạy khi không có exception xảy ra, và mệnh đề else của vòng lặp chạy khi không có break xảy ra. Để biết thêm về câu lệnh try và exception, xem Handling Exceptions.

4.6. Câu lệnh pass

Câu lệnh pass không làm gì cả. Nó có thể được dùng khi cú pháp yêu cầu một câu lệnh nhưng chương trình không cần thực hiện hành động nào. Ví dụ:

>>> while True:
...     pass  # Busy-wait for keyboard interrupt (Ctrl+C)
...

Điều này thường được dùng để tạo ra các class tối giản:

>>> class MyEmptyClass:
...     pass
...

pass cũng có thể được dùng như chỗ giữ chỗ cho phần thân của hàm hay điều kiện khi đang làm việc với mã mới, cho phép tiếp tục suy nghĩ ở mức trừu tượng hơn. pass sẽ bị bỏ qua một cách lặng lẽ:

>>> def initlog(*args):
...     pass   # Remember to implement this!
...

Với trường hợp cuối này, nhiều người dùng literal dấu chấm lửng ... thay vì pass. Cách dùng này không có ý nghĩa đặc biệt với Python và không phải là một phần của định nghĩa ngôn ngữ (ở đây có thể dùng bất kỳ biểu thức hằng nào), nhưng ... cũng được dùng theo quy ước như phần thân giữ chỗ. Xem The Ellipsis Object.

4.7. Câu lệnh match

Câu lệnh match nhận một biểu thức và so sánh giá trị của nó với các mẫu liên tiếp được đưa ra dưới dạng một hoặc nhiều khối case. Về bề ngoài điều này tương tự với câu lệnh switch trong C, Java hay JavaScript (và nhiều ngôn ngữ khác), nhưng thực ra nó gần hơn với pattern matching trong các ngôn ngữ như Rust hay Haskell. Chỉ mẫu đầu tiên khớp mới được thực thi và nó cũng có thể trích xuất các thành phần (phần tử của sequence hoặc attribute của object) từ giá trị vào các biến. Nếu không có case nào khớp, không nhánh nào được thực thi.

Dạng đơn giản nhất so sánh một giá trị chủ thể với một hoặc nhiều literal:

def http_error(status):
    match status:
        case 400:
            return "Bad request"
        case 404:
            return "Not found"
        case 418:
            return "I'm a teapot"
        case _:
            return "Something's wrong with the internet"

Lưu ý khối cuối: "tên biến" _ hoạt động như một ký tự đại diện và không bao giờ thất bại khi khớp.

Có thể kết hợp nhiều literal trong một mẫu bằng cách dùng | ("or"):

case 401 | 403 | 404:
    return "Not allowed"

Các mẫu có thể trông giống như phép gán giải nén, và có thể được dùng để gán biến:

# point is an (x, y) tuple
match point:
    case (0, 0):
        print("Origin")
    case (0, y):
        print(f"Y={y}")
    case (x, 0):
        print(f"X={x}")
    case (x, y):
        print(f"X={x}, Y={y}")
    case _:
        raise ValueError("Not a point")

Hãy nghiên cứu kỹ ví dụ đó! Mẫu đầu tiên có hai literal và có thể coi là mở rộng của mẫu literal đã trình bày ở trên. Nhưng hai mẫu tiếp theo kết hợp một literal và một biến, và biến đó gán một giá trị từ chủ thể (point). Mẫu thứ tư bắt hai giá trị, khiến nó về mặt khái niệm tương tự với phép gán giải nén (x, y) = point.

Nếu đang dùng class để cấu trúc dữ liệu, có thể dùng tên class theo sau là danh sách đối số giống như constructor, nhưng với khả năng bắt attribute vào biến:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

def where_is(point):
    match point:
        case Point(x=0, y=0):
            print("Origin")
        case Point(x=0, y=y):
            print(f"Y={y}")
        case Point(x=x, y=0):
            print(f"X={x}")
        case Point():
            print("Somewhere else")
        case _:
            print("Not a point")

Có thể dùng tham số vị trí với một số class tích hợp sẵn cung cấp thứ tự cho attribute của chúng (ví dụ: dataclasses). Cũng có thể định nghĩa vị trí cụ thể cho attribute trong các mẫu bằng cách đặt attribute đặc biệt __match_args__ trong class. Nếu được đặt thành ("x", "y"), các mẫu sau đây đều tương đương (và tất cả đều gán attribute y cho biến var):

Point(1, var)
Point(1, y=var)
Point(x=1, y=var)
Point(y=var, x=1)

Cách đọc các mẫu được khuyến nghị là nhìn chúng như dạng mở rộng của những gì bạn đặt ở bên trái của phép gán, để hiểu biến nào sẽ được gán giá trị gì. Chỉ các tên độc lập (như var ở trên) mới được câu lệnh match gán giá trị. Các tên có dấu chấm (như foo.bar), tên attribute (x=y= ở trên) hay tên class (được nhận biết bởi dấu "(...)" bên cạnh như Point ở trên) không bao giờ được gán giá trị.

Các mẫu có thể được lồng nhau tùy ý. Ví dụ, nếu có một list ngắn các Point với __match_args__ được thêm vào, có thể khớp như sau:

class Point:
    __match_args__ = ('x', 'y')
    def __init__(self, x, y):
        self.x = x
        self.y = y

match points:
    case []:
        print("No points")
    case [Point(0, 0)]:
        print("The origin")
    case [Point(x, y)]:
        print(f"Single point {x}, {y}")
    case [Point(0, y1), Point(0, y2)]:
        print(f"Two on the Y axis at {y1}, {y2}")
    case _:
        print("Something else")

Có thể thêm mệnh đề if vào một mẫu, được gọi là "guard". Nếu guard là false, match tiếp tục thử khối case tiếp theo. Lưu ý rằng việc bắt giá trị xảy ra trước khi guard được đánh giá:

match point:
    case Point(x, y) if x == y:
        print(f"Y=X at {x}")
    case Point(x, y):
        print(f"Not on the diagonal")

Một số tính năng quan trọng khác của câu lệnh này:

  • Giống như phép gán giải nén, mẫu tuple và list có ý nghĩa hoàn toàn như nhau và thực sự khớp với sequence tùy ý. Ngoại lệ quan trọng là chúng không khớp với iterator hay chuỗi.

  • Mẫu sequence hỗ trợ giải nén mở rộng: [x, y, *rest](x, y, *rest) hoạt động tương tự phép gán giải nén. Tên sau * cũng có thể là _, vì vậy (x, y, *_) khớp với sequence có ít nhất hai phần tử mà không gán các phần tử còn lại.

  • Mẫu mapping: {"bandwidth": b, "latency": l} bắt các giá trị "bandwidth""latency" từ một dict. Khác với mẫu sequence, các key thừa bị bỏ qua. Giải nén như **rest cũng được hỗ trợ. (Nhưng **_ sẽ thừa, nên không được phép.)

  • Các mẫu con có thể được bắt bằng từ khóa as:

    case (Point(x1, y1), Point(x2, y2) as p2): ...
    

    sẽ bắt phần tử thứ hai của đầu vào thành p2 (miễn là đầu vào là một sequence gồm hai điểm)

  • Hầu hết các literal được so sánh bằng phép bình đẳng, tuy nhiên các singleton True, FalseNone được so sánh bằng phép đồng nhất.

  • Các mẫu có thể dùng các hằng được đặt tên. Chúng phải là tên có dấu chấm để tránh bị hiểu là biến bắt giá trị:

    from enum import Enum
    class Color(Enum):
        RED = 'red'
        GREEN = 'green'
        BLUE = 'blue'
    
    color = Color(input("Enter your choice of 'red', 'blue' or 'green': "))
    
    match color:
        case Color.RED:
            print("I see red!")
        case Color.GREEN:
            print("Grass is green")
        case Color.BLUE:
            print("I'm feeling the blues :(")
    

Để giải thích chi tiết hơn và có thêm ví dụ, xem PEP 636 được viết theo định dạng hướng dẫn.

4.8. Định nghĩa hàm

Có thể tạo một hàm ghi dãy Fibonacci đến một giới hạn tùy ý:

>>> def fib(n):    # write Fibonacci series less than n
...     """Print a Fibonacci series less than n."""
...     a, b = 0, 1
...     while a < n:
...         print(a, end=' ')
...         a, b = b, a+b
...     print()
...
>>> # Now call the function we just defined:
>>> fib(2000)
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597

Từ khóa def mở đầu định nghĩa hàm. Sau đó phải là tên hàm và danh sách tham số hình thức trong ngoặc. Các câu lệnh tạo thành phần thân hàm bắt đầu từ dòng tiếp theo và phải được thụt lề.

Câu lệnh đầu tiên trong phần thân hàm có thể tùy chọn là một string literal; string literal này là chuỗi tài liệu của hàm, hay còn gọi là docstring. (Thêm thông tin về docstring có thể tìm thấy trong mục Chuỗi tài liệu.) Có các công cụ sử dụng docstring để tự động tạo tài liệu trực tuyến hoặc in, hoặc để người dùng duyệt qua mã tương tác; đưa docstring vào mã là thói quen tốt, vậy hãy tập thành thói quen.

Việc thực thi một hàm tạo ra một bảng ký hiệu mới dùng cho các biến cục bộ của hàm. Chính xác hơn, tất cả các phép gán biến trong một hàm lưu giá trị vào bảng ký hiệu cục bộ; còn tham chiếu biến trước tiên tìm trong bảng ký hiệu cục bộ, sau đó trong bảng ký hiệu cục bộ của các hàm bao ngoài, rồi trong bảng ký hiệu toàn cục, và cuối cùng trong bảng tên tích hợp sẵn. Do đó, các biến toàn cục và biến của các hàm bao ngoài không thể được gán giá trị trực tiếp bên trong một hàm (trừ khi, đối với biến toàn cục, được khai báo trong câu lệnh global, hoặc đối với biến của hàm bao ngoài, được khai báo trong câu lệnh nonlocal), mặc dù chúng có thể được tham chiếu.

Các tham số thực (đối số) của một lời gọi hàm được đưa vào bảng ký hiệu cục bộ của hàm được gọi khi nó được gọi; do đó, đối số được truyền theo kiểu gọi theo giá trị (trong đó giá trị luôn là tham chiếu object, không phải giá trị của object). [1] Khi một hàm gọi hàm khác, hay gọi chính nó một cách đệ quy, một bảng ký hiệu cục bộ mới được tạo ra cho lời gọi đó.

Định nghĩa hàm liên kết tên hàm với object hàm trong bảng ký hiệu hiện tại. Trình thông dịch nhận ra object được trỏ bởi tên đó là một hàm do người dùng định nghĩa. Các tên khác cũng có thể trỏ đến cùng object hàm đó và cũng có thể dùng để truy cập hàm:

>>> fib
<function fib at 10042ed0>
>>> f = fib
>>> f(100)
0 1 1 2 3 5 8 13 21 34 55 89

Nếu đến từ các ngôn ngữ khác, có thể bạn cho rằng fib không phải là hàm mà là thủ tục vì nó không trả về giá trị. Thực ra, ngay cả các hàm không có câu lệnh return cũng trả về một giá trị, dù là khá tẻ nhạt. Giá trị này được gọi là None (đây là tên tích hợp sẵn). Việc in giá trị None thường bị trình thông dịch ẩn đi nếu đó là giá trị duy nhất được in ra. Có thể thấy nó bằng cách dùng print():

>>> fib(0)
>>> print(fib(0))
None

Có thể dễ dàng viết một hàm trả về list các số trong dãy Fibonacci thay vì in ra:

>>> def fib2(n):  # return Fibonacci series up to n
...     """Return a list containing the Fibonacci series up to n."""
...     result = []
...     a, b = 0, 1
...     while a < n:
...         result.append(a)    # see below
...         a, b = b, a+b
...     return result
...
>>> f100 = fib2(100)    # call it
>>> f100                # write the result
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

Ví dụ này, như thường lệ, minh họa một số tính năng mới của Python:

  • Câu lệnh return trả về một giá trị từ hàm. return không có đối số biểu thức trả về None. Kết thúc hàm mà không có return cũng trả về None.

  • Câu lệnh result.append(a) gọi một method của list object result. Method là hàm 'thuộc về' một object và có tên là obj.methodname, trong đó obj là một object nào đó (có thể là một biểu thức), và methodname là tên của method được định nghĩa bởi kiểu của object. Các kiểu khác nhau định nghĩa các method khác nhau. Các method của các kiểu khác nhau có thể có cùng tên mà không gây ra sự mơ hồ. (Có thể định nghĩa kiểu object và method của riêng mình bằng cách dùng class, xem Classes) Method append() trong ví dụ được định nghĩa cho list object; nó thêm một phần tử mới vào cuối list. Trong ví dụ này nó tương đương với result = result + [a], nhưng hiệu quả hơn.

4.9. Thêm về định nghĩa hàm

Cũng có thể định nghĩa hàm với số lượng đối số biến đổi. Có ba dạng, có thể kết hợp với nhau.

4.9.1. Giá trị đối số mặc định

Dạng hữu ích nhất là chỉ định giá trị mặc định cho một hoặc nhiều đối số. Điều này tạo ra một hàm có thể được gọi với ít đối số hơn so với định nghĩa cho phép. Ví dụ:

def ask_ok(prompt, retries=4, reminder='Please try again!'):
    while True:
        reply = input(prompt)
        if reply in {'y', 'ye', 'yes'}:
            return True
        if reply in {'n', 'no', 'nop', 'nope'}:
            return False
        retries = retries - 1
        if retries < 0:
            raise ValueError('invalid user response')
        print(reminder)

Hàm này có thể được gọi theo nhiều cách:

  • chỉ truyền đối số bắt buộc: ask_ok('Do you really want to quit?')

  • truyền một trong các đối số tùy chọn: ask_ok('OK to overwrite the file?', 2)

  • hoặc thậm chí truyền tất cả đối số: ask_ok('OK to overwrite the file?', 2, 'Come on, only yes or no!')

Ví dụ này cũng giới thiệu từ khóa in. Nó kiểm tra xem một sequence có chứa một giá trị nhất định hay không.

Các giá trị mặc định được đánh giá tại thời điểm định nghĩa hàm trong phạm vi định nghĩa, vì vậy:

i = 5

def f(arg=i):
    print(arg)

i = 6
f()

sẽ in ra 5.

Cảnh báo quan trọng: Giá trị mặc định chỉ được đánh giá một lần. Điều này tạo ra sự khác biệt khi giá trị mặc định là một object có thể thay đổi như list, dict hoặc instance của hầu hết các class. Ví dụ, hàm sau tích lũy các đối số được truyền vào trong các lời gọi tiếp theo:

def f(a, L=[]):
    L.append(a)
    return L

print(f(1))
print(f(2))
print(f(3))

Lệnh này sẽ in ra

[1]
[1, 2]
[1, 2, 3]

Nếu không muốn giá trị mặc định được chia sẻ giữa các lời gọi tiếp theo, có thể viết hàm như sau:

def f(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L

4.9.2. Đối số từ khóa

Hàm cũng có thể được gọi bằng keyword arguments có dạng kwarg=value. Chẳng hạn, hàm sau:

def parrot(voltage, state='a stiff', action='voom', type='Norwegian Blue'):
    print("-- This parrot wouldn't", action, end=' ')
    print("if you put", voltage, "volts through it.")
    print("-- Lovely plumage, the", type)
    print("-- It's", state, "!")

chấp nhận một đối số bắt buộc (voltage) và ba đối số tùy chọn (state, actiontype). Hàm này có thể được gọi theo bất kỳ cách nào sau đây:

parrot(1000)                                          # 1 positional argument
parrot(voltage=1000)                                  # 1 keyword argument
parrot(voltage=1000000, action='VOOOOOM')             # 2 keyword arguments
parrot(action='VOOOOOM', voltage=1000000)             # 2 keyword arguments
parrot('a million', 'bereft of life', 'jump')         # 3 positional arguments
parrot('a thousand', state='pushing up the daisies')  # 1 positional, 1 keyword

nhưng tất cả các lời gọi sau đây đều không hợp lệ:

parrot()                     # required argument missing
parrot(voltage=5.0, 'dead')  # non-keyword argument after a keyword argument
parrot(110, voltage=220)     # duplicate value for the same argument
parrot(actor='John Cleese')  # unknown keyword argument

Trong một lời gọi hàm, đối số từ khóa phải đứng sau đối số vị trí. Tất cả đối số từ khóa được truyền phải khớp với một trong các đối số mà hàm chấp nhận (ví dụ: actor không phải là đối số hợp lệ cho hàm parrot), và thứ tự của chúng không quan trọng. Điều này cũng áp dụng cho các đối số không tùy chọn (ví dụ: parrot(voltage=1000) cũng hợp lệ). Không đối số nào được nhận giá trị nhiều hơn một lần. Đây là ví dụ thất bại do ràng buộc này:

>>> def function(a):
...     pass
...
>>> function(0, a=0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: function() got multiple values for argument 'a'

Khi có tham số hình thức cuối cùng ở dạng **name, nó nhận một dict (xem Mapping types --- dict, frozendict) chứa tất cả các đối số từ khóa ngoại trừ những đối số tương ứng với tham số hình thức. Có thể kết hợp điều này với tham số hình thức ở dạng *name (được mô tả trong mục tiếp theo) nhận một tuple chứa các đối số vị trí ngoài danh sách tham số hình thức. (*name phải đứng trước **name.) Ví dụ, nếu định nghĩa hàm như sau:

def cheeseshop(kind, *arguments, **keywords):
    print("-- Do you have any", kind, "?")
    print("-- I'm sorry, we're all out of", kind)
    for arg in arguments:
        print(arg)
    print("-" * 40)
    for kw in keywords:
        print(kw, ":", keywords[kw])

Nó có thể được gọi như sau:

cheeseshop("Limburger", "It's very runny, sir.",
           "It's really very, VERY runny, sir.",
           shopkeeper="Michael Palin",
           client="John Cleese",
           sketch="Cheese Shop Sketch")

và tất nhiên nó sẽ in ra:

-- Do you have any Limburger ?
-- I'm sorry, we're all out of Limburger
It's very runny, sir.
It's really very, VERY runny, sir.
----------------------------------------
shopkeeper : Michael Palin
client : John Cleese
sketch : Cheese Shop Sketch

Lưu ý rằng thứ tự in ra của các đối số từ khóa được đảm bảo khớp với thứ tự chúng được cung cấp trong lời gọi hàm.

4.9.3. Tham số đặc biệt

Theo mặc định, đối số có thể được truyền cho hàm Python theo vị trí hoặc rõ ràng theo từ khóa. Để dễ đọc và hiệu quả, có ý nghĩa khi giới hạn cách truyền đối số để lập trình viên chỉ cần nhìn vào định nghĩa hàm là có thể xác định các mục được truyền theo vị trí, theo vị trí hoặc từ khóa, hay theo từ khóa.

Định nghĩa hàm có thể trông như sau:

def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
      -----------    ----------     ----------
        |             |                  |
        |        Positional or keyword   |
        |                                - Keyword only
         -- Positional only

trong đó /* là tùy chọn. Khi được dùng, các ký hiệu này chỉ ra loại tham số theo cách đối số có thể được truyền cho hàm: chỉ vị trí, vị trí hoặc từ khóa, và chỉ từ khóa. Tham số từ khóa còn được gọi là tham số được đặt tên.

4.9.3.1. Đối số vị trí hoặc từ khóa

Nếu /* không có trong định nghĩa hàm, đối số có thể được truyền cho hàm theo vị trí hoặc từ khóa.

4.9.3.2. Tham số chỉ vị trí

Nhìn chi tiết hơn, có thể đánh dấu một số tham số là chỉ vị trí. Nếu là chỉ vị trí, thứ tự tham số có ý nghĩa quan trọng và các tham số không thể được truyền theo từ khóa. Tham số chỉ vị trí được đặt trước dấu / (gạch chéo xuôi). Dấu / được dùng để phân tách logic các tham số chỉ vị trí với phần còn lại của tham số. Nếu không có / trong định nghĩa hàm, không có tham số chỉ vị trí nào.

Các tham số sau / có thể là vị trí hoặc từ khóa hay chỉ từ khóa.

4.9.3.3. Đối số chỉ từ khóa

Để đánh dấu tham số là chỉ từ khóa, cho thấy tham số phải được truyền bằng đối số từ khóa, đặt dấu * vào danh sách đối số ngay trước tham số chỉ từ khóa đầu tiên.

4.9.3.4. Ví dụ về hàm

Xem xét các định nghĩa hàm ví dụ sau đây, chú ý kỹ đến các ký hiệu /*:

>>> def standard_arg(arg):
...     print(arg)
...
>>> def pos_only_arg(arg, /):
...     print(arg)
...
>>> def kwd_only_arg(*, arg):
...     print(arg)
...
>>> def combined_example(pos_only, /, standard, *, kwd_only):
...     print(pos_only, standard, kwd_only)

Định nghĩa hàm đầu tiên, standard_arg, là dạng quen thuộc nhất, không đặt ràng buộc nào lên cách gọi và đối số có thể được truyền theo vị trí hoặc từ khóa:

>>> standard_arg(2)
2

>>> standard_arg(arg=2)
2

Hàm thứ hai pos_only_arg bị giới hạn chỉ dùng tham số vị trí vì có dấu / trong định nghĩa hàm:

>>> pos_only_arg(1)
1

>>> pos_only_arg(arg=1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: pos_only_arg() got some positional-only arguments passed as keyword arguments: 'arg'

Hàm thứ ba kwd_only_arg chỉ cho phép đối số từ khóa như được chỉ ra bởi dấu * trong định nghĩa hàm:

>>> kwd_only_arg(3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: kwd_only_arg() takes 0 positional arguments but 1 was given

>>> kwd_only_arg(arg=3)
3

Và hàm cuối sử dụng cả ba quy ước gọi trong cùng một định nghĩa hàm:

>>> combined_example(1, 2, 3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: combined_example() takes 2 positional arguments but 3 were given

>>> combined_example(1, 2, kwd_only=3)
1 2 3

>>> combined_example(1, standard=2, kwd_only=3)
1 2 3

>>> combined_example(pos_only=1, standard=2, kwd_only=3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: combined_example() got some positional-only arguments passed as keyword arguments: 'pos_only'

Cuối cùng, xem xét định nghĩa hàm này có khả năng xung đột giữa đối số vị trí name**kwds vốn có name là một key:

def foo(name, **kwds):
    return 'name' in kwds

Không có lời gọi nào có thể khiến nó trả về True vì từ khóa 'name' luôn gắn với tham số đầu tiên. Ví dụ:

>>> foo(1, **{'name': 2})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: foo() got multiple values for argument 'name'
>>>

Nhưng khi dùng / (đối số chỉ vị trí), điều này có thể thực hiện được vì nó cho phép name là đối số vị trí và 'name' là key trong đối số từ khóa:

>>> def foo(name, /, **kwds):
...     return 'name' in kwds
...
>>> foo(1, **{'name': 2})
True

Nói cách khác, tên của các tham số chỉ vị trí có thể được dùng trong **kwds mà không gây ra sự mơ hồ.

4.9.3.5. Tóm lại

Trường hợp sử dụng sẽ quyết định tham số nào nên dùng trong định nghĩa hàm:

def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):

Hướng dẫn:

  • Dùng chỉ vị trí nếu muốn tên tham số không hiển thị với người dùng. Điều này hữu ích khi tên tham số không có ý nghĩa thực sự, khi muốn bắt buộc thứ tự đối số khi gọi hàm, hoặc khi cần nhận một số tham số vị trí và các từ khóa tùy ý.

  • Dùng chỉ từ khóa khi tên có ý nghĩa và định nghĩa hàm dễ hiểu hơn khi rõ ràng về tên, hoặc khi muốn ngăn người dùng phụ thuộc vào vị trí của đối số được truyền.

  • Với API, dùng chỉ vị trí để tránh phá vỡ API nếu tên tham số bị thay đổi trong tương lai.

4.9.4. Danh sách đối số tùy ý

Cuối cùng, tùy chọn ít được dùng nhất là chỉ định rằng hàm có thể được gọi với số lượng đối số tùy ý. Các đối số này sẽ được gói vào một tuple (xem Tuples and Sequences). Trước số lượng biến đổi đối số, có thể có không hoặc nhiều đối số thông thường.

def write_multiple_items(file, separator, *args):
    file.write(separator.join(args))

Thông thường, các đối số variadic này sẽ nằm cuối trong danh sách tham số hình thức, vì chúng thu nhặt tất cả các đối số đầu vào còn lại được truyền cho hàm. Bất kỳ tham số hình thức nào xuất hiện sau tham số *args đều là đối số 'chỉ từ khóa', nghĩa là chúng chỉ có thể được dùng như từ khóa thay vì đối số vị trí.

>>> def concat(*args, sep="/"):
...     return sep.join(args)
...
>>> concat("earth", "mars", "venus")
'earth/mars/venus'
>>> concat("earth", "mars", "venus", sep=".")
'earth.mars.venus'

4.9.5. Giải nén danh sách đối số

Tình huống ngược lại xảy ra khi đối số đã có sẵn trong một list hay tuple nhưng cần được giải nén cho một lời gọi hàm yêu cầu các đối số vị trí riêng biệt. Chẳng hạn, hàm tích hợp sẵn range() mong đợi các đối số startstop riêng biệt. Nếu chúng không có sẵn riêng biệt, hãy viết lời gọi hàm với toán tử *để giải nén đối số từ list hoặc tuple:

>>> list(range(3, 6))            # normal call with separate arguments
[3, 4, 5]
>>> args = [3, 6]
>>> list(range(*args))            # call with arguments unpacked from a list
[3, 4, 5]

Tương tự, dict có thể cung cấp đối số từ khóa bằng toán tử **

>>> def parrot(voltage, state='a stiff', action='voom'):
...     print("-- This parrot wouldn't", action, end=' ')
...     print("if you put", voltage, "volts through it.", end=' ')
...     print("E's", state, "!")
...
>>> d = {"voltage": "four million", "state": "bleedin' demised", "action": "VOOM"}
>>> parrot(**d)
-- This parrot wouldn't VOOM if you put four million volts through it. E's bleedin' demised !

4.9.6. Biểu thức lambda

Có thể tạo các hàm ẩn danh nhỏ bằng từ khóa lambda. Hàm này trả về tổng hai đối số của nó: lambda a, b: a+b. Hàm lambda có thể được dùng ở bất cứ đâu cần object hàm. Chúng bị giới hạn về cú pháp chỉ trong một biểu thức duy nhất. Về mặt ngữ nghĩa, chúng chỉ là cú pháp rút gọn cho định nghĩa hàm thông thường. Giống như định nghĩa hàm lồng nhau, hàm lambda có thể tham chiếu biến từ phạm vi chứa:

>>> def make_incrementor(n):
...     return lambda x: x + n
...
>>> f = make_incrementor(42)
>>> f(0)
42
>>> f(1)
43

Ví dụ trên dùng biểu thức lambda để trả về một hàm. Cách dùng khác là truyền một hàm nhỏ làm đối số. Chẳng hạn, list.sort() nhận hàm khóa sắp xếp key có thể là hàm lambda:

>>> pairs = [(1, 'one'), (2, 'two'), (3, 'three'), (4, 'four')]
>>> pairs.sort(key=lambda pair: pair[1])
>>> pairs
[(4, 'four'), (1, 'one'), (3, 'three'), (2, 'two')]

4.9.7. Chuỗi tài liệu

Dưới đây là một số quy ước về nội dung và định dạng của chuỗi tài liệu.

Dòng đầu tiên luôn phải là tóm tắt ngắn gọn về mục đích của object. Để ngắn gọn, nó không nên nêu rõ tên hay kiểu của object vì những thông tin đó có thể lấy được bằng cách khác (trừ khi tên đó tình cờ là một động từ mô tả hoạt động của hàm). Dòng này phải bắt đầu bằng chữ hoa và kết thúc bằng dấu chấm.

Nếu có nhiều dòng trong chuỗi tài liệu, dòng thứ hai nên để trống, phân tách trực quan phần tóm tắt với phần còn lại của mô tả. Các dòng tiếp theo nên gồm một hoặc nhiều đoạn mô tả quy ước gọi của object, tác dụng phụ của nó, v.v.

Trình phân tích cú pháp Python loại bỏ thụt lề từ các string literal nhiều dòng khi chúng đóng vai trò là docstring của module, class hoặc hàm.

Đây là ví dụ về docstring nhiều dòng:

>>> def my_function():
...     """Do nothing, but document it.
...
...     No, really, it doesn't do anything:
...
...         >>> my_function()
...         >>>
...     """
...     pass
...
>>> print(my_function.__doc__)
Do nothing, but document it.

No, really, it doesn't do anything:

    >>> my_function()
    >>>

4.9.8. Chú thích hàm

Chú thích hàm là thông tin metadata hoàn toàn tùy chọn về các kiểu được dùng trong các hàm do người dùng định nghĩa (xem PEP 3107PEP 484 để biết thêm thông tin).

Annotations được lưu trong attribute __annotations__ của hàm dưới dạng dict và không ảnh hưởng đến bất kỳ phần nào khác của hàm. Chú thích tham số được định nghĩa bằng dấu hai chấm sau tên tham số, theo sau là biểu thức đánh giá thành giá trị của chú thích. Chú thích giá trị trả về được định nghĩa bằng literal ->, theo sau là một biểu thức, nằm giữa danh sách tham số và dấu hai chấm biểu thị kết thúc câu lệnh def. Ví dụ sau có một đối số bắt buộc, một đối số tùy chọn và giá trị trả về được chú thích:

>>> def f(ham: str, eggs: str = 'eggs') -> str:
...     print("Annotations:", f.__annotations__)
...     print("Arguments:", ham, eggs)
...     return ham + ' and ' + eggs
...
>>> f('spam')
Annotations: {'ham': <class 'str'>, 'return': <class 'str'>, 'eggs': <class 'str'>}
Arguments: spam eggs
'spam and eggs'

4.10. Phần xen kẽ: Phong cách viết mã

Khi sắp viết các đoạn Python dài hơn và phức tạp hơn, đây là thời điểm tốt để bàn về phong cách viết mã. Hầu hết các ngôn ngữ có thể được viết (hay chính xác hơn là định dạng) theo nhiều phong cách khác nhau; một số dễ đọc hơn những cái còn lại. Giúp người khác dễ đọc mã của mình luôn là ý tưởng hay, và áp dụng phong cách viết mã tốt sẽ hỗ trợ rất nhiều cho điều đó.

Với Python, PEP 8 đã nổi lên như hướng dẫn phong cách mà hầu hết các dự án tuân theo; nó thúc đẩy phong cách viết mã rất dễ đọc và đẹp mắt. Mọi lập trình viên Python đều nên đọc nó vào lúc nào đó; đây là những điểm quan trọng nhất:

  • Dùng thụt lề 4 dấu cách, không dùng tab.

    4 dấu cách là sự thỏa hiệp tốt giữa thụt lề nhỏ (cho phép độ sâu lồng nhau lớn hơn) và thụt lề lớn (dễ đọc hơn). Tab gây ra sự nhầm lẫn và tốt nhất là không nên dùng.

  • Xuống dòng để mỗi dòng không vượt quá 79 ký tự.

    Điều này giúp người dùng có màn hình nhỏ và cho phép xem nhiều tệp mã cạnh nhau trên màn hình lớn hơn.

  • Dùng dòng trống để phân tách các hàm và class, cũng như các khối mã lớn hơn bên trong hàm.

  • Khi có thể, đặt chú thích trên dòng riêng của chúng.

  • Dùng docstring.

  • Dùng dấu cách xung quanh toán tử và sau dấu phẩy, nhưng không trực tiếp bên trong các cấu trúc dấu ngoặc: a = f(1, 2) + g(3, 4).

  • Đặt tên class và hàm một cách nhất quán; quy ước là dùng UpperCamelCase cho class và lowercase_with_underscores cho hàm và method. Luôn dùng self làm tên cho đối số đầu tiên của method (xem A First Look at Classes để biết thêm về class và method).

  • Không dùng các mã hoá lạ nếu mã của bạn được thiết kế để sử dụng trong môi trường quốc tế. Mặc định của Python, UTF-8 hoặc thậm chí ASCII thuần túy hoạt động tốt nhất trong mọi trường hợp.

  • Tương tự, không dùng ký tự không phải ASCII trong định danh nếu có dù chỉ một chút khả năng những người nói ngôn ngữ khác sẽ đọc hoặc bảo trì mã.

Footnotes