Lập trình python một cách Idiomatic

Lập trình python một cách Idiomatic
Photo by jakob5200 from Pixabay

“Idiomatic python” có nghĩa là viết code theo cách mà cộng đồng Python cho rằng code nên được viết như vậy. Ai là người quyết định code nên được viết như thế nào? Tất nhiên là tất cả những người phát triển Python, thông qua code của họ đã làm nên cái gì gọi là idiomatic. Đó cũng là nội dung sẽ được trình bày trong bài viết này. Người thực sự quyết định trong một số trường hợp là BDFL (Guido) hoặc PEP.

Bạn chưa biết gì về pythonic code? Không sao, trong bài viết này sẽ trình bày cho bạn một số khái niệm cơ bản, giúp chúng ta viết code Python một cách chuyên nghiệp và “Pythonic” nhất.

Mục lục

Cấu trúc điều khiển và hàm

Lệnh if

Tránh việc so sánh trực tiếp với True, False hoặc None

Với mỗi đối tượng, luôn luôn có sẵn hoặc bạn có thể định nghĩa “đúng-sai” cho nó. Việc kiểm tra một điều kiện là đúng hay sai phụ thuộc vào định nghĩa đúng-sai của những đối tượng này. Việc định nghĩa đúng-sai rất đơn giản, tất cả những điều sau đều được coi là sai (False)

  • None
  • False
  • 0 cho dữ liệu số
  • một chuỗi rỗng (xâu rỗng, list rỗng, tuple rỗng, set rỗng)
  • dictionary rỗng
  • kết quả 0 hoặc None được trả về bởi các phương thức __len__ hoặc __nonzero__

Điều kiện cuối cùng, cho phép chúng ta có thể tự định nghĩa đúng-sai cho các đối tượng thông qua các phương thức __len__ hoặc __nonzero__.

Tất cả các kết quả khác với kết quả ở trên đều được coi là đúng (True). Câu lệnh if sử dụng đúng-sai và bạn nên sử dụng đúng-sai của chính các đối tượng. Ví dụ, thay vì so sánh:

if foo == True:

Bạn chỉ cần kiểm tra như sau là đủ:

if foo:

Có nhiều lý do để làm việc này. Lý do dễ nhìn thấy nhất là cho dù code của bạn có thay đổi, foo có thể nhận giá trị int thay vì chỉ True hay False thì code của bạn vẫn hoạt động bình thường.

Ở một mức độ sâu hơn, thì lý do để làm việc này liên quan đến equalityidentity. Sử dụng phép so sánh == sẽ so sánh xem hai đối tượng có cùng giá trị hay không (được định nghĩa ở thuộc tính _eq). Sử dụng is sẽ so sánh xem hai đối tượng có phải là một hay không.

Lưu ý rằng, có một số trường hợp, is hoạt động giống như là ==, tuy nhiên đây chỉ là những trường hợp đặc biệt và bạn không nên tin tưởng chúng.

Kết quả là bạn nên tránh việc so sánh trực tiếp với những giá trị như False, None hay những chuỗi rỗng như [], {}, (). Nếu một biến my_list mà rỗng thì điều kiện if my_list: sẽ cho kết quả là False.

Tuy nhiên, có một số trường hợp, việc so sánh với None được recommend nhưng không bắt buộc. Một hàm cần kiểm tra xem tham số có giá trị mặc định là None đã thực sự được truyền giá trị vào hay chưa, thì việc so sánh với None là cần thiết. Ví dụ:

def insert_value(value, position=None):
    """Inserts a value into my container, optionally at the
    specified position"""
    if possition is not None:
        ....

Điều gì xảy ra nếu chúng ta sử dụng if position:? Nếu một người muốn điền giá trị vào vị trí 0 thì hàm của chúng ta sẽ hoạt động như là giá trị position chưa được truyền vào vì 0 cũng được coi là sai.

Lưu ý rằng, ở ví dụ trên chúng ta sử dụng is not. Việc so sánh với None (singleton trong Python) luôn luôn phải sử dụng is hoặc is not chứ không sử dụng == (PEP 8).

Hãy sử dụng giá trị đúng-sai của các đối tượng trong các lệnh kiểm tra điều kiện.

Harmful

def number_of_evil_robots_attacking():
    return 10
def should_raise_shields():
    # "We only raise Shields when one or more giant robots attack,
    # so I can just return that value..."
    return number_of_evil_robots_attacking()
if should_raise_shields() == True:
    raise_shields()
    print('Shields raised')
else:
    print('Safe! No giant robots attacking')

Idiomatic

def number_of_evil_robots_attacking():
    return 10
def should_raise_shields():
    # "We only raise Shields when one or more giant robots attack,
    # so I can just return that value..."
    return number_of_evil_robots_attacking()
if should_raise_shields():
    raise_shields()
    print('Shields raised')
else:
    print('Safe! No giant robots attacking')

Tránh việc lặp đi lặp lại biến trong lệnh if

Khi bạn muốn kiểm tra một biến với nhiều giá trị trong một lệnh, việc lặp đi lặp lại biến trong phải là một cách hay. Hãy sử dụng một “iterable” sẽ giúp code của bạn sáng sủa và dễ đọc hơn.

Harmful

is_generic_name = False
name = 'Tom'
if name == 'Tom' or name == 'Dick' or name == 'Harry':
    is_generic_name = True

Idiomatic

name = 'Tom'
is_generic_name = name in ('Tom', 'Dick', 'Harry')

Tránh việc sử dụng lệnh con của câu điều kiện cùng dòng với dấu hai chấm

Hãy sử dụng indent để phân biệt các khối lệnh (giống như code ở tất cả những chỗ khác). Điều đó giúp chúng ta dễ dàng hiểu được những gì được gọi ở các điều kiện khác nhau. Các lệnh if, elif hay else nên ở riêng một dòng và không có lệnh nào ở sau :.

Harmful

name = 'Jeff'
address = 'New York, NY'
if name: print(name)
print(address)

Idiomatic

name = 'Jeff'
address = 'New York, NY'
if name:
    print(name)
print(address)

Vòng lặp for

Sử dụng hàm enumerate thay vì tạo một biến “index”

Lập trình viên từ những ngôn ngữ khác thường tạo ra một biến “index” cho vòng lặp. Ví dụ trong ngôn ngữ C.

int i;
for (i = 0; i < container; i++)
{
    // Do stuff
}

Tuy nhiên Python có hàm enumerate có thể giúp chúng ta làm việc đó rất dễ dàng:

Harmful

my_container = ['Larry', 'Moe', 'Curly']
index = 0
for element in my_container:
    print ('{} {}'.format(index, element))
        index += 1

Idiomatic

my_container = ['Larry', 'Moe', 'Curly']
for index, element in enumerate(my_container):
    print ('{} {}'.format(index, element))

Sử dụng từ khoá in để duyệt “iterable”

Lập trình viên từ các ngôn ngữ thiếu cấu trúc for_each thường sử dụng index để duyệt qua các phần tử. Tuy nhiên Python có từ khoá in giúp chúng ta làm được việc này dễ hơn rất nhiều.

Harmful

my_list = ['Larry', 'Moe', 'Curly']
index = 0
while index < len(my_list):
    print (my_list[index])
    index += 1

Idiomatic

my_list = ['Larry', 'Moe', 'Curly']
for element in my_list:
    print (element)

Sử dụng else để thực thi code sau khi vòng lặp for kết thúc

Một điều khá ít người biết, đó là vòng lặp for của Python cũng có thể có mệnh đề else. Mệnh đề else sẽ được thực thi sau khi vòng lặp kết thúc, trừ khi nó được ngắt bằng lệnh break. Điều này cho phép bạn kiểm tra điều kiện bên trong vòng lặp for, break nếu điều kiện thoả mãn với một phần tử nào đó và câu lệnh else để thực thi code nếu không có phần tử nào thoả mãn điều kiện. Việc này hoàn toàn không cần đến một flag để lưu trữ việc điều kiện có được thoả mãn hay không.

Trong tình huống dưới đây, chúng ta cần kiểm tra email được các user đăng ký có gì bất thường hay không? Code idiomatic dễ hiểu hơn rất nhiều và không cần dùng đến flag has_malformed_email_address. Đây cũng là ví dụ rất tốt để làm quen với for...else.

Harmful

for user in get_all_users():
    has_malformed_email_address = False
    print ('Checking {}'.format(user))
    for email_address in user.get_all_email_addresses():
        if email_is_malformed(email_address):
            has_malformed_email_address = True
            print ('Has a malformed email address!')
            break
    if not has_malformed_email_address:
        print ('All email addresses are valid!')

Idiomatic

for user in get_all_users():
    print ('Checking {}'.format(user))
    for email_address in user.get_all_email_addresses():
        if email_is_malformed(email_address):
            print ('Has a malformed email address!')
            break
    else:
        print ('All email addresses are valid!')

Hàm

Tránh sử dụng '', []{} làm giá trị mặc định cho tham số

Việc này đã được đề cập khá nhiều trong các tutorial của Python. Nói ngắn gọn lại là chúng ta nên sử dụng names=None thay vì names=[] làm giá trị mặc định của các tham số. Minh hoạ dưới đây sẽ giải thích rõ hơn tại sao?

Harmful

# The default value [of a function] is evaluated only once.
# This makes a difference when the default is a mutable object
# such as a list, dictionary, or instances of most classes.  For
# example, the following function accumulates the arguments
# passed to it on subsequent calls.
def f(a, L=[]):
    L.append(a)
    return L
print(f(1))
print(f(2))
print(f(3))
# This will print
#
# [1]
# [1, 2]
# [1, 2, 3]

Idiomatic

# If you don't want the default to be shared between subsequent
# calls, you can write the function like this instead:
def f(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L
print(f(1))
print(f(2))
print(f(3))
# This will print
# [1]
# [2]
# [3]

Sử dụng *args**kwargs để nhận tham số với số lượng tùy ý

Nhiều khi các hàm cần nhận vào nhiều tham số với số lượng không biết trước, hàm có thể chỉ sử dụng một vài tham số và truyền tham số cho các hàm tiếp theo. Việc sử dụng *args**kwargs cho phép chúng ta tạo ra một hàm có thể nhận tham số với số lượng tuỳ ý.

Việc này cũng rất hữu ích khi chúng ta phát triển những API nhưng cần tương thích ngược với những phiên bản cũ. Nếu hàm chấp nhận tham số tuỳ ý, chúng ta có thể thêm vào các tham số mới mà không làm ảnh hưởng đến những phiên bản cũ sử dụng ít tham số hơn. Nếu có tài liệu tốt, mọi thứ sẽ rất trơn tru.

Harmful

def make_api_call(foo, bar, baz):
    if baz in ('Unicorn', 'Oven', 'New York'):
        return foo(bar)
    else:
        return bar(foo)
# I need to add another parameter to `make_api_call`
# without breaking everyone's existing code.
# I have two options...
def so_many_options():
# I can tack on new parameters, but only if I make
# all of them optional...
    def make_api_call(foo, bar, baz, qux=None, foo_polarity=None,
                      baz_coefficient=None, quux_capacitor=None,
                      bar_has_hopped=None, true=None, false=None,
                      file_not_found=None):
        # ... and so on ad infinitum
        return file_not_found
def version_graveyard():
        # ... or I can create a new function each time the signature
        # changes.
    def make_api_call_v2(foo, bar, baz, qux):
        return make_api_call(foo, bar, baz) - qux
    def make_api_call_v3(foo, bar, baz, qux, foo_polarity):
        if foo_polarity != 'reversed':
            return make_api_call_v2(foo, bar, baz, qux)
        return None
    def make_api_call_v4(
                foo, bar, baz, qux, foo_polarity, baz_coefficient):
        return make_api_call_v3(
                foo, bar, baz, qux, foo_polarity) * baz_coefficient
    def make_api_call_v5(
                foo, bar, baz, qux, foo_polarity,
                baz_coefficient, quux_capacitor):
        # I don't need 'foo', 'bar', or 'baz' anymore, but I have to
        # keep supporting them...
        return baz_coefficient * quux_capacitor
    def make_api_call_v6(
                foo, bar, baz, qux, foo_polarity, baz_coefficient,
                quux_capacitor, bar_has_hopped):
        if bar_has_hopped:
            baz_coefficient *= -1
            return make_api_call_v5(foo, bar, baz, qux,
                                    foo_polarity, baz_coefficient,
                                    quux_capacitor)
    def make_api_call_v7(
                foo, bar, baz, qux, foo_polarity, baz_coefficient,
                quux_capacitor, bar_has_hopped, true):
        return true
    def make_api_call_v8(
                foo, bar, baz, qux, foo_polarity, baz_coefficient,
                quux_capacitor, bar_has_hopped, true, false):
        return false
    def make_api_call_v9(
                foo, bar, baz, qux, foo_polarity, baz_coefficient,
                quux_capacitor, bar_has_hopped,
                true, false, file_not_found):
        return file_not_found

Idiomatic

def make_api_call(foo, bar, baz):
    if baz in ('Unicorn', 'Oven', 'New York'):
        return foo(bar)
    else:
        return bar(foo)
# I need to add another parameter to `make_api_call`
# without breaking everyone's existing code.
# Easy...
def new_hotness():
    def make_api_call(foo, bar, baz, *args, **kwargs):
        # Now I can accept any type and number of arguments
        # without worrying about breaking existing code.
        baz_coefficient = kwargs['the_baz']
        # I can even forward my args to a different function without
        # knowing their contents!
        return baz_coefficient in new_function(args)

Làm việc với dữ liệu

List

Sử dụng list comprehension để tạo list mới từ list đã có

Sử dụng list comprehension sẽ làm gắn gọn việc tạo list mới từ những list đã có. Điều này đặc biệt đúng nếu chúng ta vừa chuyển thể list vừa kiểm tra một số điều kiện.

Ngoài ra có nhiều lợi ích từ việc sử dụng list comprehension (và biến thể của nó, biểu thức generator) liên quan đến hiệu suất bởi vì trình thông dịch đã được tối ưu hoá cho việc này.

Harmful

some_other_list = range(10)
some_list = list()
for element in some_other_list:
    if is_prime(element):
        some_list.append(element + 5)

Idiomatic

some_other_list = range(10)
some_list = [element + 5
             for element in some_other_list
             if is_prime(element)]

Sử dụng toán tử * đại diện cho “phần còn lại” của list

Rất nhiều trường hợp, nhất là với những tham số tuỳ ý của một hàm, chúng ta cần lấy ra phần tử đầu tiên (hoặc cuối cùng) của list và giữ nguyên phần còn lại. Python 2 không có cách dễ dàng để làm việc này ngoài cách cắt list ra như ví dụ dưới đây. Nhưng Python 3 có toán tử * giúp chúng ta trong trường hợp như vậy.

Harmful

some_list = ['a', 'b', 'c', 'd', 'e']
(first, second, rest) = some_list[0], some_list[1], some_list[2:]
print(rest)
(first, middle, last) = some_list[0], some_list[1:-1], some_list[-1]
print(middle)
(head, penultimate, last) = some_list[:-2], some_list[-2], some_list[-1]
print(head)

Idiomatic

some_list = ['a', 'b', 'c', 'd', 'e']
(first, second, *rest) = some_list
print(rest)
(first, *middle, last) = some_list
print(middle)
(*head, penultimate, last) = some_list
print(head)

Dictionary

Sử dụng tham số default của dic.get để đặt giá trị mặc định

Tham số default của dic.get thường xuyên bị bỏ quên. Nếu không sử dụng tham số này (hoặc class collections.defaultdict) code của bạn sẽ khá rắc rối với một vài lệnh if.

Harmful

log_severity = None
if 'severity' in configuration:
    log_severity = configuration['severity']
else:
    log_severity = 'Info'

Idiomatic

log_severity = configuration.get('severity', 'Info')

Sử dụng dict comprehension để xây dựng dict rõ ràng và hiệu quả

List comprehension của Python thường được biết đến nhiều hơn là dict comprehension. Mục đích của chúng giống nhau: đó là xây dựng dict mới từ dict cũ một cách dễ dàng với cú pháp “comprehension”.

Harmful

user_email = {}
for user in users_list:
    if user.email:
        user_email[user.name] = user.email

Idiomatic

user_email = {user.name: user.email
              for user in users_list if user.email}

String

Nên sử dụng hàm format để định dạng xâu

Có 3 cách cơ bản để định dạng xâu (tạo xâu từ các xâu hard code và các biến). Cách làm tệ nhất là sử dụng toán tử + để nối các xâu từ hard code và biến. Một cách khá hơn là sử dụng format “kiểu cũ”, đó là định dạng chuỗi mà sử dụng % như nhiều ngôn ngữ khác.

Nhưng cách rõ ràng, dễ hiểu và idiomatic nhất là sử dụng hàm format. Tương tự như định dang “kiểu cũ”, nó cũng cần một chuỗi và thay thế các placeholder thành giá trị truyền vào. Và nó có thể làm được nhiều hơn thế. Với hàm format chúng ta có thể đặt tên cho các placeholder, truy cập các thuộc tính, định dạng độ rộng và nhiều thứ khác nữa. Sử dụng hàm format là cách định dạng xâu rõ ràng nhất.

def get_formatted_user_info_worst(user):
    # Tedious to type and prone to conversion errors
    return 'Name: ' + user.name + ', Age: ' + \
            str(user.age) + ', Sex: ' + user.sex
def get_formatted_user_info_slightly_better(user):
    # No visible connection between the format string placeholders
    # and values to use.  Also, why do I have to know the type?
    # Don't these types all have __str__ functions?
    return 'Name: %s, Age: %i, Sex: %c' % (
                               user.name, user.age, user.sex)

Idiomatic

def get_formatted_user_info(user):
    # Clear and concise.  At a glance I can tell exactly what
    # the output should be.  Note: this string could be returned
    # directly, but the string itself is too long to fit on the
    # page.
    output = 'Name: {user.name}, Age: {user.age}'
             ', Sex: {user.sex}'.format(user=user)
    return output

Sử dụng ''.join để tạo một xâu từ các phần tử của list

Đây là cách làm nhanh hơn, sử dụng ít bộ nhớ hơn, và bạn có thể thấy ai cũng dùng cách này. Lưu ý rằng, giá trị bên trong '' là ký tự ngăn cách các phần tử của list. Xâu rỗng nghĩa là chúng ta tạo một xâu mới từ các phần tử mà không có gì ngăn cách chúng.

Harmful

result_list = ['True', 'False', 'File not found']
result_string = ''
for result in result_list:
    result_string += result

Idiomatic

result_list = ['True', 'False', 'File not found']
result_string = ''.join(result_list)

Chain các hàm của xâu giúp việc chuyển đổi xâu rõ ràng hơn

Khi chúng ta cần áp dụng liên tiếp các chuyển đổi với một xâu, việc chain các hàm giúp việc này rõ ràng hơn là sử dụng các biến lưu tạm. Tuy nhiên, quá nhiều hàm liên tiếp lại khó theo dõi. Vì vậy không quá 3 hàm liên tiếp là một quy ước tốt.

Harmful

book_info = ' The Three Musketeers: Alexandre Dumas'
formatted_book_info = book_info.strip()
formatted_book_info = formatted_book_info.upper()
formatted_book_info = formatted_book_info.replace(':', ' by')

Idiomatic

book_info = ' The Three Musketeers: Alexandre Dumas'
formatted_book_info = book_info.strip().upper().replace(':', ' by')

Class

Sử dụng dấu gạch dưới cho tên biến và phương thức để đánh dấu “private”

Tất cả mọi thuộc tính của class, có thể là dữ liệu hoặc phương thức, đều là “public” trong Python. Mọi người đều có thể dễ dàng thêm thuộc tính vào một class kể cả khi nó đã được định nghĩa rồi. Hơn nữa, nếu một class được kế thừa, những class con có thể vô tình thay đổi thuộc tính của class cha. Cuối cùng, việc dùng dấu gạch dưới là dấu hiệu cho mọi người biết rằng phần nào của class là “public” về mặt logic (không được thay đổi mà không tương thích ngược) và phần nào hoàn toàn được sử dụng nội bộ mà không được dùng trực tiếp từ bên ngoài.

Những điều dưới đây là quy ước, và thường được sử dụng rộng rãi.

Thứ nhất, những thuộc tính “protected” nghĩa là chúng không được dùng trực tiếp từ bên ngoài, sẽ bắt đầu bằng 1 dấu gạch dưới. Thứ hai, những thuộc tính “private”, tức là chúng không được truy cấp từ các class con kế thừa từ class cha, sẽ bắt đầu bằng 2 dấu gạch dưới. Tất nhiên, có nhiều biến thể khác nhau của quy ước trên. Và tất cả chỉ là quy ước và các developer ngầm hiểu với nhau. Chứ thực ra, mọi thuộc tính của class đều là “public” và không gì có thể thay đổi điều đó. Nhưng những quy ước này đang được sử dụng rộng rãi, và bạn nên tôn trọng nó.

Tôi nghĩ rằng chúng ta nên sử dụng quy ước trên, thay vì chỉ dùng 1 dấu gạch dưới cho mọi thuộc tính “private”. Ít người để ý rằng, việc thêm những tiền tố vào tên cũng có một số ý nghĩa nhất định. Ví dụ, thêm 1 dấu gạch dưới sẽ làm cho đối tượng không được import khi chúng ta dùng cú pháp “all” (import *). Thêm 2 dấu gạch dưới vào thuộc tính cũng liên quan đến việc chia cắt tên của Python. Nó có một tác dụng là các class con sẽ không thay thế thuộc tính đó một cách không lường trước. Nếu trong class Foo chúng ta định nghĩa def __bar() thì nó sẽ được “chia cắt” thành _classname__attributename.

Harmful

class Foo():
    def __init__(self):
        self.id = 8
        self.value = self.get_value()
    def get_value(self):
        pass
    def should_destroy_earth(self):
        return self.id == 42
class Baz(Foo):
    def get_value(self, some_new_parameter):
    """Since 'get_value' is called from the base class's
    __init__ method and the base class definition doesn't
    take a parameter, trying to create a Baz instance will
    fail.
    """
    pass
class Qux(Foo):
    """We aren't aware of Foo's internals, and we innocently
    create an instance attribute named 'id' and set it to 42.
    This overwrites Foo's id attribute and we inadvertently
    blow up the earth.
    """
    def __init__(self):
        super(Qux, self).__init__()
        self.id = 42
        # No relation to Foo's id, purely coincidental
q = Qux()
b = Baz() # Raises 'TypeError'
q.should_destroy_earth() # returns True
q.id == 42 # returns True

Idiomatic

class Foo():
    def __init__(self):
        """Since 'id' is of vital importance to us, we don't
        want a derived class accidentally overwriting it.  We'll
        prepend with double underscores to introduce name
        mangling.
        """
        self.__id = 8
        self.value = self.__get_value() # Our 'private copy'
    def get_value(self):
        pass
    def should_destroy_earth(self):
        return self.__id == 42
    # Here, we're storing an 'private copy' of get_value,
    # and assigning it to '__get_value'.  Even if a derived
    # class overrides get_value is a way incompatible with
    # ours, we're fine
    __get_value = get_value
class Baz(Foo):
    def get_value(self, some_new_parameter):
        pass
class Qux(Foo):
    def __init__(self):
        """Now when we set 'id' to 42, it's not the same 'id'
        that 'should_destroy_earth' is concerned with.  In fact,
        if you inspect a Qux object, you'll find it doesn't
        have an __id attribute.  So we can't mistakenly change
        Foo's __id attribute even if we wanted to.
        """
        self.id = 42
        # No relation to Foo's id, purely coincidental
        super(Qux, self).__init__()
q = Qux()
b = Baz() # Works fine now
q.should_destroy_earth() # returns False
q.id == 42 # returns True
with pytest.raises(AttributeError):
    getattr(q, '__id')

Định nghĩa phương thức __str__ để hiển thị class dễ hiểu hơn

Khi định nghĩa một class mà nó được sử dụng với hàm print(), thì mặc định Python sẽ in ra một số thông tin không được thiết thực cho lắm. Bằng cách định nghĩa phương thức __str__, bạn có thể thay đổi việc gọi hàm print với các đối tượng của class theo cách mà bạn muốn.

Harmful

class Point():
    def __init__(self, x, y):
        self.x = x
        self.y = y
p = Point(1, 2)
print (p)
# Prints '<__main__.Point object at 0x91ebd0>'

Idiomatic

class Point():
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def __str__(self):
        return '{0}, {1}'.format(self.x, self.y)
p = Point(1, 2)
print (p)
# Prints '1, 2'

Set

Sử dụng tập hợp để tránh các dữ liệu lặp lại

Với list và dict, thì chúng ta thường xuyên gặp các giá trị bị lặp lại. Trong list của họ các nhân viên trong một công ty lớn, chúng ta cần tìm ra những họ phổ biến (xuất hiện nhiều lần trong list). Nếu chúng ta cần liệt kê những họ khác nhau, chúng ta có thể sử dụng tập hợp. Ba thuộc tính của tập hợp (set) khiến chúng thích hợp nhất cho bài toán này:

  1. Set chỉ lưu các phần tử 1 lần duy nhất
  2. Thêm một giá trị đã có vào set, giá trị này sẽ bị bỏ qua
  3. Một set có thể được tạo ra từ các “iterable” một cách dễ dàng

Trở lại với ví dụ của chúng ta, giả sử chúng ta đã định nghĩa hàm display để hiển thị các phần tử của một chuỗi. Liệu chúng ta có cần thay đổi hàm này nếu chuyển từ list sang dùng set.

Tất nhiên là không? Nếu hàm display được định nghĩa đúng, chúng ta chỉ cần thay list thành set là được. Đó là bởi vì, set cũng giống như list, đều là “iterable” và chúng ta có thể sử dụng vòng lặp for, cú pháp comprehension, v.v…

Harmful

unique_surnames = []
for surname in employee_surnames:
    if surname not in unique_surnames:
        unique_surnames.append(surname)
def display(elements, output_format='html'):
    if output_format == 'std_out':
        for element in elements:
            print(element)
    elif output_format == 'html':
        as_html = '<ul>'
        for element in elements:
            as_html += '<li>{}</li>'.format(element)
        return as_html + '</ul>'
    else:
        raise RuntimeError('Unknown format {}'.format(output_format))

Idiomatic

unique_surnames = set(employee_surnames)
def display(elements, output_format='html'):
    if output_format == 'std_out':
        for element in elements:
            print(element)
    elif output_format == 'html':
        as_html = '<ul>'
        for element in elements:
            as_html += '<li>{}</li>'.format(element)
        return as_html + '</ul>'
    else:
        raise RuntimeError('Unknown format {}'.format(output_format))

Sử dụng set comprehension để tạo set nhanh gọn

Cú pháp set comprehension là cú pháp khá mới trong Python, do đó, thường bị quên lãng. Cũng giống như list comprehension, set cũng có thể tạo được bằng cách sử dụng set comprehension. Cú pháp của chúng khá giống nhau, ngoại trừ dấu ngoặc ngoài cùng.

Harmful

users_first_names = set()
for user in users:
    users_first_names.add(user.first_name)

Idiomatic

users_first_names = {user.first_name for user in users}

Hiểu và sử dụng các phép toán tập hợp

Set là tập hợp, và nó khá dễ hiểu. Cũng tương tự như dict với các key và không có giá trị, class set cũng định nghĩa giao diện IterableContainer. Do đó, set có thể sử dụng vòng lặp for.

Với những lập trình viên chưa quen với tập hợp, nó thường ít được sử dụng. Để hiểu và sử dụng được nó một cách hiệu quả, thì việc hiểu nguồn gốc toán học của nó là rất quan trọng. Lý thuyết tập hợp là một nhánh của toán học nghiên cứu các vấn đề của tập hợp. Hiểu được một số khái niệm toán cơ bản của tập hợp sẽ giúp chúng ta làm việc với set rất hiệu quả.

Chúng ta chỉ cần nhớ một số phép toán đơn giản sau với tập hợp là đủ cho các thao tác thông thường.

  • Hợp (tập hợp chứa các phần tử nằm trong A hoặc B hoặc cả hai)
A | B
  • Giao (tập hợp chứa các phần tử nằm trong cả A và B)
A & B
  • Hiệu (tập hợp chứa các phần tử nằm trong A mà không nằm trong B)
A - B
  • Hiệu đối xứng (Tập hợp chứ các phần tử nằm trong A hoặc B mà không nằm trong cả hai)
A ^ B

Khi làm việc với danh sách các dữ liệu, một việc khá thông dụng là cần tìm các phần tử xuất hiện ở nhiều danh sách khác nhau. Bất cứ khi nào bạn cần tìm phần tử chung của hai hay nhiều danh sách, hãy sử dụng set.

Dưới đây là một ví dụ điển hình:

Harmful

def get_both_popular_and_active_users():
    # Assume the following two functions each return a
    # list of user names
    most_popular_users = get_list_of_most_popular_users()
    most_active_users = get_list_of_most_active_users()
    popular_and_active_users = []
    for user in most_active_users:
        if user in most_popular_users:
            popular_and_active_users.append(user)
    return popular_and_active_users

Idiomatic

def get_both_popular_and_active_users():
    # Assume the following two functions each return a
    # list of user names
    return(set(
               get_list_of_most_active_users()) & set(
                    get_list_of_most_popular_users()))

Generator

Sử dụng generator để load các chuỗi vô hạn một cách “lazy”

Nhiều trường hợp, chúng ta cần duyệt qua các chuỗi mà số phần tử của nó có thể là vô hạn. Một số trường hợp khác, chúng ta cần cung cấp giao diện tới một chuỗi có những tính toán rất lớn, tất nhiên là chúng ta không muốn người dùng ngồi chờ chỉ để xây dựng một list.

Trong hai trường hợp trên, generator là giải pháp cho chúng ta. Generator là một dạng đặc biệt trả cho chúng ta một “iterable”. Trạng thái của generator sẽ được lưu lại và lần sau khi chúng ta gọi đến generator nó sẽ chạy tiếp từ trang thái đó. Trong ví dụ dưới đây, chúng ta sẽ xem generator giúp chúng ta như thế nào trong cả hai tính huống ở trên.

Harmful

def get_twitter_stream_for_keyword(keyword):
    """Get's the 'live stream', but only at the moment
    the function is initially called.  To get more entries,
    the client code needs to keep calling
    'get_twitter_livestream_for_user'.  Not ideal.
    """
    imaginary_twitter_api = ImaginaryTwitterAPI()
    if imaginary_twitter_api.can_get_stream_data(keyword):
        return imaginary_twitter_api.get_stream(keyword)
current_stream = get_twitter_stream_for_keyword('#jeffknupp')
for tweet in current_stream:
    process_tweet(tweet)
# Uh, I want to keep showing tweets until the program is quit.
# What do I do now? Just keep calling
# get_twitter_stream_for_keyword? That seems stupid.
def get_list_of_incredibly_complex_calculation_results(data):
    return [
        first_incredibly_long_calculation(data),
        second_incredibly_long_calculation(data),
        third_incredibly_long_calculation(data),
    ]

Idiomatic

def get_twitter_stream_for_keyword(keyword):
    """Now, 'get_twitter_stream_for_keyword' is a generator
    and will continue to generate Iterable pieces of data
    one at a time until 'can_get_stream_data(user)' is
    False (which may be never).
    """
    imaginary_twitter_api = ImaginaryTwitterAPI()
    while imaginary_twitter_api.can_get_stream_data(keyword):
        yield imaginary_twitter_api.get_stream(keyword)
# Because it's a generator, I can sit in this loop until
# the client wants to break out
for tweet in get_twitter_stream_for_keyword('#jeffknupp'):
    if got_stop_signal:
        break
    process_tweet(tweet)
def get_list_of_incredibly_complex_calculation_results(data):
    """A simple example to be sure, but now when the client
    code iterates over the call to
    'get_list_of_incredibly_complex_calculation_results',
    we only do as much work as necessary to generate the
    current item.
    """
    yield first_incredibly_long_calculation(data)
    yield second_incredibly_long_calculation(data)
    yield third_incredibly_long_calculation(data)

Sử dụng biểu thức generator thay cho list comprehension cho những trường hợp đơn giản

Khi làm việc với các chuỗi, chúng ta thường xuyên phải duyệt qua các phần tử của một chuỗi biến đổi từ chuỗi ban đầu. Ví dụ, chúng ta phải in ra tên của tất của user trong hệ thống bằng chữ in hoa.

Ý tưởng thông thường sẽ là xây dựng một list và duyệt qua các phần tử của nó. List comprehension có vẻ là ý tưởng hay cho trường hợp này. Nhưng có một ý tưởng còn hay hơn nữa, nó đã được tích hợp sẵn trong Python, đó là sử dụng biểu thức generator.

Sự khác nhau giữa chúng là gì? Một list comprehension sẽ tạo ra một list với đầy đủ các phần tử. Nếu danh sách các phần tử lớn, việc này sẽ rất mất thời gian và bộ nhớ. Một generator được trả về từ biểu thứ generator, thì ngược lại, bởi vì các phần tử chỉ được sinh ra khi được gọi. Danh sách tên user cần được in ra có thể không phải là vấn đề, nhưng thử tượng bạn cần in ra đầu mục sách của một thư viện thì sao? Bạn sẽ bị tràn bộ nhớ nếu dùng list comprehension, nhưng biểu thức generator thì thoải mái. Một ưu điểm nữa của biểu thứ generator là nó có thể tạo ra các chuỗi vô hạn.

Harmful

for uppercase_name in [name.upper() for name in get_all_usernames()]:
    process_normalized_username(uppercase_name)

Idiomatic

for uppercase_name in (name.upper() for name in get_all_usernames()):
    process_normalized_username(uppercase_name)

Context manager

Sử dụng context manager để đảm bảo tài nguyên được quản lý cẩn thận

Cũng giống như nguyên tác RAII trong các ngôn ngữ như C++, context manager (đối tượng được sử dụng bên trong lệnh with) giúp chúng ta làm việc và quản lý các tài nguyên tốt hơn và an toàn hơn. Ví dụ điển hình cho trường hợp này là đọc, ghi tập tin.

Hãy nhìn vào code “harmful” dưới đây. Điều gì xảy ra sau khi raise_exception (raise một exception)? Với code dưới đây, thì nó sẽ dừng toàn bộ chương trình và chúng ta thường quên mất điều này, và kết quả là chúng ta chẳng có cách nào để đóng file lại.

Có rất nhiều class có sẵn của Python hỗ trợ việc sử dụng context manager. Hơn nữa, các class tự định nghĩa cũng dễ dàng sử dụng context manager bằng cách định nghĩa các phương thức __enter____exit__. Các hàm có thể được bao bọc bởi context manager thông qua module contextlib.

Harmful

file_handle = open(path_to_file, 'r')
for line in file_handle.readlines():
    if raise_exception(line):
        print('No! An Exception!')

Idiomatic

with open(path_to_file, 'r') as file_handle:
    for line in file_handle:
        if raise_exception(line):
            print('No! An Exception!')

Tuple

Sử dụng tuple để unpack dữ liệu

Trong Python, chúng ta có thể “unpack” dữ liệu trong một lệnh gán gộp. Điều này rất giống với LISP (decontructing bind).

Harmful

list_from_comma_separated_value_file = ['dog', 'Fido', 10]
animal = list_from_comma_separated_value_file[0]
name = list_from_comma_separated_value_file[1]
age = list_from_comma_separated_value_file[2]
output = ('{name} the {animal} is {age} years old'.format(
                             animal=animal, name=name, age=age))

Idiomatic

list_from_comma_separated_value_file = ['dog', 'Fido', 10]
(animal, name, age) = list_from_comma_separated_value_file
output = ('{name} the {animal} is {age} years old'.format(
                             animal=animal, name=name, age=age))

Sử dụng _ làm placeholder cho dữ liệu trong tuple không được dùng

Khi gán một tuple với một số dữ liệu được sắp xếp, không phải tất cả mọi dữ liệu chúng ta đều dùng đến. Thay vì tạo ra các biến với tên đầy đủ mà không dùng đến (dễ gây hiểu nhầm), chúng ta chỉ cần sử dụng _ để người khác đọc được hiểu rằng “dữ liệu này không dùng đến”.

Harmful

(name, age, temp, temp2) = get_user_info(user)
if age > 21:
    output = '{name} can drink!'.format(name=name)
# "Wait, where are temp and temp2 being used?"

Idiomatic

(name, age, _, _) = get_user_info(user)
if age > 21:
    output = '{name} can drink!'.format(name=name)
# "Clearly, only name and age are interesting"

Biến

Tránh sử dụng biến tạm khi “swap” hai giá trị

Không có lý do gì phải sử dụng một biến tạm thời để “swap” hai giá trị trong Python. Chúng ta có thể dùng tuple để làm việc này.

Harmful

foo = 'Foo'
bar = 'Bar'
temp = foo
foo = bar
bar = temp

Idiomatic

foo = 'Foo'
bar = 'Bar'
(foo, bar) = (bar, foo)

Tổ chức code

Module và package

Sử dụng module để đóng gói khi các ngôn ngữ khác dùng đối tượng

Trong khi Python có hỗ trợ lập trình hướng đối tượng, tuy nhiên không ai yêu cầu bạn phải làm như vậy. Các lập trình Python có kinh nghiệm sử dụng các class và tính đa hình tương đối ít. Tại sao lại như vậy?

Phần lớn dữ liệu được lưu trong các class có thể dễ dàng lưu dưới dạng các list, dict hay set. Python có sẵn rất nhiều các hàm và thư viện chuẩn đã được tối ưu hóa (cả về thiết kế lẫn cài đặt) để làm việc với những dữ liệu này. Một người có thể dễ dàng thuyết phục rằng class chỉ nên được dùng khi nào cần thiết và không phần lớn là không cần đến chúng.

Trong Java, class là đơn vị cơ bản để đóng gói. Mỗi file là một class của Java, bất kể ý nghĩa hay code chạy ở bên trong. Nếu tôi cho một số các hàm tiện ích vào bên trong một class tên là Utility, chúng ta sẽ khó hiểu được một đối tượng “Utility” là thế nào. Tất nhiên đây chỉ là một ví dụ, nhưng vấn đề đã rất rõ ràng. Một khi chúng ta buộc phải dùng class cho mọi thứ, chúng ta có thể phải mang tư tưởng đó sang các ngôn ngữ khác.

Trong Python, nhóm các hàm và dữ liệu liên quan đến nhau, rất tự nhiên, được đóng gói vào các module. Nếu tôi sử dụng một framework viết Web MVC để tạo ra một trang “Chirp”, có thể tôi sẽ có một package tên là chirp với các module model, view và controller bên trong. Nếu “Chirp” là một dự án đầy triển vọng, và code có thể rất lớn, các module này cũng có thể trở thành package. Package controller có thể có chứa nhiều module controller khác nhau. Không thứ gì trong số chúng cần liên hệ với nhau ngoại trừ việc chúng đều ở trong package controller.

Nếu tất cả các module đều trở thành class, thì khả năng tương tác ngay lập tức trở thành vấn đề. Chúng ta phải cẩn thận và chính xác xác định các phương thức mà chúng là sẽ public, trạng thái nào sẽ được cập nhật, và cách mà class của chúng ta hỗ trợ test. Và thay vì list hay dict, các đối tượng PersistenceProcessing của chúng ta, chúng ta phải code cho chúng.

Lưu ý rằng, không mô tả nào của “Chirp” đòi hỏi việc sử dụng class. Một lệnh import đơn giản sẽ giúp việc chia sẻ code và đóng gói dễ hơn rất nhiều. Truyền trạng thái như là các tham số một cách rõ ràng giúp giữ mọi thứ liên kết với nhau. Và việc nhận, xử lý và truyền dữ liệu trong hệ thống của chúng ta dễ hơn rất nhiều.

Tất nhiên, class là cách đơn giản, tự nhiên và thích hợp để đại diện cho các “sự vật”. Nhiều trường hợp, lập trình hướng đối tượng là một paradigm tốt. Tuy nhiên, không nên chỉ sử dụng một paradigm duy nhất.

Định dạng

Sử dụng toàn ký tự hoa để định nghĩa hằng số global

Để phân biệt các hằng số được định nghĩa ở tầng module (hoặc global trong một file script duy nhất) với các tên được import, hãy sử dụng toàn ký tự hoa.

Harmful

seconds_in_a_day = 60 * 60 * 24
# ...
def display_uptime(uptime_in_seconds):
    percentage_run_time = (uptime_in_seconds/seconds_in_a_day) * 100
    # "Huh!? Where did seconds_in_a_day come from?"
    return 'The process was up {percent} percent of the day'.format(
                                       percent=int(percentage_run_time))
# ...
uptime_in_seconds = 60 * 60 * 24
display_uptime(uptime_in_seconds)

Idiomatic

SECONDS_IN_A_DAY = 60 * 60 * 24
# ...
def display_uptime(uptime_in_seconds):
    percentage_run_time = (uptime_in_seconds/SECONDS_IN_A_DAY) * 100
    # "Clearly SECONDS_IN_A_DAY is a constant defined
    # elsewhere in this module."
    return 'The process was up {percent} percent of the day'.format(
                                       percent=int(percentage_run_time))
# ...
uptime_in_seconds = 60 * 60 * 24
display_uptime(uptime_in_seconds)

Tránh đặt nhiều lệnh trên cùng dòng

Mặc dù ngôn ngữ cho phép chúng ta dùng dấu chấm phẩy (;) để kết thúc lệnh, nhưng việc dùng nó mà không có lý do gì đặc biệt sẽ làm code khó hiểu hơn. Đặc biệt khi bạn dùng nó để đặt các lệnh trên một dòng, nhất là kết hợp với if, elifelse thì vấn đề còn tồi tệ hơn nữa.

Harmful

if this_is_bad_code: rewrite_code(); make_it_more_readable();

Idiomatic

if this_is_bad_code:
    rewrite_code()
    make_it_more_readable()

Định dang code tuân theo PEP 8

Python có một bộ tiêu chuẩn về định dạng code của ngôn ngữ là PEP 8. Nếu bạn xem các commit của một dự án Python, bạn có thể thấy một vài commit liên quan đến sửa code theo PEP 8. Lý do rất đơn giản: Nếu tất cả chúng ta đều đồng ý với một bộ quy tắc chung về đặt tên là định dạng code, thì code Python sẽ rất dễ hiểu với cả người mới và những lập trình viên có kinh nghiệm. PEP 8 là một ví dụ rất rõ ràng về các idiom trong cộng đồng Python. Hãy đọc nó, cài đặt cách package hỗ trợ vào editor của bạn (editor nào cũng có cả) và viết code theo cách mà cộng đồng Python đã tuân theo. Dưới đây là một số ví dụ:

ClassCamelCaseclass StringManipulator():
VariableWords joined by _joined_by_underscore = True
FunctionWords joined by _def multi_word_name(words):
ConstantAll uppercaseSECRET_KEY = 42

Về cơ bản những thứ chưa được liệt kê nên được đặt tên giống quy tắc với đặt tên biến/hàm (từ ngăn cách bằng dấu gạch dưới).

Script thực thi

Sử dụng sys.exit trong script để trả về đúng mã lỗi

Chúng ta có thể cho một loạt code vào bên trong if __name__ == '__main__' mà không trả về bất cứ thứ gì. Tuy nhiên, nên tránh việc này.

Hãy sử dụng một hàm chính chứa các code cần thiết để chạy. Sử dụng sys.exit trong hàm đó để trả về mã lỗi nếu có bất cứ thứ gì bất thường, và trả về 0 nếu mọi chuyện bình thường. Code bên trong if __name__ == '__main__' chỉ nên gọi sys.exit với kết quả trả về của hàm chính này là tham số.

Bằng cách này, chúng ta cho phép các script có thể được dùng trong pipeline của Unix, có thể theo dõi script chạy thành công hay thất bại mà không cần code thêm các xử lý mới, và các chương trình khác có thể gọi script rất an toàn.

Harmful

if __name__ == '__main__':
    import sys
    # What happens if no argument is passed on the
    # command line?
    if len(sys.argv) > 1:
        argument = sys.argv[1]
        result = do_stuff(argument)
        # Again, what if this is False? How would other
        # programs know?
        if result:
            do_stuff_with_result(result)

Idiomatic

def main():
    import sys
    if len(sys.argv) < 2:
        # Calling sys.exit with a string automatically
        # prints the string to stderr and exits with
        # a value of '1' (error)
        sys.exit('You forgot to pass an argument')
    argument = sys.argv[1]
    result = do_stuff(argument)
    if not result:
        sys.exit(1)
        # We can also exit with just the return code
    do_stuff_with_result(result)
    # Optional, since the return value without this return
    # statment would default to None, which sys.exit treats
    # as 'exit with 0'
    return 0
# The three lines below are the canonical script entry
# point lines.  You'll see them often in other Python scripts
if __name__ == '__main__':
    sys.exit(main())

Sử dụng if __name__ == '__main__' để làm file vừa có thể chạy vừa có thể import vào file khác

Không như hàm main() của ngôn ngữ C, Python không có quy tắc nào cho điểm bắt đầu của script. Trình thông dịch sẽ thực hiện ngay lập tức các lệnh sau khi load từ mã nguồn. Nếu bạn muốn một file vừa là một module Python có thể import vào file khác, vừa có thể chạy như một script độc lập, hãy sử dụng if __name__ == '__main__'.

Harmful

import sys
import os
FIRST_NUMBER = float(sys.argv[1])
SECOND_NUMBER = float(sys.argv[2])
def divide(a, b):
    return a/b
# I can't import this file (for the super
# useful 'divide' function) without the following
# code being executed.
if SECOND_NUMBER != 0:
    print(divide(FIRST_NUMBER, SECOND_NUMBER))

Idiomatic

import sys
import os
def divide(a, b):
    return a/b
# Will only run if script is executed directly,
# not when the file is imported as a module
if __name__ == '__main__':
    first_number = float(sys.argv[1])
    second_number = float(sys.argv[2])
    if second_number != 0:
        print(divide(first_number, second_number))

Import

Sử dụng import tuyệt đối thay vì tương đối

Khi import một module, chúng ta có thể chọn một trong hai “kiểu” import đó là import tương đối và tuyệt đối. Import tuyệt đối sẽ xác định vị trí của module (như kiểu <package>.<module>.<submodule>) từ một trong số các vị trí trong sys.path.

Import tương đối xác định vị trí của module so với module hiện tại trong hệ thống file. Nếu bạn đang ở module package.subpackage.module và bạn muốn import package.other_module, bạn có thể sử dụng các dấu chấm để import tương đối, ví dụ from ..other_module import foo. Một dấu chấm . thể hiện module hiện tại. Mỗi dấu chấm thêm vào nghĩa là chúng ta chuyển đến package cha của package hiện tại. Chú ý rằng, import tương đối luôn luôn có dạng from ... import .... import foo luôn được hiểu là import tuyệt đối.

Đổi lại, khi sử dụng import tuyệt đối, bạn cần viết import package.other_module (có thể sử dụng mệnh đề as đề alias các phương thức cho ngắn hơn).

Tại sao chúng ta nên sử dụng import tuyệt đối? Import tương đối làm lộn xộn namespace của các module. Nếu viết from foo import bar thì bạn đã bó buộc tên bar ở trong namespace của module của bạn. Với những người đọc code của bạn, sẽ rất khó để hiển bar từ đâu mà ra, nhất là khi bạn sử dụng các hàm phức tạp và module lớn. Ngược lại foo.bar giúp chúng ta dễ dàng hiểu hơn nguồn gốc của bar, nơi mà nó được định nghĩa.

Harmful

# My location is package.sub_package.module
# and I want to import package.other_module.
# The following should be avoided:
from ...package import other_module

Idiomatic

# My location is package.sub_package.another_sub_package.module
# and I want to import package.other_module.
# Either of the following are acceptable:
import package.other_module
import package.other_module as other

Không sử dụng from foo import * để import nội dung module

Sử dụng dấu * (ví dụ from foo import *) là cách nhanh nhất đề làm lộn xộn namespace của bạn. Điều này thậm chí gây ra một số vấn đề nếu tên bạn định nghĩa trùng với tên được import từ package.

Nhưng nếu bạn cần phải import một lượng lớn các đối tượng từ package foo bạn phải làm thế nào? Hãy sử dụng ngoặc đơn () để nhóm các đối tượng trong một lệnh import. Bạn không cần phải viết 10 dòng để import từ cùng một module, và namespace của bạn vẫn rất gọn gàng.

Tốt hơn cả, bạn nên sử dụng import tuyệt đối. Nếu tên package hay module quá dài, bạn có thể dùng mệnh đề as đề thu gọn chúng.

Harmful

from foo import *

Idiomatic

from foo import (bar, baz, qux,
                 quux, quuux)
# or even better...
import foo

Sắp xếp các lệnh import theo thứ tự chuẩn

Khi dự án lớn dần lên (nhất là với các dự án phát triển Web), sẽ có rất nhiều lệnh import khác nhau được thực hiện. Hãy để tất cả các lệnh import ở phần trên cùng của mỗi file, chọn một thứ tự chuẩn để sắp xếp các lệnh import và tuân thủ theo chuẩn này. Bởi vì thứ tự import nhiều khi không quan trọng, nhưng thứ tự sau được recommend bởi Python’s Programming FAQ:

  1. Các module thư viện chuẩn
  2. Các module thư viện bên thứ 3 được cài trong site-packages
  3. Các module trong dự án hiện tại

Nhiều người chọn sắp xếp các lệnh import theo thứ tự từ điển. Nhiều người khác nghĩ rằng như thế là ngu ngốc. Thực ra, chẳng có vấn đề gì cả. Những gì bạn cần làm là chọn ra một chuẩn quy tắc và tuân thủ theo nó.

Harmful

import os.path
# Some function and class definitions,
# one of which uses os.path
# ....
import concurrent.futures
from flask import render_template
# Stuff using futures and Flask's render_template
# ....
from flask import (Flask, request, session, g,
                   redirect, url_for, abort,
                   render_template, flash, _app_ctx_stack)
import requests
# Code using flask and requests
# ....
if __name__ == '__main__':
    # Imports when imported as a module are not so
    # costly that they need to be relegated to inside
    # an 'if __name__ == '__main__'' block...
    import this_project.utilities.sentient_network as skynet
    import this_project.widgets
    import sys

Idiomatic

# Easy to see exactly what my dependencies are and where to
# make changes if a module or package name changes
import concurrent.futures
import os.path
import sys
from flask import (Flask, request, session, g,
                   redirect, url_for, abort,
                   render_template, flash, _app_ctx_stack)
import requests
import this_project.utilities.sentient_network as skynet
import this_project.widgets

Lời khuyên chung

Tránh “tái phát minh bánh xe”

Hiểu nội dung của thư viện Python chuẩn

Một phần của việc viết mã idiomatic chính là làm chủ các thư viện chuẩn. Code mà vô tình thực hiện những chức năng mà thư viện chuẩn đã có là tín hiệu rõ ràng của một lập trình viên mới của Python. Python thường được nói là “batteries included” với một lý do chính đáng. Các thư viện chuẩn bao gồm các package bao phủ hàng loạt lĩnh vực. Sử dụng các thư viện chuẩn cho chúng ta hai lợi ích chính. Rõ ràng nhất, là chúng ta tiết kiệm cho mình rất nhiều thời gian khi không cần phải cài đặt các chức năng từ đầu. Một lợi ích quan trọng khác là những người đọc hay maintain code của bạn sẽ có thời gian dễ dàng hơn khi sử dụng các package quen thuộc.

Hãy nhớ rằng, việc học viết code Python một cách idiomatic là viết code sáng sủa, rõ ràng, có thể duy trì và không có bug. Không có gì đảm bảo những phẩm chất đó ở code của bạn dễ dàng hơn việc sử dụng lại code đã được viết và maintain bởi các developer Python. Bởi vì các bug được tìm và sửa trong các thư viện chuẩn, code của bạn cũng được nâng cao mà bạn chẳng cần phải làm gì cả.

Hiểu về PyPI (the Python Package Index)

Nếu thư viện chuẩn của Python không có package nào liên quan đến bài toán của bạn, bạn vẫn có cơ hội tìm thấy ở PyPI. Vào thời điểm bài viết này, có trên 27000 package đang được liệt kê ở index. Nếu bạn đang cần tìm các package để giải quyết một bài toán, và bạn không thể tìm thấy package nào liên quan trong PyPI, rất có thể là nó không tồn tại.

Trang index có thể tìm kiếm dễ dàng và có cả các package cho Python 2 và Python 3. Tất nhiên, không phải package nào cũng được tạo ra và duy trì như nhau. Nên bạn cần kiểm tra các update của chúng. Một package có tài liệu được lưu trên các trang bên ngoài như ReadTheDocs là một dấu hiệu tốt, và thường mã nguồn của chúng sẽ có ở trên Github hoặc Bitbucket.

Nếu bạn tìm thấy một package đầy hứa hẹn, làm thế nào bạn có thể cài đặt nó? Đến nay, một công cụ khá phổ biến để quản lý các package của bên thứ 3 là pip. Lệnh pip install <package name> sẽ download và cài đặt phiên bản mới nhất của package vào thứ thư mục site-packages. Nếu bạn cần chỉ định phiên bản của package, pip cũng hỗ trợ việc này.

Nếu bạn có một package mà sẽ hữu ích cho rất nhiều người, tôi recommend bạn nên công bố nó cho cộng đồng Python bằng cách đưa nó lên PyPI. Rất nhiều người sẽ cảm ơn bạn vì nghĩa cử cao đẹp đó.

Module cần lưu ý

Hiểu nội dung của module itertools

Nếu bạn thường xuyên vào các trang như StackOverflow, bạn có thể để ý rằng những câu trả lời cho câu hỏi như “Tại sao Python lại thiếu thư viện rất cần thiết này?” đều dẫn đến module itertools. Những người theo đuổi lập trình hàm cho rằng itertools cung cấp những hàm nên được coi là những khối xây dựng cơ bản. Hơn nữa, các tài liệu hướng dẫn cho itertools có một phần là Recipes cung cấp cách cài đặt idiomatic những cấu trúc thông dụng của lập trình hàm, chỉ cần sử dụng module itertools mà thôi. Có một số lý do khiến cho một vài developer Python quan tâm tới phần “Recipes” và module itertools nói chung. Một phần của việc viết code idiomatic là biết khi nào bạn “tái phát minh bánh xe”.

Sử dụng hàm của os.path khi làm việc với đường dẫn các file, thư mục

Khi viết một script dạng command-line, những developer mới thường sử dụng đến các phép toán của xâu để thao tác với dường dẫn của file và thư mục. Python có một module rất đầy đủ và dành riêng cho việc này, đó là os.path. Sử dụng os.path làm giảm nguy cơ xảy ra các lỗi phổ biến, làm code của bạn khả chuyển hơn, và dễ hiểu hơn.

Harmful

from datetime import date
import os
filename_to_archive = 'test.txt'
new_filename = 'test.bak'
target_directory = './archives'
today = date.today()
os.mkdir('./archives/' + str(today))
os.rename(
          filename_to_archive,
          target_directory + '/' + str(today) + '/' + new_filename)
          today) + '/' + new_filename)

Idiomatic

from datetime import date
import os
current_directory = os.getcwd()
filename_to_archive = 'test.txt'
new_filename = os.path.splitext(filename_to_archive)[0] + '.bak'
target_directory = os.path.join(current_directory, 'archives')
today = date.today()
new_path = os.path.join(target_directory, str(today))
if (os.path.isdir(target_directory)):
    if not os.path.exists(new_path):
        os.mkdir(new_path)
    os.rename(
              os.path.join(current_directory, filename_to_archive),
              os.path.join(new_path, new_filename))

Tôi xin lỗi nếu bài viết có bất kỳ typo nào. Nếu bạn nhận thấy điều gì bất thường, xin hãy cho tôi biết.

Nếu có bất điều gì muốn nói, bạn có thể liên hệ với tôi qua các mạng xã hội, tạo discussion hoặc report issue trên Github.