Bản lược dịch của bài nói chuyện "Code Like a Pythonista: Idiomatic Python" do David Goodger trình bày tại PyCon 2007 và OSCON 2007.

Ngày 26 tháng 07 năm 2007, David Goodger gửi bài nói chuyện Code Like a Pythonista: Idiomatic Python mà ông đã trình bày ở PyCon 2007 và OSCON 2007 lên trang nhà. Đây là bài lược dịch những ý chính.

Trước khi vào bài, xin được giới thiệu qua về hai từ mà chúng ta hay gặp là pythonistapythoneer. Pythonista chỉ một người theo đuổi ngôn ngữ Python cuồng nhiệt, như là một cổ động viên bóng đá trung thành. Pythoneer chỉ một người luôn đi đầu và nắm bắt những điểm mới của ngôn ngữ Python, như là nhà khoa học tiên phong trong lĩnh vực của họ. Thông thường, hai từ này đều dùng để chỉ một người rất yêu thích Python, và có thể được hoán chuyển.

Kiểu lập trình: Dễ đọc là quan trọng

Programs must be written for people to read, and only incidentally for machines to execute.

—Abelson & Sussman, Structure and Interpretation of Computer Programs

Tạm dịch: Chương trình phải được viết ra để cho người đọc, và chỉ là sự trùng hợp để cho máy thực thi.

Khoảng trắng

  • Bốn (4) khoảng trắng ở mỗi nấc thụt vào
  • Không dùng tab
  • Không bao giờ lẫn lộn tab và khoảng trắng
  • Cách một dòng giữa các hàm
  • Cách hai dòng giữa các lớp
  • Chừa một khoảng trắng sau dấu phẩy , trong từ điển, danh sách, bộ, danh sách tham số, và sau dấu hai chấm : trong từ điển nhưng không phải trước nó
  • Chừa một khoảng trắng trước và sau phép gán và so sánh (trừ khi trong danh sách tham số)
  • Không chừa khoảng trắng trong ngoặc tròn, hoặc ngay trước các danh sách tham số
  • Không chừa khoảng trắng trong các chuỗi tài liệu

Cách đặt tên

  • joined_lower cho hàm, phương thức và thuộc tính
  • joined_lower hoặc ALL_CAPS cho hằng
  • CapWords cho lớp
  • camelCase chỉ dùng để hợp với những thói quen đã có
  • Với các thuộc tính: public, _internal, và __private nhưng hạn chế dùng kiểu __private

Những câu lệnh ghép

Tốt:

if foo == 'blah':
    do_something()
do_one()
do_two()
do_three()

Xấu:

if foo == 'blah': do_something()
do_one(); do_two(); do_three()

Việc thiếu thụt vào ở mã "Xấu" che mất câu lệnh if. Sử dụng nhiều câu lệnh trên cùng một dòng là mang tội lớn.

Hoán đổi giá trị

Trong các ngôn ngữ khác:

temp = a;
a = b;
b = temp;

Trong Python:

b, a = a, b

Dấu _ tương tác

Đây là một chức năng thật sự hữu dụng mà ít người biết.

Trong môi trường thông dịch tương tác, khi bạn định giá một biểu thức hoặc gọi một hàm, kết quả sẽ được gán vào một tên tạm, _ (dấu gạch chân).

>>> 1 + 1
2
>>> _
2

_ chứa biểu thức được in cuối cùng. Khi kết quả là None, không có gì được in ra nên _ không thay đổi. Dấu gạch chân chỉ có tác dụng trong môi trường tương tác, không có tác dụng trong một mô-đun.

Tạo chuỗi từ các chuỗi con

Dùng:

colors = ['red', 'blue', 'green', 'yellow']
result = ''.join(colors)

Không dùng:

colors = ['red', 'blue', 'green', 'yellow']
result = ''
for s in colors:
    result += s

Trường hợp hay gặp khi danh sách có nhiều hơn một phần từ:

colors = ['red', 'blue', 'green', 'yellow']
print 'Choose', ', '.join(colors[:-1]), 'or', colors[-1]

In ra:

Choose red, blue, green or yellow

Dùng in khi có thể

Tốt:

for key in d:
    print key
  • in thường là nhanh hơn
  • có thể dùng được với danh sách, bộ, từ điển, và tập hợp

Xấu:

for key in d.keys():
    print key

Chỉ dùng được cho các đối tượng có phương thức keys().

Nhưng sẽ cần dùng keys() khi sửa đổi từ điển:

for key in d.keys():
    d[str(key)] = d[key]

keys() tạo ra một danh sách các khóa riêng để lặp. Nếu không, bạn sẽ gặp phải lỗi RuntimeError vì từ điển bị thay đổi trong khi lặp.

Để thống nhất, hãy dùng key in dict thay vì dict.has_key(key).

Phương thức get() của từ điển

Chúng ta thường khởi tạo các phần tử từ điển trước khi dùng. Đây là cách không hay:

navs = {}
for (portfolio, equity, position) in data:
    if portfolio not in navs:
        navs[portfolio] = 0
    navs[portfolio] += position * prices[equity]

Dùng dict.get(key, default) sẽ tránh được việc kiểm tra:

navs = {}
for (portfolio, equity, position) in data:
    navs[portfolio] = navs.get(portfolio, 0) + position * prices[equity]

Phương thức setdefault() của từ điển

Cách dở để khởi tạo một từ điển:

equities = {}
for (portfolio, equity) in data:
    if portfolio in equities:
        equities[portfolio].append(equity)
    else:
        equities[portfolio] = [equity]

Dùng dict.setdefault(key, default) nhanh gọn hơn nhiều:

equities = {}
for (portfolio, equity) in data:
    equities.setdefault(portfolio, []).append(equity)

dict.setdefault() tương đương với lấy, hoặc thiết lập rồi lấy. Nó rất hiệu quả nếu khóa từ điển cần nhiều thời gian để tính, hoặc vì nó dài nên khó nhập. Tuy nhiên giá trị default luôn luôn được tính cho dù có cần dùng hay không.

Phương thức setdefault() cũng có thể được dùng riêng vì hiệu quả phụ của nó:

navs = {}
for (portfolio, equity, position) in data:
    navs.setdefault(portfolio, 0)
    navs[portfolio] += position * prices[equity]

defaultdict

Trong Python 2.5, defaultdict là một phần của mô-đun collections. Nó giống như từ điển thường nhưng với hai đặc điểm khác:

  • Nó nhận một tham số là một hàm nhà máy mặc định (default factory functions) và
  • Khi khóa không thể tìm thấy trong từ điển, hàm này sẽ được gọi và kết quả trả về sẽ được dùng để khởi tạo khóa đó

Đây là ví dụ trước, mà mỗi phần tử trong từ điển là một danh sách rỗng, nhưng dùng defaultdict:

from collections import defaultdict

equities = defaultdict(list)
for (portfolio, equity) in data:
    equities[portfolio].append(equity)

Trong ví dụ này, hàm nhà máy mặc định là list. Để tạo từ điển giá trị mặc định là 0 thì dùng int:

navs = defaultdict(int)
for (portfolio, equity, position) in data:
    navs[portfolio] += position * prices[equity]

Vì khóa mới luôn được tạo nên bạn sẽ không gặp KeyError với defaultdict. Bạn phải dùng in để kiểm tra xem một khóa đã có trong từ điển hay chưa.

Kiểm tra giá trị đúng

# dùng:           # không dùng:
if x:             if x == True:
    pass              pass

Nếu là một danh sách:

# dùng:           # không dùng:
if items:         if len(items) != 0:
    pass              pass

                  # and definitely not this:
                  if items != []:
                      pass

Để điều khiển giá trị đúng của các trường hợp của một lớp người dùng định nghĩa, sử dụng các phương thức đặc biệt __nonzero__ hoặc __len__. Dùng __len__ nếu lớp đó là một lớp chứa (container) có chiều dài:

class MyContainer(object):

    def __init__(self, data):
        self.data = data

    def __len__(self):
        """Return my length."""
        return len(self.data)

Nếu lớp đó không phải là một lớp chứa thì dùng __nonzero__:

class MyClass(object):

    def __init__(self, value):
        self.value = value

    def __nonzero__(self):
        """Return my truth value (True or False)."""
        # This could be arbitrarily complex:
        return bool(self.value)

Trong Python 3.0, __nonzero__ được đổi tên thành __bool__ để đồng nhất với kiểu bool có sẵn. Thêm dòng sau vào lớp của bạn cho nó tương hợp hơn:

__bool__ = __nonzero__

Chỉ mục và phần tử

Dùng enumerate() để lặp:

for (index, item) in enumerate(items):
    print index, item

# thay vì:              # thay vì:
index = 0               for i in range(len(items)):
for item in items:          print i, items[i]
    print index, item
    index += 1

enumerate() trả về một bộ lặp (iterator) (một bộ sinh, generator, là một kiểu bộ lặp):

>>> enumerate(items)
<enumerate object at 0xb73ee0f4>
>>> e = enumerate(items)
>>> e.next()
(0, 'zero')
>>> e.next()
(1, 'one')
>>> e.next()
(2, 'two')
>>> e.next()
(3, 'three')
>>> e.next()
Traceback (most recent call last):
  File "", line 1, in ?
StopIteration

Các ngôn ngữ khác có biến

Trong các ngôn ngữ khác, gán vào một biến là đưa giá trị vào một hộp.

int a = 1; a1box

Hộp a bây giờ chứa một số nguyên 1.

Gán một giá trị khác vào cùng biến đó sẽ thay thế những gì đã có trong hộp.

a = 2; a2box

Gán một biến vào một biến khác sẽ tạo một bản sao của giá trị trong hộp này và đặt nó vào hộp mới.

int b = a; b2box a2box

b là một hộp mới, có giá trị là bản sao của số nguyên 2. Trong khi hộp a có một bản sao riêng.

Python có tên

Trong Python, tên hoặc định danh giống như thẻ tên gắn vào một đối tượng.

a = 1 a1tag

Ở đây, số nguyên 1 được gắn một thẻ tên a.

Nếu chúng ta gán lại vào a, chúng ta chỉ chuyển thẻ a vào một đối tượng khác.

a = 2 a2tag 1

Giờ đây tên a được gắn vào số nguyên 2. Số nguyên 1 có thể vẫn còn tồn tại nhưng nó không còn thẻ tên a.

Nếu chúng ta gán một tên vào một tên khác, chúng ta chỉ là gắn một thẻ tên khác vào đối tượng đã có.

b = a ab2tag

Tên b chỉ là một thẻ tên thứ hai được gắn vào cùng một đối tượng như thẻ tên a.

Trong Python, biến là những thẻ tên, không phải là những hộp được đánh tên.

Gộp danh sách (list comprehension, hay listcomp)

Thông thường:

new_list = []
for item in a_list:
    if condition(item):
        new_list.append(fn(item))

Với listcomp:

new_list = [fn(item) for item in a_list
            if condition(item)]

Listcomp rõ ràng và xúc tích. Listcomp có thể chứa nhiều vòng for hoặc câu lệnh if nhưng nếu nhiều hơn 2 hay 3 thì tốt nhất là nên dùng cách thông thường. Ví dụ danh sách bình phương của các số lẻ từ 0 tới 9:

>>> [n ** 2 for n in range(10) if n % 2]
[1, 9, 25, 49, 81]

Biểu thức bộ sinh (generator expression, genexp)

Để tính tổng bình phương từ 1 đến 100 ta có thể dùng vòng lặp for:

total = 0
for num in range(1, 101):
    total += num * num

Hoặc dùng hàm sum() với listcomp:

total = sum([num * num for num in range(1, 101)])

Hoặc sum() với genexp:

total = sum(num * num for num in xrange(1, 101))

Biểu thức bộ sinh giống như gộp danh sách nhưng nó lười (lazy). Listcomp tạo danh sách ngay lập tức, còn genexp tạo từng giá trị một. Khi chúng ta không cần một danh sách mà chỉ cần từng giá trị của danh sách đó, genext rất hữu dụng. Ví dụ như để tính tổng bình phương từ 1 tới 1000000000, chúng ta sẽ dùng hết bộ nhớ nếu ta dùng listcomp, nhưng với genexp thì chuyện này có thể được thực hiện (mặc dù hơi lâu):

total = sum(num * num
            for num in xrange(1, 1000000000))

Sắp xếp với DSU

DSU là Decorate-Sort-Undecorate (trang hoàng, sắp xếp, khử trang hoàng).

Thay vì tạo một hàm so sánh riêng, ta có thể tạo một danh sách tạm sẽ được sắp xếp. Ví dụ để sắp xếp một danh sách các chuỗi theo thứ tự chữ cái thường của chúng:

a_list = "Mot Hai Ba".split()
# a_list = ["Mot", "Hai", "Ba"]

# Decorate:
to_sort = [(lower(x), x)
           for x in a_list]
# to_sort = [("mot", "Mot"), ("hai", "Hai"), ("ba", "Ba")]

# Sort:
to_sort.sort()
# to_sort = [("ba", "Ba"), ("hai", "Hai"), ("mot", "Mot")]

# Undecorate:
a_list = [item[-1] for item in to_sort]
# a_list = ["Ba", "Hai", "Mot"]

Đây là sự đổi chác giữa bộ nhớ và thời gian. Đơn giản và nhanh hơn, nhưng tốn bộ nhớ hơn vì chúng ta cần tạo một danh sách mới.

Bộ sinh (generator)

Chúng ta đã gặp biểu thức bộ sinh. Chúng ta cũng có thể tạo những bộ sinh phức tạp riêng như là những hàm:

def my_range_generator(stop):
    value = 0
    while value < stop:
        yield value
        value += 1

for i in my_range_generator(10):
    do_something(i)

Từ khóa yield biến một hàm thành một bộ sinh. Khi bạn gọi một hàm bộ sinh (generator function), thay vì thực thi mã ngay lập tức, Python trả về một đối tượng bộ sinh (generator object), cũng là một bộ lặp vì nó có phương thức next(). Vòng for gọi phương thức next() của bộ lặp cho đến khi biệt lệ StopIteration được nâng. Bạn có thể tự nâng StopIteration hoặc nó sẽ được nâng khi đến cuối bộ sinh.

Vòng for có một vế else sẽ được thực thi khi mà bộ lặp chạy xong, nhưng không được thực thi khi thoát khỏi bằng câu lệnh break. Ví dụ nếu chúng ta muốn kiểm tra xem điều kiện nào đó có thỏa với một phần tử bất kỳ của một dãy:

for item in sequence:
    if condition(item):
        break
    else:
        raise Exception('Condition not satisfied.')

Ví dụ để lọc các dòng trắng khỏi bộ đọc CSV hoặc các phần tử của một danh sách:

def filter_rows(row_iterator):
    for row in row_iterator:
        if row:
            yield row

data_file = open(path, 'rb')
irows = filter_rows(csv.reader(data_file))

Hoặc đọc từng dòng từ tập tin văn bản:

datafile = open('datafile')
for line in datafile:
    do_something(line)

Điều này làm được là vì các đối tượng tập tin hỗ trợ phương thức next() y như các bộ lặp khác: danh sách, bộ, từ điển (cho các khóa của nó), và các bộ sinh.

Một điều cần lưu ý là bạn không thể dùng lẫn lộn next()read() ở các đối tượng tập tin trừ khi bạn dùng Python 2.5 trở lên.

EAFP v.s. LBYL

Easier to Ask Forgiveness than Permission: Dễ xin sự tha thứ hơn là sự cho phép. Ý là cứ làm đi và tìm sự tha thứ sau nếu làm sai, còn hơn là tìm sự cho phép trước khi làm.

Look Before You Leap: Nhìn trước khi nhảy. Ý là phải xem xét hết các trường hợp có thể xảy ra trước khi làm.

Thông thường EAFP được ưa chuộng hơn.

Ví dụ như để ép kiểu một biến thành kiểu chuỗi, ta có thể gói đoạn mã trong một câu lệnh try thay vì dùng isinstance(). Và thông thường thì bạn sẽ nhận ra giải pháp tổng quát hơn là nếu bạn cố tìm ra mọi trường hợp có thể.

try:
    return str(x)
except TypeError:
    ...

Luôn luôn chỉ rõ kiểu biệt lệ! Không bao giờ dùng vế except đơn giản vì nó sẽ chụp luôn cả những biệt lệ không lường trước, làm cho mã của bạn khó gỡ rối.

Không dùng from module import *

Thay vào đó, tham chiếu tới tên qua tên mô-đun:

import module
module.name

Hoặc dùng tên mô-đun ngắn:

import long_module_name as mod
mod.name

Hoặc tự nhập vào các tên bạn cần:

from module import name
name

Mô-đun và kịch bản

Để vừa tạo một mô-đun và một kịch bản chạy được:

if __name__ == '__main__':
    # script code here

Trừ trường hợp rất cần thiết, bạn không nên đặt mã thực thi ở mức cao nhất mà hãy đặt chúng ở trong các hàm, các lớp, hoặc các phương thức rồi dùng nó trong if __name__ == '__main__'.

Cấu trúc mô-đun

Một mô-đun nên có cấu trúc như sau:

"""module docstring"""

# imports
# constants
# exception classes
# interface functions
# classes
# internal functions & classes

def main(...):
    ...

if __name__ == '__main__':
    status = main()
    sys.exit(status)

Gói

package/
    __init__.py    # <-- Lưu ý
    module1.py
    subpackage/
        __init__.py
        module2.py
  • Dùng để quản lý dự án
  • Giảm các mục trong đường dẫn nạp (load-path)
  • Giảm xung đột tên

Ví dụ:

import package.module1
from packages.subpackage import module2
from packages.subpackage.module2 import name

Trong Python 2.5 chúng ta có nhập tuyệt đối (absolute import) và nhập tương đối (relative import):

from __future__ import absolute_import

Đơn giản tốt hơn phức tạp

Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it.

—Brian W. Kernighan, cùng tác giả của The C Programming Language và là chữ K trong AWK

Tạm dịch: Gỡ rối khó gấp hai lần viết mã. Cho nên, nếu bạn viết mã lanh lợi nhất có thể, thì bạn, theo định nghĩa, không đủ thông minh để gỡ rối. Nói một cách khác, hãy giữ cho chương trình của bạn đơn giản.

Đừng sáng tạo lại bánh xe

Trước khi viết mã, hãy:

  • Xem qua thư viện chuẩn của Python
  • Xem qua chỉ mục các gói Python (còn được biết đến là Cửa hàng phô mai, Cheese Shop)
  • Tìm qua mạng